diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b0f6e64..e504e1bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,5 +52,7 @@ jobs: cache: true - name: Setup run: just setup + - name: Formatting + run: cargo fmt --check - name: Lint run: just lint diff --git a/crates/credentials/Cargo.toml b/crates/credentials/Cargo.toml index 6fa98ac7..9e949500 100644 --- a/crates/credentials/Cargo.toml +++ b/crates/credentials/Cargo.toml @@ -26,4 +26,5 @@ uuid = { version = "1.8.0", features = ["v4"] } [dev-dependencies] crypto = { path = "../crypto" } serde_canonical_json = "1.0.0" -tokio = { version = "1.34.0", features = ["macros", "test-util"] } \ No newline at end of file +tokio = { version = "1.34.0", features = ["macros", "test-util"] } +test-helpers = { path = "../test-helpers" } diff --git a/crates/credentials/src/lib.rs b/crates/credentials/src/lib.rs index eb83d563..5c02d6a3 100644 --- a/crates/credentials/src/lib.rs +++ b/crates/credentials/src/lib.rs @@ -1,2 +1,2 @@ pub mod presentation_definition; -pub mod vc; +pub mod verifiable_credential; diff --git a/crates/credentials/src/presentation_definition.rs b/crates/credentials/src/presentation_definition.rs index 2ece465d..620690b5 100644 --- a/crates/credentials/src/presentation_definition.rs +++ b/crates/credentials/src/presentation_definition.rs @@ -236,12 +236,13 @@ impl JsonSchemaBuilder { #[cfg(test)] mod tests { use std::collections::HashSet; - use std::fs; + + use test_helpers::TestVectorFile; use super::PresentationDefinition; #[derive(Debug, serde::Deserialize)] - struct SelectCredentialsVectorInput { + struct VectorInput { #[serde(rename = "presentationDefinition")] pub presentation_definition: PresentationDefinition, #[serde(rename = "credentialJwts")] @@ -249,34 +250,16 @@ mod tests { } #[derive(Debug, serde::Deserialize)] - struct SelectCredentialsVectorOutput { + struct VectorOutput { #[serde(rename = "selectedCredentials")] pub selected_credentials: Vec, } - #[derive(Debug, serde::Deserialize)] - struct SelectCredentialsVector { - pub description: String, - pub input: SelectCredentialsVectorInput, - pub output: SelectCredentialsVectorOutput, - } - - #[derive(Debug, serde::Deserialize)] - struct Vectors { - pub vectors: Vec, - } - - fn load_json_fixture(file_path: &str) -> Vectors { - let data = fs::read_to_string(file_path).unwrap(); - let json = serde_json::from_str(&data).unwrap(); - json - } - #[test] fn test_web5_spec_test_vectors() { - let json_path = - "../../web5-spec/test-vectors/presentation_exchange/select_credentials.json"; - let vectors = load_json_fixture(json_path); + let path = "presentation_exchange/select_credentials.json"; + let vectors: TestVectorFile = + TestVectorFile::load_from_path(path); for vector in vectors.vectors { let presentation_definition = vector.input.presentation_definition; diff --git a/crates/credentials/src/vc.rs b/crates/credentials/src/verifiable_credential.rs similarity index 99% rename from crates/credentials/src/vc.rs rename to crates/credentials/src/verifiable_credential.rs index 0e953ad9..597eb4eb 100644 --- a/crates/credentials/src/vc.rs +++ b/crates/credentials/src/verifiable_credential.rs @@ -329,7 +329,7 @@ mod test { use crypto::Curve; use dids::{ document::VerificationMethodType, - method::{ + methods::{ jwk::{DidJwk, DidJwkCreateOptions}, Create, }, @@ -339,7 +339,7 @@ mod test { use uuid::Uuid; fn create_bearer_did() -> BearerDid { - let key_manager = Arc::new(LocalKeyManager::new_in_memory()); + let key_manager = Arc::new(LocalKeyManager::new()); let options = DidJwkCreateOptions { curve: Curve::Ed25519, }; diff --git a/crates/credentials/tests/resources/pd_sanctions.json b/crates/credentials/tests/resources/pd_sanctions.json deleted file mode 100644 index 15245cf9..00000000 --- a/crates/credentials/tests/resources/pd_sanctions.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "id": "ec11a434-fe24-479b-aae0-511428b37e4f", - "format": { - "jwt_vc": { - "alg": [ - "ES256K", - "EdDSA" - ] - } - }, - "input_descriptors": [ - { - "id": "7b928839-f0b1-4237-893d-b27124b57952", - "constraints": { - "fields": [ - { - "path": [ - "$.iss", - "$.vc.issuer" - ], - "filter": { - "type": "string", - "pattern": "^did:[^:]+:.+" - } - }, - { - "path": [ - "$.vc.type[*]", - "$.type[*]" - ], - "filter": { - "type": "string", - "const": "SanctionsCredential" - } - } - ] - } - } - ] -} diff --git a/crates/dids/Cargo.toml b/crates/dids/Cargo.toml index 4c29219a..ac2e95e7 100644 --- a/crates/dids/Cargo.toml +++ b/crates/dids/Cargo.toml @@ -7,6 +7,7 @@ repository.workspace = true license-file.workspace = true [dependencies] +byteorder = "1.5.0" crypto = { path = "../crypto" } did-jwk = "0.1.1" did-web = "0.2.2" diff --git a/crates/dids/src/bearer.rs b/crates/dids/src/bearer.rs index c0f02de8..3a562558 100644 --- a/crates/dids/src/bearer.rs +++ b/crates/dids/src/bearer.rs @@ -67,7 +67,7 @@ mod test { use super::*; use crate::{ document::VerificationMethodType, - method::{ + methods::{ jwk::{DidJwk, DidJwkCreateOptions}, Create, }, @@ -77,7 +77,7 @@ mod test { #[tokio::test] async fn test_from_key_manager() { - let key_manager = Arc::new(LocalKeyManager::new_in_memory()); + let key_manager = Arc::new(LocalKeyManager::new()); let options = DidJwkCreateOptions { curve: Curve::Ed25519, }; @@ -85,10 +85,10 @@ mod test { let private_keys = key_manager.export_private_keys().unwrap(); let bearer_did = - BearerDid::from_key_manager(&did_jwk_bearer_did.identifier.uri, key_manager) + BearerDid::from_key_manager(&did_jwk_bearer_did.identifier.uri, key_manager.clone()) .await .unwrap(); - let bearer_did_private_keys = bearer_did.key_manager.export_private_keys().unwrap(); + let bearer_did_private_keys = key_manager.export_private_keys().unwrap(); assert_eq!(bearer_did.identifier.uri, did_jwk_bearer_did.identifier.uri); assert_eq!(private_keys.len(), bearer_did_private_keys.len()); @@ -100,7 +100,7 @@ mod test { #[test] fn test_sign() { - let key_manager = Arc::new(LocalKeyManager::new_in_memory()); + let key_manager = Arc::new(LocalKeyManager::new()); let options = DidJwkCreateOptions { curve: Curve::Ed25519, }; diff --git a/crates/dids/src/lib.rs b/crates/dids/src/lib.rs index d3e98e42..a3335aa4 100644 --- a/crates/dids/src/lib.rs +++ b/crates/dids/src/lib.rs @@ -1,5 +1,5 @@ pub mod bearer; pub mod document; pub mod identifier; -pub mod method; +pub mod methods; pub mod resolver; diff --git a/crates/dids/src/methods/dht/bep44.rs b/crates/dids/src/methods/dht/bep44.rs new file mode 100644 index 00000000..ebcee7f9 --- /dev/null +++ b/crates/dids/src/methods/dht/bep44.rs @@ -0,0 +1,249 @@ +use std::{ + io::Cursor, + time::{SystemTime, SystemTimeError, UNIX_EPOCH}, +}; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use crypto::CryptoError; +use keys::key::{KeyError, PublicKey}; + +/// Minimum size of a bep44 encoded message +/// Signature is 64 bytes and seq is 8 byets +const MIN_MESSAGE_LEN: usize = 72; +/// Maximum size of a bep44 v field +const MAX_V_LEN: usize = 1000; +/// Maximum size a bep44 encoded message +const MAX_MESSAGE_LEN: usize = MAX_V_LEN + MIN_MESSAGE_LEN; + +/// Errors that can occur when working with Bep44 messages for did:dht. +#[derive(thiserror::Error, Debug)] +pub enum Bep44EncodingError { + #[error(transparent)] + SystemTimeError(#[from] SystemTimeError), + #[error(transparent)] + CryptoError(#[from] CryptoError), + #[error("Failure creating DID: {0}")] + BigEndianError(String), + #[error( + "Message must have size between {MIN_MESSAGE_LEN} and {MAX_MESSAGE_LEN} but got size {0}" + )] + SizeError(usize), + #[error(transparent)] + SignatureError(#[from] KeyError), +} + +#[derive(Debug, PartialEq)] +pub struct Bep44Message { + /// The sequence number of the message, used to ensure the latest version of + /// the data is retrieved and updated. It's a monotonically increasing number. + pub seq: u64, + /// The signature of the message, ensuring the authenticity and integrity + /// of the data. It's computed over the bencoded sequence number and value. + pub sig: Vec, + /// The actual data being stored or retrieved from the DHT network, typically + /// encoded in a format suitable for DNS packet representation of a DID Document. + pub v: Vec, +} + +fn signable(seq: u64, message: &[u8]) -> Vec { + let mut signable = format!("3:seqi{}e1:v{}:", seq, message.len()).into_bytes(); + signable.extend(message); + signable +} + +fn encode_seq(seq: u64) -> Result, Bep44EncodingError> { + let mut seq_bytes = vec![]; + seq_bytes.write_u64::(seq).map_err(|_| { + Bep44EncodingError::BigEndianError("Failed to write big endian seq".to_string()) + })?; + Ok(seq_bytes) +} + +fn decode_seq(seq_bytes: &[u8]) -> Result { + let mut rdr = Cursor::new(seq_bytes); + let seq = rdr.read_u64::().map_err(|_| { + Bep44EncodingError::BigEndianError("Failed to read big endian seq".to_string()) + })?; + Ok(seq) +} + +/// Represents a BEP44 message, which is used for storing and retrieving data +/// in the Mainline DHT network. +/// +/// A BEP44 message is used primarily in the context of the DID DHT method +/// for publishing and resolving DID documents in the DHT network. This type +/// encapsulates the data structure required for such operations in accordance +/// with BEP44. +/// +/// See [BEP44 Specification](https://www.bittorrent.org/beps/bep_0044.html) +impl Bep44Message { + pub fn new(message: &[u8], sign: F) -> Result + where + F: Fn(Vec) -> Result, KeyError>, + { + let message_len = message.len(); + if message_len > MAX_V_LEN { + return Err(Bep44EncodingError::SizeError(message_len)); + } + + let seq = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + + let signable = signable(seq, message); + let sig = sign(signable)?; + + Ok(Bep44Message { + sig, + seq, + v: message.to_vec(), + }) + } + + pub fn encode(&self) -> Result, Bep44EncodingError> { + let seq_bytes = encode_seq(self.seq)?; + + let mut encoded = Vec::new(); + encoded.extend(self.sig.iter()); + encoded.extend(seq_bytes); + encoded.extend(self.v.iter()); + + Ok(encoded) + } + + pub fn decode(message_bytes: &[u8]) -> Result { + let message_len = message_bytes.len(); + if !(MIN_MESSAGE_LEN..=MAX_MESSAGE_LEN).contains(&message_len) { + return Err(Bep44EncodingError::SizeError(message_len)); + } + + let sig = &message_bytes[0..64]; + let seq = decode_seq(&message_bytes[64..72])?; + let v = &message_bytes[72..]; + + Ok(Self { + seq, + sig: sig.to_owned(), + v: v.to_owned(), + }) + } + + pub fn verify(&self, public_key: &dyn PublicKey) -> Result<(), Bep44EncodingError> { + let signable = signable(self.seq, &self.v); + public_key.verify(&signable, &self.sig)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crypto::{ed25519::Ed25519, CurveOperations}; + use keys::key::PrivateKey; + + use super::*; + + #[test] + fn test_new_verify() { + let message = "Hello World".as_bytes(); + + let private_key = Ed25519::generate().expect("Failed to generate Ed25519 key"); + + let result_bep44_message = + Bep44Message::new(message, |payload| -> Result, KeyError> { + private_key.sign(&payload) + }); + assert!(result_bep44_message.is_ok()); + + let bep44_message = result_bep44_message.unwrap(); + let public_key = private_key + .to_public() + .expect("Failed to convert private key to public key"); + let verify_result = bep44_message.verify(public_key.as_ref()); + assert!(verify_result.is_ok()); + } + + #[test] + fn test_new_message_too_big() { + let too_big = vec![0; 10_000]; + let error = Bep44Message::new(&too_big, |_| -> Result, KeyError> { Ok(vec![]) }) + .expect_err("Should have returned error for malformed signature"); + + match error { + Bep44EncodingError::SizeError(size) => assert_eq!(size, 10_000), + _ => panic!(), + } + } + + #[test] + fn test_new_sign_fails() { + let message = "Hello World".as_bytes(); + + let error = Bep44Message::new(message, |_| -> Result, KeyError> { + Err(KeyError::CurveNotFound) + }) + .expect_err("Should have returned error for malformed signature"); + + match error { + Bep44EncodingError::SignatureError(_) => {} + _ => panic!(), + } + } + + #[test] + fn test_verify_malformed_sig() { + let message = "Hello World".as_bytes(); + + let private_key = Ed25519::generate().expect("Failed to generate Ed25519 key"); + + let mut bep44_message = + Bep44Message::new(message, |payload| -> Result, KeyError> { + private_key.sign(&payload) + }) + .unwrap(); + + // Overwrite sig with malformed signature + bep44_message.sig = vec![0, 1, 2, 3]; + let public_key = private_key + .to_public() + .expect("Failed to convert private key to public key"); + let verify_result = bep44_message.verify(public_key.as_ref()); + assert!(verify_result.is_err()); + } + + #[test] + fn test_encoded_decode() { + let message = "Hello World".as_bytes(); + + let private_key = Ed25519::generate().expect("Failed to generate Ed25519 key"); + + let bep44_message = Bep44Message::new(message, |payload| -> Result, KeyError> { + private_key.sign(&payload) + }) + .unwrap(); + + let encoded = bep44_message + .encode() + .expect("Failed to encode bep44 message"); + let decoded = Bep44Message::decode(&encoded).expect("Failed to decode bep44 message"); + + assert_eq!(bep44_message, decoded); + } + + #[test] + fn test_decode_size_limits() { + let too_short = vec![1, 2, 3]; + let error = Bep44Message::decode(&too_short) + .expect_err("Should error because bep44 message is too short"); + match error { + Bep44EncodingError::SizeError(_) => {} + _ => panic!(), + } + + let too_long = vec![0; 2000]; + let error = Bep44Message::decode(&too_long) + .expect_err("Should error because bep44 message is too long"); + match error { + Bep44EncodingError::SizeError(_) => {} + _ => panic!(), + } + } +} diff --git a/crates/dids/src/methods/dht/mod.rs b/crates/dids/src/methods/dht/mod.rs new file mode 100644 index 00000000..15e05f00 --- /dev/null +++ b/crates/dids/src/methods/dht/mod.rs @@ -0,0 +1,5 @@ +pub mod bep44; + +// TODO: Implement Method, Create, and Resolve traits and add update method +/// Concrete implementation for a did:dht DID +pub struct DidDht; diff --git a/crates/dids/src/method/jwk.rs b/crates/dids/src/methods/jwk.rs similarity index 94% rename from crates/dids/src/method/jwk.rs rename to crates/dids/src/methods/jwk.rs index 73bf4827..86f7da04 100644 --- a/crates/dids/src/method/jwk.rs +++ b/crates/dids/src/methods/jwk.rs @@ -1,12 +1,12 @@ use std::sync::Arc; use crate::identifier::Identifier; -use crate::method::{MethodError, Resolve}; +use crate::methods::{MethodError, Resolve}; use crate::resolver::ResolutionResult; -use crate::{bearer::BearerDid, method::Create}; +use crate::{bearer::BearerDid, methods::Create}; use crate::{ document::{Document, VerificationMethod}, - method::Method, + methods::Method, }; use crypto::Curve; use did_jwk::DIDJWK as SpruceDidJwkMethod; @@ -36,7 +36,7 @@ impl Create for DidJwk { let key_alias = key_manager.generate_private_key(options.curve, Some("0".to_string()))?; let public_key = key_manager.get_public_key(&key_alias)?; let public_jwk = public_key.jwk()?; - let jwk_string = serde_json::to_string(public_jwk.as_ref()).map_err(|_| { + let jwk_string = serde_json::to_string(&public_jwk).map_err(|_| { MethodError::DidCreationFailure("failed to serialize public jwk".to_string()) })?; let spruce_jwk: SpruceJwk = @@ -58,7 +58,7 @@ impl Create for DidJwk { id: verification_method_id.clone(), r#type: "JsonWebKey".to_string(), controller: uri.clone(), - public_key_jwk: public_jwk.as_ref().clone(), + public_key_jwk: public_jwk.clone(), }; let document = Document { @@ -105,7 +105,7 @@ mod tests { use keys::key_manager::local_key_manager::LocalKeyManager; fn create_did_jwk() -> BearerDid { - let key_manager = Arc::new(LocalKeyManager::new_in_memory()); + let key_manager = Arc::new(LocalKeyManager::new()); let options = DidJwkCreateOptions { curve: Curve::Ed25519, }; diff --git a/crates/dids/src/method/mod.rs b/crates/dids/src/methods/mod.rs similarity index 99% rename from crates/dids/src/method/mod.rs rename to crates/dids/src/methods/mod.rs index ae7e9534..bcea0382 100644 --- a/crates/dids/src/method/mod.rs +++ b/crates/dids/src/methods/mod.rs @@ -1,3 +1,4 @@ +pub mod dht; pub mod jwk; pub mod spruce_mappers; pub mod web; diff --git a/crates/dids/src/method/spruce_mappers/document.rs b/crates/dids/src/methods/spruce_mappers/document.rs similarity index 100% rename from crates/dids/src/method/spruce_mappers/document.rs rename to crates/dids/src/methods/spruce_mappers/document.rs diff --git a/crates/dids/src/method/spruce_mappers/document_metadata.rs b/crates/dids/src/methods/spruce_mappers/document_metadata.rs similarity index 100% rename from crates/dids/src/method/spruce_mappers/document_metadata.rs rename to crates/dids/src/methods/spruce_mappers/document_metadata.rs diff --git a/crates/dids/src/method/spruce_mappers/mod.rs b/crates/dids/src/methods/spruce_mappers/mod.rs similarity index 100% rename from crates/dids/src/method/spruce_mappers/mod.rs rename to crates/dids/src/methods/spruce_mappers/mod.rs diff --git a/crates/dids/src/method/spruce_mappers/resolution_metadata.rs b/crates/dids/src/methods/spruce_mappers/resolution_metadata.rs similarity index 100% rename from crates/dids/src/method/spruce_mappers/resolution_metadata.rs rename to crates/dids/src/methods/spruce_mappers/resolution_metadata.rs diff --git a/crates/dids/src/method/spruce_mappers/resolution_result.rs b/crates/dids/src/methods/spruce_mappers/resolution_result.rs similarity index 100% rename from crates/dids/src/method/spruce_mappers/resolution_result.rs rename to crates/dids/src/methods/spruce_mappers/resolution_result.rs diff --git a/crates/dids/src/method/web.rs b/crates/dids/src/methods/web.rs similarity index 96% rename from crates/dids/src/method/web.rs rename to crates/dids/src/methods/web.rs index 6dc55c47..86336ccf 100644 --- a/crates/dids/src/method/web.rs +++ b/crates/dids/src/methods/web.rs @@ -1,4 +1,4 @@ -use crate::method::{Method, ResolutionResult, Resolve}; +use crate::methods::{Method, ResolutionResult, Resolve}; use did_web::DIDWeb as SpruceDidWebMethod; use ssi_dids::did_resolve::{DIDResolver, ResolutionInputMetadata}; diff --git a/crates/dids/src/resolver.rs b/crates/dids/src/resolver.rs index 1cb5c0c0..4ab6b9d0 100644 --- a/crates/dids/src/resolver.rs +++ b/crates/dids/src/resolver.rs @@ -1,8 +1,8 @@ use crate::document::Document; use crate::identifier::Identifier; -use crate::method::jwk::DidJwk; -use crate::method::web::DidWeb; -use crate::method::{Method, Resolve}; +use crate::methods::jwk::DidJwk; +use crate::methods::web::DidWeb; +use crate::methods::{Method, Resolve}; use serde::{Deserialize, Serialize}; #[derive(Debug)] diff --git a/crates/jws/src/lib.rs b/crates/jws/src/lib.rs index ac9b5896..a018230a 100644 --- a/crates/jws/src/lib.rs +++ b/crates/jws/src/lib.rs @@ -19,7 +19,7 @@ pub enum JwsError { #[error(transparent)] ResolutionError(#[from] ResolutionError), #[error("algorithm not found {0}")] - AlgorithmNotFound(String), + AlgorithmNotSupported(String), #[error(transparent)] CryptoError(#[from] CryptoError), #[error("serde json error {0}")] @@ -129,7 +129,7 @@ impl CompactJws { match alg.as_str() { "EdDSA" => Ed25519::verify(&public_key, &to_verify.into_bytes(), &decoded_signature), "ES256K" => Secp256k1::verify(&public_key, &to_verify.into_bytes(), &decoded_signature), - _ => return Err(JwsError::AlgorithmNotFound(alg)), + _ => return Err(JwsError::AlgorithmNotSupported(alg)), }?; Ok(jws_decoded) } @@ -139,7 +139,7 @@ impl CompactJws { mod tests { use super::*; use crypto::Curve; - use dids::method::{ + use dids::methods::{ jwk::{DidJwk, DidJwkCreateOptions}, Create, }; @@ -149,9 +149,9 @@ mod tests { #[tokio::test] async fn test_jws_sign_and_verify() { - let key_manager = LocalKeyManager::new_in_memory(); + let key_manager = Arc::new(LocalKeyManager::new()); let bearer_did = DidJwk::create( - Arc::new(key_manager), + key_manager, DidJwkCreateOptions { curve: Curve::Ed25519, }, @@ -190,16 +190,353 @@ mod tests { } #[test] - fn test_jws_decode_error() { + fn test_jws_decode() { + let key_manager = Arc::new(LocalKeyManager::new()); + let bearer_did = DidJwk::create( + key_manager, + DidJwkCreateOptions { + curve: Curve::Ed25519, + }, + ) + .expect("failed to create bearer did"); + + let key_id = bearer_did.document.verification_method[0].id.clone(); + + let header = JwsHeader { + alg: "EdDSA".to_string(), + kid: key_id.clone(), + typ: "JWT".to_string(), + }; + let payload = json!({ + "sub": "1234567890", + "name": "John Doe", + "iat": 1516239022 + }) + .to_string() + .into_bytes(); + + let compact_jws = CompactJws::sign( + &bearer_did, + &KeySelector::KeyId { + key_id: key_id.clone(), + }, + &header, + &payload, + ) + .unwrap(); + + let decoded_jws = CompactJws::decode(&compact_jws).unwrap(); + + assert_eq!(decoded_jws.header, header); + assert_eq!(decoded_jws.payload, payload); + assert_eq!(decoded_jws.signature.is_empty(), false); + assert_eq!(decoded_jws.parts.len(), 3); + } + + #[test] + fn test_jws_decode_incorrect_parts_error() { let invalid_jws = "invalid.jws"; let result = CompactJws::decode(invalid_jws); assert!(matches!(result, Err(JwsError::IncorrectPartsLength(_)))); } - // TODO https://github.com/TBD54566975/web5-rs/issues/166 - // - not base64 encoded signature - // - base64 encoded signature but not valid cryptographic signature - // - not supported algorithm - // - did doc doesn't resolve - // - did doc missing vm + #[tokio::test] + async fn test_jws_verify_malformed_header_alg_error() { + let key_manager = Arc::new(LocalKeyManager::new()); + let bearer_did = DidJwk::create( + key_manager, + DidJwkCreateOptions { + curve: Curve::Ed25519, + }, + ) + .expect("failed to create bearer did"); + + let key_id = bearer_did.document.verification_method[0].id.clone(); + + let header = JwsHeader { + alg: "".to_string(), + kid: key_id.clone(), + typ: "JWT".to_string(), + }; + let payload = json!({ + "sub": "1234567890", + "name": "John Doe", + "iat": 1516239022 + }) + .to_string() + .into_bytes(); + + let compact_jws = CompactJws::sign( + &bearer_did, + &KeySelector::KeyId { + key_id: key_id.clone(), + }, + &header, + &payload, + ) + .unwrap(); + + let result = CompactJws::verify(&compact_jws).await; + + assert!(matches!(result, Err(JwsError::MalformedHeader(_)))); + } + + #[tokio::test] + async fn test_jws_verify_malformed_header_kid_error() { + let key_manager = Arc::new(LocalKeyManager::new()); + let bearer_did = DidJwk::create( + key_manager, + DidJwkCreateOptions { + curve: Curve::Ed25519, + }, + ) + .expect("failed to create bearer did"); + + let key_id = bearer_did.document.verification_method[0].id.clone(); + + let header = JwsHeader { + alg: "EdDSA".to_string(), + kid: "".to_string(), + typ: "JWT".to_string(), + }; + let payload = json!({ + "sub": "1234567890", + "name": "John Doe", + "iat": 1516239022 + }) + .to_string() + .into_bytes(); + + let compact_jws = CompactJws::sign( + &bearer_did, + &KeySelector::KeyId { + key_id: key_id.clone(), + }, + &header, + &payload, + ) + .unwrap(); + + let result = CompactJws::verify(&compact_jws).await; + + assert!(matches!(result, Err(JwsError::MalformedHeader(_)))); + } + + #[tokio::test] + async fn test_jws_verify_algorithm_not_supported_error() { + let key_manager = Arc::new(LocalKeyManager::new()); + let bearer_did = DidJwk::create( + key_manager, + DidJwkCreateOptions { + curve: Curve::Ed25519, + }, + ) + .expect("failed to create bearer did"); + + let key_id = bearer_did.document.verification_method[0].id.clone(); + + let header = JwsHeader { + alg: "UNSUPPORTED_ALG".to_string(), + kid: key_id.clone(), + typ: "JWT".to_string(), + }; + let payload = json!({ + "sub": "1234567890", + "name": "John Doe", + "iat": 1516239022 + }) + .to_string() + .into_bytes(); + + let compact_jws = CompactJws::sign( + &bearer_did, + &KeySelector::KeyId { + key_id: key_id.clone(), + }, + &header, + &payload, + ) + .unwrap(); + + let result = CompactJws::verify(&compact_jws).await; + + assert!(matches!(result, Err(JwsError::AlgorithmNotSupported(_)))); + } + + #[tokio::test] + async fn test_jws_verify_resolution_error() { + let key_manager = Arc::new(LocalKeyManager::new()); + let bearer_did = DidJwk::create( + key_manager, + DidJwkCreateOptions { + curve: Curve::Ed25519, + }, + ) + .expect("failed to create bearer did"); + + let key_id = bearer_did.document.verification_method[0].id.clone(); + + let header = JwsHeader { + alg: "EdDSA".to_string(), + kid: "did:jwk:123#123".to_string(), + typ: "JWT".to_string(), + }; + let payload = json!({ + "sub": "1234567890", + "name": "John Doe", + "iat": 1516239022 + }) + .to_string() + .into_bytes(); + + let compact_jws = CompactJws::sign( + &bearer_did, + &KeySelector::KeyId { + key_id: key_id.clone(), + }, + &header, + &payload, + ) + .unwrap(); + + let result = CompactJws::verify(&compact_jws).await; + + assert!(matches!(result, Err(JwsError::ResolutionError(_)))); + } + + #[tokio::test] + async fn test_jws_verify_document_verification_method_error() { + let key_manager = Arc::new(LocalKeyManager::new()); + let bearer_did = DidJwk::create( + key_manager, + DidJwkCreateOptions { + curve: Curve::Ed25519, + }, + ) + .expect("failed to create bearer did"); + + let key_id = bearer_did.document.verification_method[0].id.clone(); + + let header = JwsHeader { + alg: "EdDSA".to_string(), + kid: key_id.clone() + "123", + typ: "JWT".to_string(), + }; + let payload = json!({ + "sub": "1234567890", + "name": "John Doe", + "iat": 1516239022 + }) + .to_string() + .into_bytes(); + + let compact_jws = CompactJws::sign( + &bearer_did, + &KeySelector::KeyId { + key_id: key_id.clone(), + }, + &header, + &payload, + ) + .unwrap(); + + let result = CompactJws::verify(&compact_jws).await; + + assert!(matches!(result, Err(JwsError::DocumentError(_)))); + } + + #[tokio::test] + async fn test_jws_verify_signature_decode_error() { + let key_manager = Arc::new(LocalKeyManager::new()); + let bearer_did = DidJwk::create( + key_manager, + DidJwkCreateOptions { + curve: Curve::Ed25519, + }, + ) + .expect("failed to create bearer did"); + + let key_id = bearer_did.document.verification_method[0].id.clone(); + + let header = JwsHeader { + alg: "EdDSA".to_string(), + kid: key_id.clone(), + typ: "JWT".to_string(), + }; + let payload = json!({ + "sub": "1234567890", + "name": "John Doe", + "iat": 1516239022 + }) + .to_string() + .into_bytes(); + + let compact_jws = CompactJws::sign( + &bearer_did, + &KeySelector::KeyId { + key_id: key_id.clone(), + }, + &header, + &payload, + ) + .unwrap() + + "123"; + + let result = CompactJws::verify(&compact_jws).await; + + assert!(matches!(result, Err(JwsError::DecodeError(_)))); + } + + #[tokio::test] + async fn test_jws_verify_signature_crypto_error() { + let key_manager = Arc::new(LocalKeyManager::new()); + let bearer_did = DidJwk::create( + key_manager, + DidJwkCreateOptions { + curve: Curve::Ed25519, + }, + ) + .expect("failed to create bearer did"); + + let key_id = bearer_did.document.verification_method[0].id.clone(); + + let header = JwsHeader { + alg: "EdDSA".to_string(), + kid: key_id.clone(), + typ: "JWT".to_string(), + }; + let payload = json!({ + "sub": "1234567890", + "name": "John Doe", + "iat": 1516239022 + }) + .to_string() + .into_bytes(); + + let invalid_key_manager = Arc::new(LocalKeyManager::new()); + let invalid_bearer_did = DidJwk::create( + invalid_key_manager, + DidJwkCreateOptions { + curve: Curve::Ed25519, + }, + ) + .expect("failed to create bearer did"); + let invalid_key_id = invalid_bearer_did.document.verification_method[0] + .id + .clone(); + + let compact_jws = CompactJws::sign( + &invalid_bearer_did, + &KeySelector::KeyId { + key_id: invalid_key_id.clone(), + }, + &header, + &payload, + ) + .unwrap(); + + let result = CompactJws::verify(&compact_jws).await; + + assert!(matches!(result, Err(JwsError::CryptoError(_)))); + } } diff --git a/crates/jwt/src/jws.rs b/crates/jwt/src/jws.rs index 6008a8ae..f9256c37 100644 --- a/crates/jwt/src/jws.rs +++ b/crates/jwt/src/jws.rs @@ -74,7 +74,7 @@ mod tests { use crypto::Curve; use dids::{ document::KeySelector, - method::{ + methods::{ jwk::{DidJwk, DidJwkCreateOptions}, Create, }, @@ -84,9 +84,9 @@ mod tests { #[tokio::test] async fn test_sign_and_verify() { - let key_manager = LocalKeyManager::new_in_memory(); + let key_manager = Arc::new(LocalKeyManager::new()); let bearer_did = DidJwk::create( - Arc::new(key_manager), + key_manager, DidJwkCreateOptions { curve: Curve::Ed25519, }, diff --git a/crates/keys/src/key.rs b/crates/keys/src/key.rs index dc8b4f0c..d676ab47 100644 --- a/crates/keys/src/key.rs +++ b/crates/keys/src/key.rs @@ -20,7 +20,7 @@ pub enum KeyError { pub trait Key: Send + Sync { fn alias(&self) -> Result; - fn jwk(&self) -> Result, KeyError>; + fn jwk(&self) -> Result; } pub trait PublicKey: Key + Send + Sync { @@ -42,8 +42,8 @@ impl Key for Jwk { Ok(thumbprint) } - fn jwk(&self) -> Result, KeyError> { - Ok(Arc::new(self.clone())) + fn jwk(&self) -> Result { + Ok(self.clone()) } } diff --git a/crates/keys/src/key_manager/key_store/in_memory_key_store.rs b/crates/keys/src/key_manager/key_store/in_memory_key_store.rs deleted file mode 100644 index 059e4bf6..00000000 --- a/crates/keys/src/key_manager/key_store/in_memory_key_store.rs +++ /dev/null @@ -1,103 +0,0 @@ -use crate::key::{PrivateKey, PublicKey}; -use crate::key_manager::key_store::{KeyStore, KeyStoreError}; -use crypto::ed25519::Ed25519; -use crypto::secp256k1::Secp256k1; -use crypto::{Curve, CurveOperations}; -use std::collections::HashMap; -use std::sync::{Arc, RwLock}; - -pub struct InMemoryKeyStore { - map: RwLock>>, -} - -impl InMemoryKeyStore { - pub fn new() -> Self { - Self { - map: RwLock::new(HashMap::new()), - } - } -} - -impl KeyStore for InMemoryKeyStore { - fn generate_new( - &self, - curve: Curve, - key_alias: Option, - ) -> Result { - let private_key = Arc::new(match curve { - Curve::Ed25519 => Ed25519::generate(), - Curve::Secp256k1 => Secp256k1::generate(), - }?); - let key_alias = match key_alias { - Some(key_alias) => key_alias, - None => private_key.compute_thumbprint()?, - }; - let mut map_lock = self.map.write().map_err(|e| { - KeyStoreError::InternalKeyStoreError(format!("unable to acquire Mutex lock: {}", e)) - })?; - map_lock.insert(key_alias.clone(), private_key); - Ok(key_alias) - } - - fn get_all_aliases(&self) -> Result, KeyStoreError> { - let map_lock = self.map.read().map_err(|e| { - KeyStoreError::InternalKeyStoreError(format!("Unable to acquire Mutex lock: {}", e)) - })?; - let aliases = map_lock.keys().cloned().collect(); - Ok(aliases) - } - - fn sign(&self, key_alias: &str, payload: &[u8]) -> Result, KeyStoreError> { - let map_lock = self.map.read().map_err(|e| { - KeyStoreError::InternalKeyStoreError(format!("Unable to acquire Mutex lock: {}", e)) - })?; - let private_key = map_lock - .get(key_alias) - .ok_or(KeyStoreError::KeyNotFound(key_alias.to_string()))?; - - let signed_payload = private_key.sign(payload)?; - - Ok(signed_payload) - } - - fn get_public_key(&self, key_alias: &str) -> Result, KeyStoreError> { - let map_lock = self.map.read().map_err(|e| { - KeyStoreError::InternalKeyStoreError(format!("Unable to acquire Mutex lock: {}", e)) - })?; - let private_key = map_lock - .get(key_alias) - .ok_or(KeyStoreError::KeyNotFound(key_alias.to_string()))?; - let public_key = private_key.to_public()?; - Ok(public_key) - } - - fn export_private_keys(&self) -> Result>, KeyStoreError> { - let map_lock = self.map.read().map_err(|e| { - KeyStoreError::InternalKeyStoreError(format!("Unable to acquire Mutex lock: {}", e)) - })?; - let private_keys = map_lock.values().cloned().collect(); - Ok(private_keys) - } - - fn import_private_keys( - &self, - private_keys: Vec>, - ) -> Result<(), KeyStoreError> { - let mut map_lock = self.map.write().map_err(|e| { - KeyStoreError::InternalKeyStoreError(format!("Unable to acquire Mutex lock: {}", e)) - })?; - - for key in private_keys { - let key_alias = key.alias()?; - map_lock.insert(key_alias, key); - } - - Ok(()) - } -} - -impl Default for InMemoryKeyStore { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/keys/src/key_manager/key_store/mod.rs b/crates/keys/src/key_manager/key_store/mod.rs deleted file mode 100644 index 93ab3b0c..00000000 --- a/crates/keys/src/key_manager/key_store/mod.rs +++ /dev/null @@ -1,50 +0,0 @@ -pub mod in_memory_key_store; - -use crate::key::{KeyError, PrivateKey, PublicKey}; -use crypto::{CryptoError, Curve}; -use jwk::JwkError; -use std::sync::Arc; - -#[derive(thiserror::Error, Debug, Clone, PartialEq)] -pub enum KeyStoreError { - #[error("{0}")] - InternalKeyStoreError(String), - #[error(transparent)] - KeyError(#[from] KeyError), - #[error("key not found {0}")] - KeyNotFound(String), - #[error(transparent)] - CryptoError(#[from] CryptoError), - #[error(transparent)] - JwkError(#[from] JwkError), - #[error("{0}")] - UnsupportedOperation(String), -} - -// Trait for storing and retrieving private keys. -// -// Implementations of this trait should be thread-safe and allow for concurrent access. -pub trait KeyStore: Send + Sync { - fn generate_new( - &self, - curve: Curve, - key_alias: Option, - ) -> Result; - fn get_all_aliases(&self) -> Result, KeyStoreError>; - fn sign(&self, key_alias: &str, payload: &[u8]) -> Result, KeyStoreError>; - fn get_public_key(&self, key_alias: &str) -> Result, KeyStoreError>; - - fn export_private_keys(&self) -> Result>, KeyStoreError> { - Err(KeyStoreError::UnsupportedOperation( - "exporting private keys is not supported".to_string(), - )) - } - fn import_private_keys( - &self, - _private_keys: Vec>, - ) -> Result<(), KeyStoreError> { - Err(KeyStoreError::UnsupportedOperation( - "importing private keys is not supported".to_string(), - )) - } -} diff --git a/crates/keys/src/key_manager/local_key_manager.rs b/crates/keys/src/key_manager/local_key_manager.rs index 9ab61f4d..653ed21f 100644 --- a/crates/keys/src/key_manager/local_key_manager.rs +++ b/crates/keys/src/key_manager/local_key_manager.rs @@ -1,28 +1,35 @@ use crate::key::{PrivateKey, PublicKey}; -use crate::key_manager::key_store::in_memory_key_store::InMemoryKeyStore; -use crate::key_manager::key_store::KeyStore; use crate::key_manager::{KeyManager, KeyManagerError}; -use crypto::Curve; -use std::sync::Arc; +use crypto::ed25519::Ed25519; +use crypto::secp256k1::Secp256k1; +use crypto::{Curve, CurveOperations}; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use super::KeyImporter; /// Implementation of the [`KeyManager`] trait with key generation local to the device/platform it /// is being run. Key storage is provided by a [`KeyStore`] trait implementation, allowing the keys /// to be stored wherever is most appropriate for the application. pub struct LocalKeyManager { - key_store: Arc, + map: RwLock>>, } impl LocalKeyManager { - /// Constructs a new `LocalKeyManager` that stores keys in the provided `KeyStore`. - pub fn new(key_store: Arc) -> Self { - Self { key_store } - } - - pub fn new_in_memory() -> Self { + /// Constructs a new `LocalKeyManager` that stores keys in memory. + pub fn new() -> Self { Self { - key_store: Arc::new(InMemoryKeyStore::new()), + map: RwLock::new(HashMap::new()), } } + + pub fn export_private_keys(&self) -> Result>, KeyManagerError> { + let map_lock = self.map.read().map_err(|e| { + KeyManagerError::InternalKeyStoreError(format!("Unable to acquire Mutex lock: {}", e)) + })?; + let private_keys = map_lock.values().cloned().collect(); + Ok(private_keys) + } } impl KeyManager for LocalKeyManager { @@ -31,41 +38,81 @@ impl KeyManager for LocalKeyManager { curve: Curve, key_alias: Option, ) -> Result { - let key_alias = self.key_store.generate_new(curve, key_alias)?; + let private_key = Arc::new(match curve { + Curve::Ed25519 => Ed25519::generate(), + Curve::Secp256k1 => Secp256k1::generate(), + }?); + let key_alias = match key_alias { + Some(key_alias) => key_alias, + None => private_key.compute_thumbprint()?, + }; + let mut map_lock = self.map.write().map_err(|e| { + KeyManagerError::InternalKeyStoreError(format!("unable to acquire Mutex lock: {}", e)) + })?; + map_lock.insert(key_alias.clone(), private_key); Ok(key_alias) } fn get_public_key(&self, key_alias: &str) -> Result, KeyManagerError> { - let public_key = self.key_store.get_public_key(key_alias)?; + let map_lock = self.map.read().map_err(|e| { + KeyManagerError::InternalKeyStoreError(format!("Unable to acquire Mutex lock: {}", e)) + })?; + let private_key = map_lock + .get(key_alias) + .ok_or(KeyManagerError::KeyNotFound(key_alias.to_string()))?; + let public_key = private_key.to_public()?; Ok(public_key) } fn sign(&self, key_alias: &str, payload: &[u8]) -> Result, KeyManagerError> { - let signed_payload = self.key_store.sign(key_alias, payload)?; - Ok(signed_payload) - } + let map_lock: std::sync::RwLockReadGuard>> = + self.map.read().map_err(|e| { + KeyManagerError::InternalKeyStoreError(format!( + "Unable to acquire Mutex lock: {}", + e + )) + })?; + let private_key = map_lock + .get(key_alias) + .ok_or(KeyManagerError::KeyNotFound(key_alias.to_string()))?; + + let signed_payload = private_key.sign(payload)?; - fn export_private_keys(&self) -> Result>, KeyManagerError> { - let private_keys = self.key_store.export_private_keys()?; - Ok(private_keys) + Ok(signed_payload) } +} - fn import_private_keys( +impl KeyImporter for LocalKeyManager { + fn import_with_alias( &self, - private_keys: Vec>, + private_key: Arc, + key_alias: &str, ) -> Result<(), KeyManagerError> { - self.key_store.import_private_keys(private_keys)?; + let mut map_lock = self.map.write().map_err(|e| { + KeyManagerError::InternalKeyStoreError(format!("Unable to acquire Mutex lock: {}", e)) + })?; + + map_lock.insert(key_alias.to_owned(), private_key); + Ok(()) } } +impl Default for LocalKeyManager { + fn default() -> Self { + Self::new() + } +} + #[cfg(test)] mod tests { + use crate::key::Key; + use super::*; #[test] fn test_generate_private_key() { - let key_manager = LocalKeyManager::new_in_memory(); + let key_manager = LocalKeyManager::new(); key_manager .generate_private_key(Curve::Ed25519, None) @@ -84,7 +131,7 @@ mod tests { #[test] fn test_get_public_key() { - let key_manager = LocalKeyManager::new_in_memory(); + let key_manager = LocalKeyManager::new(); let key_alias = key_manager .generate_private_key(Curve::Ed25519, None) @@ -97,7 +144,7 @@ mod tests { #[test] fn test_sign() { - let key_manager = LocalKeyManager::new_in_memory(); + let key_manager = LocalKeyManager::new(); let key_alias = key_manager .generate_private_key(Curve::Ed25519, None) .unwrap(); @@ -110,4 +157,53 @@ mod tests { let public_key = key_manager.get_public_key(&key_alias).unwrap(); assert!(!public_key.verify(payload, &signature).is_err()); } + + #[test] + fn test_import() { + let key_manager = LocalKeyManager::new(); + let private_key = Arc::new(Ed25519::generate().unwrap()); + + let key_alias = key_manager + .import(private_key.clone()) + .expect("Failed to import private key"); + let default_alias = private_key + .alias() + .expect("Failed to generate private key alias"); + assert_eq!(key_alias, default_alias); + + let key_manager_public_key = key_manager + .get_public_key(&key_alias) + .expect("Failed to get public key") + .jwk() + .expect("Failed to get alias of public key"); + let public_key = private_key + .to_public() + .expect("Failed to convert private key to public") + .jwk() + .expect("Failed to get alias of public key"); + assert_eq!(public_key, key_manager_public_key) + } + + #[test] + fn test_import_with_alias() { + let key_manager = LocalKeyManager::new(); + let private_key = Arc::new(Ed25519::generate().unwrap()); + + let key_alias = "1234".to_string(); + key_manager + .import_with_alias(private_key.clone(), &key_alias) + .expect("Failed to import private key with alias"); + + let key_manager_public_key = key_manager + .get_public_key(&key_alias) + .expect("Failed to get public key") + .jwk() + .expect("Failed to get alias of public key"); + let public_key = private_key + .to_public() + .expect("Failed to convert private key to public") + .jwk() + .expect("Failed to get alias of public key"); + assert_eq!(public_key, key_manager_public_key) + } } diff --git a/crates/keys/src/key_manager/mod.rs b/crates/keys/src/key_manager/mod.rs index 200f9f5c..b84b66c5 100644 --- a/crates/keys/src/key_manager/mod.rs +++ b/crates/keys/src/key_manager/mod.rs @@ -1,21 +1,24 @@ -pub mod key_store; pub mod local_key_manager; use crate::key::{KeyError, PrivateKey, PublicKey}; -use crate::key_manager::key_store::KeyStoreError; -use crypto::Curve; +use crypto::{CryptoError, Curve}; +use jwk::JwkError; use std::sync::Arc; #[derive(thiserror::Error, Debug, Clone, PartialEq)] pub enum KeyManagerError { + #[error(transparent)] + CryptoError(#[from] CryptoError), + #[error(transparent)] + JwkError(#[from] JwkError), #[error("Key generation failed")] KeyGenerationFailed, - #[error("Signing key not found in KeyManager")] - SigningKeyNotFound, #[error(transparent)] KeyError(#[from] KeyError), - #[error(transparent)] - KeyStoreError(#[from] KeyStoreError), + #[error("{0}")] + InternalKeyStoreError(String), + #[error("key not found {0}")] + KeyNotFound(String), } /// A key management trait for generating, storing, and utilizing keys private keys and their @@ -39,23 +42,22 @@ pub trait KeyManager: Send + Sync { /// Signs the provided payload using the private key identified by the provided `key_alias`. fn sign(&self, key_alias: &str, payload: &[u8]) -> Result, KeyManagerError>; +} - /// Exports all private keys managed by this key manager. - /// Default implementation returns an error indicating the feature is not supported. - fn export_private_keys(&self) -> Result>, KeyManagerError> { - Err(KeyStoreError::UnsupportedOperation( - "exporting private keys is not supported".to_string(), - ))? - } - - /// Imports a list of private keys into the key manager. - /// Default implementation returns an error indicating the feature is not supported. - fn import_private_keys( +pub trait KeyImporter: KeyManager { + /// Imports a private key with a custom key alias into the key manager. + /// Returns the key alias + fn import_with_alias( &self, - _private_keys: Vec>, - ) -> Result<(), KeyManagerError> { - Err(KeyStoreError::UnsupportedOperation( - "importing private keys is not supported".to_string(), - ))? + private_key: Arc, + key_alias: &str, + ) -> Result<(), KeyManagerError>; + + /// Imports a private key into the key manager using private_key.alias() as the alias + /// Returns the key alias + fn import(&self, private_key: Arc) -> Result { + let key_alias = private_key.alias()?; + self.import_with_alias(private_key, &key_alias)?; + Ok(key_alias) } } diff --git a/crates/test-helpers/Cargo.toml b/crates/test-helpers/Cargo.toml new file mode 100644 index 00000000..db29fc1a --- /dev/null +++ b/crates/test-helpers/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "test-helpers" +version = "0.1.0" +edition = "2021" +homepage.workspace = true +repository.workspace = true +license-file.workspace = true + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +serde_with = { workspace = true } \ No newline at end of file diff --git a/crates/test-helpers/src/lib.rs b/crates/test-helpers/src/lib.rs new file mode 100644 index 00000000..0a6271bc --- /dev/null +++ b/crates/test-helpers/src/lib.rs @@ -0,0 +1,29 @@ +use std::{fs, path::PathBuf}; + +use serde::de::DeserializeOwned; + +#[derive(Debug, serde::Deserialize)] +pub struct TestVector { + pub description: String, + pub input: I, + pub output: O, +} + +#[derive(Debug, serde::Deserialize)] +pub struct TestVectorFile { + pub description: String, + pub vectors: Vec>, +} + +impl TestVectorFile { + pub fn load_from_path(file_path: &str) -> TestVectorFile + where + I: DeserializeOwned, + O: DeserializeOwned, + { + let mut vector_path = PathBuf::from("../../web5-spec/test-vectors/"); + vector_path.push(file_path); + let data = fs::read_to_string(vector_path).unwrap(); + serde_json::from_str(&data).unwrap() + } +}