diff --git a/Cargo.toml b/Cargo.toml index d1601a0b..b9a2e492 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ tokio = { version = "1.26.0", features = ["rt", "macros", "rt-multi-thread"] } serde = { version = "1.0", features = ["derive"]} serde_json = "1.0" serde_with = "3.0" +reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] } url = { version = "2.3", features = ["serde"] } getset = "0.1" identity_credential = { git = "https://git@github.com/iotaledger/identity.rs", rev = "4f27434", default-features = false, features = ["validator", "credential", "presentation"] } diff --git a/oid4vc-core/Cargo.toml b/oid4vc-core/Cargo.toml index 681ed0bf..a64ab2eb 100644 --- a/oid4vc-core/Cargo.toml +++ b/oid4vc-core/Cargo.toml @@ -4,27 +4,20 @@ version = "0.1.0" edition = "2021" [dependencies] -tokio.workspace = true serde.workspace = true serde_json = "1.0" serde_with = "2.3" anyhow = "1.0.70" -chrono = "0.4.24" getset = "0.1.2" jsonwebtoken = "8.2.0" -reqwest = { version = "0.11.14", default-features = false, features = ["json", "rustls-tls"] } base64-url = "2.0.0" async-trait = "0.1.68" did_url = "0.1.0" -url = { version = "2.3.1", features = ["serde"] } is_empty = "0.2.0" -serde_urlencoded = "0.7.1" -derive_more = "0.99.16" -identity_credential.workspace = true -futures = "0.3" +rand = "0.7" [dev-dependencies] ed25519-dalek = "1.0.1" -rand = "0.7" lazy_static = "1.4.0" derivative = "2.2.0" +tokio.workspace = true diff --git a/oid4vc-core/src/authentication/subject.rs b/oid4vc-core/src/authentication/subject.rs index 80ec6d92..9002972e 100644 --- a/oid4vc-core/src/authentication/subject.rs +++ b/oid4vc-core/src/authentication/subject.rs @@ -2,6 +2,8 @@ use crate::{Collection, Sign, SubjectSyntaxType, Verify}; use anyhow::Result; use std::{str::FromStr, sync::Arc}; +pub type SigningSubject = Arc; + // TODO: Use a URI of some sort. /// This [`Subject`] trait is used to sign and verify JWTs. pub trait Subject: Sign + Verify + Send + Sync { @@ -28,8 +30,8 @@ impl TryFrom<[Arc; N]> for Subjects { Ok(Self::from( subjects .iter() - .map(|subject| (subject.type_().unwrap(), subject.clone())) - .collect::>(), + .map(|subject| subject.type_().map(|subject_type| (subject_type, subject.clone()))) + .collect::>>()?, )) } } diff --git a/oid4vc-core/src/collection.rs b/oid4vc-core/src/collection.rs index f80c7ad0..e7d76c29 100644 --- a/oid4vc-core/src/collection.rs +++ b/oid4vc-core/src/collection.rs @@ -1,6 +1,7 @@ use crate::SubjectSyntaxType; use std::{collections::HashMap, sync::Arc}; +#[derive(Clone)] pub struct Collection(pub HashMap>); impl Collection { diff --git a/oid4vc-core/src/jwt.rs b/oid4vc-core/src/jwt.rs index fdc8c025..fd3cb3e7 100644 --- a/oid4vc-core/src/jwt.rs +++ b/oid4vc-core/src/jwt.rs @@ -20,12 +20,8 @@ impl JsonWebToken where C: Serialize, { - pub fn new(payload: C) -> Self { - JsonWebToken { - // TODO: Undo hardcoding and consider not using the jsonwebtoken crate. - header: Header::new(Algorithm::EdDSA), - payload, - } + pub fn new(header: Header, payload: C) -> Self { + JsonWebToken { header, payload } } pub fn kid(mut self, kid: String) -> Self { @@ -51,14 +47,14 @@ where Ok(jsonwebtoken::decode::(jwt, &key, &Validation::new(algorithm))?.claims) } -pub fn encode(signer: Arc, claims: C) -> Result +pub fn encode(signer: Arc, header: Header, claims: C) -> Result where C: Serialize + Send, S: Sign + ?Sized, { let kid = signer.key_id().ok_or(anyhow!("No key identifier found."))?; - let jwt = JsonWebToken::new(claims).kid(kid); + let jwt = JsonWebToken::new(header, claims).kid(kid); let message = [base64_url_encode(&jwt.header)?, base64_url_encode(&jwt.payload)?].join("."); @@ -95,7 +91,7 @@ mod tests { "nonce": "nonce", }); let subject = TestSubject::new("did:test:123".to_string(), "key_id".to_string()).unwrap(); - let encoded = encode(Arc::new(subject), claims).unwrap(); + let encoded = encode(Arc::new(subject), Header::new(Algorithm::EdDSA), claims).unwrap(); let verifier = MockVerifier::new(); let (kid, algorithm) = extract_header(&encoded).unwrap(); diff --git a/oid4vc-core/src/lib.rs b/oid4vc-core/src/lib.rs index 4ce5a6ad..08321d12 100644 --- a/oid4vc-core/src/lib.rs +++ b/oid4vc-core/src/lib.rs @@ -13,11 +13,13 @@ pub use authentication::{ }; pub use collection::Collection; pub use decoder::Decoder; +use rand::{distributions::Alphanumeric, Rng}; pub use rfc7519_claims::RFC7519Claims; +use serde::Serialize; pub use subject_syntax_type::{DidMethod, SubjectSyntaxType}; #[cfg(test)] -pub mod test_utils; +mod test_utils; #[macro_export] macro_rules! builder_fn { @@ -36,3 +38,26 @@ macro_rules! builder_fn { } }; } + +// Helper function that allows to serialize custom structs into a query string. +pub fn to_query_value(value: &T) -> anyhow::Result { + serde_json::to_string(value) + .map(|s| s.chars().filter(|c| !c.is_whitespace()).collect::()) + .map_err(|e| e.into()) +} + +pub fn generate_authorization_code(length: usize) -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(length) + .map(char::from) + .collect() +} + +pub fn generate_nonce(length: usize) -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(length) + .map(char::from) + .collect() +} diff --git a/oid4vc-manager/Cargo.toml b/oid4vc-manager/Cargo.toml index 136b41f9..64b88c3e 100644 --- a/oid4vc-manager/Cargo.toml +++ b/oid4vc-manager/Cargo.toml @@ -24,6 +24,12 @@ identity_iota = "0.6" identity_core = { git = "https://git@github.com/iotaledger/identity.rs", rev = "4f27434" } identity_credential.workspace = true futures = "0.3" +axum = "0.6" +axum-auth = "0.4" +reqwest.workspace = true +jsonwebtoken = "8.3" +paste = "1.0" +tower-http = { version = "0.4", features = ["cors"]} [dev-dependencies] ed25519-dalek = "1.0.1" @@ -31,3 +37,5 @@ rand = "0.7" lazy_static = "1.4.0" derivative = "2.2.0" wiremock = "0.5.18" +jwt = "0.16" + diff --git a/oid4vc-manager/src/lib.rs b/oid4vc-manager/src/lib.rs index c2e22ee9..5cc96ef2 100644 --- a/oid4vc-manager/src/lib.rs +++ b/oid4vc-manager/src/lib.rs @@ -1,4 +1,6 @@ pub mod managers; pub mod methods; +pub mod servers; +pub mod storage; pub use managers::{provider::ProviderManager, relying_party::RelyingPartyManager}; diff --git a/oid4vc-manager/src/managers/credential_issuer.rs b/oid4vc-manager/src/managers/credential_issuer.rs new file mode 100644 index 00000000..438ee8d9 --- /dev/null +++ b/oid4vc-manager/src/managers/credential_issuer.rs @@ -0,0 +1,83 @@ +use crate::storage::Storage; +use anyhow::Result; +use oid4vc_core::{Subject, Subjects}; +use oid4vci::{ + credential_format_profiles::CredentialFormatCollection, + credential_issuer::{ + authorization_server_metadata::AuthorizationServerMetadata, + credential_issuer_metadata::CredentialIssuerMetadata, CredentialIssuer, + }, + credential_offer::{CredentialOffer, CredentialOfferQuery, CredentialsObject, Grants}, +}; +use reqwest::Url; +use std::{net::TcpListener, sync::Arc}; + +#[derive(Clone)] +pub struct CredentialIssuerManager, CFC: CredentialFormatCollection> { + pub credential_issuer: CredentialIssuer, + pub subjects: Arc, + pub storage: S, + pub listener: Arc, +} + +impl + Clone, CFC: CredentialFormatCollection> CredentialIssuerManager { + pub fn new( + listener: Option, + storage: S, + subjects: [Arc; N], + ) -> Result { + // `TcpListener::bind("127.0.0.1:0")` will bind to a random port. + let listener = listener.unwrap_or_else(|| TcpListener::bind("127.0.0.1:0").unwrap()); + let issuer_url: Url = format!("http://{:?}", listener.local_addr()?).parse()?; + Ok(Self { + credential_issuer: CredentialIssuer { + subject: subjects + .get(0) + .ok_or_else(|| anyhow::anyhow!("No subjects found."))? + .clone(), + metadata: CredentialIssuerMetadata { + credential_issuer: issuer_url.clone(), + authorization_server: None, + credential_endpoint: issuer_url.join("/credential")?, + batch_credential_endpoint: None, + deferred_credential_endpoint: None, + credentials_supported: storage.get_credentials_supported(), + display: None, + }, + authorization_server_metadata: AuthorizationServerMetadata { + issuer: issuer_url.clone(), + authorization_endpoint: issuer_url.join("/authorize")?, + token_endpoint: issuer_url.join("/token")?, + ..Default::default() + }, + }, + subjects: Arc::new(Subjects::try_from(subjects)?), + storage, + listener: Arc::new(listener), + }) + } + + pub fn credential_issuer_url(&self) -> Result { + Ok(self.credential_issuer.metadata.credential_issuer.clone()) + } + + pub fn credential_offer_uri(&self) -> Result { + let credential = self + .credential_issuer + .metadata + .credentials_supported + .get(0) + .ok_or_else(|| anyhow::anyhow!("No credentials supported."))? + .credential_format + .clone(); + Ok(CredentialOfferQuery::CredentialOffer(CredentialOffer { + credential_issuer: self.credential_issuer.metadata.credential_issuer.clone(), + credentials: vec![CredentialsObject::ByValue(credential)], + grants: Some(Grants { + authorization_code: self.storage.get_authorization_code(), + pre_authorized_code: self.storage.get_pre_authorized_code(), + }), + }) + .to_string()) + } +} diff --git a/oid4vc-manager/src/managers/mod.rs b/oid4vc-manager/src/managers/mod.rs index 250fd7dc..c395ab36 100644 --- a/oid4vc-manager/src/managers/mod.rs +++ b/oid4vc-manager/src/managers/mod.rs @@ -1,3 +1,4 @@ +pub mod credential_issuer; pub mod presentation; pub mod provider; pub mod relying_party; diff --git a/oid4vc-manager/src/servers/credential_issuer.rs b/oid4vc-manager/src/servers/credential_issuer.rs new file mode 100644 index 00000000..ab06c87e --- /dev/null +++ b/oid4vc-manager/src/servers/credential_issuer.rs @@ -0,0 +1,190 @@ +use std::time::Duration; + +use crate::{managers::credential_issuer::CredentialIssuerManager, storage::Storage}; +use anyhow::Result; +use axum::{ + extract::State, + http::{header::CONTENT_TYPE, Method, StatusCode}, + response::{AppendHeaders, IntoResponse}, + routing::{get, post}, + Form, Json, Router, +}; +use axum_auth::AuthBearer; +use oid4vc_core::{Decoder, Subjects}; +use oid4vci::{ + authorization_request::AuthorizationRequest, credential_format_profiles::CredentialFormatCollection, + credential_request::CredentialRequest, token_request::TokenRequest, +}; +use serde::de::DeserializeOwned; +use tokio::task::JoinHandle; +use tower_http::cors::AllowOrigin; + +pub struct Server +where + S: Storage, + CFC: CredentialFormatCollection, +{ + pub credential_issuer_manager: CredentialIssuerManager, + pub server: Option>, + pub extension: Option>>, + pub detached: bool, +} + +impl + Clone, CFC: CredentialFormatCollection + Clone + DeserializeOwned + 'static> Server { + pub fn setup( + credential_issuer_manager: CredentialIssuerManager, + extension: Option>>, + ) -> Result { + Ok(Self { + credential_issuer_manager, + server: None, + extension, + detached: false, + }) + } + + pub fn detached(mut self, detached: bool) -> Self { + self.detached = detached; + self + } + + pub async fn start_server(&mut self) -> Result<()> { + let credential_issuer_manager = self.credential_issuer_manager.clone(); + let listener = credential_issuer_manager.listener.try_clone()?; + let extension = self.extension.take(); + + let server = axum::Server::from_tcp(listener) + .expect("Failed to start server.") + .serve( + Router::new() + .route( + "/.well-known/oauth-authorization-server", + get(oauth_authorization_server), + ) + .route("/.well-known/openid-credential-issuer", get(openid_credential_issuer)) + .route("/authorize", get(authorize)) + .route("/token", post(token)) + .route("/credential", post(credential)) + .merge(extension.unwrap_or_default()) + .layer( + tower_http::cors::CorsLayer::new() + .allow_methods([Method::GET, Method::POST]) + .allow_origin(AllowOrigin::any()) + .allow_headers([CONTENT_TYPE]) + .max_age(Duration::from_secs(3600)), + ) + .with_state(credential_issuer_manager) + .into_make_service(), + ); + + if self.detached { + self.server.replace(tokio::spawn( + async move { server.await.expect("Failed to start server.") }, + )); + } else { + server.await.expect("Failed to start server.") + } + Ok(()) + } + + pub fn stop_server(&mut self) -> Result<()> { + self.server + .as_ref() + .ok_or(anyhow::anyhow!("Server not started."))? + .abort(); + Ok(()) + } +} + +async fn oauth_authorization_server, CFC: CredentialFormatCollection>( + State(credential_issuer_manager): State>, +) -> impl IntoResponse { + ( + StatusCode::OK, + Json( + credential_issuer_manager + .credential_issuer + .authorization_server_metadata, + ), + ) +} + +async fn openid_credential_issuer, CFC: CredentialFormatCollection>( + State(credential_issuer_manager): State>, +) -> impl IntoResponse { + ( + StatusCode::OK, + Json(credential_issuer_manager.credential_issuer.metadata), + ) +} + +async fn authorize, CFC: CredentialFormatCollection>( + State(credential_issuer_manager): State>, + Json(_authorization_request): Json>, +) -> impl IntoResponse { + ( + // TODO: should be 302 Found + implement proper error response. + StatusCode::OK, + Json(credential_issuer_manager.storage.get_authorization_response().unwrap()), + ) +} + +async fn token, CFC: CredentialFormatCollection>( + State(credential_issuer_manager): State>, + Form(token_request): Form, +) -> impl IntoResponse { + match credential_issuer_manager + .storage + .get_token_response(token_request) + .take() + { + Some(token_response) => ( + StatusCode::OK, + AppendHeaders([("Cache-Control", "no-store")]), + Json(token_response), + ) + .into_response(), + // TODO: handle error response + _ => ( + StatusCode::BAD_REQUEST, + AppendHeaders([("Cache-Control", "no-store")]), + Json("Pre-authorized code not found"), + ) + .into_response(), + } +} + +async fn credential, CFC: CredentialFormatCollection>( + State(credential_issuer_manager): State>, + AuthBearer(access_token): AuthBearer, + Json(credential_request): Json>, +) -> impl IntoResponse { + // TODO: The bunch of unwrap's here should be replaced with error responses as described here: https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html#name-credential-error-response + let proof = credential_issuer_manager + .credential_issuer + .validate_proof( + credential_request.proof.unwrap(), + Decoder::from(&Subjects::try_from([credential_issuer_manager.credential_issuer.subject.clone()]).unwrap()), + ) + .await + .unwrap(); + ( + StatusCode::OK, + AppendHeaders([("Cache-Control", "no-store")]), + Json( + credential_issuer_manager + .storage + .get_credential_response( + access_token, + proof.rfc7519_claims.iss().as_ref().unwrap().parse().unwrap(), + credential_issuer_manager + .credential_issuer + .metadata + .credential_issuer + .clone(), + credential_issuer_manager.credential_issuer.subject.clone(), + ) + .unwrap(), + ), + ) +} diff --git a/oid4vc-manager/src/servers/mod.rs b/oid4vc-manager/src/servers/mod.rs new file mode 100644 index 00000000..9efd2ab9 --- /dev/null +++ b/oid4vc-manager/src/servers/mod.rs @@ -0,0 +1 @@ +pub mod credential_issuer; diff --git a/oid4vc-manager/src/storage.rs b/oid4vc-manager/src/storage.rs new file mode 100644 index 00000000..c4d170aa --- /dev/null +++ b/oid4vc-manager/src/storage.rs @@ -0,0 +1,32 @@ +use oid4vc_core::authentication::subject::SigningSubject; +use oid4vci::{ + authorization_response::AuthorizationResponse, + credential_format_profiles::CredentialFormatCollection, + credential_issuer::credentials_supported::CredentialsSupportedObject, + credential_offer::{AuthorizationCode, PreAuthorizedCode}, + credential_response::CredentialResponse, + token_request::TokenRequest, + token_response::TokenResponse, +}; +use reqwest::Url; + +// Represents the Credential Issuer's server logic. +pub trait Storage: Send + Sync + 'static +where + CFC: CredentialFormatCollection, +{ + fn get_credentials_supported(&self) -> Vec>; + fn get_authorization_response(&self) -> Option; + fn get_authorization_code(&self) -> Option; + fn get_pre_authorized_code(&self) -> Option; + fn get_token_response(&self, token_request: TokenRequest) -> Option; + fn get_credential_response( + &self, + access_token: String, + subject_did: Url, + issuer_did: Url, + subject: SigningSubject, + ) -> Option; + fn get_state(&self) -> Option; + fn set_state(&mut self, state: String); +} diff --git a/oid4vc-manager/tests/common/credentials/university_degree.json b/oid4vc-manager/tests/common/credentials/university_degree.json new file mode 100644 index 00000000..8a29345e --- /dev/null +++ b/oid4vc-manager/tests/common/credentials/university_degree.json @@ -0,0 +1,19 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": [ + "VerifiableCredential", + "PersonalInformation" + ], + "issuanceDate": "2022-01-01T00:00:00Z", + "issuer": {}, + "credentialSubject": { + "id": {}, + "givenName": "Ferris", + "familyName": "Crabman", + "email": "ferris.crabman@crabmail.com", + "birthdate": "1985-05-21" + } +} diff --git a/oid4vc-manager/tests/common/credentials_supported_objects/university_degree.json b/oid4vc-manager/tests/common/credentials_supported_objects/university_degree.json new file mode 100644 index 00000000..fd433a72 --- /dev/null +++ b/oid4vc-manager/tests/common/credentials_supported_objects/university_degree.json @@ -0,0 +1,58 @@ +{ + "format": "jwt_vc_json", + "id": "UniversityDegree_JWT", + "cryptographic_binding_methods_supported": [ + "did:key", + "did:iota" + ], + "cryptographic_suites_supported": [ + "EdDSA" + ], + "credential_definition":{ + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "last_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "degree": {}, + "gpa": { + "display": [ + { + "name": "GPA" + } + ] + } + } + }, + "proof_types_supported": [ + "jwt" + ], + "display": [ + { + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alt_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ] +} diff --git a/oid4vc-manager/tests/common/memory_storage.rs b/oid4vc-manager/tests/common/memory_storage.rs new file mode 100644 index 00000000..61d18f9b --- /dev/null +++ b/oid4vc-manager/tests/common/memory_storage.rs @@ -0,0 +1,117 @@ +use std::fs::File; + +use jsonwebtoken::{Algorithm, Header}; +use lazy_static::lazy_static; +use oid4vc_core::{authentication::subject::SigningSubject, generate_authorization_code, jwt}; +use oid4vc_manager::storage::Storage; +use oid4vci::{ + authorization_response::AuthorizationResponse, + credential_format_profiles::CredentialFormatCollection, + credential_issuer::credentials_supported::CredentialsSupportedObject, + credential_offer::{AuthorizationCode, PreAuthorizedCode}, + credential_response::CredentialResponse, + token_request::TokenRequest, + token_response::TokenResponse, + VerifiableCredentialJwt, +}; +use oid4vp::ClaimFormatDesignation; +use reqwest::Url; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +lazy_static! { + pub static ref CODE: String = generate_authorization_code(16); + pub static ref PRE_AUTHORIZED_CODE: PreAuthorizedCode = PreAuthorizedCode { + pre_authorized_code: generate_authorization_code(16), + ..Default::default() + }; + pub static ref USER_PIN: String = "493536".to_string(); + pub static ref ACCESS_TOKEN: String = "czZCaGRSa3F0MzpnWDFmQmF0M2JW".to_string(); + pub static ref C_NONCE: String = "tZignsnFbp".to_string(); +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct MemoryStorage; + +impl Storage for MemoryStorage { + fn get_credentials_supported(&self) -> Vec> { + let credentials_supported_object = + File::open("./tests/common/credentials_supported_objects/university_degree.json").unwrap(); + vec![serde_json::from_reader(credentials_supported_object).unwrap()] + } + + fn get_authorization_code(&self) -> Option { + None + } + + fn get_authorization_response(&self) -> Option { + Some(AuthorizationResponse { + code: CODE.clone(), + state: None, + }) + } + + fn get_pre_authorized_code(&self) -> Option { + Some(PRE_AUTHORIZED_CODE.clone()) + } + + fn get_token_response(&self, token_request: TokenRequest) -> Option { + match token_request { + TokenRequest::AuthorizationCode { code, .. } => code == CODE.clone(), + TokenRequest::PreAuthorizedCode { + pre_authorized_code, .. + } => pre_authorized_code == PRE_AUTHORIZED_CODE.pre_authorized_code, + } + .then_some(TokenResponse { + // TODO: dynamically create this. + access_token: ACCESS_TOKEN.clone(), + token_type: "bearer".to_string(), + expires_in: Some(86400), + refresh_token: None, + scope: None, + c_nonce: Some(C_NONCE.clone()), + c_nonce_expires_in: Some(86400), + }) + } + + fn get_credential_response( + &self, + access_token: String, + subject_did: Url, + issuer_did: Url, + signer: SigningSubject, + ) -> Option { + let credential = File::open("./tests/common/credentials/university_degree.json").unwrap(); + let mut verifiable_credential: serde_json::Value = serde_json::from_reader(credential).unwrap(); + verifiable_credential["issuer"] = serde_json::json!(issuer_did); + verifiable_credential["credentialSubject"]["id"] = serde_json::json!(subject_did); + + (access_token == ACCESS_TOKEN.clone()).then_some(CredentialResponse { + format: ClaimFormatDesignation::JwtVcJson, + credential: serde_json::to_value( + jwt::encode( + signer.clone(), + Header::new(Algorithm::EdDSA), + VerifiableCredentialJwt::builder() + .sub(subject_did.clone()) + .iss(issuer_did.clone()) + .iat(0) + .exp(9999999999i64) + .verifiable_credential(verifiable_credential) + .build() + .ok(), + ) + .ok(), + ) + .ok(), + transaction_id: None, + c_nonce: Some(C_NONCE.clone()), + c_nonce_expires_in: Some(86400), + }) + } + + fn get_state(&self) -> Option { + None + } + + fn set_state(&mut self, _state: String) {} +} diff --git a/oid4vc-manager/tests/common/mod.rs b/oid4vc-manager/tests/common/mod.rs index 12f802e4..6f67dc1a 100644 --- a/oid4vc-manager/tests/common/mod.rs +++ b/oid4vc-manager/tests/common/mod.rs @@ -1,4 +1,6 @@ // Move this to the mock repo. +pub mod memory_storage; + use anyhow::Result; use async_trait::async_trait; use derivative::{self, Derivative}; @@ -120,3 +122,10 @@ impl Storage for MemoryStorage { present } } + +// Get the claims from a JWT without performing validation. +pub fn get_jwt_claims(jwt: serde_json::Value) -> serde_json::Value { + let decoded_token: jwt::Token = + jwt::Token::parse_unverified(jwt.as_str().unwrap()).unwrap(); + decoded_token.claims().clone() +} diff --git a/oid4vc-manager/tests/mod.rs b/oid4vc-manager/tests/mod.rs index 8fdfa975..6f79bc68 100644 --- a/oid4vc-manager/tests/mod.rs +++ b/oid4vc-manager/tests/mod.rs @@ -1,3 +1,4 @@ pub mod common; +pub mod oid4vci; pub mod siopv2; pub mod siopv2_oid4vp; diff --git a/oid4vc-manager/tests/oid4vci/authorization_code.rs b/oid4vc-manager/tests/oid4vci/authorization_code.rs new file mode 100644 index 00000000..d4498481 --- /dev/null +++ b/oid4vc-manager/tests/oid4vci/authorization_code.rs @@ -0,0 +1,131 @@ +use crate::common::{get_jwt_claims, memory_storage::MemoryStorage}; +use did_key::{generate, Ed25519KeyPair}; +use oid4vc_core::Subject; +use oid4vc_manager::{ + managers::credential_issuer::CredentialIssuerManager, methods::key_method::KeySubject, + servers::credential_issuer::Server, +}; +use oid4vci::{ + authorization_details::{AuthorizationDetailsObject, OpenIDCredential}, + credential_format_profiles::CredentialFormats, + token_request::{AuthorizationCode, TokenRequest}, + Wallet, +}; +use std::sync::Arc; + +// TODO: Current Authorization Code Flow is not fully conformant to the spec. Issue: https://github.com/impierce/openid4vc/issues/46 +#[tokio::test] +async fn test_authorization_code_flow() { + // Setup the credential issuer. + let mut credential_issuer = Server::setup( + CredentialIssuerManager::<_, CredentialFormats>::new( + None, + MemoryStorage, + [Arc::new(KeySubject::from_keypair(generate::(Some( + "this-is-a-very-UNSAFE-issuer-secret-key".as_bytes().try_into().unwrap(), + ))))], + ) + .unwrap(), + None, + ) + .unwrap() + .detached(true); + credential_issuer.start_server().await.unwrap(); + + // Create a new subject. + let subject = KeySubject::new(); + let subject_did = subject.identifier().unwrap(); + + // Create a new wallet. + let wallet = Wallet::::new(Arc::new(subject)); + + // Get the credential issuer url. + let credential_issuer_url = credential_issuer + .credential_issuer_manager + .credential_issuer_url() + .unwrap(); + + // Get the authorization server metadata. + let authorization_server_metadata = wallet + .get_authorization_server_metadata(credential_issuer_url.clone()) + .await + .unwrap(); + + // Get the credential issuer metadata. + let credential_issuer_metadata = wallet + .get_credential_issuer_metadata(credential_issuer_url.clone()) + .await + .unwrap(); + + // Get the credential format for a university degree. + let university_degree_credential_format = credential_issuer_metadata + .credentials_supported + .get(0) + .unwrap() + .clone() + .credential_format; + + // Get the authorization code. + let authorization_response = wallet + .get_authorization_code( + authorization_server_metadata.authorization_endpoint, + vec![AuthorizationDetailsObject { + type_: OpenIDCredential, + locations: None, + credential_format: university_degree_credential_format.clone(), + } + .into()], + ) + .await + .unwrap(); + + let token_request = TokenRequest::AuthorizationCode { + grant_type: AuthorizationCode, + code: authorization_response.code, + code_verifier: None, + redirect_uri: None, + }; + + // Get the access token. + let token_response = wallet + .get_access_token(authorization_server_metadata.token_endpoint, token_request) + .await + .unwrap(); + + // Get the credential. + let credential_response = wallet + .get_credential( + credential_issuer_metadata, + &token_response, + university_degree_credential_format, + ) + .await + .unwrap(); + + // Decode the JWT without performing validation + let claims = get_jwt_claims(credential_response.credential.unwrap().clone()); + + // Check the credential. + assert_eq!( + claims["vc"], + serde_json::json!({ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": [ + "VerifiableCredential", + "PersonalInformation" + ], + "issuanceDate": "2022-01-01T00:00:00Z", + "issuer": credential_issuer_url, + "credentialSubject": { + "id": subject_did, + "givenName": "Ferris", + "familyName": "Crabman", + "email": "ferris.crabman@crabmail.com", + "birthdate": "1985-05-21" + } + }) + ) +} diff --git a/oid4vc-manager/tests/oid4vci/mod.rs b/oid4vc-manager/tests/oid4vci/mod.rs new file mode 100644 index 00000000..c683b8a1 --- /dev/null +++ b/oid4vc-manager/tests/oid4vci/mod.rs @@ -0,0 +1,2 @@ +pub mod authorization_code; +pub mod pre_authorized_code; diff --git a/oid4vc-manager/tests/oid4vci/pre_authorized_code.rs b/oid4vc-manager/tests/oid4vci/pre_authorized_code.rs new file mode 100644 index 00000000..750b0528 --- /dev/null +++ b/oid4vc-manager/tests/oid4vci/pre_authorized_code.rs @@ -0,0 +1,128 @@ +use crate::common::{get_jwt_claims, memory_storage::MemoryStorage}; +use did_key::{generate, Ed25519KeyPair}; +use oid4vc_core::Subject; +use oid4vc_manager::{ + managers::credential_issuer::CredentialIssuerManager, methods::key_method::KeySubject, + servers::credential_issuer::Server, +}; +use oid4vci::{ + credential_format_profiles::CredentialFormats, + credential_offer::{CredentialOffer, CredentialOfferQuery, CredentialsObject, Grants}, + token_request::{PreAuthorizedCode, TokenRequest}, + Wallet, +}; +use std::sync::Arc; + +#[tokio::test] +async fn test_pre_authorized_code_flow() { + // Setup the credential issuer. + let mut credential_issuer = Server::<_, CredentialFormats>::setup( + CredentialIssuerManager::new( + None, + MemoryStorage, + [Arc::new(KeySubject::from_keypair(generate::(Some( + "this-is-a-very-UNSAFE-issuer-secret-key".as_bytes().try_into().unwrap(), + ))))], + ) + .unwrap(), + None, + ) + .unwrap() + .detached(true); + credential_issuer.start_server().await.unwrap(); + + // Get the credential offer url. + let credential_offer_url = credential_issuer + .credential_issuer_manager + .credential_offer_uri() + .unwrap(); + + // Parse the credential offer url. + let credential_offer: CredentialOffer = match credential_offer_url.parse().unwrap() { + CredentialOfferQuery::CredentialOffer(credential_offer) => credential_offer, + _ => unreachable!(), + }; + + // The credential offer contains a credential format for a university degree. + let university_degree_credential_format = match credential_offer.credentials.get(0).unwrap().clone() { + CredentialsObject::ByValue(credential_format) => credential_format, + _ => unreachable!(), + }; + + // The credential offer contains a credential issuer url. + let credential_issuer_url = credential_offer.credential_issuer; + + // Create a new subject. + let subject = KeySubject::new(); + let subject_did = subject.identifier().unwrap(); + + // Create a new wallet. + let wallet = Wallet::new(Arc::new(subject)); + + // Get the authorization server metadata. + let authorization_server_metadata = wallet + .get_authorization_server_metadata(credential_issuer_url.clone()) + .await + .unwrap(); + + // Get the credential issuer metadata. + let credential_issuer_metadata = wallet + .get_credential_issuer_metadata(credential_issuer_url.clone()) + .await + .unwrap(); + + // Create a token request with grant_type `pre_authorized_code`. + let token_request = match credential_offer.grants { + Some(Grants { + pre_authorized_code, .. + }) => TokenRequest::PreAuthorizedCode { + grant_type: PreAuthorizedCode, + pre_authorized_code: pre_authorized_code.unwrap().pre_authorized_code, + user_pin: Some("493536".to_string()), + }, + None => unreachable!(), + }; + + // Get an access token. + let token_response = wallet + .get_access_token(authorization_server_metadata.token_endpoint, token_request) + .await + .unwrap(); + + // Get the credential. + let credential_response = wallet + .get_credential( + credential_issuer_metadata, + &token_response, + university_degree_credential_format, + ) + .await + .unwrap(); + + // Decode the JWT without performing validation + let claims = get_jwt_claims(credential_response.credential.unwrap().clone()); + + // Check the credential. + assert_eq!( + claims["vc"], + serde_json::json!({ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": [ + "VerifiableCredential", + "PersonalInformation" + ], + "issuanceDate": "2022-01-01T00:00:00Z", + "issuer": credential_issuer_url, + "credentialSubject": { + "id": subject_did, + "givenName": "Ferris", + "familyName": "Crabman", + "email": "ferris.crabman@crabmail.com", + "birthdate": "1985-05-21" + } + }) + ) +} diff --git a/oid4vc-manager/tests/siopv2_oid4vp/implicit.rs b/oid4vc-manager/tests/siopv2_oid4vp/implicit.rs index 9dcc6cea..fa6cbecc 100644 --- a/oid4vc-manager/tests/siopv2_oid4vp/implicit.rs +++ b/oid4vc-manager/tests/siopv2_oid4vp/implicit.rs @@ -1,6 +1,7 @@ use did_key::{generate, Ed25519KeyPair}; use identity_core::common::{Object, Url}; use identity_credential::{credential::Jwt, presentation::JwtPresentation}; +use jsonwebtoken::{Algorithm, Header}; use lazy_static::lazy_static; use oid4vc_core::{jwt, Subject}; use oid4vc_manager::{ @@ -61,7 +62,6 @@ lazy_static! { .unwrap(); } -// TODO: Refactor this once the mock crate is created. #[tokio::test] async fn test_implicit_flow() { // Create a new issuer. @@ -93,6 +93,8 @@ async fn test_implicit_flow() { .and_then(TryInto::try_into) .unwrap(); + dbg!(RequestUrl::Request(Box::new(authorization_request.clone())).to_string()); + // Create a provider manager and validate the authorization request. let provider_manager = ProviderManager::new([subject]).unwrap(); let authorization_request = provider_manager @@ -136,7 +138,17 @@ async fn test_implicit_flow() { .unwrap(); // Encode the verifiable credential as a JWT. - let jwt = jwt::encode(Arc::new(issuer), &verifiable_credential).unwrap(); + let jwt = jwt::encode( + Arc::new(issuer), + Header { + alg: Algorithm::EdDSA, + ..Default::default() + }, + &verifiable_credential, + ) + .unwrap(); + + dbg!(&jwt); // Create a verifiable presentation using the JWT. let verifiable_presentation = JwtPresentation::builder(Url::parse(subject_did).unwrap(), Object::new()) diff --git a/oid4vci/Cargo.toml b/oid4vci/Cargo.toml index 64a7edaa..66e6be65 100644 --- a/oid4vci/Cargo.toml +++ b/oid4vci/Cargo.toml @@ -10,7 +10,19 @@ repository.workspace = true [dependencies] oid4vc-core = { path = "../oid4vc-core" } +dif-presentation-exchange = { path = "../dif-presentation-exchange" } getset.workspace = true serde.workspace = true serde_json.workspace = true -anyhow = "1.0" \ No newline at end of file +serde_with.workspace = true +tokio.workspace = true +anyhow = "1.0" +reqwest.workspace = true +serde_urlencoded = "0.7" +derivative = "2.2.0" +paste = "1.0" +lazy_static = "1.4" +jsonwebtoken = "8.3" + +[dev-dependencies] +wiremock = "0.5" diff --git a/oid4vci/src/authorization_details.rs b/oid4vci/src/authorization_details.rs new file mode 100644 index 00000000..70a36330 --- /dev/null +++ b/oid4vci/src/authorization_details.rs @@ -0,0 +1,311 @@ +use crate::{ + credential_format_profiles::{CredentialFormatCollection, CredentialFormats}, + serialize_unit_struct, +}; +use reqwest::Url; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +/// Represents an object of the `authorization_details` field of the `AuthorizationRequest` object in the Authorization Code Flow as +/// described in [OpenID4VCI](https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html#name-request-issuance-of-a-certa) +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct AuthorizationDetailsObject +where + CFC: CredentialFormatCollection, +{ + #[serde(rename = "type")] + pub type_: OpenIDCredential, + pub locations: Option>, + #[serde(flatten)] + pub credential_format: CFC, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct OpenIDCredential; + +serialize_unit_struct!("openid_credential", OpenIDCredential); + +#[cfg(test)] +mod tests { + use super::*; + use crate::credential_format_profiles::{ + iso_mdl::mso_mdoc::MsoMdoc, + w3c_verifiable_credentials::{ + jwt_vc_json::{self, JwtVcJson}, + ldp_vc::{self, LdpVc}, + }, + CredentialFormat, CredentialFormats, + }; + use serde::de::DeserializeOwned; + use serde_json::json; + use std::{fs::File, path::Path}; + + fn json_example(path: &str) -> T + where + T: DeserializeOwned, + { + let file_path = Path::new(path); + let file = File::open(file_path).expect("file does not exist"); + serde_json::from_reader::<_, T>(file).expect("could not parse json") + } + + #[test] + fn test_authorization_details_serde_jwt_vc_json() { + let jwt_vc_json = json!({ + "type": "openid_credential", + "format": "jwt_vc_json", + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": {}, + "last_name": {}, + "degree": {} + } + } + }); + + let authorization_details_mso_mdoc: AuthorizationDetailsObject = + serde_json::from_value(jwt_vc_json.clone()).unwrap(); + + // Assert that the json Value is deserialized into the correct type. + assert_eq!( + authorization_details_mso_mdoc, + AuthorizationDetailsObject { + type_: OpenIDCredential, + locations: None, + credential_format: CredentialFormats::JwtVcJson(CredentialFormat { + format: JwtVcJson, + parameters: ( + jwt_vc_json::CredentialDefinition { + type_: vec!["VerifiableCredential".into(), "UniversityDegreeCredential".into()], + credential_subject: Some(json!({ + "given_name": {}, + "last_name": {}, + "degree": {} + })), + }, + None + ) + .into() + }), + }, + ); + + // Assert that the `AuthorizationDetailsObject` can be serialized back into the original json Value. + assert_eq!( + serde_json::to_value(authorization_details_mso_mdoc).unwrap(), + jwt_vc_json + ); + } + + #[test] + fn test_authorization_details_serde_mso_mdoc() { + let mso_mdoc = json!({ + "type": "openid_credential", + "format": "mso_mdoc", + "doctype": "org.iso.18013.5.1.mDL", + "claims": { + "org.iso.18013.5.1": { + "given_name": {}, + "family_name": {}, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + } + }); + + let authorization_details_mso_mdoc: AuthorizationDetailsObject = + serde_json::from_value(mso_mdoc.clone()).unwrap(); + + // Assert that the json Value is deserialized into the correct type. + assert_eq!( + authorization_details_mso_mdoc, + AuthorizationDetailsObject { + type_: OpenIDCredential, + locations: None, + credential_format: CredentialFormats::MsoMdoc(CredentialFormat { + format: MsoMdoc, + parameters: ( + "org.iso.18013.5.1.mDL".to_string(), + Some(json!({ + "org.iso.18013.5.1": { + "given_name": {}, + "family_name": {}, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + })), + None + ) + .into() + }), + }, + ); + + // Assert that the `AuthorizationDetailsObject` can be serialized back into the original json Value. + assert_eq!(serde_json::to_value(authorization_details_mso_mdoc).unwrap(), mso_mdoc); + } + + #[test] + fn test_oid4vci_examples() { + // Examples from + // https://bitbucket.org/openid/connect/src/master/openid-4-verifiable-credential-issuance/examples/. + + assert_eq!( + vec![AuthorizationDetailsObject { + type_: OpenIDCredential, + locations: None, + credential_format: CredentialFormats::JwtVcJson(CredentialFormat { + format: JwtVcJson, + parameters: ( + jwt_vc_json::CredentialDefinition { + type_: vec!["VerifiableCredential".into(), "UniversityDegreeCredential".into()], + credential_subject: Some(json!({ + "given_name": {}, + "family_name": {}, + "degree": {} + })), + }, + None + ) + .into() + }), + }], + json_example::>("tests/examples/authorization_details_jwt_vc_json.json") + ); + + assert_eq!( + vec![AuthorizationDetailsObject { + type_: OpenIDCredential, + locations: None, + credential_format: CredentialFormats::LdpVc(CredentialFormat { + format: LdpVc, + parameters: ( + ldp_vc::CredentialDefinition { + context: vec![ + "https://www.w3.org/2018/credentials/v1".into(), + "https://www.w3.org/2018/credentials/examples/v1".into() + ], + type_: vec!["VerifiableCredential".into(), "UniversityDegreeCredential".into()], + credential_subject: Some(json!({ + "given_name": {}, + "family_name": {}, + "degree": {} + })), + }, + None + ) + .into() + }), + }], + json_example::>("tests/examples/authorization_details_ldp_vc.json") + ); + + assert_eq!( + vec![AuthorizationDetailsObject { + type_: OpenIDCredential, + locations: None, + credential_format: CredentialFormats::MsoMdoc(CredentialFormat { + format: MsoMdoc, + parameters: ( + "org.iso.18013.5.1.mDL".to_string(), + Some(json!({ + "org.iso.18013.5.1": { + "given_name": {}, + "family_name": {}, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + })), + None + ) + .into() + }), + }], + json_example::>("tests/examples/authorization_details_mso_mdoc.json") + ); + + assert_eq!( + vec![ + AuthorizationDetailsObject { + type_: OpenIDCredential, + locations: None, + credential_format: CredentialFormats::LdpVc(CredentialFormat { + format: LdpVc, + parameters: ( + ldp_vc::CredentialDefinition { + context: vec![ + "https://www.w3.org/2018/credentials/v1".into(), + "https://www.w3.org/2018/credentials/examples/v1".into() + ], + type_: vec!["VerifiableCredential".into(), "UniversityDegreeCredential".into()], + credential_subject: None, + }, + None + ) + .into() + }), + }, + AuthorizationDetailsObject { + type_: OpenIDCredential, + locations: None, + credential_format: CredentialFormats::MsoMdoc(CredentialFormat { + format: MsoMdoc, + parameters: ("org.iso.18013.5.1.mDL".to_string(), None, None).into() + }), + } + ], + json_example::>( + "tests/examples/authorization_details_multiple_credentials.json" + ) + ); + + assert_eq!( + vec![AuthorizationDetailsObject { + type_: OpenIDCredential, + locations: Some(vec!["https://credential-issuer.example.com".parse().unwrap()]), + credential_format: CredentialFormats::JwtVcJson(CredentialFormat { + format: JwtVcJson, + parameters: ( + jwt_vc_json::CredentialDefinition { + type_: vec!["VerifiableCredential".into(), "UniversityDegreeCredential".into()], + credential_subject: None, + }, + None + ) + .into() + }), + }], + json_example::>("tests/examples/authorization_details_with_as.json") + ); + + assert_eq!( + vec![AuthorizationDetailsObject { + type_: OpenIDCredential, + locations: None, + credential_format: CredentialFormats::JwtVcJson(CredentialFormat { + format: JwtVcJson, + parameters: ( + jwt_vc_json::CredentialDefinition { + type_: vec!["VerifiableCredential".into(), "UniversityDegreeCredential".into()], + credential_subject: None, + }, + None + ) + .into() + }), + }], + json_example::>("tests/examples/authorization_details.json") + ); + } +} diff --git a/oid4vci/src/authorization_request.rs b/oid4vci/src/authorization_request.rs new file mode 100644 index 00000000..d4eecce7 --- /dev/null +++ b/oid4vci/src/authorization_request.rs @@ -0,0 +1,20 @@ +use crate::{ + authorization_details::AuthorizationDetailsObject, credential_format_profiles::CredentialFormatCollection, +}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +/// The Authorization Request is used to request authorization as described here: https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html#name-authorization-request. +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug)] +pub struct AuthorizationRequest +where + CFC: CredentialFormatCollection, +{ + pub response_type: String, + pub client_id: String, + pub redirect_uri: Option, + pub scope: Option, + pub state: Option, + pub authorization_details: Vec>, +} diff --git a/oid4vci/src/authorization_response.rs b/oid4vci/src/authorization_response.rs new file mode 100644 index 00000000..4a0255af --- /dev/null +++ b/oid4vci/src/authorization_response.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +// TODO: Temporary solution for the Authorization Code Flow. Eventually this should be implemented as described +// here: https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html#name-successful-authorization-re +#[derive(Serialize, Deserialize, Debug)] +pub struct AuthorizationResponse { + pub code: String, + pub state: Option, +} diff --git a/oid4vci/src/credential_format_profiles/iso_mdl/mod.rs b/oid4vci/src/credential_format_profiles/iso_mdl/mod.rs new file mode 100644 index 00000000..17c943fd --- /dev/null +++ b/oid4vci/src/credential_format_profiles/iso_mdl/mod.rs @@ -0,0 +1 @@ +pub mod mso_mdoc; diff --git a/oid4vci/src/credential_format_profiles/iso_mdl/mso_mdoc.rs b/oid4vci/src/credential_format_profiles/iso_mdl/mso_mdoc.rs new file mode 100644 index 00000000..171cc857 --- /dev/null +++ b/oid4vci/src/credential_format_profiles/iso_mdl/mso_mdoc.rs @@ -0,0 +1,8 @@ +use crate::credential_format; + +credential_format!("mso_mdoc", MsoMdoc, { + doctype: String, + claims: Option, + // TODO: https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html#appendix-E.2.2-2.3 + order: Option> +}); diff --git a/oid4vci/src/credential_format_profiles/mod.rs b/oid4vci/src/credential_format_profiles/mod.rs new file mode 100644 index 00000000..917d749d --- /dev/null +++ b/oid4vci/src/credential_format_profiles/mod.rs @@ -0,0 +1,65 @@ +pub mod iso_mdl; +pub mod w3c_verifiable_credentials; + +use self::{ + iso_mdl::mso_mdoc::MsoMdoc, + w3c_verifiable_credentials::{jwt_vc_json::JwtVcJson, jwt_vc_json_ld::JwtVcJsonLd, ldp_vc::LdpVc}, +}; +use serde::{Deserialize, Serialize}; + +#[macro_export] +macro_rules! credential_format { + ($format:literal, $name:ty, {$($field_name:ident: $field_type:ty),*}) => { + paste::paste! { + #[derive(Debug, Clone, Eq, PartialEq)] + pub struct $name; + impl $crate::credential_format_profiles::Format for $name { + type Parameters = [< $name Parameters >]; + } + + #[serde_with::skip_serializing_none] + #[derive(Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq, Clone)] + pub struct [< $name Parameters >] { + $(pub $field_name: $field_type),* + } + + #[allow(unused_parens)] + impl From<($($field_type),*)> for [< $name Parameters >] { + fn from(($($field_name),*): ($($field_type),*)) -> Self { + Self { + $($field_name),* + } + } + } + + $crate::serialize_unit_struct!($format, $name); + } + }; +} + +pub trait Format: std::fmt::Debug + Serialize + Eq + PartialEq { + type Parameters: std::fmt::Debug + Serialize + for<'de> Deserialize<'de> + Eq + PartialEq + Clone; +} + +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct CredentialFormat +where + F: Format, +{ + pub format: F, + #[serde(flatten)] + pub parameters: F::Parameters, +} + +pub trait CredentialFormatCollection: Serialize + Send + Sync + Clone {} + +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +#[serde(untagged)] +pub enum CredentialFormats { + JwtVcJson(CredentialFormat), + LdpVc(CredentialFormat), + JwtVcJsonLd(CredentialFormat), + MsoMdoc(CredentialFormat), + Other(serde_json::Value), +} +impl CredentialFormatCollection for CredentialFormats {} diff --git a/oid4vci/src/credential_format_profiles/w3c_verifiable_credentials/jwt_vc_json.rs b/oid4vci/src/credential_format_profiles/w3c_verifiable_credentials/jwt_vc_json.rs new file mode 100644 index 00000000..4258f805 --- /dev/null +++ b/oid4vci/src/credential_format_profiles/w3c_verifiable_credentials/jwt_vc_json.rs @@ -0,0 +1,17 @@ +use crate::credential_format; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +credential_format!("jwt_vc_json", JwtVcJson, { + credential_definition: CredentialDefinition, + order: Option +}); + +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct CredentialDefinition { + #[serde(rename = "type")] + pub type_: Vec, + #[serde(rename = "credentialSubject")] + pub credential_subject: Option, +} diff --git a/oid4vci/src/credential_format_profiles/w3c_verifiable_credentials/jwt_vc_json_ld.rs b/oid4vci/src/credential_format_profiles/w3c_verifiable_credentials/jwt_vc_json_ld.rs new file mode 100644 index 00000000..4f37f0b1 --- /dev/null +++ b/oid4vci/src/credential_format_profiles/w3c_verifiable_credentials/jwt_vc_json_ld.rs @@ -0,0 +1,19 @@ +use crate::credential_format; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +credential_format!("jwt_vc_json-ld", JwtVcJsonLd, { + credential_definition: CredentialDefinition, + order: Option +}); + +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct CredentialDefinition { + #[serde(rename = "@context")] + pub context: Vec, + #[serde(rename = "type")] + pub type_: Vec, + #[serde(rename = "credentialSubject")] + pub credential_subject: Option, +} diff --git a/oid4vci/src/credential_format_profiles/w3c_verifiable_credentials/ldp_vc.rs b/oid4vci/src/credential_format_profiles/w3c_verifiable_credentials/ldp_vc.rs new file mode 100644 index 00000000..a322968c --- /dev/null +++ b/oid4vci/src/credential_format_profiles/w3c_verifiable_credentials/ldp_vc.rs @@ -0,0 +1,19 @@ +use crate::credential_format; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +credential_format!("ldp_vc", LdpVc, { + credential_definition: CredentialDefinition, + order: Option +}); + +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct CredentialDefinition { + #[serde(rename = "@context")] + pub context: Vec, + #[serde(rename = "type")] + pub type_: Vec, + #[serde(rename = "credentialSubject")] + pub credential_subject: Option, +} diff --git a/oid4vci/src/credential_format_profiles/w3c_verifiable_credentials/mod.rs b/oid4vci/src/credential_format_profiles/w3c_verifiable_credentials/mod.rs new file mode 100644 index 00000000..066bd49b --- /dev/null +++ b/oid4vci/src/credential_format_profiles/w3c_verifiable_credentials/mod.rs @@ -0,0 +1,3 @@ +pub mod jwt_vc_json; +pub mod jwt_vc_json_ld; +pub mod ldp_vc; diff --git a/oid4vci/src/credential_issuer/authorization_server_metadata.rs b/oid4vci/src/credential_issuer/authorization_server_metadata.rs new file mode 100644 index 00000000..350793d6 --- /dev/null +++ b/oid4vci/src/credential_issuer/authorization_server_metadata.rs @@ -0,0 +1,38 @@ +use derivative::{self, Derivative}; +use reqwest::Url; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +// Authorization Server Metadata as described here: https://www.rfc-editor.org/rfc/rfc8414.html#section-2 +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize, Derivative)] +#[derivative(Default)] +pub struct AuthorizationServerMetadata { + // TODO: Temporary solution + #[derivative(Default(value = "Url::parse(\"https://example.com\").unwrap()"))] + pub issuer: Url, + #[derivative(Default(value = "Url::parse(\"https://example.com\").unwrap()"))] + pub authorization_endpoint: Url, + #[derivative(Default(value = "Url::parse(\"https://example.com\").unwrap()"))] + pub token_endpoint: Url, + pub jwks_uri: Option, + pub registration_endpoint: Option, + pub scopes_supported: Option>, + pub response_types_supported: Option>, + pub response_modes_supported: Option>, + pub grant_types_supported: Option>, + pub token_endpoint_auth_methods_supported: Option>, + pub token_endpoint_auth_signing_alg_values_supported: Option>, + pub service_documentation: Option, + pub ui_locales_supported: Option>, + pub op_policy_uri: Option, + pub op_tos_uri: Option, + pub revocation_endpoint: Option, + pub revocation_endpoint_auth_methods_supported: Option>, + pub revocation_endpoint_auth_signing_alg_values_supported: Option>, + pub introspection_endpoint: Option, + pub introspection_endpoint_auth_methods_supported: Option>, + pub introspection_endpoint_auth_signing_alg_values_supported: Option>, + pub code_challenge_methods_supported: Option>, + // Additional authorization server metadata parameters MAY also be used. +} diff --git a/oid4vci/src/credential_issuer/credential_issuer_metadata.rs b/oid4vci/src/credential_issuer/credential_issuer_metadata.rs new file mode 100644 index 00000000..2b7a68a2 --- /dev/null +++ b/oid4vci/src/credential_issuer/credential_issuer_metadata.rs @@ -0,0 +1,308 @@ +use super::credentials_supported::CredentialsSupportedObject; +use crate::credential_format_profiles::CredentialFormatCollection; +use reqwest::Url; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use serde_with::skip_serializing_none; + +/// Credential Issuer Metadata as described here: +/// https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata. +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct CredentialIssuerMetadata +where + CFC: CredentialFormatCollection, +{ + pub credential_issuer: Url, + pub authorization_server: Option, + pub credential_endpoint: Url, + pub batch_credential_endpoint: Option, + pub deferred_credential_endpoint: Option, + pub credentials_supported: Vec>, + pub display: Option>, +} + +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug)] +pub struct CredentialsSupportedDisplay { + name: String, + locale: Option, + logo: Option, + description: Option, + background_color: Option, + text_color: Option, + #[serde(flatten)] + other: Option>, +} + +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug)] +pub struct Logo { + url: Option, + alt_text: Option, + #[serde(flatten)] + other: Option>, +} + +#[cfg(test)] +mod tests { + use crate::credential_format_profiles::{ + iso_mdl::mso_mdoc::MsoMdoc, + w3c_verifiable_credentials::{ + jwt_vc_json::{self, JwtVcJson}, + ldp_vc::{self, LdpVc}, + }, + CredentialFormat, CredentialFormats, + }; + + use super::*; + use serde::de::DeserializeOwned; + use serde_json::json; + use std::{fs::File, path::Path}; + + fn json_example(path: &str) -> T + where + T: DeserializeOwned, + { + let file_path = Path::new(path); + let file = File::open(file_path).expect("file does not exist"); + serde_json::from_reader::<_, T>(file).expect("could not parse json") + } + + #[test] + fn test_oid4vci_examples() { + // Examples from + // https://bitbucket.org/openid/connect/src/master/openid-4-verifiable-credential-issuance/examples/. + + assert_eq!( + CredentialIssuerMetadata { + credential_endpoint: Url::parse("https://server.example.com/credential").unwrap(), + credentials_supported: vec![ + CredentialsSupportedObject { + id: Some("UniversityDegree_LDP".to_string()), + credential_format: CredentialFormats::LdpVc(CredentialFormat { + format: LdpVc, + parameters: ( + ldp_vc::CredentialDefinition { + context: vec![ + "https://www.w3.org/2018/credentials/v1".to_string(), + "https://www.w3.org/2018/credentials/examples/v1".to_string(), + ], + type_: vec![ + "VerifiableCredential".to_string(), + "UniversityDegreeCredential".to_string() + ], + credential_subject: Some(json!({ + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + }, + { + "name": "名前", + "locale": "ja-JP" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "degree": {}, + "gpa": { + "display": [ + { + "name": "GPA" + } + ] + } + })) + }, + None + ) + .into() + }), + scope: None, + cryptographic_binding_methods_supported: Some(vec!["did".to_string()]), + cryptographic_suites_supported: Some(vec!["Ed25519Signature2018".to_string()]), + proof_types_supported: None, + display: Some(vec![ + json!({ + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alternative_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + }), + json!({ + "name": "在籍証明書", + "locale": "ja-JP", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alternative_text": "大学のロゴ" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + }) + ]), + }, + CredentialsSupportedObject { + id: None, + credential_format: CredentialFormats::JwtVcJson(CredentialFormat { + format: JwtVcJson, + parameters: ( + jwt_vc_json::CredentialDefinition { + type_: vec![ + "VerifiableCredential".to_string(), + "UniversityDegreeCredential".to_string() + ], + credential_subject: Some(json!({ + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + }, + { + "name": "名前", + "locale": "ja-JP" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "degree": {}, + "gpa": { + "display": [ + { + "name": "GPA" + } + ] + } + })) + }, + None + ) + .into() + }), + scope: None, + cryptographic_binding_methods_supported: Some(vec!["did".to_string()]), + cryptographic_suites_supported: Some(vec!["ES256K".to_string()]), + proof_types_supported: None, + display: Some(vec![ + json!({ + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alternative_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + }), + json!({ + "name": "在籍証明書", + "locale": "ja-JP", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alternative_text": "大学のロゴ" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + }) + ]), + }, + CredentialsSupportedObject { + id: None, + credential_format: CredentialFormats::MsoMdoc(CredentialFormat { + format: MsoMdoc, + parameters: ( + "org.iso.18013.5.1.mDL".to_string(), + Some(json!({ + "org.iso.18013.5.1": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + }, + { + "name": "名前", + "locale": "ja-JP" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + })), + None + ) + .into() + }), + scope: None, + cryptographic_binding_methods_supported: Some(vec!["mso".to_string()]), + cryptographic_suites_supported: Some(vec![ + "ES256".to_string(), + "ES384".to_string(), + "ES512".to_string() + ]), + proof_types_supported: None, + display: Some(vec![ + json!({ + "name": "Mobile Driving License", + "locale": "en-US", + "logo": { + "url": "https://examplestate.com/public/mdl.png", + "alternative_text": "a square figure of a mobile driving license" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + }), + json!({ + "name": "在籍証明書", + "locale": "ja-JP", + "logo": { + "url": "https://examplestate.com/public/mdl.png", + "alternative_text": "大学のロゴ" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + }) + ]), + } + ], + credential_issuer: "https://server.example.com".parse().unwrap(), + authorization_server: None, + batch_credential_endpoint: None, + deferred_credential_endpoint: None, + display: None, + }, + json_example::>("tests/examples/issuer_metadata.json") + ); + } +} diff --git a/oid4vci/src/credential_issuer/credentials_supported.rs b/oid4vci/src/credential_issuer/credentials_supported.rs new file mode 100644 index 00000000..7f4154e0 --- /dev/null +++ b/oid4vci/src/credential_issuer/credentials_supported.rs @@ -0,0 +1,251 @@ +use crate::{ + credential_format_profiles::{CredentialFormatCollection, CredentialFormats}, + ProofType, +}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +/// Credentials Supported object as described here: https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html#name-objects-comprising-credenti. +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct CredentialsSupportedObject +where + CFC: CredentialFormatCollection, +{ + pub id: Option, + #[serde(flatten)] + pub credential_format: CFC, + pub scope: Option, + pub cryptographic_binding_methods_supported: Option>, + pub cryptographic_suites_supported: Option>, + pub proof_types_supported: Option>, + pub display: Option>, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::credential_format_profiles::{ + iso_mdl::mso_mdoc::MsoMdoc, + w3c_verifiable_credentials::{ + jwt_vc_json::{self, JwtVcJson}, + ldp_vc::{self, LdpVc}, + }, + CredentialFormat, CredentialFormats, + }; + use serde::de::DeserializeOwned; + use serde_json::json; + use std::{fs::File, path::Path}; + + fn json_example(path: &str) -> T + where + T: DeserializeOwned, + { + let file_path = Path::new(path); + let file = File::open(file_path).expect("file does not exist"); + serde_json::from_reader::<_, T>(file).expect("could not parse json") + } + + #[test] + fn test_oid4vci_examples() { + // Examples from + // https://bitbucket.org/openid/connect/src/master/openid-4-verifiable-credential-issuance/examples/. + + assert_eq!( + CredentialsSupportedObject { + id: Some("UniversityDegree_JWT".to_string()), + credential_format: CredentialFormats::JwtVcJson(CredentialFormat { + format: JwtVcJson, + parameters: ( + jwt_vc_json::CredentialDefinition { + type_: vec![ + "VerifiableCredential".to_string(), + "UniversityDegreeCredential".to_string() + ], + credential_subject: Some(json!({ + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "degree": {}, + "gpa": { + "display": [ + { + "name": "GPA" + } + ] + } + })), + }, + None + ) + .into() + }), + scope: None, + cryptographic_binding_methods_supported: Some(vec!["did:example".to_string()]), + cryptographic_suites_supported: Some(vec!["ES256K".to_string()]), + proof_types_supported: Some(vec![ProofType::Jwt]), + display: Some(vec![json!({ + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alt_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + })]) + }, + json_example::("tests/examples/credential_metadata_jwt_vc_json.json") + ); + + assert_eq!( + CredentialsSupportedObject { + id: None, + credential_format: CredentialFormats::LdpVc(CredentialFormat { + format: LdpVc, + parameters: ( + ldp_vc::CredentialDefinition { + context: vec![ + "https://www.w3.org/2018/credentials/v1".to_string(), + "https://www.w3.org/2018/credentials/examples/v1".to_string() + ], + type_: vec![ + "VerifiableCredential".to_string(), + "UniversityDegreeCredential".to_string() + ], + credential_subject: Some(json!({ + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "degree": {}, + "gpa": { + "display": [ + { + "name": "GPA" + } + ] + } + })), + }, + None + ) + .into() + }), + scope: None, + cryptographic_binding_methods_supported: Some(vec!["did:example".to_string()]), + cryptographic_suites_supported: Some(vec!["Ed25519Signature2018".to_string()]), + proof_types_supported: None, + display: Some(vec![json!({ + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alt_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + })]) + }, + json_example::("tests/examples/credential_metadata_ldp_vc.json") + ); + + assert_eq!( + CredentialsSupportedObject { + id: None, + credential_format: CredentialFormats::MsoMdoc(CredentialFormat { + format: MsoMdoc, + parameters: ( + "org.iso.18013.5.1.mDL".to_string(), + Some(json!({ + "org.iso.18013.5.1": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + }, + { + "name": "名前", + "locale": "ja-JP" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + })), + None + ) + .into() + }), + scope: None, + cryptographic_binding_methods_supported: Some(vec!["mso".to_string()]), + cryptographic_suites_supported: Some(vec![ + "ES256".to_string(), + "ES384".to_string(), + "ES512".to_string() + ]), + proof_types_supported: None, + display: Some(vec![ + json!({ + "name": "Mobile Driving License", + "locale": "en-US", + "logo": { + "url": "https://examplestate.com/public/mdl.png", + "alt_text": "a square figure of a mobile driving license" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + }), + json!({ + "name": "在籍証明書", + "locale": "ja-JP", + "logo": { + "url": "https://examplestate.com/public/mdl.png", + "alt_text": "大学のロゴ" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + }) + ]) + }, + json_example::("tests/examples/credential_metadata_mso_mdoc.json") + ); + } +} diff --git a/oid4vci/src/credential_issuer/mod.rs b/oid4vci/src/credential_issuer/mod.rs new file mode 100644 index 00000000..ac6facfa --- /dev/null +++ b/oid4vci/src/credential_issuer/mod.rs @@ -0,0 +1,28 @@ +pub mod authorization_server_metadata; +pub mod credential_issuer_metadata; +pub mod credentials_supported; + +use self::{ + authorization_server_metadata::AuthorizationServerMetadata, credential_issuer_metadata::CredentialIssuerMetadata, +}; +use crate::{credential_format_profiles::CredentialFormatCollection, proof::ProofOfPossession, Proof}; +use oid4vc_core::{authentication::subject::SigningSubject, Decoder}; + +#[derive(Clone)] +pub struct CredentialIssuer +where + CFC: CredentialFormatCollection, +{ + pub subject: SigningSubject, + pub metadata: CredentialIssuerMetadata, + pub authorization_server_metadata: AuthorizationServerMetadata, +} + +impl CredentialIssuer { + pub async fn validate_proof(&self, proof: Proof, decoder: Decoder) -> anyhow::Result { + match proof { + Proof::Jwt { jwt, .. } => decoder.decode(jwt).await, + Proof::Cwt { .. } => unimplemented!("CWT is not supported yet"), + } + } +} diff --git a/oid4vci/src/credential_offer.rs b/oid4vci/src/credential_offer.rs new file mode 100644 index 00000000..de3506c1 --- /dev/null +++ b/oid4vci/src/credential_offer.rs @@ -0,0 +1,324 @@ +use crate::credential_format_profiles::{CredentialFormatCollection, CredentialFormats}; +use anyhow::Result; +use oid4vc_core::to_query_value; +use reqwest::Url; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::{Map, Value}; +use serde_with::skip_serializing_none; + +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] +pub struct AuthorizationCode { + pub issuer_state: Option, +} + +#[skip_serializing_none] +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone, Default)] +pub struct PreAuthorizedCode { + #[serde(rename = "pre-authorized_code")] + pub pre_authorized_code: String, + #[serde(default)] + pub user_pin_required: bool, + pub interval: Option, +} + +/// Credential Offer as described in https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer. +#[skip_serializing_none] +#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)] +pub struct CredentialOffer +where + CFC: CredentialFormatCollection, +{ + pub credential_issuer: Url, + pub credentials: Vec>, + pub grants: Option, +} + +#[derive(Deserialize, Serialize, Debug, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum CredentialOfferQuery +where + CFC: CredentialFormatCollection, +{ + CredentialOfferUri(Url), + CredentialOffer(CredentialOffer), +} + +impl std::str::FromStr for CredentialOfferQuery { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let map: Map = s + .parse::()? + .query_pairs() + .map(|(key, value)| { + let value = serde_json::from_str::(&value).unwrap_or(Value::String(value.into_owned())); + Ok((key.into_owned(), value)) + }) + .collect::>()?; + serde_json::from_value(Value::Object(map)).map_err(Into::into) + } +} + +impl std::fmt::Display for CredentialOfferQuery { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CredentialOfferQuery::CredentialOfferUri(url) => write!(f, "{}", url), + CredentialOfferQuery::CredentialOffer(offer) => { + let mut url = Url::parse("openid-credential-offer://").map_err(|_| std::fmt::Error)?; + url.query_pairs_mut() + .append_pair("credential_offer", &to_query_value(offer).map_err(|_| std::fmt::Error)?); + write!(f, "{}", url) + } + } + } +} + +#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)] +#[serde(untagged)] +pub enum CredentialsObject +where + CFC: CredentialFormatCollection, +{ + ByReference(String), + ByValue(CFC), +} + +/// Grants as described in https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer-parameters. +#[skip_serializing_none] +#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone, Default)] +pub struct Grants { + pub authorization_code: Option, + #[serde(rename = "urn:ietf:params:oauth:grant-type:pre-authorized_code")] + pub pre_authorized_code: Option, +} + +#[cfg(test)] +mod tests { + use std::{fs::File, path::Path}; + + use super::*; + use crate::credential_format_profiles::{ + iso_mdl::mso_mdoc::MsoMdoc, + w3c_verifiable_credentials::{ + jwt_vc_json::{self, JwtVcJson}, + ldp_vc::{self, LdpVc}, + }, + CredentialFormat, CredentialFormats, + }; + use serde_json::json; + + fn json_example(path: &str) -> T + where + T: DeserializeOwned, + { + let file_path = Path::new(path); + let file = File::open(file_path).expect("file does not exist"); + serde_json::from_reader::<_, T>(file).expect("could not parse json") + } + + #[test] + fn test_credential_offer_serde() { + let json = json!({ + "credential_issuer": "https://credential-issuer.example.com/", + "credentials": [ + "UniversityDegree_JWT", + { + "format": "mso_mdoc", + "doctype": "org.iso.18013.5.1.mDL" + } + ], + "grants": { + "authorization_code": { + "issuer_state": "eyJhbGciOiJSU0Et...FYUaBy" + }, + "urn:ietf:params:oauth:grant-type:pre-authorized_code": { + "pre-authorized_code": "adhjhdjajkdkhjhdj", + "user_pin_required": true + } + } + }); + + let credential_offer: CredentialOffer = serde_json::from_value(json.clone()).unwrap(); + + // Assert that the json Value is deserialized into the correct type. + assert_eq!( + credential_offer, + CredentialOffer { + credential_issuer: "https://credential-issuer.example.com".parse().unwrap(), + credentials: vec![ + CredentialsObject::ByReference("UniversityDegree_JWT".to_string()), + CredentialsObject::ByValue(CredentialFormats::MsoMdoc(CredentialFormat { + format: MsoMdoc, + parameters: ("org.iso.18013.5.1.mDL".to_string(), None, None).into() + })) + ], + grants: Some(Grants { + pre_authorized_code: Some(PreAuthorizedCode { + pre_authorized_code: "adhjhdjajkdkhjhdj".to_string(), + user_pin_required: true, + ..Default::default() + }), + authorization_code: Some(AuthorizationCode { + issuer_state: Some("eyJhbGciOiJSU0Et...FYUaBy".to_string()) + }) + }) + } + ); + + // Assert that the `CredentialOffer` can be serialized back into the original json Value. + assert_eq!(serde_json::to_value(credential_offer).unwrap(), json); + } + + #[test] + fn test_oid4vci_examples() { + // Examples from + // https://bitbucket.org/openid/connect/src/master/openid-4-verifiable-credential-issuance/examples/. + + assert_eq!( + CredentialOffer { + credential_issuer: "https://credential-issuer.example.com".parse().unwrap(), + credentials: vec![CredentialsObject::ByReference("UniversityDegree_LDP".to_string()),], + grants: Some(Grants { + authorization_code: None, + pre_authorized_code: Some(PreAuthorizedCode { + pre_authorized_code: "adhjhdjajkdkhjhdj".to_string(), + user_pin_required: true, + ..Default::default() + }) + }) + }, + json_example::("tests/examples/credential_offer_by_reference.json") + ); + + assert_eq!( + CredentialOffer { + credential_issuer: "https://credential-issuer.example.com".parse().unwrap(), + credentials: vec![CredentialsObject::ByValue(CredentialFormats::JwtVcJson( + CredentialFormat { + format: JwtVcJson, + parameters: ( + jwt_vc_json::CredentialDefinition { + type_: vec![ + "VerifiableCredential".to_string(), + "UniversityDegreeCredential".to_string() + ], + credential_subject: None + }, + None + ) + .into() + } + )),], + grants: Some(Grants { + authorization_code: Some(AuthorizationCode { + issuer_state: Some("eyJhbGciOiJSU0Et...FYUaBy".to_string()) + }), + pre_authorized_code: None + }) + }, + json_example::("tests/examples/credential_offer_jwt_vc_json.json") + ); + + assert_eq!( + CredentialOffer { + credential_issuer: "https://credential-issuer.example.com".parse().unwrap(), + credentials: vec![CredentialsObject::ByValue(CredentialFormats::LdpVc(CredentialFormat { + format: LdpVc, + parameters: ( + ldp_vc::CredentialDefinition { + context: vec![ + "https://www.w3.org/2018/credentials/v1".to_string(), + "https://www.w3.org/2018/credentials/examples/v1".to_string() + ], + type_: vec![ + "VerifiableCredential".to_string(), + "UniversityDegreeCredential".to_string() + ], + credential_subject: None + }, + None + ) + .into() + })),], + grants: None + }, + json_example::("tests/examples/credential_offer_ldp_vc.json") + ); + + assert_eq!( + CredentialOffer { + credential_issuer: "https://credential-issuer.example.com".parse().unwrap(), + credentials: vec![CredentialsObject::ByValue(CredentialFormats::MsoMdoc( + CredentialFormat { + format: MsoMdoc, + parameters: ("org.iso.18013.5.1.mDL".to_string(), None, None).into() + } + )),], + grants: Some(Grants { + authorization_code: None, + pre_authorized_code: Some(PreAuthorizedCode { + pre_authorized_code: "adhjhdjajkdkhjhdj".to_string(), + user_pin_required: true, + ..Default::default() + }) + }) + }, + json_example::("tests/examples/credential_offer_mso_mdoc.json") + ); + + assert_eq!( + CredentialOffer { + credential_issuer: "https://credential-issuer.example.com".parse().unwrap(), + credentials: vec![ + CredentialsObject::ByReference("UniversityDegree_JWT".to_string()), + CredentialsObject::ByValue(CredentialFormats::MsoMdoc(CredentialFormat { + format: MsoMdoc, + parameters: ("org.iso.18013.5.1.mDL".to_string(), None, None).into() + })), + ], + grants: Some(Grants { + authorization_code: Some(AuthorizationCode { + issuer_state: Some("eyJhbGciOiJSU0Et...FYUaBy".to_string()) + }), + pre_authorized_code: Some(PreAuthorizedCode { + pre_authorized_code: "adhjhdjajkdkhjhdj".to_string(), + user_pin_required: true, + ..Default::default() + }) + }) + }, + json_example::("tests/examples/credential_offer_multiple_credentials.json") + ); + + assert_eq!( + CredentialOffer { + credential_issuer: "https://credential-issuer.example.com".parse().unwrap(), + credentials: vec![CredentialsObject::ByValue(CredentialFormats::JwtVcJson( + CredentialFormat { + format: JwtVcJson, + parameters: ( + jwt_vc_json::CredentialDefinition { + type_: vec![ + "VerifiableCredential".to_string(), + "UniversityDegreeCredential".to_string() + ], + credential_subject: None + }, + None + ) + .into() + } + )),], + grants: Some(Grants { + authorization_code: None, + pre_authorized_code: Some(PreAuthorizedCode { + pre_authorized_code: "adhjhdjajkdkhjhdj".to_string(), + user_pin_required: true, + ..Default::default() + }) + }) + }, + json_example::("tests/examples/credential_offer_pre-authz_code.json") + ); + } +} diff --git a/oid4vci/src/credential_request.rs b/oid4vci/src/credential_request.rs new file mode 100644 index 00000000..403bb274 --- /dev/null +++ b/oid4vci/src/credential_request.rs @@ -0,0 +1,330 @@ +use crate::{ + credential_format_profiles::{CredentialFormatCollection, CredentialFormats}, + proof::Proof, +}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +/// Credential Request as described here: https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html#name-credential-request +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct CredentialRequest +where + CFC: CredentialFormatCollection, +{ + #[serde(flatten)] + pub credential_format: CFC, + pub proof: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + credential_format_profiles::{ + iso_mdl::mso_mdoc::MsoMdoc, + w3c_verifiable_credentials::{ + jwt_vc_json::{self, CredentialDefinition, JwtVcJson}, + jwt_vc_json_ld::{self, JwtVcJsonLd}, + ldp_vc::{self, LdpVc}, + }, + CredentialFormat, CredentialFormats, + }, + Jwt, + }; + use serde::de::DeserializeOwned; + use serde_json::json; + use std::{fs::File, path::Path}; + + fn json_example(path: &str) -> T + where + T: DeserializeOwned, + { + let file_path = Path::new(path); + let file = File::open(file_path).expect("file does not exist"); + serde_json::from_reader::<_, T>(file).expect("could not parse json") + } + + #[test] + fn test_credential_request_serde_jwt_vc_json() { + let jwt_vc_json = json!({ + "format": "jwt_vc_json", + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": {}, + "family_name": {}, + "degree": {} + } + }, + "proof": { + "proof_type": "jwt", + "jwt":"eyJraWQiOiJkaWQ6ZXhhbXBsZ...KPxgihac0aW9EkL1nOzM" + } + }); + + let credential_request_jwt_vc_json: CredentialRequest = serde_json::from_value(jwt_vc_json.clone()).unwrap(); + + // Assert that the json Value is deserialized into the correct type. + assert_eq!( + credential_request_jwt_vc_json, + CredentialRequest { + credential_format: CredentialFormats::JwtVcJson(CredentialFormat { + format: JwtVcJson, + parameters: ( + CredentialDefinition { + type_: vec![ + "VerifiableCredential".to_string(), + "UniversityDegreeCredential".to_string() + ], + credential_subject: Some(json!({ + "given_name": {}, + "family_name": {}, + "degree": {} + })), + }, + None + ) + .into() + }), + proof: Some(Proof::Jwt { + proof_type: Jwt, + jwt: "eyJraWQiOiJkaWQ6ZXhhbXBsZ...KPxgihac0aW9EkL1nOzM".to_string() + }) + }, + ); + + // Assert that the `CredentialRequest` can be serialized back into the original json Value. + assert_eq!( + serde_json::to_value(credential_request_jwt_vc_json).unwrap(), + jwt_vc_json + ); + } + + #[test] + fn test_credential_request_serde_mso_mdoc() { + let mso_mdoc = json!({ + "format": "mso_mdoc", + "doctype": "org.iso.18013.5.1.mDL", + "claims": { + "org.iso.18013.5.1": { + "given_name": {}, + "family_name": {}, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + }, + "proof": { + "proof_type": "jwt", + "jwt": "eyJraWQiOiJkaWQ6ZXhhbXBsZ...KPxgihac0aW9EkL1nOzM" + } + }); + + let credential_request_mso_mdoc: CredentialRequest = serde_json::from_value(mso_mdoc.clone()).unwrap(); + + // Assert that the json Value is deserialized into the correct type. + assert_eq!( + credential_request_mso_mdoc, + CredentialRequest { + credential_format: CredentialFormats::MsoMdoc(CredentialFormat { + format: MsoMdoc, + parameters: ( + "org.iso.18013.5.1.mDL".to_string(), + Some(json!({ + "org.iso.18013.5.1": { + "given_name": {}, + "family_name": {}, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + })), + None + ) + .into() + }), + proof: Some(Proof::Jwt { + proof_type: Jwt, + jwt: "eyJraWQiOiJkaWQ6ZXhhbXBsZ...KPxgihac0aW9EkL1nOzM".to_string() + }) + }, + ); + + // Assert that the `CredentialRequest` can be serialized back into the original json Value. + assert_eq!(serde_json::to_value(credential_request_mso_mdoc).unwrap(), mso_mdoc); + } + + #[test] + fn test_oid4vci_examples() { + // Examples from + // https://bitbucket.org/openid/connect/src/master/openid-4-verifiable-credential-issuance/examples/. + + assert_eq!( + CredentialRequest { + credential_format: CredentialFormats::MsoMdoc(CredentialFormat { + format: MsoMdoc, + parameters: ("org.iso.18013.5.1.mDL".to_string(), None, None).into() + }), + proof: Some(Proof::Jwt { + proof_type: Jwt, + jwt: "eyJraWQiOiJkaWQ6ZXhhbXBsZ...KPxgihac0aW9EkL1nOzM".to_string() + }) + }, + json_example::("tests/examples/credential_request_iso_mdl.json") + ); + + assert_eq!( + CredentialRequest { + credential_format: CredentialFormats::MsoMdoc(CredentialFormat { + format: MsoMdoc, + parameters: ( + "org.iso.18013.5.1.mDL".to_string(), + Some(json!({ + "org.iso.18013.5.1": { + "given_name": {}, + "family_name": {}, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + })), + None + ) + .into() + }), + proof: Some(Proof::Jwt { + proof_type: Jwt, + jwt: "eyJraWQiOiJkaWQ6ZXhhbXBsZ...KPxgihac0aW9EkL1nOzM".to_string() + }) + }, + json_example::("tests/examples/credential_request_iso_mdl_with_claims.json") + ); + + assert_eq!( + CredentialRequest { + credential_format: CredentialFormats::JwtVcJsonLd(CredentialFormat { + format: JwtVcJsonLd, + parameters: ( + jwt_vc_json_ld::CredentialDefinition { + context: vec![ + "https://www.w3.org/2018/credentials/v1".to_string(), + "https://www.w3.org/2018/credentials/examples/v1".to_string() + ], + type_: vec![ + "VerifiableCredential".to_string(), + "UniversityDegreeCredential".to_string() + ], + credential_subject: Some(json!({ + "degree": { + "type":{} + } + })), + }, + None + ) + .into() + }), + proof: Some(Proof::Jwt { + proof_type: Jwt, + jwt: "eyJraWQiOiJkaWQ6ZXhhbXBsZ...KPxgihac0aW9EkL1nOzM".to_string() + }) + }, + json_example::("tests/examples/credential_request_jwt_vc_json-ld.json") + ); + + assert_eq!( + CredentialRequest { + credential_format: CredentialFormats::JwtVcJson(CredentialFormat { + format: JwtVcJson, + parameters: ( + jwt_vc_json::CredentialDefinition { + type_: vec![ + "VerifiableCredential".to_string(), + "UniversityDegreeCredential".to_string() + ], + credential_subject: None + }, + None + ) + .into() + }), + proof: Some(Proof::Jwt { + proof_type: Jwt, + jwt: "eyJraWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEva2V5cy8xIiwiYWxnIjoiRVMyNTYiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJzNkJoZFJrcXQzIiwiYXVkIjoiaHR0cHM6Ly9zZXJ2ZXIuZXhhbXBsZS5jb20iLCJpYXQiOiIyMDE4LTA5LTE0VDIxOjE5OjEwWiIsIm5vbmNlIjoidFppZ25zbkZicCJ9.ewdkIkPV50iOeBUqMXCC_aZKPxgihac0aW9EkL1nOzM".to_string() + }) + }, + json_example::( + "tests/examples/credential_request_jwt_vc_json.json" + ) + ); + + assert_eq!( + CredentialRequest { + credential_format: CredentialFormats::JwtVcJson(CredentialFormat { + format: JwtVcJson, + parameters: ( + jwt_vc_json::CredentialDefinition { + type_: vec![ + "VerifiableCredential".to_string(), + "UniversityDegreeCredential".to_string() + ], + credential_subject: Some(json!({ + "given_name": {}, + "family_name": {}, + "degree": {} + })) + }, + None + ) + .into() + }), + proof: Some(Proof::Jwt { + proof_type: Jwt, + jwt: "eyJraWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEva2V5cy8xIiwiYWxnIjoiRVMyNTYiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJzNkJoZFJrcXQzIiwiYXVkIjoiaHR0cHM6Ly9zZXJ2ZXIuZXhhbXBsZS5jb20iLCJpYXQiOiIyMDE4LTA5LTE0VDIxOjE5OjEwWiIsIm5vbmNlIjoidFppZ25zbkZicCJ9.ewdkIkPV50iOeBUqMXCC_aZKPxgihac0aW9EkL1nOzM".to_string() + }) + }, + json_example::( + "tests/examples/credential_request_jwt_vc_json_with_claims.json" + ) + ); + + assert_eq!( + CredentialRequest { + credential_format: CredentialFormats::LdpVc(CredentialFormat { + format: LdpVc, + parameters: ( + ldp_vc::CredentialDefinition { + context: vec![ + "https://www.w3.org/2018/credentials/v1".to_string(), + "https://www.w3.org/2018/credentials/examples/v1".to_string() + ], + type_: vec![ + "VerifiableCredential".to_string(), + "UniversityDegreeCredential".to_string() + ], + credential_subject: Some(json!({ + "degree": { + "type": {} + } + })) + }, + None + ) + .into() + }), + proof: Some(Proof::Jwt { + proof_type: Jwt, + jwt: "eyJraWQiOiJkaWQ6ZXhhbXBsZ...KPxgihac0aW9EkL1nOzM".to_string() + }) + }, + json_example::("tests/examples/credential_request_ldp_vc.json") + ); + } +} diff --git a/oid4vci/src/credential_response.rs b/oid4vci/src/credential_response.rs new file mode 100644 index 00000000..7d3b99ff --- /dev/null +++ b/oid4vci/src/credential_response.rs @@ -0,0 +1,12 @@ +use dif_presentation_exchange::ClaimFormatDesignation; +use serde::{Deserialize, Serialize}; + +/// Credential Response as described here: https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html#name-credential-response. +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct CredentialResponse { + pub format: ClaimFormatDesignation, + pub credential: Option, + pub transaction_id: Option, + pub c_nonce: Option, + pub c_nonce_expires_in: Option, +} diff --git a/oid4vci/src/lib.rs b/oid4vci/src/lib.rs index 4f33803e..d7646316 100644 --- a/oid4vci/src/lib.rs +++ b/oid4vci/src/lib.rs @@ -1,3 +1,65 @@ +pub mod authorization_details; +pub mod authorization_request; +pub mod authorization_response; pub mod credential; +pub mod credential_format_profiles; +pub mod credential_issuer; +pub mod credential_offer; +pub mod credential_request; +pub mod credential_response; +pub mod proof; +pub mod token_request; +pub mod token_response; +pub mod wallet; pub use credential::{VerifiableCredentialJwt, VerifiableCredentialJwtBuilder}; +pub use proof::{Cwt, Jwt, Proof, ProofType}; +pub use wallet::Wallet; + +// macro that generates a serialize/deserialize implementation for a unit struct. +#[macro_export] +macro_rules! serialize_unit_struct { + ($format:literal, $name:ident) => { + impl serde::Serialize for $name { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str($format) + } + } + + impl<'de> serde::Deserialize<'de> for $name { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = $name; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str($format) + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + if value == $format { + Ok($name) + } else { + Err(serde::de::Error::custom(format!( + "expected {}, found {}", + $format, value + ))) + } + } + } + + deserializer.deserialize_str(Visitor) + } + } + }; +} diff --git a/oid4vci/src/proof.rs b/oid4vci/src/proof.rs new file mode 100644 index 00000000..8cfaec1e --- /dev/null +++ b/oid4vci/src/proof.rs @@ -0,0 +1,91 @@ +use crate::serialize_unit_struct; +use jsonwebtoken::{Algorithm, Header}; +use oid4vc_core::{builder_fn, jwt, RFC7519Claims, Subject}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +#[derive(Debug, PartialEq, Eq)] +pub struct Jwt; + +#[derive(Debug, PartialEq, Eq)] +pub struct Cwt; + +serialize_unit_struct!("jwt", Jwt); +serialize_unit_struct!("cwt", Cwt); + +/// Key Proof Type (JWT or CWT) and the proof itself, as described here: https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html#name-key-proof-types. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(untagged)] +pub enum Proof { + Jwt { proof_type: Jwt, jwt: String }, + Cwt { proof_type: Cwt, cwt: String }, +} + +impl Proof { + pub fn builder() -> ProofBuilder { + ProofBuilder::default() + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum ProofType { + Jwt, + Cwt, +} + +#[derive(Default)] +pub struct ProofBuilder { + proof_type: Option, + rfc7519_claims: RFC7519Claims, + nonce: Option, + signer: Option>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ProofOfPossession { + #[serde(flatten)] + pub rfc7519_claims: RFC7519Claims, + pub nonce: String, +} + +impl ProofBuilder { + pub fn build(self) -> anyhow::Result { + anyhow::ensure!(self.rfc7519_claims.aud.is_some(), "aud claim is required"); + anyhow::ensure!(self.rfc7519_claims.iat.is_some(), "iat claim is required"); + anyhow::ensure!(self.nonce.is_some(), "nonce claim is required"); + + match self.proof_type { + Some(ProofType::Jwt) => Ok(Proof::Jwt { + proof_type: Jwt, + jwt: jwt::encode( + self.signer.ok_or(anyhow::anyhow!("No subject found"))?.clone(), + Header { + alg: Algorithm::EdDSA, + typ: Some("openid4vci-proof+jwt".to_string()), + ..Default::default() + }, + ProofOfPossession { + rfc7519_claims: self.rfc7519_claims, + nonce: self.nonce.ok_or(anyhow::anyhow!("No nonce found"))?, + }, + )?, + }), + Some(ProofType::Cwt) => todo!(), + None => Err(anyhow::anyhow!("proof_type is required")), + } + } + + pub fn signer(mut self, signer: Arc) -> Self { + self.signer = Some(signer); + self + } + + builder_fn!(proof_type, ProofType); + builder_fn!(rfc7519_claims, iss, String); + builder_fn!(rfc7519_claims, aud, String); + // TODO: fix this, required by jsonwebtoken crate. + builder_fn!(rfc7519_claims, exp, i64); + builder_fn!(rfc7519_claims, iat, i64); + builder_fn!(nonce, String); +} diff --git a/oid4vci/src/token_request.rs b/oid4vci/src/token_request.rs new file mode 100644 index 00000000..899c8beb --- /dev/null +++ b/oid4vci/src/token_request.rs @@ -0,0 +1,72 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use crate::serialize_unit_struct; + +#[derive(Debug, PartialEq)] +pub struct PreAuthorizedCode; +serialize_unit_struct!( + "urn:ietf:params:oauth:grant-type:pre-authorized_code", + PreAuthorizedCode +); + +#[derive(Debug, PartialEq)] +pub struct AuthorizationCode; +serialize_unit_struct!("authorization_code", AuthorizationCode); + +/// Token Request as described here: https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html#name-token-request. +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(untagged)] +pub enum TokenRequest { + AuthorizationCode { + grant_type: AuthorizationCode, + code: String, + code_verifier: Option, + redirect_uri: Option, + }, + PreAuthorizedCode { + grant_type: PreAuthorizedCode, + #[serde(rename = "pre-authorized_code")] + pre_authorized_code: String, + user_pin: Option, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_token_request_serde() { + assert_eq!( + serde_urlencoded::from_str::( + "grant_type=authorization_code\ + &code=SplxlOBeZQQYbYS6WxSbIA\ + &code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk\ + &redirect_uri=https%3A%2F%2FWallet.example.org%2Fcb", + ) + .unwrap(), + TokenRequest::AuthorizationCode { + grant_type: AuthorizationCode, + code: "SplxlOBeZQQYbYS6WxSbIA".to_string(), + code_verifier: Some("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk".to_string()), + redirect_uri: Some("https://Wallet.example.org/cb".to_string()), + } + ); + + assert_eq!( + serde_urlencoded::from_str::( + "grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code\ + &pre-authorized_code=SplxlOBeZQQYbYS6WxSbIA\ + &user_pin=493536" + ) + .unwrap(), + TokenRequest::PreAuthorizedCode { + grant_type: PreAuthorizedCode, + pre_authorized_code: "SplxlOBeZQQYbYS6WxSbIA".to_string(), + user_pin: Some("493536".to_string()) + } + ); + } +} diff --git a/oid4vci/src/token_response.rs b/oid4vci/src/token_response.rs new file mode 100644 index 00000000..af37ac7f --- /dev/null +++ b/oid4vci/src/token_response.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +/// Token Response as described here: https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html#name-successful-token-response. +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct TokenResponse { + pub access_token: String, + pub token_type: String, + pub expires_in: Option, + pub refresh_token: Option, + pub scope: Option, + pub c_nonce: Option, + pub c_nonce_expires_in: Option, +} diff --git a/oid4vci/src/wallet/mod.rs b/oid4vci/src/wallet/mod.rs new file mode 100644 index 00000000..61b5341f --- /dev/null +++ b/oid4vci/src/wallet/mod.rs @@ -0,0 +1,132 @@ +use crate::authorization_details::AuthorizationDetailsObject; +use crate::authorization_request::AuthorizationRequest; +use crate::authorization_response::AuthorizationResponse; +use crate::credential_format_profiles::{CredentialFormatCollection, CredentialFormats}; +use crate::credential_issuer::{ + authorization_server_metadata::AuthorizationServerMetadata, credential_issuer_metadata::CredentialIssuerMetadata, +}; +use crate::credential_request::CredentialRequest; +use crate::proof::{Proof, ProofType}; +use crate::{credential_response::CredentialResponse, token_request::TokenRequest, token_response::TokenResponse}; +use anyhow::Result; +use oid4vc_core::authentication::subject::SigningSubject; +use reqwest::Url; +use serde::de::DeserializeOwned; + +pub struct Wallet +where + CFC: CredentialFormatCollection + DeserializeOwned, +{ + pub subject: SigningSubject, + pub client: reqwest::Client, + phantom: std::marker::PhantomData, +} + +impl Wallet { + pub fn new(subject: SigningSubject) -> Self { + Self { + subject, + client: reqwest::Client::new(), + phantom: std::marker::PhantomData, + } + } + + pub async fn get_authorization_server_metadata( + &self, + credential_issuer_url: Url, + ) -> Result { + self.client + .get(credential_issuer_url.join(".well-known/oauth-authorization-server")?) + .send() + .await? + .json::() + .await + .map_err(|_| anyhow::anyhow!("Failed to get authorization server metadata")) + } + + pub async fn get_credential_issuer_metadata( + &self, + credential_issuer_url: Url, + ) -> Result> { + self.client + .get(credential_issuer_url.join(".well-known/openid-credential-issuer")?) + .send() + .await? + .json::>() + .await + .map_err(|_| anyhow::anyhow!("Failed to get credential issuer metadata")) + } + + pub async fn get_authorization_code( + &self, + authorization_endpoint: Url, + authorization_details: Vec>, + ) -> Result { + self.client + .get(authorization_endpoint) + // TODO: must be `form`, but `AuthorizationRequest needs to be able to serilalize properly. + .json(&AuthorizationRequest { + response_type: "code".to_string(), + client_id: self.subject.identifier()?, + redirect_uri: None, + scope: None, + state: None, + authorization_details, + }) + .send() + .await? + .json::() + .await + .map_err(|_| anyhow::anyhow!("Failed to get authorization code")) + } + + pub async fn get_access_token(&self, token_endpoint: Url, token_request: TokenRequest) -> Result { + self.client + .post(token_endpoint) + .form(&token_request) + .send() + .await? + .json() + .await + .map_err(|e| e.into()) + } + + pub async fn get_credential( + &self, + credential_issuer_metadata: CredentialIssuerMetadata, + token_response: &TokenResponse, + credential_format: CFC, + ) -> Result { + let credential_request = CredentialRequest { + credential_format, + proof: Some( + Proof::builder() + .proof_type(ProofType::Jwt) + .signer(self.subject.clone()) + .iss(self.subject.identifier()?) + .aud(credential_issuer_metadata.credential_issuer) + .iat(1571324800) + .exp(9999999999i64) + // TODO: so is this REQUIRED or OPTIONAL? + .nonce( + token_response + .c_nonce + .as_ref() + .ok_or(anyhow::anyhow!("No c_nonce found."))? + .clone(), + ) + .build()?, + ), + }; + + self.client + .post(credential_issuer_metadata.credential_endpoint) + .bearer_auth(token_response.access_token.clone()) + .json(&credential_request) + .send() + .await? + .json() + .await + .map_err(|e| e.into()) + } +} diff --git a/oid4vci/tests/examples/authorization_details.json b/oid4vci/tests/examples/authorization_details.json new file mode 100644 index 00000000..e6b47cef --- /dev/null +++ b/oid4vci/tests/examples/authorization_details.json @@ -0,0 +1,12 @@ +[ + { + "type": "openid_credential", + "format": "jwt_vc_json", + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ] + } + } +] diff --git a/oid4vci/tests/examples/authorization_details_jwt_vc_json.json b/oid4vci/tests/examples/authorization_details_jwt_vc_json.json new file mode 100644 index 00000000..54b0975e --- /dev/null +++ b/oid4vci/tests/examples/authorization_details_jwt_vc_json.json @@ -0,0 +1,17 @@ +[ + { + "type": "openid_credential", + "format": "jwt_vc_json", + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": {}, + "family_name": {}, + "degree": {} + } + } + } +] diff --git a/oid4vci/tests/examples/authorization_details_ldp_vc.json b/oid4vci/tests/examples/authorization_details_ldp_vc.json new file mode 100644 index 00000000..af20a402 --- /dev/null +++ b/oid4vci/tests/examples/authorization_details_ldp_vc.json @@ -0,0 +1,21 @@ +[ + { + "type": "openid_credential", + "format": "ldp_vc", + "credential_definition": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": {}, + "family_name": {}, + "degree": {} + } + } + } +] diff --git a/oid4vci/tests/examples/authorization_details_mso_mdoc.json b/oid4vci/tests/examples/authorization_details_mso_mdoc.json new file mode 100644 index 00000000..ea4a24fb --- /dev/null +++ b/oid4vci/tests/examples/authorization_details_mso_mdoc.json @@ -0,0 +1,17 @@ +[ + { + "type": "openid_credential", + "format": "mso_mdoc", + "doctype": "org.iso.18013.5.1.mDL", + "claims": { + "org.iso.18013.5.1": { + "given_name": {}, + "family_name": {}, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + } + } +] diff --git a/oid4vci/tests/examples/authorization_details_multiple_credentials.json b/oid4vci/tests/examples/authorization_details_multiple_credentials.json new file mode 100644 index 00000000..0f259b5b --- /dev/null +++ b/oid4vci/tests/examples/authorization_details_multiple_credentials.json @@ -0,0 +1,21 @@ +[ + { + "type":"openid_credential", + "format": "ldp_vc", + "credential_definition": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ] + } + }, + { + "type":"openid_credential", + "format": "mso_mdoc", + "doctype":"org.iso.18013.5.1.mDL" + } +] diff --git a/oid4vci/tests/examples/authorization_details_with_as.json b/oid4vci/tests/examples/authorization_details_with_as.json new file mode 100644 index 00000000..d31083df --- /dev/null +++ b/oid4vci/tests/examples/authorization_details_with_as.json @@ -0,0 +1,15 @@ +[ + { + "type": "openid_credential", + "locations": [ + "https://credential-issuer.example.com" + ], + "format": "jwt_vc_json", + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ] + } + } +] diff --git a/oid4vci/tests/examples/credential_metadata_jwt_vc_json.json b/oid4vci/tests/examples/credential_metadata_jwt_vc_json.json new file mode 100644 index 00000000..56b8638b --- /dev/null +++ b/oid4vci/tests/examples/credential_metadata_jwt_vc_json.json @@ -0,0 +1,57 @@ +{ + "format": "jwt_vc_json", + "id": "UniversityDegree_JWT", + "cryptographic_binding_methods_supported": [ + "did:example" + ], + "cryptographic_suites_supported": [ + "ES256K" + ], + "credential_definition":{ + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "degree": {}, + "gpa": { + "display": [ + { + "name": "GPA" + } + ] + } + } + }, + "proof_types_supported": [ + "jwt" + ], + "display": [ + { + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alt_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ] +} diff --git a/oid4vci/tests/examples/credential_metadata_ldp_vc.json b/oid4vci/tests/examples/credential_metadata_ldp_vc.json new file mode 100644 index 00000000..9bdd0159 --- /dev/null +++ b/oid4vci/tests/examples/credential_metadata_ldp_vc.json @@ -0,0 +1,65 @@ +{ + "format": "ldp_vc", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "cryptographic_binding_methods_supported": [ + "did:example" + ], + "cryptographic_suites_supported": [ + "Ed25519Signature2018" + ], + "credential_definition": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "degree": {}, + "gpa": { + "display": [ + { + "name": "GPA" + } + ] + } + } + }, + "display": [ + { + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alt_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ] +} diff --git a/oid4vci/tests/examples/credential_metadata_mso_mdoc.json b/oid4vci/tests/examples/credential_metadata_mso_mdoc.json new file mode 100644 index 00000000..732a4a49 --- /dev/null +++ b/oid4vci/tests/examples/credential_metadata_mso_mdoc.json @@ -0,0 +1,60 @@ +{ + "format": "mso_mdoc", + "doctype": "org.iso.18013.5.1.mDL", + "cryptographic_binding_methods_supported": [ + "mso" + ], + "cryptographic_suites_supported": [ + "ES256", "ES384", "ES512" + ], + "display": [ + { + "name": "Mobile Driving License", + "locale": "en-US", + "logo": { + "url": "https://examplestate.com/public/mdl.png", + "alt_text": "a square figure of a mobile driving license" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + }, + { + "name": "在籍証明書", + "locale": "ja-JP", + "logo": { + "url": "https://examplestate.com/public/mdl.png", + "alt_text": "大学のロゴ" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "claims": { + "org.iso.18013.5.1": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + }, + { + "name": "名前", + "locale": "ja-JP" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + } +} diff --git a/oid4vci/tests/examples/credential_offer_by_reference.json b/oid4vci/tests/examples/credential_offer_by_reference.json new file mode 100644 index 00000000..09f6f16d --- /dev/null +++ b/oid4vci/tests/examples/credential_offer_by_reference.json @@ -0,0 +1,12 @@ +{ + "credential_issuer": "https://credential-issuer.example.com", + "credentials": [ + "UniversityDegree_LDP" + ], + "grants": { + "urn:ietf:params:oauth:grant-type:pre-authorized_code": { + "pre-authorized_code": "adhjhdjajkdkhjhdj", + "user_pin_required": true + } + } +} diff --git a/oid4vci/tests/examples/credential_offer_jwt_vc_json.json b/oid4vci/tests/examples/credential_offer_jwt_vc_json.json new file mode 100644 index 00000000..57050ef4 --- /dev/null +++ b/oid4vci/tests/examples/credential_offer_jwt_vc_json.json @@ -0,0 +1,19 @@ +{ + "credential_issuer": "https://credential-issuer.example.com", + "credentials": [ + { + "format": "jwt_vc_json", + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ] + } + } + ], + "grants": { + "authorization_code": { + "issuer_state": "eyJhbGciOiJSU0Et...FYUaBy" + } + } +} diff --git a/oid4vci/tests/examples/credential_offer_ldp_vc.json b/oid4vci/tests/examples/credential_offer_ldp_vc.json new file mode 100644 index 00000000..01b154ee --- /dev/null +++ b/oid4vci/tests/examples/credential_offer_ldp_vc.json @@ -0,0 +1,18 @@ +{ + "credential_issuer": "https://credential-issuer.example.com", + "credentials": [ + { + "format": "ldp_vc", + "credential_definition": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ] + } + } + ] +} diff --git a/oid4vci/tests/examples/credential_offer_mso_mdoc.json b/oid4vci/tests/examples/credential_offer_mso_mdoc.json new file mode 100644 index 00000000..fc75a197 --- /dev/null +++ b/oid4vci/tests/examples/credential_offer_mso_mdoc.json @@ -0,0 +1,15 @@ +{ + "credential_issuer": "https://credential-issuer.example.com", + "credentials": [ + { + "format": "mso_mdoc", + "doctype": "org.iso.18013.5.1.mDL" + } + ], + "grants": { + "urn:ietf:params:oauth:grant-type:pre-authorized_code": { + "pre-authorized_code": "adhjhdjajkdkhjhdj", + "user_pin_required": true + } + } +} diff --git a/oid4vci/tests/examples/credential_offer_multiple_credentials.json b/oid4vci/tests/examples/credential_offer_multiple_credentials.json new file mode 100644 index 00000000..cc9d0ac0 --- /dev/null +++ b/oid4vci/tests/examples/credential_offer_multiple_credentials.json @@ -0,0 +1,19 @@ +{ + "credential_issuer": "https://credential-issuer.example.com", + "credentials": [ + "UniversityDegree_JWT", + { + "format": "mso_mdoc", + "doctype": "org.iso.18013.5.1.mDL" + } + ], + "grants": { + "authorization_code": { + "issuer_state": "eyJhbGciOiJSU0Et...FYUaBy" + }, + "urn:ietf:params:oauth:grant-type:pre-authorized_code": { + "pre-authorized_code": "adhjhdjajkdkhjhdj", + "user_pin_required": true + } + } +} diff --git a/oid4vci/tests/examples/credential_offer_pre-authz_code.json b/oid4vci/tests/examples/credential_offer_pre-authz_code.json new file mode 100644 index 00000000..d8564a92 --- /dev/null +++ b/oid4vci/tests/examples/credential_offer_pre-authz_code.json @@ -0,0 +1,20 @@ +{ + "credential_issuer": "https://credential-issuer.example.com", + "credentials": [ + { + "format": "jwt_vc_json", + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ] + } + } + ], + "grants": { + "urn:ietf:params:oauth:grant-type:pre-authorized_code": { + "pre-authorized_code": "adhjhdjajkdkhjhdj", + "user_pin_required": true + } + } +} diff --git a/oid4vci/tests/examples/credential_request_iso_mdl.json b/oid4vci/tests/examples/credential_request_iso_mdl.json new file mode 100644 index 00000000..25bf45cd --- /dev/null +++ b/oid4vci/tests/examples/credential_request_iso_mdl.json @@ -0,0 +1,8 @@ +{ + "format": "mso_mdoc", + "doctype": "org.iso.18013.5.1.mDL", + "proof": { + "proof_type": "jwt", + "jwt": "eyJraWQiOiJkaWQ6ZXhhbXBsZ...KPxgihac0aW9EkL1nOzM" + } +} diff --git a/oid4vci/tests/examples/credential_request_iso_mdl_with_claims.json b/oid4vci/tests/examples/credential_request_iso_mdl_with_claims.json new file mode 100644 index 00000000..6c63868a --- /dev/null +++ b/oid4vci/tests/examples/credential_request_iso_mdl_with_claims.json @@ -0,0 +1,18 @@ +{ + "format": "mso_mdoc", + "doctype": "org.iso.18013.5.1.mDL", + "claims": { + "org.iso.18013.5.1": { + "given_name": {}, + "family_name": {}, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + }, + "proof": { + "proof_type": "jwt", + "jwt": "eyJraWQiOiJkaWQ6ZXhhbXBsZ...KPxgihac0aW9EkL1nOzM" + } +} diff --git a/oid4vci/tests/examples/credential_request_jwt_vc_json-ld.json b/oid4vci/tests/examples/credential_request_jwt_vc_json-ld.json new file mode 100644 index 00000000..3b27084e --- /dev/null +++ b/oid4vci/tests/examples/credential_request_jwt_vc_json-ld.json @@ -0,0 +1,22 @@ +{ + "format": "jwt_vc_json-ld", + "credential_definition": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject":{ + "degree":{ + "type":{} + } + } + }, + "proof": { + "proof_type": "jwt", + "jwt": "eyJraWQiOiJkaWQ6ZXhhbXBsZ...KPxgihac0aW9EkL1nOzM" + } +} diff --git a/oid4vci/tests/examples/credential_request_jwt_vc_json.json b/oid4vci/tests/examples/credential_request_jwt_vc_json.json new file mode 100644 index 00000000..3811f6e3 --- /dev/null +++ b/oid4vci/tests/examples/credential_request_jwt_vc_json.json @@ -0,0 +1,13 @@ +{ + "format": "jwt_vc_json", + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ] + }, + "proof": { + "proof_type": "jwt", + "jwt":"eyJraWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEva2V5cy8xIiwiYWxnIjoiRVMyNTYiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJzNkJoZFJrcXQzIiwiYXVkIjoiaHR0cHM6Ly9zZXJ2ZXIuZXhhbXBsZS5jb20iLCJpYXQiOiIyMDE4LTA5LTE0VDIxOjE5OjEwWiIsIm5vbmNlIjoidFppZ25zbkZicCJ9.ewdkIkPV50iOeBUqMXCC_aZKPxgihac0aW9EkL1nOzM" + } +} diff --git a/oid4vci/tests/examples/credential_request_jwt_vc_json_with_claims.json b/oid4vci/tests/examples/credential_request_jwt_vc_json_with_claims.json new file mode 100644 index 00000000..779b2575 --- /dev/null +++ b/oid4vci/tests/examples/credential_request_jwt_vc_json_with_claims.json @@ -0,0 +1,18 @@ +{ + "format": "jwt_vc_json", + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": {}, + "family_name": {}, + "degree": {} + } + }, + "proof": { + "proof_type": "jwt", + "jwt":"eyJraWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEva2V5cy8xIiwiYWxnIjoiRVMyNTYiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJzNkJoZFJrcXQzIiwiYXVkIjoiaHR0cHM6Ly9zZXJ2ZXIuZXhhbXBsZS5jb20iLCJpYXQiOiIyMDE4LTA5LTE0VDIxOjE5OjEwWiIsIm5vbmNlIjoidFppZ25zbkZicCJ9.ewdkIkPV50iOeBUqMXCC_aZKPxgihac0aW9EkL1nOzM" + } +} diff --git a/oid4vci/tests/examples/credential_request_ldp_vc.json b/oid4vci/tests/examples/credential_request_ldp_vc.json new file mode 100644 index 00000000..2dd26522 --- /dev/null +++ b/oid4vci/tests/examples/credential_request_ldp_vc.json @@ -0,0 +1,22 @@ +{ + "format": "ldp_vc", + "credential_definition": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "degree": { + "type": {} + } + } + }, + "proof": { + "proof_type": "jwt", + "jwt": "eyJraWQiOiJkaWQ6ZXhhbXBsZ...KPxgihac0aW9EkL1nOzM" + } +} diff --git a/oid4vci/tests/examples/issuer_metadata.json b/oid4vci/tests/examples/issuer_metadata.json new file mode 100644 index 00000000..cb1b157c --- /dev/null +++ b/oid4vci/tests/examples/issuer_metadata.json @@ -0,0 +1,205 @@ +{ + "credential_endpoint": "https://server.example.com/credential", + "credentials_supported": [ + { + "id":"UniversityDegree_LDP", + "format": "ldp_vc", + "cryptographic_binding_methods_supported": [ + "did" + ], + "cryptographic_suites_supported": [ + "Ed25519Signature2018" + ], + "credential_definition": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + }, + { + "name": "名前", + "locale": "ja-JP" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "degree": {}, + "gpa": { + "display": [ + { + "name": "GPA" + } + ] + } + } + }, + "display": [ + { + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alternative_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + }, + { + "name": "在籍証明書", + "locale": "ja-JP", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alternative_text": "大学のロゴ" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ] + }, + { + "format": "jwt_vc_json", + "cryptographic_binding_methods_supported": [ + "did" + ], + "cryptographic_suites_supported": [ + "ES256K" + ], + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + }, + { + "name": "名前", + "locale": "ja-JP" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "degree": {}, + "gpa": { + "display": [ + { + "name": "GPA" + } + ] + } + } + }, + "display": [ + { + "name": "University Credential", + "locale": "en-US", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alternative_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + }, + { + "name": "在籍証明書", + "locale": "ja-JP", + "logo": { + "url": "https://exampleuniversity.com/public/logo.png", + "alternative_text": "大学のロゴ" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ] + }, + { + "format": "mso_mdoc", + "doctype": "org.iso.18013.5.1.mDL", + "cryptographic_binding_methods_supported": [ + "mso" + ], + "cryptographic_suites_supported": [ + "ES256", "ES384", "ES512" + ], + "display": [ + { + "name": "Mobile Driving License", + "locale": "en-US", + "logo": { + "url": "https://examplestate.com/public/mdl.png", + "alternative_text": "a square figure of a mobile driving license" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + }, + { + "name": "在籍証明書", + "locale": "ja-JP", + "logo": { + "url": "https://examplestate.com/public/mdl.png", + "alternative_text": "大学のロゴ" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "claims": { + "org.iso.18013.5.1": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + }, + { + "name": "名前", + "locale": "ja-JP" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + } + } + ], + "credential_issuer": "https://server.example.com" +} diff --git a/siopv2/Cargo.toml b/siopv2/Cargo.toml index 2d504500..3db1c97e 100644 --- a/siopv2/Cargo.toml +++ b/siopv2/Cargo.toml @@ -20,7 +20,7 @@ getset.workspace = true anyhow = "1.0.70" chrono = "0.4.24" jsonwebtoken = "8.2.0" -reqwest = { version = "0.11.14", default-features = false, features = ["json", "rustls-tls"] } +reqwest.workspace = true base64-url = "2.0.0" async-trait = "0.1.68" did_url = "0.1.0" diff --git a/siopv2/src/provider.rs b/siopv2/src/provider.rs index 7f1bf2b6..ca410b30 100644 --- a/siopv2/src/provider.rs +++ b/siopv2/src/provider.rs @@ -4,11 +4,9 @@ use crate::{ use anyhow::Result; use chrono::{Duration, Utc}; use identity_credential::presentation::JwtPresentation; -use oid4vc_core::{jwt, Decoder, Subject}; +use jsonwebtoken::{Algorithm, Header}; +use oid4vc_core::{authentication::subject::SigningSubject, jwt, Decoder}; use oid4vp::{token::vp_token::VpToken, PresentationSubmission}; -use std::sync::Arc; - -pub type SigningSubject = Arc; /// A Self-Issued OpenID Provider (SIOP), which is responsible for generating and signing [`IdToken`]'s in response to /// [`AuthorizationRequest`]'s from [crate::relying_party::RelyingParty]'s (RPs). The [`Provider`] acts as a trusted intermediary between the RPs and @@ -76,7 +74,7 @@ impl Provider { .claims(user_claims) .build()?; - let jwt = jwt::encode(self.subject.clone(), id_token)?; + let jwt = jwt::encode(self.subject.clone(), Header::new(Algorithm::EdDSA), id_token)?; builder = builder.id_token(jwt); } ResponseType::IdTokenVpToken => { @@ -90,7 +88,7 @@ impl Provider { .claims(user_claims) .build()?; - let jwt = jwt::encode(self.subject.clone(), id_token)?; + let jwt = jwt::encode(self.subject.clone(), Header::new(Algorithm::EdDSA), id_token)?; builder = builder.id_token(jwt); if let (Some(verifiable_presentation), Some(presentation_submission)) = @@ -106,7 +104,7 @@ impl Provider { .verifiable_presentation(verifiable_presentation) .build()?; - let jwt = jwt::encode(self.subject.clone(), vp_token)?; + let jwt = jwt::encode(self.subject.clone(), Header::new(Algorithm::EdDSA), vp_token)?; builder = builder.vp_token(jwt).presentation_submission(presentation_submission); } else { anyhow::bail!("Verifiable presentation is required for this response type."); @@ -131,8 +129,8 @@ impl Provider { mod tests { use super::*; use crate::test_utils::TestSubject; - use oid4vc_core::{SubjectSyntaxType, Validator, Validators}; - use std::str::FromStr; + use oid4vc_core::{Subject, SubjectSyntaxType, Validator, Validators}; + use std::{str::FromStr, sync::Arc}; #[tokio::test] async fn test_provider() { diff --git a/siopv2/src/relying_party.rs b/siopv2/src/relying_party.rs index 2ccc57fe..5053985b 100644 --- a/siopv2/src/relying_party.rs +++ b/siopv2/src/relying_party.rs @@ -1,6 +1,7 @@ -use crate::{provider::SigningSubject, AuthorizationRequest, AuthorizationResponse, IdToken}; +use crate::{AuthorizationRequest, AuthorizationResponse, IdToken}; use anyhow::Result; -use oid4vc_core::{jwt, Decoder}; +use jsonwebtoken::{Algorithm, Header}; +use oid4vc_core::{authentication::subject::SigningSubject, jwt, Decoder}; use oid4vci::VerifiableCredentialJwt; use std::collections::HashMap; @@ -21,7 +22,7 @@ impl RelyingParty { } pub fn encode(&self, request: &AuthorizationRequest) -> Result { - jwt::encode(self.subject.clone(), request) + jwt::encode(self.subject.clone(), Header::new(Algorithm::EdDSA), request) } /// Validates a [`AuthorizationResponse`] by decoding the header of the id_token, fetching the public key corresponding to