diff --git a/Cargo.lock b/Cargo.lock index 0c4b629..20dafaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4954,7 +4954,8 @@ version = "0.15.0" dependencies = [ "ed25519-dalek", "multibase", - "ssi-claims", + "ssi-claims-core", + "ssi-crypto", "ssi-dids-core", "ssi-jwk", "ssi-jws", @@ -5015,6 +5016,7 @@ dependencies = [ "itertools 0.13.0", "serde", "ssi-claims", + "ssi-crypto", "ssi-dids-core", "ssi-json-ld", "ssi-vc", diff --git a/Cargo.toml b/Cargo.toml index db2bf3e..184970f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,8 @@ categories = ["cryptography", "wasm"] [workspace.dependencies] ssi-claims = { git = "https://github.com/vaultie/ssi", default-features = false, features = ["ed25519", "w3c"] } +ssi-claims-core = { git = "https://github.com/vaultie/ssi", default-features = false } +ssi-crypto = { git = "https://github.com/vaultie/ssi", default-features = false } ssi-dids-core = { git = "https://github.com/vaultie/ssi", default-features = false } ssi-json-ld = { git = "https://github.com/vaultie/ssi", default-features = false } ssi-jwk = { git = "https://github.com/vaultie/ssi", default-features = false, features = ["ed25519"] } diff --git a/crates/teddybear-crypto/Cargo.toml b/crates/teddybear-crypto/Cargo.toml index 32e2b6a..d214026 100644 --- a/crates/teddybear-crypto/Cargo.toml +++ b/crates/teddybear-crypto/Cargo.toml @@ -7,7 +7,8 @@ repository.workspace = true categories.workspace = true [dependencies] -ssi-claims = { workspace = true } +ssi-claims-core = { workspace = true } +ssi-crypto = { workspace = true } ssi-dids-core = { workspace = true } ssi-jwk = { workspace = true } ssi-jws = { workspace = true } diff --git a/crates/teddybear-crypto/src/lib.rs b/crates/teddybear-crypto/src/lib.rs index 1c59b11..0eee7cd 100644 --- a/crates/teddybear-crypto/src/lib.rs +++ b/crates/teddybear-crypto/src/lib.rs @@ -1,9 +1,9 @@ use std::{borrow::Cow, sync::Arc}; -use ed25519_dalek::SigningKey; +use ed25519_dalek::{SigningKey, VerifyingKey}; use ssi_dids_core::{ document::DIDVerificationMethod, method_resolver::VerificationMethodDIDResolver, - resolution::Options, DIDResolver, Document, Unexpected, DID, + resolution::Options, DIDResolver, DIDURLBuf, Document, Unexpected, DID, DIDURL, }; use ssi_jwk::{Algorithm, Base64urlUInt, OctetParams, Params}; use ssi_jws::{ @@ -41,11 +41,23 @@ pub enum Error { #[derive(Clone, Debug)] pub struct KeyInfo { + id: DIDURLBuf, jwk: JWK, } +impl KeyInfo { + pub fn id(&self) -> &DIDURL { + self.id.as_did_url() + } +} + #[derive(Clone, Debug)] -pub struct Public; +pub struct Public { + // verifying_key here in not necessary at the moment, but it might be useful later. + // FIXME: Remove the #[allow(dead_code)] as soon as the field starts being used. + #[allow(dead_code)] + verifying_key: VerifyingKey, +} #[derive(Clone, Debug)] pub struct Private { @@ -143,48 +155,27 @@ impl Ed25519 { impl Ed25519 { pub async fn from_jwk(jwk: JWK) -> Result { - let (document, ed25519, x25519) = Ed25519::<()>::parts_from_jwk(jwk).await?; + let did = DidKey.generate(&jwk).ok_or(Error::MissingPrivateKey)?; + let (document, ed25519, x25519, verifying_key) = + Ed25519::<()>::parts_from_did(&did).await?; Ok(Self { document, ed25519, x25519, - raw: Public, + raw: Public { verifying_key }, }) } pub async fn from_did(did: &str) -> Result { let did = DID::new(did).map_err(|e| e.1)?; - - let document = VerificationMethodDIDResolver::<_, Ed25519VerificationKey2020>::new(DidKey) - .resolve_with(did, Options::default()) - .await? - .document - .into_document(); - - let ed25519 = extract_key_info( - String::from("Ed25519"), - document - .verification_method - .first() - .expect("teddybear-did-key should provide at least one ed25519 key"), - )?; - - let x25519 = extract_key_info( - String::from("X25519"), - document - .verification_relationships - .key_agreement - .first() - .and_then(|val| val.as_value()) - .expect("teddybear-did-key should provide at least one x25519 key"), - )?; + let (document, ed25519, x25519, verifying_key) = Self::parts_from_did(did).await?; Ok(Self { document, ed25519, x25519, - raw: Public, + raw: Public { verifying_key }, }) } } @@ -229,9 +220,7 @@ impl Ed25519 { } async fn parts_from_jwk(mut jwk: JWK) -> Result<(Document, KeyInfo, KeyInfo), Error> { - let did = DidKey - .generate(&jwk) - .expect("ed25519 key should produce a correct did document"); + let did = DidKey.generate(&jwk).ok_or(Error::MissingPrivateKey)?; let document = VerificationMethodDIDResolver::<_, Ed25519VerificationKey2020>::new(DidKey) .resolve_with(did.as_did(), Options::default()) @@ -239,18 +228,47 @@ impl Ed25519 { .document .into_document(); - jwk.key_id = Some( + let id = document + .verification_method + .first() + .expect("at least one key is expected") + .id + .clone(); + + // JWK structure is preserved as much as possible, so we can't use + // extract_key_info for acquiring Ed25519 key from DID. + jwk.key_id = Some(id.to_string()); + jwk.algorithm = Some(Algorithm::EdDSA); + + let (x25519, _) = extract_key_info::( + document + .verification_relationships + .key_agreement + .first() + .and_then(|val| val.as_value()) + .expect("teddybear-did-key should provide at least one x25519 key"), + )?; + + Ok((document, KeyInfo { id, jwk }, x25519)) + } + + async fn parts_from_did( + did: &DID, + ) -> Result<(Document, KeyInfo, KeyInfo, VerifyingKey), Error> { + let document = VerificationMethodDIDResolver::<_, Ed25519VerificationKey2020>::new(DidKey) + .resolve_with(did, Options::default()) + .await? + .document + .into_document(); + + let (ed25519, verifying_key) = extract_key_info::( document .verification_method .first() - .expect("at least one key is expected") - .id - .to_string(), - ); - jwk.algorithm = Some(Algorithm::EdDSA); + .expect("teddybear-did-key should provide at least one ed25519 key"), + )?; - let x25519 = extract_key_info( - String::from("X25519"), + let (x25519, _) = extract_key_info::( document .verification_relationships .key_agreement @@ -259,7 +277,7 @@ impl Ed25519 { .expect("teddybear-did-key should provide at least one x25519 key"), )?; - Ok((document, KeyInfo { jwk }, x25519)) + Ok((document, ed25519, x25519, verifying_key)) } } @@ -281,7 +299,7 @@ impl Signer for Ed25519 { async fn for_method( &self, method: Cow<'_, Ed25519VerificationKey2020>, - ) -> Result, ssi_claims::SignatureError> { + ) -> Result, ssi_claims_core::SignatureError> { if method.id.as_str() != self.ed25519_did() { return Ok(None); } @@ -312,11 +330,41 @@ pub fn verify_jws_with_embedded_jwk(jws: &str) -> Result<(JWK, Vec), Error> Ok((key, jws.payload)) } +struct Ed25519KeyInfo; +struct X25519KeyInfo; + +trait RawExtract { + type Output; + + const CURVE: &'static str; + + fn extract(value: &[u8]) -> Result; +} + +impl RawExtract for Ed25519KeyInfo { + type Output = VerifyingKey; + + const CURVE: &'static str = "Ed25519"; + + fn extract(value: &[u8]) -> Result { + Ok(VerifyingKey::try_from(value)?) + } +} + +impl RawExtract for X25519KeyInfo { + type Output = (); + + const CURVE: &'static str = "X25519"; + + fn extract(_: &[u8]) -> Result { + Ok(()) + } +} + #[inline] -fn extract_key_info( - curve: String, +fn extract_key_info( verification_method: &DIDVerificationMethod, -) -> Result { +) -> Result<(KeyInfo, T::Output), Error> { let public_key_multibase = verification_method .properties .get("publicKeyMultibase") @@ -325,14 +373,18 @@ fn extract_key_info( let public_key = multibase::decode(public_key_multibase)?.1; + let raw_output = T::extract(&public_key[2..])?; + let mut jwk = JWK::from(Params::OKP(OctetParams { - curve, + curve: T::CURVE.to_string(), public_key: Base64urlUInt(public_key[2..].to_owned()), private_key: None, })); - jwk.key_id = Some(verification_method.id.to_string()); + let id = verification_method.id.clone(); + + jwk.key_id = Some(id.to_string()); jwk.algorithm = Some(Algorithm::EdDSA); - Ok(KeyInfo { jwk }) + Ok((KeyInfo { id, jwk }, raw_output)) } diff --git a/crates/teddybear-vc/Cargo.toml b/crates/teddybear-vc/Cargo.toml index e847bce..fe75f50 100644 --- a/crates/teddybear-vc/Cargo.toml +++ b/crates/teddybear-vc/Cargo.toml @@ -8,6 +8,7 @@ categories.workspace = true [dependencies] ssi-claims = { workspace = true } +ssi-crypto = { workspace = true } ssi-dids-core = { workspace = true } ssi-json-ld = { workspace = true } ssi-vc = { workspace = true } diff --git a/crates/teddybear-vc/src/lib.rs b/crates/teddybear-vc/src/lib.rs index 9c40336..fddec3d 100644 --- a/crates/teddybear-vc/src/lib.rs +++ b/crates/teddybear-vc/src/lib.rs @@ -58,6 +58,12 @@ pub async fn issue_vc<'a>( credential.validate_credential(¶ms)?; + let verification_method = Ed25519VerificationKey2020::from_public_key( + key.ed25519.id().as_iri().to_owned(), + key.document().id.as_uri().to_owned(), + key.raw_signing_key().verifying_key(), + ); + Ok(Ed25519Signature2020 .sign_with( SignatureEnvironment { @@ -67,7 +73,7 @@ pub async fn issue_vc<'a>( CredentialRef(credential), resolver, key, - ProofOptions::default(), + ProofOptions::from_method(ReferenceOrOwned::Owned(verification_method)), (), ) .await?) @@ -94,6 +100,12 @@ pub async fn present_vp<'a>( let resolver = CustomResolver::new(DidKey); + let verification_method = Ed25519VerificationKey2020::from_public_key( + key.ed25519.id().as_iri().to_owned(), + key.document().id.as_uri().to_owned(), + key.raw_signing_key().verifying_key(), + ); + Ok(Ed25519Signature2020 .sign_with( SignatureEnvironment { @@ -106,6 +118,7 @@ pub async fn present_vp<'a>( ProofOptions { proof_purpose: ProofPurpose::Authentication, domains: domain.map(|val| vec![val]).unwrap_or_default(), + verification_method: Some(ReferenceOrOwned::Owned(verification_method)), challenge, ..Default::default() }, diff --git a/flake.lock b/flake.lock index d8eecdc..54f0f1e 100644 --- a/flake.lock +++ b/flake.lock @@ -60,6 +60,18 @@ "type": "github" } }, + "identity-context": { + "flake": false, + "locked": { + "narHash": "sha256-eKihBHNN2dhmsVoi/Axntx9pF48ecy9IZhfqvECM+Ks=", + "type": "file", + "url": "https://w3c.credential.nexus/identity.jsonld" + }, + "original": { + "type": "file", + "url": "https://w3c.credential.nexus/identity.jsonld" + } + }, "nix-filter": { "locked": { "lastModified": 1710156097, @@ -96,6 +108,7 @@ "crane": "crane", "fenix": "fenix", "flake-utils": "flake-utils", + "identity-context": "identity-context", "nix-filter": "nix-filter", "nixpkgs": "nixpkgs" } diff --git a/flake.nix b/flake.nix index 022c852..e1d69d5 100644 --- a/flake.nix +++ b/flake.nix @@ -33,6 +33,11 @@ owner = "numtide"; repo = "flake-utils"; }; + + identity-context = { + url = "https://w3c.credential.nexus/identity.jsonld"; + flake = false; + }; }; outputs = { @@ -41,6 +46,7 @@ fenix, nix-filter, flake-utils, + identity-context, ... }: flake-utils.lib.eachDefaultSystem ( @@ -168,10 +174,10 @@ inherit cjs esm uni; e2e-test = pkgs.callPackage ./nix/node-testing.nix { - inherit uni; + inherit identity-context uni; src = ./tests; - yarnLockHash = "sha256-KdtWLlP0jHiQXyWcUuE3sKjrgfHnGRlObg7u4g4FBNE="; + yarnLockHash = "sha256-Zcm5jr6hHq34vmRBq8ATLaqzg9svBIjL41OKcH5Enf4="; }; unit-test = craneLib.cargoTest (nativeArgs diff --git a/nix/node-testing.nix b/nix/node-testing.nix index ce9b54e..555d8e5 100644 --- a/nix/node-testing.nix +++ b/nix/node-testing.nix @@ -2,6 +2,7 @@ fetchurl, fetchYarnDeps, fixup-yarn-lock, + identity-context, nodejs-slim, src, stdenvNoCC, @@ -40,6 +41,8 @@ stdenvNoCC.mkDerivation { certificate = ./data/crt.der; + identityContext = identity-context; + postPatch = '' export HOME=$(mktemp -d) diff --git a/tests/package.json b/tests/package.json index 413daa1..c19ad3e 100644 --- a/tests/package.json +++ b/tests/package.json @@ -15,7 +15,7 @@ "@digitalbazaar/ed25519-verification-key-2020": "^4.1.0", "@digitalbazaar/minimal-cipher": "^5.1.1", "@digitalbazaar/x25519-key-agreement-key-2020": "^3.0.1", - "@vaultie/teddybear": "^0.14.4", + "@vaultie/teddybear": "^0.15.0", "jose": "^5.4.1" } } diff --git a/tests/src/ed25519.test.ts b/tests/src/ed25519.test.ts index 1098e02..4d20cde 100644 --- a/tests/src/ed25519.test.ts +++ b/tests/src/ed25519.test.ts @@ -28,10 +28,25 @@ describe("can execute common private key operations", () => { it("can extract JWK values", async () => { const key = await PrivateEd25519.generate(); - key.toEd25519PublicJWK(); - key.toX25519PublicJWK(); - key.toEd25519PrivateJWK(); - key.toX25519PrivateJWK(); + const pubEd25519 = key.toEd25519PublicJWK().toJSON(); + expect(pubEd25519).toHaveProperty("crv", "Ed25519"); + expect(pubEd25519).toHaveProperty("x"); + expect(pubEd25519).not.toHaveProperty("d"); + + const prvEd25519 = key.toEd25519PrivateJWK().toJSON(); + expect(prvEd25519).toHaveProperty("crv", "Ed25519"); + expect(prvEd25519).toHaveProperty("x"); + expect(prvEd25519).toHaveProperty("d"); + + const pubX25519 = key.toX25519PublicJWK().toJSON(); + expect(pubX25519).toHaveProperty("crv", "X25519"); + expect(pubX25519).toHaveProperty("x"); + expect(pubX25519).not.toHaveProperty("d"); + + const prvX25519 = key.toX25519PrivateJWK().toJSON(); + expect(prvX25519).toHaveProperty("crv", "X25519"); + expect(prvX25519).toHaveProperty("x"); + expect(prvX25519).toHaveProperty("d"); }); it("can sign JWS values", async () => { diff --git a/tests/src/vc.test.ts b/tests/src/vc.test.ts index 5614875..7911520 100644 --- a/tests/src/vc.test.ts +++ b/tests/src/vc.test.ts @@ -1,28 +1,105 @@ -import { ContextLoader } from "@vaultie/teddybear"; -import { describe, it } from "vitest"; +import { ContextLoader, PrivateEd25519 } from "@vaultie/teddybear"; +import { readFile } from "node:fs/promises"; +import { TestAPI, describe, it } from "vitest"; + +const vcTest: TestAPI<{ contextLoader: ContextLoader; key: PrivateEd25519 }> = + it.extend({ + contextLoader: async ({}, use) => { + await use( + new ContextLoader({ + "https://w3c.credential.nexus/identity": ( + await readFile(process.env.identityContext!) + ).toString("utf-8"), + }), + ); + }, + key: async ({}, use) => { + const key = await PrivateEd25519.generate(); + await use(key); + }, + }); describe("can execute verifiable credentials operations", () => { it("can create a default context loader", () => new ContextLoader()); - it("can create a custom context loader", () => - new ContextLoader({ - "https://example.com": JSON.stringify({ - "@context": { - "@version": 1.1, - "@protected": true, - VaultieRecordReference: { - "@id": "https://vaultie.io/VaultieRecordReference/v1", - "@context": { - "@version": 1.1, - "@protected": true, - id: "@id", - record: { - "@id": "https://vaultie.io/VaultieRecordReference/v1#record", - "@type": "https://schema.org#Text", - }, + vcTest("can issue a test credential", ({ contextLoader, key }) => + key.issueVC( + { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://w3c.credential.nexus/identity", + ], + type: ["VerifiableCredential", "Identity"], + id: "https://example.com/test", + issuer: key.documentDID(), + issuanceDate: new Date().toISOString(), + credentialSubject: { + type: "Person", + givenName: "John", + familyName: "Doe", + birthDate: "2000-01-01", + document: { + type: "Document", + identifier: { + type: "Identifier", + idType: "documentNumber", + idValue: "123-123-123", + }, + documentType: "identificationCard", + issuingCountry: "AA", + issuingState: "AA", + issuanceDate: "2020-01-01", + expirationDate: "2030-01-01", + }, + }, + }, + contextLoader, + ), + ); + + vcTest("can sign a test presentation", async ({ contextLoader, key }) => { + const verifiableCredential = await key.issueVC( + { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://w3c.credential.nexus/identity", + ], + type: ["VerifiableCredential", "Identity"], + id: "https://example.com/test", + issuer: key.documentDID(), + issuanceDate: new Date().toISOString(), + credentialSubject: { + type: "Person", + givenName: "John", + familyName: "Doe", + birthDate: "2000-01-01", + document: { + type: "Document", + identifier: { + type: "Identifier", + idType: "documentNumber", + idValue: "123-123-123", }, + documentType: "identificationCard", + issuingCountry: "AA", + issuingState: "AA", + issuanceDate: "2020-01-01", + expirationDate: "2030-01-01", }, }, - }), - })); + }, + contextLoader, + ); + + await key.issueVP( + { + "@context": ["https://www.w3.org/ns/credentials/v2"], + type: ["VerifiablePresentation"], + verifiableCredential, + }, + contextLoader, + undefined, + undefined, + ); + }); }); diff --git a/tests/yarn.lock b/tests/yarn.lock index cbc1d09..ffb7bd0 100644 --- a/tests/yarn.lock +++ b/tests/yarn.lock @@ -317,10 +317,10 @@ dependencies: undici-types "~5.26.4" -"@vaultie/teddybear@^0.14.4": - version "0.14.4" - resolved "https://registry.yarnpkg.com/@vaultie/teddybear/-/teddybear-0.14.4.tgz#8b872890bfb1f217d3ef77c3aeff8041da1e1ad8" - integrity sha512-rLq1lzEb40iDsStXfMA4PREB/BEZ860MF0huohIRzHIYtznP68qX4P77ro7MWj2bnlmTCOr+Nsy7EQTXiF29kA== +"@vaultie/teddybear@^0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@vaultie/teddybear/-/teddybear-0.15.0.tgz#67cc07292dbc51aa83a18798a83fc6df57ce281d" + integrity sha512-tetTWfyjTFZqTNhvXUBfd8eR+UOQSRSs7VXmhrvZtll416xTqqsNaOGk9VHMqI5WD5OOntooy9vbEuawY1u+IA== "@vitest/expect@1.6.0": version "1.6.0"