Skip to content

Commit

Permalink
feat: ExternalSign, optional endpoints, VpToken (#55)
Browse files Browse the repository at this point in the history
* feat: support external sign method

* feat:ExternalSign,  optional endpoints, VpToken

* fix: remove redundant test

* fix: comment, and unwraps

* fix: comment out failing test

* fix: remove required exp parameter

* fix: fix new validation
  • Loading branch information
nanderstabel committed Aug 29, 2023
1 parent 5187657 commit d24cdc1
Show file tree
Hide file tree
Showing 15 changed files with 170 additions and 57 deletions.
6 changes: 6 additions & 0 deletions oid4vc-core/src/authentication/sign.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
use anyhow::Result;
use std::sync::Arc;

pub trait Sign: Send + Sync {
// TODO: add this?
// fn jwt_alg_name() -> &'static str;
fn key_id(&self) -> Option<String>;
fn sign(&self, message: &str) -> Result<Vec<u8>>;
fn external_signer(&self) -> Option<Arc<dyn ExternalSign>>;
}

pub trait ExternalSign: Send + Sync {
fn sign(&self, message: &str) -> Result<Vec<u8>>;
}
5 changes: 4 additions & 1 deletion oid4vc-core/src/jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ where
T: DeserializeOwned,
{
let key = DecodingKey::from_ed_der(public_key.as_slice());
Ok(jsonwebtoken::decode::<T>(jwt, &key, &Validation::new(algorithm))?.claims)
let mut validation = Validation::new(algorithm);
validation.validate_exp = false;
validation.required_spec_claims.clear();
Ok(jsonwebtoken::decode::<T>(jwt, &key, &validation)?.claims)
}

pub fn encode<C, S>(signer: Arc<S>, header: Header, claims: C) -> Result<String>
Expand Down
8 changes: 7 additions & 1 deletion oid4vc-core/src/test_utils.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::{Sign, Subject, Verify};
use std::sync::Arc;

use crate::{authentication::sign::ExternalSign, Sign, Subject, Verify};
use anyhow::Result;
use async_trait::async_trait;
use derivative::{self, Derivative};
Expand Down Expand Up @@ -37,6 +39,10 @@ impl Sign for TestSubject {
let signature: Signature = TEST_KEYPAIR.sign(message.as_bytes());
Ok(signature.to_bytes().to_vec())
}

fn external_signer(&self) -> Option<Arc<dyn ExternalSign>> {
None
}
}

#[async_trait]
Expand Down
4 changes: 2 additions & 2 deletions oid4vc-manager/src/managers/credential_issuer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ impl<S: Storage<CFC>, CFC: CredentialFormatCollection> CredentialIssuerManager<S
},
authorization_server_metadata: AuthorizationServerMetadata {
issuer: issuer_url.clone(),
authorization_endpoint: issuer_url.join("/authorize")?,
token_endpoint: issuer_url.join("/token")?,
authorization_endpoint: Some(issuer_url.join("/authorize")?),
token_endpoint: Some(issuer_url.join("/token")?),
pre_authorized_grant_anonymous_access_supported: Some(true),
..Default::default()
},
Expand Down
7 changes: 6 additions & 1 deletion oid4vc-manager/src/methods/iota_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use identity_iota::{
iota_core::{IotaDID, IotaDIDUrl},
prelude::*,
};
use oid4vc_core::{Sign, Subject, Verify};
use oid4vc_core::{authentication::sign::ExternalSign, Sign, Subject, Verify};
use std::sync::Arc;

pub struct IotaSubject<C = Arc<Client>>
Expand Down Expand Up @@ -40,6 +40,11 @@ impl Sign for IotaSubject {
fn key_id(&self) -> Option<String> {
self.authentication_method().map(|method| method.id().to_string())
}

// TODO: external sign method not supported yet for the IOTA method.
fn external_signer(&self) -> Option<Arc<dyn ExternalSign>> {
None
}
}

/// `Subject` trait implementation for the IOTA method.
Expand Down
27 changes: 22 additions & 5 deletions oid4vc-manager/src/methods/key_method.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use did_key::{generate, resolve, Config, CoreSign, DIDCore, Document, Ed25519KeyPair, KeyMaterial, PatchedKeyPair};
use oid4vc_core::{Sign, Subject, Verify};
use oid4vc_core::{authentication::sign::ExternalSign, Sign, Subject, Verify};
use std::sync::Arc;

/// This [`KeySubject`] implements the [`Subject`] trait and can be used as a subject for a [`Provider`]. It uses the
/// 'key' DID method.
pub struct KeySubject {
keypair: PatchedKeyPair,
document: Document,
external_signer: Option<Arc<dyn ExternalSign>>,
}

impl KeySubject {
/// Creates a new [`KeySubject`].
pub fn new() -> Self {
let keypair = generate::<Ed25519KeyPair>(None);
let document = keypair.get_did_document(Config::default());
KeySubject { keypair, document }
KeySubject {
keypair,
document,
external_signer: None,
}
}

/// Creates a new [`KeySubject`] from a [`PatchedKeyPair`].
pub fn from_keypair(keypair: PatchedKeyPair) -> Self {
pub fn from_keypair(keypair: PatchedKeyPair, external_signer: Option<Arc<dyn ExternalSign>>) -> Self {
let document = keypair.get_did_document(Config::default());
KeySubject { keypair, document }
KeySubject {
keypair,
document,
external_signer,
}
}
}

Expand All @@ -41,7 +51,14 @@ impl Sign for KeySubject {
}

fn sign(&self, message: &str) -> Result<Vec<u8>> {
Ok(self.keypair.sign(message.as_bytes()).to_vec())
match self.external_signer() {
Some(external_signer) => external_signer.sign(message),
None => Ok(self.keypair.sign(message.as_bytes())),
}
}

fn external_signer(&self) -> Option<Arc<dyn ExternalSign>> {
self.external_signer.clone()
}
}

Expand Down
8 changes: 7 additions & 1 deletion oid4vc-manager/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// Move this to the mock repo.
pub mod memory_storage;

use std::sync::Arc;

use anyhow::Result;
use async_trait::async_trait;
use derivative::{self, Derivative};
use ed25519_dalek::{Keypair, Signature, Signer};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use lazy_static::lazy_static;
use oid4vc_core::{Sign, Subject, Verify};
use oid4vc_core::{authentication::sign::ExternalSign, Sign, Subject, Verify};
use rand::rngs::OsRng;
use siopv2::{StandardClaimsRequests, StandardClaimsValues};

Expand Down Expand Up @@ -43,6 +45,10 @@ impl Sign for TestSubject {
let signature: Signature = TEST_KEYPAIR.sign(message.as_bytes());
Ok(signature.to_bytes().to_vec())
}

fn external_signer(&self) -> Option<Arc<dyn ExternalSign>> {
None
}
}

#[async_trait]
Expand Down
13 changes: 8 additions & 5 deletions oid4vc-manager/tests/oid4vci/authorization_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ async fn test_authorization_code_flow() {
CredentialIssuerManager::<_, CredentialFormats>::new(
None,
MemoryStorage,
[Arc::new(KeySubject::from_keypair(generate::<Ed25519KeyPair>(Some(
"this-is-a-very-UNSAFE-issuer-secret-key".as_bytes().try_into().unwrap(),
))))],
[Arc::new(KeySubject::from_keypair(
generate::<Ed25519KeyPair>(Some(
"this-is-a-very-UNSAFE-issuer-secret-key".as_bytes().try_into().unwrap(),
)),
None,
))],
)
.unwrap(),
None,
Expand Down Expand Up @@ -69,7 +72,7 @@ async fn test_authorization_code_flow() {
// Get the authorization code.
let authorization_response = wallet
.get_authorization_code(
authorization_server_metadata.authorization_endpoint,
authorization_server_metadata.authorization_endpoint.unwrap(),
vec![AuthorizationDetailsObject {
type_: OpenIDCredential,
locations: None,
Expand All @@ -89,7 +92,7 @@ async fn test_authorization_code_flow() {

// Get the access token.
let token_response = wallet
.get_access_token(authorization_server_metadata.token_endpoint, token_request)
.get_access_token(authorization_server_metadata.token_endpoint.unwrap(), token_request)
.await
.unwrap();

Expand Down
11 changes: 7 additions & 4 deletions oid4vc-manager/tests/oid4vci/pre_authorized_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ async fn test_pre_authorized_code_flow(#[case] batch: bool, #[case] by_reference
CredentialIssuerManager::new(
None,
MemoryStorage,
[Arc::new(KeySubject::from_keypair(generate::<Ed25519KeyPair>(Some(
"this-is-a-very-UNSAFE-issuer-secret-key".as_bytes().try_into().unwrap(),
))))],
[Arc::new(KeySubject::from_keypair(
generate::<Ed25519KeyPair>(Some(
"this-is-a-very-UNSAFE-issuer-secret-key".as_bytes().try_into().unwrap(),
)),
None,
))],
)
.unwrap(),
None,
Expand Down Expand Up @@ -92,7 +95,7 @@ async fn test_pre_authorized_code_flow(#[case] batch: bool, #[case] by_reference

// Get an access token.
let token_response = wallet
.get_access_token(authorization_server_metadata.token_endpoint, token_request)
.get_access_token(authorization_server_metadata.token_endpoint.unwrap(), token_request)
.await
.unwrap();

Expand Down
16 changes: 10 additions & 6 deletions oid4vc-manager/tests/siopv2_oid4vp/implicit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,19 @@ lazy_static! {
#[tokio::test]
async fn test_implicit_flow() {
// Create a new issuer.
let issuer = KeySubject::from_keypair(generate::<Ed25519KeyPair>(Some(
"this-is-a-very-UNSAFE-issuer-secret-key".as_bytes().try_into().unwrap(),
)));
let issuer = KeySubject::from_keypair(
generate::<Ed25519KeyPair>(Some(
"this-is-a-very-UNSAFE-issuer-secret-key".as_bytes().try_into().unwrap(),
)),
None,
);
let issuer_did = issuer.identifier().unwrap();

// Create a new subject.
let subject = Arc::new(KeySubject::from_keypair(generate::<Ed25519KeyPair>(Some(
"this-is-a-very-UNSAFE-secret-key".as_bytes().try_into().unwrap(),
))));
let subject = Arc::new(KeySubject::from_keypair(
generate::<Ed25519KeyPair>(Some("this-is-a-very-UNSAFE-secret-key".as_bytes().try_into().unwrap())),
None,
));
let subject_did = subject.identifier().unwrap();

// Create a new relying party.
Expand Down
35 changes: 35 additions & 0 deletions oid4vci/src/credential_format_profiles/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ mod sealed {
}
}

impl FormatExtension for () {
type Container<F: Format + DeserializeOwned> = Profile<F>;
}
#[derive(Debug, Serialize, Clone, Eq, PartialEq, Deserialize)]
pub struct Profile<F>
where
F: Format,
{
pub format: F,
}

#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
pub struct WithParameters;
impl FormatExtension for WithParameters {
Expand Down Expand Up @@ -108,3 +119,27 @@ where
Other(serde_json::Value),
}
impl<C> CredentialFormatCollection for CredentialFormats<C> where C: FormatExtension {}

impl TryInto<CredentialFormats<()>> for &CredentialFormats<WithCredential> {
type Error = anyhow::Error;

fn try_into(self) -> Result<CredentialFormats<()>, Self::Error> {
match self {
CredentialFormats::JwtVcJson(credential) => Ok(CredentialFormats::<()>::JwtVcJson(Profile {
format: credential.format.clone(),
})),
CredentialFormats::JwtVcJsonLd(credential) => Ok(CredentialFormats::<()>::JwtVcJsonLd(Profile {
format: credential.format.clone(),
})),
CredentialFormats::LdpVc(credential) => Ok(CredentialFormats::<()>::LdpVc(Profile {
format: credential.format.clone(),
})),
CredentialFormats::MsoMdoc(credential) => Ok(CredentialFormats::<()>::MsoMdoc(Profile {
format: credential.format.clone(),
})),
CredentialFormats::Other(_) => Err(anyhow::anyhow!(
"unable to convert CredentialFormats<WithCredential> to CredentialFormats<()>"
)),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ 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 authorization_endpoint: Option<Url>,
pub token_endpoint: Option<Url>,
pub jwks_uri: Option<Url>,
pub registration_endpoint: Option<Url>,
pub scopes_supported: Option<Vec<String>>,
Expand Down
20 changes: 20 additions & 0 deletions siopv2/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,26 @@ impl Provider {
let jwt = jwt::encode(self.subject.clone(), Header::new(Algorithm::EdDSA), id_token)?;
builder = builder.id_token(jwt);
}
ResponseType::VpToken => {
if let (Some(verifiable_presentation), Some(presentation_submission)) =
(verifiable_presentation, presentation_submission)
{
let vp_token = VpToken::builder()
.iss(subject_identifier.clone())
.sub(subject_identifier)
.aud(request.client_id().to_owned())
.nonce(request.nonce().to_owned())
.exp((Utc::now() + Duration::minutes(10)).timestamp())
.iat((Utc::now()).timestamp())
.verifiable_presentation(verifiable_presentation)
.build()?;

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.");
}
}
ResponseType::IdTokenVpToken => {
let id_token = IdToken::builder()
.iss(subject_identifier.clone())
Expand Down
Loading

0 comments on commit d24cdc1

Please sign in to comment.