From b4d0a75794f67d8d1176a2b0af02a199dc4f3920 Mon Sep 17 00:00:00 2001 From: Kendall Weihe Date: Wed, 8 May 2024 14:40:15 -0400 Subject: [PATCH] Refactor jwt #144 (#173) --- crates/credentials/src/vc.rs | 62 ++++++++--------- crates/jwt/src/jwe.rs | 40 +++++++++++ crates/jwt/src/jws.rs | 127 +++++++++++++++++++++++++++++++++++ crates/jwt/src/lib.rs | 8 ++- crates/jwt/src/lib_v2.rs | 42 ++++++++++++ 5 files changed, 244 insertions(+), 35 deletions(-) create mode 100644 crates/jwt/src/jwe.rs create mode 100644 crates/jwt/src/jws.rs create mode 100644 crates/jwt/src/lib_v2.rs diff --git a/crates/credentials/src/vc.rs b/crates/credentials/src/vc.rs index 95dc6ba3..abb21576 100644 --- a/crates/credentials/src/vc.rs +++ b/crates/credentials/src/vc.rs @@ -1,7 +1,9 @@ -use base64::{engine::general_purpose, Engine as _}; use dids::{bearer::BearerDid, document::KeySelector}; -use jws::{splice_parts, JwsError, JwsHeader}; -use jwt::{sign_jwt, verify_jwt, Claims, JwtError}; +use jws::v2::JwsError; +use jwt::{ + jws::Jwt, + lib_v2::{Claims, JwtError, RegisteredClaims}, +}; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, sync::Arc}; @@ -72,9 +74,8 @@ impl VerifiableCredential { bearer_did: &BearerDid, key_selector: &KeySelector, ) -> Result { - let header = JwsHeader::from_bearer_did(bearer_did, key_selector, "JWT")?; let claims = VcJwtClaims { - base_claims: Claims { + registered_claims: RegisteredClaims { issuer: Some(self.issuer.clone()), jti: Some(self.id.clone()), subject: Some(self.credential_subject.id.clone()), @@ -85,45 +86,40 @@ impl VerifiableCredential { vc: self.clone(), }; - let encoded_header = header.encode()?; - let encoded_claims = claims.encode()?; + let jwt = Jwt::sign(bearer_did, key_selector, None, &claims)?; - let vcjwt = sign_jwt(bearer_did, key_selector, &encoded_header, &encoded_claims)?; - Ok(vcjwt) + Ok(jwt) + } + + pub async fn verify(jwt: &str) -> Result { + let jwt_decoded = Jwt::verify::(jwt).await?; + + // TODO Implement semantic VC verification rules https://github.com/TBD54566975/web5-rs/issues/151 + + Ok(jwt_decoded.claims.vc) + } + + pub fn decode(jwt: &str) -> Result { + let jwt_decoded = Jwt::decode::(jwt)?; + + Ok(jwt_decoded.claims.vc) } } +// todo we should remove this altogether in the follow-up PR, but it would break bindings so leaving it for now pub async fn verify_vcjwt(jwt: &str) -> Result, CredentialError> { - verify_jwt(jwt).await?; - let claims = VcJwtClaims::new_from_compact_jws(jwt)?; - Ok(Arc::new(claims.vc)) + let jwt_decoded = Jwt::verify::(jwt).await?; + Ok(Arc::new(jwt_decoded.claims.vc)) } #[derive(Serialize, Deserialize, Debug, Default)] pub struct VcJwtClaims { - vc: VerifiableCredential, + pub vc: VerifiableCredential, #[serde(flatten)] - base_claims: Claims, + pub registered_claims: RegisteredClaims, } -impl VcJwtClaims { - pub fn new_from_compact_jws(compact_jws: &str) -> Result { - let parts = splice_parts(compact_jws)?; - let decoded_bytes = general_purpose::URL_SAFE_NO_PAD - .decode(&parts[1]) - .map_err(|e| JwsError::DecodingError(e.to_string()))?; - let claims: Self = serde_json::from_slice(&decoded_bytes) - .map_err(|e| JwsError::DeserializationError(e.to_string()))?; - Ok(claims) - } - - pub fn encode(&self) -> Result { - let json_str = serde_json::to_string(&self) - .map_err(|e| JwsError::SerializationError(e.to_string()))?; - let encoded_str = general_purpose::URL_SAFE_NO_PAD.encode(json_str.as_bytes()); - Ok(encoded_str) - } -} +impl Claims for VcJwtClaims {} #[cfg(test)] mod test { @@ -238,7 +234,7 @@ mod test { let vcjwt = vc.sign(&bearer_did, &key_selector).unwrap(); assert!(!vcjwt.is_empty()); - let verified_vc = verify_vcjwt(&vcjwt).await.unwrap(); + let verified_vc = VerifiableCredential::verify(&vcjwt).await.unwrap(); assert_eq!(vc.id, verified_vc.id); assert_eq!(vc.issuer, verified_vc.issuer); assert_eq!(vc.credential_subject.id, verified_vc.credential_subject.id); diff --git a/crates/jwt/src/jwe.rs b/crates/jwt/src/jwe.rs new file mode 100644 index 00000000..36674ce2 --- /dev/null +++ b/crates/jwt/src/jwe.rs @@ -0,0 +1,40 @@ +use crate::lib_v2::{Claims, JwtError}; +use dids::{bearer::BearerDid, document::KeySelector}; +use jws::v2::JwsHeader; + +// A JWT can be implemented as either a JWS or JWE, this module is the implementation of a JWT as a JWE + +// TODO implement https://github.com/TBD54566975/web5-rs/issues/174 + +pub struct JwtDecoded { + // TODO other properties for JWE + pub claims: T, + pub parts: Vec, +} + +pub struct Jwt; + +impl Jwt { + pub fn sign( + _bearer_did: &BearerDid, + _key_selector: &KeySelector, + _header: Option, + _claims: &T, + ) -> Result { + unimplemented!() + } + + pub fn decode(_jwt: &str) -> Result, JwtError> { + unimplemented!() + } + + pub async fn verify(_jwt: &str) -> Result, JwtError> { + unimplemented!() + } +} + +#[cfg(test)] +mod tests { + // use super::*; + // TODO tests +} diff --git a/crates/jwt/src/jws.rs b/crates/jwt/src/jws.rs new file mode 100644 index 00000000..2c0ba3b1 --- /dev/null +++ b/crates/jwt/src/jws.rs @@ -0,0 +1,127 @@ +use crate::lib_v2::{Claims, JwtError}; +use dids::{bearer::BearerDid, document::KeySelector}; +use jws::v2::{CompactJws, JwsHeader}; + +// A JWT can be implemented as either a JWS or JWE, this module is the implementation of a JWT as a JWS + +pub struct JwtDecoded { + pub header: JwsHeader, + pub claims: T, + pub signature: String, + pub parts: Vec, +} + +pub struct Jwt; + +impl Jwt { + pub fn sign( + bearer_did: &BearerDid, + key_selector: &KeySelector, + header: Option, + claims: &T, + ) -> Result { + let jws_header = match header { + Some(h) => h, + None => { + let verification_method = + bearer_did.document.get_verification_method(key_selector)?; + + JwsHeader { + alg: verification_method.public_key_jwk.alg.clone(), + kid: verification_method.id.clone(), + typ: "JWT".to_string(), + } + } + }; + + let serialized_claims = serde_json::to_string(claims)?.into_bytes(); + + let jwt = CompactJws::sign(bearer_did, key_selector, &jws_header, &serialized_claims)?; + Ok(jwt) + } + + pub fn decode(jwt: &str) -> Result, JwtError> { + let jws_decoded = CompactJws::decode(jwt)?; + + let claims = serde_json::from_slice::(&jws_decoded.payload)?; + + Ok(JwtDecoded { + header: jws_decoded.header, + claims, + signature: jws_decoded.signature, + parts: jws_decoded.parts, + }) + } + + pub async fn verify(jwt: &str) -> Result, JwtError> { + let jws_decoded = CompactJws::verify(jwt).await?; + + let claims = serde_json::from_slice::(&jws_decoded.payload)?; + + Ok(JwtDecoded { + header: jws_decoded.header, + claims, + signature: jws_decoded.signature, + parts: jws_decoded.parts, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::lib_v2::RegisteredClaims; + use crypto::Curve; + use dids::{ + document::KeySelector, + method::{ + jwk::{DidJwk, DidJwkCreateOptions}, + Method, + }, + }; + use keys::key_manager::local_key_manager::LocalKeyManager; + use std::sync::Arc; + + #[tokio::test] + async fn test_sign_and_verify() { + let key_manager = LocalKeyManager::new_in_memory(); + let bearer_did = DidJwk::create( + Arc::new(key_manager), + DidJwkCreateOptions { + curve: Curve::Ed25519, + }, + ) + .expect("failed to create bearer did"); + + let claims = RegisteredClaims { + issuer: Some(bearer_did.identifier.uri.clone()), + ..Default::default() + }; + + let key_id = bearer_did.document.verification_method[0].id.clone(); + let jwt = Jwt::sign( + &bearer_did, + &KeySelector::KeyId { + key_id: key_id.clone(), + }, + None, + &claims, + ) + .unwrap(); + + let jwt_decoded = Jwt::verify::(&jwt).await.unwrap(); + + // default JwsHeader + assert_eq!("JWT".to_string(), jwt_decoded.header.typ); + assert_eq!(key_id, jwt_decoded.header.kid); + assert_eq!( + bearer_did.document.verification_method[0] + .public_key_jwk + .alg, + jwt_decoded.header.alg + ); + + // claims + assert_eq!(claims.issuer, jwt_decoded.claims.issuer); + } +} diff --git a/crates/jwt/src/lib.rs b/crates/jwt/src/lib.rs index 4776f761..ac3bed10 100644 --- a/crates/jwt/src/lib.rs +++ b/crates/jwt/src/lib.rs @@ -1,6 +1,10 @@ +pub mod jwe; +pub mod jws; +pub mod lib_v2; + +use ::jws::{sign_compact_jws, verify_compact_jws, JwsError, JwsHeader}; use base64::{engine::general_purpose, Engine as _}; use dids::{bearer::BearerDid, document::KeySelector}; -use jws::{sign_compact_jws, verify_compact_jws, JwsError, JwsHeader}; use serde::{Deserialize, Serialize}; #[derive(thiserror::Error, Debug, Clone, PartialEq)] @@ -89,6 +93,7 @@ pub async fn verify_jwt(jwt: &str) -> Result<(), JwtError> { #[cfg(test)] mod test { + use ::jws::splice_parts; use crypto::Curve; use dids::{ document::VerificationMethodType, @@ -97,7 +102,6 @@ mod test { Method, }, }; - use jws::splice_parts; use keys::key_manager::local_key_manager::LocalKeyManager; use super::*; diff --git a/crates/jwt/src/lib_v2.rs b/crates/jwt/src/lib_v2.rs new file mode 100644 index 00000000..e6928449 --- /dev/null +++ b/crates/jwt/src/lib_v2.rs @@ -0,0 +1,42 @@ +use dids::document::DocumentError; +use jws::v2::JwsError; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Error as SerdeJsonError; + +#[derive(thiserror::Error, Debug, Clone, PartialEq)] +pub enum JwtError { + #[error(transparent)] + JwsError(#[from] JwsError), + #[error(transparent)] + DocumentError(#[from] DocumentError), + #[error("serde json error {0}")] + SerdeJsonError(String), +} + +impl From for JwtError { + fn from(err: SerdeJsonError) -> Self { + JwtError::SerdeJsonError(err.to_string()) + } +} + +pub trait Claims: Serialize + DeserializeOwned {} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct RegisteredClaims { + #[serde(rename = "iss", skip_serializing_if = "Option::is_none")] + pub issuer: Option, + #[serde(rename = "sub", skip_serializing_if = "Option::is_none")] + pub subject: Option, + #[serde(rename = "aud", skip_serializing_if = "Option::is_none")] + pub audience: Option, + #[serde(rename = "exp", skip_serializing_if = "Option::is_none")] + pub expiration: Option, + #[serde(rename = "nbf", skip_serializing_if = "Option::is_none")] + pub not_before: Option, + #[serde(rename = "iat", skip_serializing_if = "Option::is_none")] + pub issued_at: Option, + #[serde(rename = "jti", skip_serializing_if = "Option::is_none")] + pub jti: Option, +} + +impl Claims for RegisteredClaims {}