Skip to content

Commit

Permalink
Refactor jwt #144 (#173)
Browse files Browse the repository at this point in the history
  • Loading branch information
KendallWeihe authored May 8, 2024
1 parent 6e8cc22 commit b4d0a75
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 35 deletions.
62 changes: 29 additions & 33 deletions crates/credentials/src/vc.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -72,9 +74,8 @@ impl VerifiableCredential {
bearer_did: &BearerDid,
key_selector: &KeySelector,
) -> Result<String, CredentialError> {
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()),
Expand All @@ -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<Self, CredentialError> {
let jwt_decoded = Jwt::verify::<VcJwtClaims>(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<Self, CredentialError> {
let jwt_decoded = Jwt::decode::<VcJwtClaims>(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<Arc<VerifiableCredential>, CredentialError> {
verify_jwt(jwt).await?;
let claims = VcJwtClaims::new_from_compact_jws(jwt)?;
Ok(Arc::new(claims.vc))
let jwt_decoded = Jwt::verify::<VcJwtClaims>(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<Self, CredentialError> {
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<String, CredentialError> {
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 {
Expand Down Expand Up @@ -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);
Expand Down
40 changes: 40 additions & 0 deletions crates/jwt/src/jwe.rs
Original file line number Diff line number Diff line change
@@ -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<T: Claims> {
// TODO other properties for JWE
pub claims: T,
pub parts: Vec<String>,
}

pub struct Jwt;

impl Jwt {
pub fn sign<T: Claims>(
_bearer_did: &BearerDid,
_key_selector: &KeySelector,
_header: Option<JwsHeader>,
_claims: &T,
) -> Result<String, JwtError> {
unimplemented!()
}

pub fn decode<T: Claims>(_jwt: &str) -> Result<JwtDecoded<T>, JwtError> {
unimplemented!()
}

pub async fn verify<T: Claims>(_jwt: &str) -> Result<JwtDecoded<T>, JwtError> {
unimplemented!()
}
}

#[cfg(test)]
mod tests {
// use super::*;
// TODO tests
}
127 changes: 127 additions & 0 deletions crates/jwt/src/jws.rs
Original file line number Diff line number Diff line change
@@ -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<T: Claims> {
pub header: JwsHeader,
pub claims: T,
pub signature: String,
pub parts: Vec<String>,
}

pub struct Jwt;

impl Jwt {
pub fn sign<T: Claims>(
bearer_did: &BearerDid,
key_selector: &KeySelector,
header: Option<JwsHeader>,
claims: &T,
) -> Result<String, JwtError> {
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<T: Claims>(jwt: &str) -> Result<JwtDecoded<T>, JwtError> {
let jws_decoded = CompactJws::decode(jwt)?;

let claims = serde_json::from_slice::<T>(&jws_decoded.payload)?;

Ok(JwtDecoded {
header: jws_decoded.header,
claims,
signature: jws_decoded.signature,
parts: jws_decoded.parts,
})
}

pub async fn verify<T: Claims>(jwt: &str) -> Result<JwtDecoded<T>, JwtError> {
let jws_decoded = CompactJws::verify(jwt).await?;

let claims = serde_json::from_slice::<T>(&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::<RegisteredClaims>(&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);
}
}
8 changes: 6 additions & 2 deletions crates/jwt/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -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,
Expand All @@ -97,7 +102,6 @@ mod test {
Method,
},
};
use jws::splice_parts;
use keys::key_manager::local_key_manager::LocalKeyManager;

use super::*;
Expand Down
42 changes: 42 additions & 0 deletions crates/jwt/src/lib_v2.rs
Original file line number Diff line number Diff line change
@@ -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<SerdeJsonError> 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<String>,
#[serde(rename = "sub", skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
#[serde(rename = "aud", skip_serializing_if = "Option::is_none")]
pub audience: Option<String>,
#[serde(rename = "exp", skip_serializing_if = "Option::is_none")]
pub expiration: Option<i64>,
#[serde(rename = "nbf", skip_serializing_if = "Option::is_none")]
pub not_before: Option<i64>,
#[serde(rename = "iat", skip_serializing_if = "Option::is_none")]
pub issued_at: Option<i64>,
#[serde(rename = "jti", skip_serializing_if = "Option::is_none")]
pub jti: Option<String>,
}

impl Claims for RegisteredClaims {}

0 comments on commit b4d0a75

Please sign in to comment.