Skip to content

Commit

Permalink
feat: add support for multiple signing algorithms (#76)
Browse files Browse the repository at this point in the history
* feat: add support for multiple singing algorithms

* docs: improve TODO comments

* feat: add selelect functionality for subject syntax type

* docs: add TODO comment regarding `subject_syntax_types_supported` parameter

* fix: fix typos
  • Loading branch information
nanderstabel authored Jul 16, 2024
1 parent 7c6a6eb commit dac4102
Show file tree
Hide file tree
Showing 41 changed files with 560 additions and 135 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ getset = "0.1"
identity_core = "1.2.0"
identity_credential = { version = "1.2.0", default-features = false, features = ["validator", "credential", "presentation"] }
is_empty = "0.2"
jsonwebtoken = "8.2"
jsonwebtoken = "9.3"
monostate = "0.1"
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
reqwest-middleware = "0.2"
Expand Down
1 change: 1 addition & 0 deletions dif-presentation-exchange/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ repository.workspace = true
getset.workspace = true
jsonpath_lib = "0.3"
jsonschema = "0.17"
jsonwebtoken.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_with.workspace = true
Expand Down
4 changes: 3 additions & 1 deletion dif-presentation-exchange/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ pub mod presentation_definition;
pub mod presentation_submission;

pub use input_evaluation::evaluate_input;
pub use presentation_definition::{ClaimFormatDesignation, InputDescriptor, PresentationDefinition};
pub use presentation_definition::{
ClaimFormatDesignation, ClaimFormatProperty, InputDescriptor, PresentationDefinition,
};
pub use presentation_submission::{InputDescriptorMappingObject, PathNested, PresentationSubmission};
5 changes: 3 additions & 2 deletions dif-presentation-exchange/src/presentation_definition.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use getset::Getters;
use jsonwebtoken::Algorithm;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::collections::HashMap;
Expand Down Expand Up @@ -59,7 +60,7 @@ pub enum ClaimFormatDesignation {
#[derive(Deserialize, Debug, PartialEq, Clone, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ClaimFormatProperty {
Alg(Vec<String>),
Alg(Vec<Algorithm>),
ProofType(Vec<String>),
}

Expand Down Expand Up @@ -268,7 +269,7 @@ mod tests {
purpose: None,
format: Some(HashMap::from_iter(vec![(
ClaimFormatDesignation::MsoMdoc,
ClaimFormatProperty::Alg(vec!["EdDSA".to_string(), "ES256".to_string()])
ClaimFormatProperty::Alg(vec![Algorithm::EdDSA, Algorithm::ES256])
)])),
constraints: Constraints {
limit_disclosure: Some(LimitDisclosure::Required),
Expand Down
2 changes: 1 addition & 1 deletion oid4vc-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ did_url = "0.1.0"
ed25519-dalek = { version = "2.0.0", features = ["rand_core"] }
getset = "0.1.2"
is_empty = "0.2.0"
jsonwebtoken = "8.2.0"
jsonwebtoken.workspace = true
lazy_static = "1.4.0"
rand = "0.8"
serde.workspace = true
Expand Down
5 changes: 3 additions & 2 deletions oid4vc-core/src/authentication/sign.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use anyhow::Result;
use async_trait::async_trait;
use jsonwebtoken::Algorithm;
use std::sync::Arc;

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

Expand Down
3 changes: 2 additions & 1 deletion oid4vc-core/src/authentication/subject.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{Sign, Verify};
use anyhow::Result;
use async_trait::async_trait;
use jsonwebtoken::Algorithm;
use std::sync::Arc;

pub type SigningSubject = Arc<dyn Subject>;
Expand All @@ -9,5 +10,5 @@ pub type SigningSubject = Arc<dyn Subject>;
/// This [`Subject`] trait is used to sign and verify JWTs.
#[async_trait]
pub trait Subject: Sign + Verify + Send + Sync {
async fn identifier(&self, subject_syntax_type: &str) -> Result<String>;
async fn identifier(&self, subject_syntax_type: &str, algorithm: Algorithm) -> Result<String>;
}
3 changes: 3 additions & 0 deletions oid4vc-core/src/client_metadata.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::collections::HashMap;
use url::Url;

/// [`ClientMetadata`] is a request parameter used by a [`crate::RelyingParty`] to communicate its capabilities to a
Expand All @@ -16,6 +17,8 @@ pub enum ClientMetadataResource<T = ()> {
/// expanded with Extensions and profiles.
#[serde(flatten)]
extension: T,
#[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
other: HashMap<String, serde_json::Value>,
},
ClientMetadataUri(String),
}
15 changes: 11 additions & 4 deletions oid4vc-core/src/jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,28 +43,35 @@ pub fn decode<T>(jwt: &str, public_key: Vec<u8>, algorithm: Algorithm) -> Result
where
T: DeserializeOwned,
{
let key = DecodingKey::from_ed_der(public_key.as_slice());
let decoding_key = match algorithm {
Algorithm::EdDSA => DecodingKey::from_ed_der(public_key.as_slice()),
Algorithm::ES256 => DecodingKey::from_ec_der(public_key.as_slice()),
_ => return Err(anyhow!("Unsupported algorithm.")),
};

let mut validation = Validation::new(algorithm);
validation.validate_exp = false;
validation.validate_aud = false;
validation.required_spec_claims.clear();
Ok(jsonwebtoken::decode::<T>(jwt, &key, &validation)?.claims)
Ok(jsonwebtoken::decode::<T>(jwt, &decoding_key, &validation)?.claims)
}

pub async fn encode<C, S>(signer: Arc<S>, header: Header, claims: C, subject_syntax_type: &str) -> Result<String>
where
C: Serialize,
S: Sign + ?Sized,
{
let algorithm = header.alg;
let kid = signer
.key_id(subject_syntax_type)
.key_id(subject_syntax_type, algorithm)
.await
.ok_or(anyhow!("No key identifier found."))?;

let jwt = JsonWebToken::new(header, claims).kid(kid);

let message = [base64_url_encode(&jwt.header)?, base64_url_encode(&jwt.payload)?].join(".");

let proof_value = signer.sign(&message, subject_syntax_type).await?;
let proof_value = signer.sign(&message, subject_syntax_type, algorithm).await?;
let signature = base64_url::encode(proof_value.as_slice());
let message = [message, signature].join(".");
Ok(message)
Expand Down
16 changes: 16 additions & 0 deletions oid4vc-core/src/openid4vc_extension.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{authorization_response::AuthorizationResponse, Subject, SubjectSyntaxType, Validator};
use jsonwebtoken::Algorithm;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{future::Future, sync::Arc};

Expand Down Expand Up @@ -29,11 +30,26 @@ pub trait Extension: Serialize + PartialEq + Sized + std::fmt::Debug + Clone + S
_extension_parameters: &<Self::RequestHandle as RequestHandle>::Parameters,
_user_input: &<Self::ResponseHandle as ResponseHandle>::Input,
_subject_syntax_type: impl TryInto<SubjectSyntaxType>,
_signing_algorithm: impl TryInto<Algorithm>,
) -> impl Future<Output = anyhow::Result<Vec<String>>> {
// Will be overwritten by the extension.
async { Err(anyhow::anyhow!("Not implemented.")) }
}

fn get_relying_party_supported_algorithms(
_authorization_request: &<Self::RequestHandle as RequestHandle>::Parameters,
) -> impl Future<Output = anyhow::Result<Vec<Algorithm>>> {
// Will be overwritten by the extension.
async { Err(anyhow::anyhow!("Not implemented.")) }
}

fn get_relying_party_supported_syntax_types(
_authorization_request: &<Self::RequestHandle as RequestHandle>::Parameters,
) -> impl Future<Output = anyhow::Result<Vec<SubjectSyntaxType>>> {
// Will be overwritten by the extension.
async { Err(anyhow::anyhow!("Not implemented.")) }
}

fn build_authorization_response(
_jwts: Vec<String>,
_user_input: <Self::ResponseHandle as ResponseHandle>::Input,
Expand Down
8 changes: 8 additions & 0 deletions oid4vc-core/src/subject_syntax_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ impl TryFrom<&str> for SubjectSyntaxType {
}
}

impl TryFrom<String> for SubjectSyntaxType {
type Error = anyhow::Error;

fn try_from(value: String) -> Result<Self, Self::Error> {
SubjectSyntaxType::from_str(value.as_str())
}
}

impl From<DidMethod> for SubjectSyntaxType {
fn from(did_method: DidMethod) -> Self {
SubjectSyntaxType::Did(did_method)
Expand Down
7 changes: 4 additions & 3 deletions oid4vc-core/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use anyhow::Result;
use async_trait::async_trait;
use derivative::{self, Derivative};
use ed25519_dalek::{Signature, Signer, SigningKey};
use jsonwebtoken::Algorithm;
use lazy_static::lazy_static;
use rand::rngs::OsRng;

Expand Down Expand Up @@ -32,11 +33,11 @@ impl TestSubject {

#[async_trait]
impl Sign for TestSubject {
async fn key_id(&self, _subject_syntax_type: &str) -> Option<String> {
async fn key_id(&self, _subject_syntax_type: &str, _algorithm: Algorithm) -> Option<String> {
Some(self.key_id.clone())
}

async fn sign(&self, message: &str, _subject_syntax_type: &str) -> Result<Vec<u8>> {
async fn sign(&self, message: &str, _subject_syntax_type: &str, _algorithm: Algorithm) -> Result<Vec<u8>> {
let signature: Signature = TEST_KEYPAIR.sign(message.as_bytes());
Ok(signature.to_bytes().to_vec())
}
Expand All @@ -55,7 +56,7 @@ impl Verify for TestSubject {

#[async_trait]
impl Subject for TestSubject {
async fn identifier(&self, _subject_syntax_type: &str) -> Result<String> {
async fn identifier(&self, _subject_syntax_type: &str, _algorithm: Algorithm) -> Result<String> {
Ok(self.did.to_string())
}
}
Expand Down
2 changes: 1 addition & 1 deletion oid4vc-manager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ futures = "0.3"
getset.workspace = true
identity_core.workspace = true
identity_credential.workspace = true
jsonwebtoken = "8.3"
jsonwebtoken.workspace = true
paste = "1.0"
reqwest.workspace = true
serde.workspace = true
Expand Down
28 changes: 24 additions & 4 deletions oid4vc-manager/src/managers/provider.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::Result;
use jsonwebtoken::Algorithm;
use oid4vc_core::{
authorization_request::{AuthorizationRequest, Object},
authorization_response::AuthorizationResponse,
Expand All @@ -17,17 +18,36 @@ pub struct ProviderManager {
impl ProviderManager {
pub fn new(
subject: Arc<dyn Subject>,
default_subject_syntax_type: impl TryInto<SubjectSyntaxType>,
supported_subject_syntax_types: Vec<impl TryInto<SubjectSyntaxType>>,
supported_signing_algorithms: Vec<Algorithm>,
) -> Result<Self> {
Ok(Self {
provider: Provider::new(subject, default_subject_syntax_type)?,
provider: Provider::new(subject, supported_subject_syntax_types, supported_signing_algorithms)?,
})
}

pub async fn validate_request(&self, authorization_request: String) -> Result<AuthorizationRequest<Object>> {
self.provider.validate_request(authorization_request).await
}

pub async fn get_matching_signing_algorithm<E: Extension>(
&self,
authorization_request: &AuthorizationRequest<Object<E>>,
) -> Result<Algorithm> {
self.provider
.get_matching_signing_algorithm(authorization_request)
.await
}

pub async fn get_matching_subject_syntax_type<E: Extension>(
&self,
authorization_request: &AuthorizationRequest<Object<E>>,
) -> Result<SubjectSyntaxType> {
self.provider
.get_matching_subject_syntax_type(authorization_request)
.await
}

pub async fn generate_response<E: Extension + OpenID4VC>(
&self,
authorization_request: &AuthorizationRequest<Object<E>>,
Expand All @@ -43,7 +63,7 @@ impl ProviderManager {
self.provider.send_response(authorization_response).await
}

pub fn default_subject_syntax_type(&self) -> &SubjectSyntaxType {
&self.provider.default_subject_syntax_type
pub fn default_subject_syntax_types(&self) -> &Vec<SubjectSyntaxType> {
&self.provider.supported_subject_syntax_types
}
}
15 changes: 14 additions & 1 deletion oid4vc-manager/src/managers/relying_party.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::Result;
use jsonwebtoken::Algorithm;
use oid4vc_core::{
authorization_request::{AuthorizationRequest, Object},
authorization_response::AuthorizationResponse,
Expand All @@ -11,23 +12,35 @@ use std::sync::Arc;
/// Manager struct for [`siopv2::RelyingParty`].
pub struct RelyingPartyManager {
pub relying_party: RelyingParty,
// TODO: this should be replaced with `client_metadata`
pub supported_signing_algorithms: Vec<Algorithm>,
}

impl RelyingPartyManager {
pub fn new(
subject: Arc<dyn Subject>,
default_subject_syntax_type: impl TryInto<SubjectSyntaxType>,
supported_signing_algorithms: Vec<Algorithm>,
) -> Result<Self> {
Ok(Self {
relying_party: RelyingParty::new(subject, default_subject_syntax_type)?,
supported_signing_algorithms,
})
}

pub async fn encode<E: Extension>(
&self,
authorization_request: &AuthorizationRequest<Object<E>>,
) -> Result<String> {
self.relying_party.encode(authorization_request).await
self.relying_party
.encode(
authorization_request,
*self
.supported_signing_algorithms
.first()
.ok_or(anyhow::anyhow!("No supported signing algorithms"))?,
)
.await
}

pub async fn validate_response<E: Extension>(
Expand Down
14 changes: 9 additions & 5 deletions oid4vc-manager/src/methods/key_method.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use did_key::{generate, resolve, Config, CoreSign, DIDCore, Document, Ed25519KeyPair, KeyMaterial, PatchedKeyPair};
use jsonwebtoken::Algorithm;
use oid4vc_core::{authentication::sign::ExternalSign, Sign, Subject, Verify};
use std::sync::Arc;

Expand Down Expand Up @@ -43,14 +44,14 @@ impl Default for KeySubject {

#[async_trait]
impl Sign for KeySubject {
async fn key_id(&self, _subject_syntax_type: &str) -> Option<String> {
async fn key_id(&self, _subject_syntax_type: &str, _algorithm: Algorithm) -> Option<String> {
self.document
.authentication
.as_ref()
.and_then(|authentication_methods| authentication_methods.first().cloned())
}

async fn sign(&self, message: &str, _subject_syntax_type: &str) -> Result<Vec<u8>> {
async fn sign(&self, message: &str, _subject_syntax_type: &str, _algorithm: Algorithm) -> Result<Vec<u8>> {
match self.external_signer() {
Some(external_signer) => external_signer.sign(message),
None => Ok(self.keypair.sign(message.as_bytes())),
Expand All @@ -71,7 +72,7 @@ impl Verify for KeySubject {

#[async_trait]
impl Subject for KeySubject {
async fn identifier(&self, _subject_syntax_type: &str) -> Result<String> {
async fn identifier(&self, _subject_syntax_type: &str, _algorithm: Algorithm) -> Result<String> {
Ok(self.document.id.clone())
}
}
Expand Down Expand Up @@ -111,6 +112,7 @@ async fn resolve_public_key(kid: &str) -> Result<Vec<u8>> {
mod tests {
use super::*;
use crate::{ProviderManager, RelyingPartyManager};
use jsonwebtoken::Algorithm;
use oid4vc_core::authorization_request::{AuthorizationRequest, Object};
use siopv2::siopv2::SIOPv2;
use std::sync::Arc;
Expand All @@ -121,7 +123,8 @@ mod tests {
let subject = KeySubject::new();

// Create a new provider manager.
let provider_manager = ProviderManager::new(Arc::new(subject), "did:key").unwrap();
let provider_manager =
ProviderManager::new(Arc::new(subject), vec!["did:key"], vec![Algorithm::EdDSA]).unwrap();

// Get a new SIOP authorization_request with response mode `direct_post` for cross-device communication.
let request_url = "\
Expand Down Expand Up @@ -153,7 +156,8 @@ mod tests {
.unwrap();

// Let the relying party validate the authorization_response.
let relying_party_manager = RelyingPartyManager::new(Arc::new(KeySubject::new()), "did:key").unwrap();
let relying_party_manager =
RelyingPartyManager::new(Arc::new(KeySubject::new()), "did:key", vec![Algorithm::EdDSA]).unwrap();
assert!(relying_party_manager
.validate_response(&authorization_response)
.await
Expand Down
Loading

0 comments on commit dac4102

Please sign in to comment.