From 468852a433c38bc85ec8741e737f44fd7c32ec85 Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 11 Mar 2026 14:16:10 -0600 Subject: [PATCH 1/2] apply p256k key fix to vcomp --- integrity-signer/src/signer/mod.rs | 2 + integrity-signer/src/signer/p256_jwk.rs | 58 ++++++++++++++++++ integrity-signer/src/signer/p256_signer.rs | 43 +++---------- integrity-signer/src/signer/vcomp_notary.rs | 68 ++++++++++++++++++--- 4 files changed, 128 insertions(+), 43 deletions(-) create mode 100644 integrity-signer/src/signer/p256_jwk.rs diff --git a/integrity-signer/src/signer/mod.rs b/integrity-signer/src/signer/mod.rs index 381ffc2..93452c8 100644 --- a/integrity-signer/src/signer/mod.rs +++ b/integrity-signer/src/signer/mod.rs @@ -12,6 +12,8 @@ pub mod akv_signer; pub mod auth_service_signer; #[cfg(feature = "signer-ed25519")] pub mod ed25519_signer; +#[cfg(any(feature = "signer-p256", feature = "signer-vcomp-notary"))] +pub(crate) mod p256_jwk; #[cfg(feature = "signer-p256")] pub mod p256_signer; #[cfg(feature = "signer-secp256k1")] diff --git a/integrity-signer/src/signer/p256_jwk.rs b/integrity-signer/src/signer/p256_jwk.rs new file mode 100644 index 0000000..2b9c7d2 --- /dev/null +++ b/integrity-signer/src/signer/p256_jwk.rs @@ -0,0 +1,58 @@ +use anyhow::{anyhow, Result}; +use base64::engine::{general_purpose::URL_SAFE_NO_PAD as BASE64_URL_NO_PAD, Engine}; +use did_key::{Document, KeyFormat}; +use p256::{ + ecdsa::{SigningKey, VerifyingKey}, + EncodedPoint, +}; +#[cfg(feature = "signer-vcomp-notary")] +use p256::{elliptic_curve::sec1::ToEncodedPoint, PublicKey}; + +pub(crate) fn p256_encoded_point_from_secret_key(secret_key: &[u8]) -> Result { + let signing_key = + SigningKey::from_bytes(secret_key.into()).map_err(|e| anyhow!("Invalid P-256 key: {e}"))?; + let verifying_key = VerifyingKey::from(&signing_key); + + Ok(verifying_key.to_encoded_point(false)) +} + +#[cfg(feature = "signer-vcomp-notary")] +pub(crate) fn p256_encoded_point_from_public_key(public_key: &[u8]) -> Result { + let public_key = PublicKey::from_sec1_bytes(public_key) + .map_err(|e| anyhow!("Invalid P-256 public key: {e}"))?; + + Ok(public_key.to_encoded_point(false)) +} + +pub(crate) fn fix_p256_jwk_from_encoded_point( + did_doc: &mut Document, + encoded_point: &EncodedPoint, + secret_key: Option<&[u8]>, +) -> Result<()> { + let x_bytes = encoded_point + .x() + .ok_or_else(|| anyhow!("Failed to get x coordinate"))?; + let y_bytes = encoded_point + .y() + .ok_or_else(|| anyhow!("Failed to get y coordinate"))?; + + let x_b64 = BASE64_URL_NO_PAD.encode(x_bytes); + let y_b64 = BASE64_URL_NO_PAD.encode(y_bytes); + let d_b64 = secret_key.map(|secret_key| BASE64_URL_NO_PAD.encode(secret_key)); + + for vm in &mut did_doc.verification_method { + if let Some(KeyFormat::JWK(ref mut jwk)) = vm.public_key { + jwk.x = Some(x_b64.clone()); + jwk.y = Some(y_b64.clone()); + } + if let Some(KeyFormat::JWK(ref mut jwk)) = vm.private_key { + jwk.x = Some(x_b64.clone()); + jwk.y = Some(y_b64.clone()); + if let Some(ref d_b64) = d_b64 { + jwk.d = Some(d_b64.clone()); + } + } + } + + Ok(()) +} diff --git a/integrity-signer/src/signer/p256_signer.rs b/integrity-signer/src/signer/p256_signer.rs index f8f6553..893a6f2 100644 --- a/integrity-signer/src/signer/p256_signer.rs +++ b/integrity-signer/src/signer/p256_signer.rs @@ -1,11 +1,12 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; -use base64::engine::{general_purpose::URL_SAFE_NO_PAD as BASE64_URL_NO_PAD, Engine}; -use did_key::{CoreSign, DIDCore, Document, Generate, KeyFormat, KeyMaterial, P256KeyPair}; -use p256::ecdsa::{SigningKey, VerifyingKey}; +use did_key::{CoreSign, DIDCore, Document, Generate, KeyMaterial, P256KeyPair}; use serde::{Deserialize, Serialize}; -use crate::signer::Signer; +use crate::signer::{ + p256_jwk::{fix_p256_jwk_from_encoded_point, p256_encoded_point_from_secret_key}, + Signer, +}; /// Signer implementation using P-256 (secp256r1) elliptic curve. #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] @@ -67,38 +68,8 @@ impl P256Signer { /// and omits the y field. This function extracts the correct x and y coordinates from /// the uncompressed public key and updates the JWK. fn fix_p256_jwk(did_doc: &mut Document, secret_key: &[u8]) -> Result<()> { - // Derive the public key from the secret key to get uncompressed coordinates - let signing_key = - SigningKey::from_bytes(secret_key.into()).map_err(|e| anyhow!("Invalid P-256 key: {e}"))?; - let verifying_key = VerifyingKey::from(&signing_key); - - // Get the uncompressed public key point (65 bytes: 04 prefix + 32 bytes x + 32 bytes y) - let encoded_point = verifying_key.to_encoded_point(false); - let x_bytes = encoded_point - .x() - .ok_or_else(|| anyhow!("Failed to get x coordinate"))?; - let y_bytes = encoded_point - .y() - .ok_or_else(|| anyhow!("Failed to get y coordinate"))?; - - let x_b64 = BASE64_URL_NO_PAD.encode(x_bytes); - let y_b64 = BASE64_URL_NO_PAD.encode(y_bytes); - let d_b64 = BASE64_URL_NO_PAD.encode(secret_key); - - // Update the verification method's JWK - for vm in &mut did_doc.verification_method { - if let Some(KeyFormat::JWK(ref mut jwk)) = vm.public_key { - jwk.x = Some(x_b64.clone()); - jwk.y = Some(y_b64.clone()); - } - if let Some(KeyFormat::JWK(ref mut jwk)) = vm.private_key { - jwk.x = Some(x_b64.clone()); - jwk.y = Some(y_b64.clone()); - jwk.d = Some(d_b64.clone()); - } - } - - Ok(()) + let encoded_point = p256_encoded_point_from_secret_key(secret_key)?; + fix_p256_jwk_from_encoded_point(did_doc, &encoded_point, Some(secret_key)) } #[async_trait] diff --git a/integrity-signer/src/signer/vcomp_notary.rs b/integrity-signer/src/signer/vcomp_notary.rs index 75c8f21..3a3074a 100644 --- a/integrity-signer/src/signer/vcomp_notary.rs +++ b/integrity-signer/src/signer/vcomp_notary.rs @@ -12,7 +12,10 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use tokio::net::TcpStream; -use crate::signer::Signer; +use crate::signer::{ + p256_jwk::{fix_p256_jwk_from_encoded_point, p256_encoded_point_from_public_key}, + Signer, +}; /// Signer implementation for verified computing notary services. #[derive(Clone, Debug, Serialize, Deserialize)] @@ -38,6 +41,18 @@ fn strip_urn_cid(cid: &str) -> &str { } impl VCompNotarySigner { + fn did_doc_from_public_key(pub_key: &[u8]) -> Result { + let key_pair = P256KeyPair::from_public_key(pub_key); + let mut did_doc = key_pair.get_did_document(did_key::Config { + use_jose_format: true, + serialize_secrets: true, + }); + let encoded_point = p256_encoded_point_from_public_key(pub_key)?; + fix_p256_jwk_from_encoded_point(&mut did_doc, &encoded_point, None)?; + + Ok(did_doc) + } + /// Creates a new VCompNotarySigner by connecting to a verified computing notary service. /// /// # Arguments @@ -69,12 +84,7 @@ impl VCompNotarySigner { let pub_key = hex::decode(pub_key)?; log::trace!("Importing a secp256r1 VComp Notary signer"); - let key_pair = P256KeyPair::from_public_key(&pub_key); - - let did_doc = key_pair.get_did_document(did_key::Config { - use_jose_format: true, - serialize_secrets: true, - }); + let did_doc = Self::did_doc_from_public_key(&pub_key)?; let response = client .get(format!("{url}/get_dids")) @@ -232,3 +242,47 @@ impl Signer for VCompNotarySigner { Ok(Some(self.did_doc.clone())) } } + +#[cfg(test)] +mod tests { + use base64::engine::{general_purpose::URL_SAFE_NO_PAD as BASE64_URL_NO_PAD, Engine}; + use did_key::KeyFormat; + use p256::ecdsa::SigningKey; + + use super::*; + + #[test] + fn compressed_vcomp_public_key_repairs_verification_method_jwk() { + let signing_key = SigningKey::from_bytes((&[7u8; 32]).into()).unwrap(); + let verifying_key = signing_key.verifying_key(); + let compressed_pub_key = verifying_key.to_encoded_point(true); + let uncompressed_pub_key = verifying_key.to_encoded_point(false); + + let key_pair = P256KeyPair::from_public_key(compressed_pub_key.as_bytes()); + let broken_did_doc = key_pair.get_did_document(did_key::Config { + use_jose_format: true, + serialize_secrets: true, + }); + + let fixed_did_doc = + VCompNotarySigner::did_doc_from_public_key(compressed_pub_key.as_bytes()).unwrap(); + + let expected_x = BASE64_URL_NO_PAD.encode(uncompressed_pub_key.x().unwrap()); + let expected_y = BASE64_URL_NO_PAD.encode(uncompressed_pub_key.y().unwrap()); + let compressed_b64 = BASE64_URL_NO_PAD.encode(compressed_pub_key.as_bytes()); + + let broken_jwk = match &broken_did_doc.verification_method[0].public_key { + Some(KeyFormat::JWK(jwk)) => jwk, + _ => panic!("expected JWK verification method"), + }; + assert_eq!(broken_jwk.x.as_deref(), Some(compressed_b64.as_str())); + assert_eq!(broken_jwk.y, None); + + let fixed_jwk = match &fixed_did_doc.verification_method[0].public_key { + Some(KeyFormat::JWK(jwk)) => jwk, + _ => panic!("expected JWK verification method"), + }; + assert_eq!(fixed_jwk.x.as_deref(), Some(expected_x.as_str())); + assert_eq!(fixed_jwk.y.as_deref(), Some(expected_y.as_str())); + } +} From 02f9701d046c39fb90a9027b880084f3bee1ea94 Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 11 Mar 2026 14:16:24 -0600 Subject: [PATCH 2/2] remove unused uuid crate --- .envrc | 2 +- Cargo.lock | 2 -- integrity-lineage-models/Cargo.toml | 3 +-- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.envrc b/.envrc index 0ee3c12..6e911c7 100644 --- a/.envrc +++ b/.envrc @@ -1,4 +1,4 @@ -use flake +use flake . if [[ -f .env ]]; then dotenv .env diff --git a/Cargo.lock b/Cargo.lock index 3cd606d..c5e56a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3623,7 +3623,6 @@ dependencies = [ "ssi-json-ld", "tokio", "utoipa", - "uuid", ] [[package]] @@ -7624,7 +7623,6 @@ dependencies = [ "quote", "regex", "syn 2.0.114", - "uuid", ] [[package]] diff --git a/integrity-lineage-models/Cargo.toml b/integrity-lineage-models/Cargo.toml index 352997a..b183d75 100644 --- a/integrity-lineage-models/Cargo.toml +++ b/integrity-lineage-models/Cargo.toml @@ -23,8 +23,7 @@ serde_jcs = "0.1.0" serde_json = "1.0" ssi = { version = "0.7", features = ["w3c", "http-did", "example-http-issuer", "ed25519", "secp256r1"] } ssi-json-ld = "0.2" -utoipa = { version = "3", features = ["axum_extras", "openapi_extensions", "uuid"] } -uuid = { version = "1.4", features = ["v4", "serde"] } +utoipa = { version = "3", features = ["axum_extras", "openapi_extensions"] } [target.'cfg(target_arch = "wasm32")'.dependencies] blake3 = "1.5"