Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions oid4vc-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ license.workspace = true
anyhow = "1.0.70"
async-trait = "0.1.68"
base64 = "0.22"
# TODO: remove this dependency and use base64 instead, see: https://github.com/impierce/openid4vc/pull/127
base64-url = "2.0.0"
derivative = "2.2.0"
derive_more = "0.99.16"
did-key.workspace = true
Expand Down
5 changes: 3 additions & 2 deletions oid4vc-core/src/jwt.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::Sign;
use anyhow::{anyhow, Result};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use getset::Getters;
use jsonwebtoken::{Algorithm, DecodingKey, Header, Validation};
use serde::de::DeserializeOwned;
Expand Down Expand Up @@ -72,7 +73,7 @@ where
let message = [base64_url_encode(&jwt.header)?, base64_url_encode(&jwt.payload)?].join(".");

let proof_value = signer.sign(&message, subject_syntax_type, algorithm).await?;
let signature = base64_url::encode(proof_value.as_slice());
let signature = URL_SAFE_NO_PAD.encode(proof_value.as_slice());
Comment thread
Oran-Dan marked this conversation as resolved.
let message = [message, signature].join(".");
Ok(message)
}
Expand All @@ -81,7 +82,7 @@ pub fn base64_url_encode<T>(value: &T) -> Result<String>
where
T: ?Sized + Serialize,
{
Ok(base64_url::encode(serde_json::to_vec(value)?.as_slice()))
Ok(URL_SAFE_NO_PAD.encode(serde_json::to_vec(value)?.as_slice()))
}

#[cfg(feature = "test-utils")]
Expand Down
80 changes: 80 additions & 0 deletions oid4vc-core/src/utils/did.rs
Comment thread
Oran-Dan marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use crate::utils::jwt::{get_unverified_jwt_claims, sd_jwt_to_jwt};
use jsonwebtoken::decode_header;

/// Extract the `kid` from a JWT header as a DID URL.
///
/// If the `kid` is a relative DID fragment such as `#key-1`, this function
/// prefixes it with the unverified `iss` claim from the JWT payload to produce
/// an absolute DID URL.
///
/// Returns an error if the JWT header cannot be decoded, if `kid` is missing,
/// or if a relative `kid` cannot be expanded because `iss` is missing or not a string.
pub fn extract_normalized_did_kid_from_jwt(jwt: &str) -> Result<String, anyhow::Error> {
let jwt = sd_jwt_to_jwt(jwt);

let jwt_header = decode_header(jwt).map_err(|e| anyhow::anyhow!("Failed to decode JWT header: {e}"))?;
let mut key_id = jwt_header
.kid
.ok_or_else(|| anyhow::anyhow!("Missing 'kid' in JWT header"))?;

if key_id.starts_with('#') {
let claims = get_unverified_jwt_claims(&serde_json::json!(jwt))?;
let iss = claims
.get("iss")
.ok_or_else(|| anyhow::anyhow!("Missing 'iss' claim"))?
.as_str()
.ok_or_else(|| anyhow::anyhow!("'iss' claim is not a string"))?;

key_id = format!("{iss}{key_id}");
}

Ok(key_id)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn resolve_key_id_with_relative_reference() {
// JWT with relative key_id (starts with '#')
let jwt =
"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6IiNteWtleSJ9.eyJpc3MiOiJkaWQ6ZXhhbXBsZTppc3N1ZXIifQ.signature";
let result = extract_normalized_did_kid_from_jwt(jwt);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "did:example:issuer#mykey");
}

#[test]
fn resolve_key_id_with_absolute_reference() {
// JWT with absolute key_id (doesn't start with '#')
let jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpleGFtcGxlOmlzc3VlciNteWtleSJ9.eyJpc3MiOiJkaWQ6ZXhhbXBsZTppc3N1ZXIifQ.signature";
let result = extract_normalized_did_kid_from_jwt(jwt);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "did:example:issuer#mykey");
}

#[test]
fn resolve_key_id_missing_kid() {
// JWT without kid in header
let jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6ZXhhbXBsZTppc3N1ZXIifQ.signature";
let result = extract_normalized_did_kid_from_jwt(jwt);
assert!(result.is_err());
}

#[test]
fn resolve_key_id_missing_iss_claim() {
// JWT with relative key_id but missing 'iss' claim
let jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6IiNteWtleSJ9.e30.signature";
let result = extract_normalized_did_kid_from_jwt(jwt);
assert!(result.is_err());
}

#[test]
fn resolve_key_id_iss_not_string() {
// JWT with relative key_id but 'iss' claim is not a string
let jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6IiNteWtleSJ9.eyJpc3MiOjEyMzQ1fQ.signature";
let result = extract_normalized_did_kid_from_jwt(jwt);
assert!(result.is_err());
}
}
1 change: 1 addition & 0 deletions oid4vc-core/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod did;
pub mod form_urlencoded;
pub mod jwt;
pub mod predicates;
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,9 @@ pub struct AuthorizationServerMetadata {
pub pushed_authorization_request_endpoint: Option<Url>,
#[serde(default)]
pub require_pushed_authorization_requests: Option<bool>,
// Interactive Authorization Endpoint (Section 6, OID4VCI 1.1)
pub interactive_authorization_endpoint: Option<Url>,
#[serde(default)]
pub require_interactive_authorization_request: Option<bool>,
// Additional authorization server metadata parameters MAY also be used.
}
131 changes: 69 additions & 62 deletions oid4vci/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,22 @@ where
}

/// Authorization Error Response as described here: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-authorization-error-respons
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Error)]
#[serde(rename_all = "snake_case")]
pub enum AuthorizationErrorResponse {
#[error("Access Denied")]
AccessDenied,
#[error("Invalid Request")]
InvalidRequest,
#[error("Unauthorized Client")]
UnauthorizedClient,
#[error("Unsupported Response Type")]
UnsupportedResponseType,
#[error("Invalid Scope")]
InvalidScope,
#[error("Server Error")]
ServerError,
#[error("Temporarily Unavailable")]
TemporarilyUnavailable,
}

Expand All @@ -77,29 +84,21 @@ impl ErrorStatusCode for AuthorizationErrorResponse {
}
}

impl std::error::Error for AuthorizationErrorResponse {}
impl Display for AuthorizationErrorResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AccessDenied => write!(f, "Access Denied"),
Self::InvalidRequest => write!(f, "Invalid Request"),
Self::UnauthorizedClient => write!(f, "Unauthorized Client"),
Self::UnsupportedResponseType => write!(f, "Unsupported Response Type"),
Self::InvalidScope => write!(f, "Invalid Scope"),
Self::ServerError => write!(f, "Server Error"),
Self::TemporarilyUnavailable => write!(f, "Temporarily Unavailable"),
}
}
}
/// Token Error Response as described here: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-token-error-response
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Error)]
#[serde(rename_all = "snake_case")]
pub enum TokenErrorResponse {
#[error("Invalid Request")]
InvalidRequest,
#[error("Invalid Client")]
InvalidClient,
#[error("Invalid Grant")]
InvalidGrant,
#[error("Unauthorized Client")]
UnauthorizedClient,
#[error("Unsupported Grant Type")]
UnsupportedGrantType,
#[error("Invalid Scope")]
InvalidScope,
}

Expand All @@ -115,30 +114,24 @@ impl ErrorStatusCode for TokenErrorResponse {
}
}
}
impl std::error::Error for TokenErrorResponse {}
impl Display for TokenErrorResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidRequest => write!(f, "Invalid Request"),
Self::InvalidClient => write!(f, "Invalid Client"),
Self::InvalidGrant => write!(f, "Invalid Grant"),
Self::UnauthorizedClient => write!(f, "Unauthorized Client"),
Self::UnsupportedGrantType => write!(f, "Unsupported Grant Type"),
Self::InvalidScope => write!(f, "Invalid Scope"),
}
}
}

/// Credential Error Response as defined in OpenID4VCI 1.0 - https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-request-errors
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Error)]
#[serde(rename_all = "snake_case")]
pub enum CredentialErrorResponse {
#[error("Invalid Credential Request")]
InvalidCredentialRequest,
#[error("Unknown Credential Configuration")]
UnknownCredentialConfiguration,
#[error("Unknown Credential Identifier")]
UnknownCredentialIdentifier,
#[error("Invalid Proof")]
InvalidProof,
#[error("Invalid Nonce")]
InvalidNonce,
#[error("Invalid Encryption Parameters")]
InvalidEncryptionParameters,
#[error("Credential Request Denied")]
CredentialRequestDenied,
}

Expand All @@ -156,32 +149,25 @@ impl ErrorStatusCode for CredentialErrorResponse {
}
}

impl std::error::Error for CredentialErrorResponse {}
impl Display for CredentialErrorResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidCredentialRequest => write!(f, "Invalid Credential Request"),
Self::UnknownCredentialConfiguration => write!(f, "Unknown Credential Configuration"),
Self::UnknownCredentialIdentifier => write!(f, "Unknown Credential Identifier"),
Self::InvalidProof => write!(f, "Invalid Proof"),
Self::InvalidNonce => write!(f, "Invalid Nonce"),
Self::InvalidEncryptionParameters => write!(f, "Invalid Encryption Parameters"),
Self::CredentialRequestDenied => write!(f, "Credential Request Denied"),
}
}
}

/// Deferred Credential Error Response as described here: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-deferred-credential-error-r
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Error)]
#[serde(rename_all = "snake_case")]
pub enum DeferredCredentialErrorResponse {
#[error("Invalid Credential Request")]
InvalidCredentialRequest,
#[error("Unknown Credential Configuration")]
UnknownCredentialConfiguration,
#[error("Unknown Credential Identifier")]
UnknownCredentialIdentifier,
#[error("Invalid Proof")]
InvalidProof,
#[error("Invalid Nonce")]
InvalidNonce,
#[error("Invalid Encryption Parameters")]
InvalidEncryptionParameters,
#[error("Credential Request Denied")]
CredentialRequestDenied,
#[error("Invalid Transaction ID")]
InvalidTransactionId,
}

Expand All @@ -200,26 +186,13 @@ impl ErrorStatusCode for DeferredCredentialErrorResponse {
}
}

impl std::error::Error for DeferredCredentialErrorResponse {}
impl Display for DeferredCredentialErrorResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidCredentialRequest => write!(f, "Invalid Credential Request"),
Self::UnknownCredentialConfiguration => write!(f, "Unknown Credential Configuration"),
Self::UnknownCredentialIdentifier => write!(f, "Unknown Credential Identifier"),
Self::InvalidProof => write!(f, "Invalid Proof"),
Self::InvalidNonce => write!(f, "Invalid Nonce"),
Self::InvalidEncryptionParameters => write!(f, "Invalid Encryption Parameters"),
Self::CredentialRequestDenied => write!(f, "Credential Request Denied"),
Self::InvalidTransactionId => write!(f, "Invalid Transaction ID"),
}
}
}
/// Notification Error Response as defined in OpenID4VCI 1.0: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-notification-error-response
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Error)]
#[serde(rename_all = "snake_case")]
pub enum NotificationErrorResponse {
#[error("Invalid Notification Request")]
InvalidNotificationRequest,
#[error("Invalid Notification ID")]
InvalidNotificationId,
}

Expand All @@ -232,6 +205,40 @@ impl ErrorStatusCode for NotificationErrorResponse {
}
}

// TODO: Validate error response serialization against the OID4VCI spec.
/// Interactive Authorization Error Response as defined in OID4VCI 1.1, Section 6.2.3.
///
/// In addition to standard PAR error processing rules (RFC 9126, Section 2.3), this adds
/// the `missing_interaction_type` error code.
#[derive(Debug, Serialize, Deserialize, Error)]
#[serde(rename_all = "snake_case")]
pub enum InteractiveAuthorizationErrorResponse {
/// The `interaction_types_supported` parameter is missing a required interaction type.
#[error("Missing Interaction Type")]
MissingInteractionType,
/// Standard OAuth error codes may also appear.
#[error("Invalid Request")]
InvalidRequest,
#[error("Invalid Client")]
InvalidClient,
#[error("Unauthorized Client")]
UnauthorizedClient,
#[error("Access Denied")]
AccessDenied,
}

impl ErrorStatusCode for InteractiveAuthorizationErrorResponse {
fn status_code(&self) -> StatusCode {
match self {
Self::MissingInteractionType => StatusCode::BAD_REQUEST,
Self::InvalidRequest => StatusCode::BAD_REQUEST,
Self::InvalidClient => StatusCode::UNAUTHORIZED,
Self::UnauthorizedClient => StatusCode::UNAUTHORIZED,
Self::AccessDenied => StatusCode::FORBIDDEN,
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading
Loading