use crate::error::AcdpError;
use crate::types::primitives::ContentHash;
use base64::{engine::general_purpose::STANDARD, Engine};
use ed25519_dalek::{Signer as _, SigningKey as DalekSigningKey};
use zeroize::ZeroizeOnDrop;
#[derive(ZeroizeOnDrop)]
pub struct SigningKey(DalekSigningKey);
impl SigningKey {
pub fn from_bytes(bytes: &[u8; 32]) -> Self {
Self(DalekSigningKey::from_bytes(bytes))
}
pub fn from_slice(bytes: &[u8]) -> Result<Self, AcdpError> {
let arr: [u8; 32] = bytes.try_into().map_err(|_| {
AcdpError::InvalidSignature(format!(
"signing key must be 32 bytes, got {}",
bytes.len()
))
})?;
Ok(Self::from_bytes(&arr))
}
pub fn generate() -> Self {
Self(DalekSigningKey::generate(&mut rand_core::OsRng))
}
pub fn sign_content_hash(&self, hash: &ContentHash) -> String {
let sig = self.0.sign(hash.as_str().as_bytes());
STANDARD.encode(sig.to_bytes())
}
pub fn verifying_key_bytes(&self) -> [u8; 32] {
self.0.verifying_key().to_bytes()
}
pub fn seed_bytes(&self) -> [u8; 32] {
self.0.to_bytes()
}
pub fn sign_string(&self, input: &str) -> String {
let sig = self.0.sign(input.as_bytes());
STANDARD.encode(sig.to_bytes())
}
}
impl std::fmt::Debug for SigningKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("SigningKey(…)")
}
}
pub struct P256SigningKey(p256::ecdsa::SigningKey);
impl P256SigningKey {
pub fn generate() -> Self {
Self(p256::ecdsa::SigningKey::random(&mut rand_core::OsRng))
}
pub fn from_bytes(bytes: &[u8; 32]) -> Result<Self, AcdpError> {
p256::ecdsa::SigningKey::from_bytes(bytes.into())
.map(Self)
.map_err(|e| AcdpError::SchemaViolation(format!("p256 key parse: {e}")))
}
pub fn from_slice(bytes: &[u8]) -> Result<Self, AcdpError> {
let arr: [u8; 32] = bytes.try_into().map_err(|_| {
AcdpError::SchemaViolation(format!(
"p256 signing key must be 32 bytes, got {}",
bytes.len()
))
})?;
Self::from_bytes(&arr)
}
pub fn sign_content_hash(&self, hash: &ContentHash) -> String {
use p256::ecdsa::{signature::Signer as _, Signature};
let sig: Signature = self.0.sign(hash.as_str().as_bytes());
STANDARD.encode(sig.to_bytes())
}
pub fn seed_bytes(&self) -> [u8; 32] {
let fb = self.0.to_bytes();
let mut out = [0u8; 32];
out.copy_from_slice(fb.as_ref());
out
}
pub fn sign_string(&self, input: &str) -> String {
use p256::ecdsa::{signature::Signer as _, Signature};
let sig: Signature = self.0.sign(input.as_bytes());
STANDARD.encode(sig.to_bytes())
}
pub fn verifying_key_sec1(&self) -> Vec<u8> {
self.0
.verifying_key()
.to_encoded_point(false)
.as_bytes()
.to_vec()
}
pub fn verifying_key_jwk(&self) -> serde_json::Value {
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
let sec1 = self.verifying_key_sec1();
let x_b64 = URL_SAFE_NO_PAD.encode(&sec1[1..33]);
let y_b64 = URL_SAFE_NO_PAD.encode(&sec1[33..65]);
serde_json::json!({
"kty": "EC",
"crv": "P-256",
"x": x_b64,
"y": y_b64,
})
}
pub fn did_verification_method(&self, method_id: &str, controller: &str) -> serde_json::Value {
serde_json::json!({
"id": method_id,
"type": "JsonWebKey2020",
"controller": controller,
"publicKeyJwk": self.verifying_key_jwk(),
})
}
}
impl std::fmt::Debug for P256SigningKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("P256SigningKey(…)")
}
}
#[derive(Debug)]
pub enum AcdpSigningKey {
Ed25519(SigningKey),
P256(P256SigningKey),
}
impl AcdpSigningKey {
pub fn sign_content_hash(&self, hash: &ContentHash) -> (&'static str, String) {
match self {
Self::Ed25519(k) => ("ed25519", k.sign_content_hash(hash)),
Self::P256(k) => ("ecdsa-p256", k.sign_content_hash(hash)),
}
}
pub fn algorithm(&self) -> &'static str {
match self {
Self::Ed25519(_) => "ed25519",
Self::P256(_) => "ecdsa-p256",
}
}
}
impl From<SigningKey> for AcdpSigningKey {
fn from(k: SigningKey) -> Self {
Self::Ed25519(k)
}
}
impl From<P256SigningKey> for AcdpSigningKey {
fn from(k: P256SigningKey) -> Self {
Self::P256(k)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ed25519_generate_produces_distinct_keys() {
let a = SigningKey::generate();
let b = SigningKey::generate();
assert_ne!(
a.verifying_key_bytes(),
b.verifying_key_bytes(),
"OsRng-backed generate() must not yield identical keys"
);
}
#[test]
fn p256_generate_produces_distinct_keys() {
let a = P256SigningKey::generate();
let b = P256SigningKey::generate();
assert_ne!(
a.verifying_key_sec1(),
b.verifying_key_sec1(),
"OsRng-backed P256 generate() must not yield identical keys"
);
}
#[test]
fn p256_sign_verify_round_trip() {
use crate::crypto::verify::verify_ecdsa_p256;
let key = P256SigningKey::generate();
let hash = ContentHash(
"sha256:f170150ddbf59d99794e7797824591b374d459782084597b644ecc57a41031b5".into(),
);
let sig = key.sign_content_hash(&hash);
assert_eq!(sig.len(), 88, "p256 wire signature MUST be 88 base64 chars");
let pub_sec1 = key.verifying_key_sec1();
verify_ecdsa_p256(&pub_sec1, &sig, hash.as_str())
.expect("round-trip p256 signature must verify");
}
#[test]
fn p256_verifying_key_jwk_round_trips_to_sec1() {
use crate::did::document::VerificationMethod;
let key = P256SigningKey::generate();
let jwk = key.verifying_key_jwk();
assert_eq!(jwk["kty"], "EC");
assert_eq!(jwk["crv"], "P-256");
let vm = VerificationMethod {
id: "did:web:agents.example.com:test#key-1".into(),
method_type: "JsonWebKey2020".into(),
controller: "did:web:agents.example.com:test".into(),
public_key_jwk: Some(jwk),
public_key_multibase: None,
};
let sec1_via_jwk = vm.ecdsa_p256_public_key_sec1().unwrap();
assert_eq!(sec1_via_jwk, key.verifying_key_sec1());
assert_eq!(vm.declared_algorithm(), Some("ecdsa-p256"));
}
#[test]
fn p256_did_verification_method_assembles() {
use crate::did::document::VerificationMethod;
let key = P256SigningKey::generate();
let vm_value = key.did_verification_method(
"did:web:agents.example.com:alice#key-1",
"did:web:agents.example.com:alice",
);
assert_eq!(vm_value["type"], "JsonWebKey2020");
let vm: VerificationMethod = serde_json::from_value(vm_value).unwrap();
assert_eq!(vm.id, "did:web:agents.example.com:alice#key-1");
assert_eq!(vm.declared_algorithm(), Some("ecdsa-p256"));
let sec1 = vm.ecdsa_p256_public_key_sec1().unwrap();
assert_eq!(sec1, key.verifying_key_sec1());
}
#[test]
fn p256_sign_against_wrong_message_fails() {
use crate::crypto::verify::verify_ecdsa_p256;
let key = P256SigningKey::generate();
let hash = ContentHash("sha256:".to_owned() + &"a".repeat(64));
let sig = key.sign_content_hash(&hash);
let pub_sec1 = key.verifying_key_sec1();
let err =
verify_ecdsa_p256(&pub_sec1, &sig, "sha256:0000000000000000").expect_err("must fail");
assert!(matches!(err, AcdpError::InvalidSignature(_)));
}
#[test]
fn p256_der_encoded_signature_rejected() {
use crate::crypto::verify::verify_ecdsa_p256;
let key = P256SigningKey::generate();
let hash = ContentHash("sha256:".to_owned() + &"f".repeat(64));
use p256::ecdsa::signature::Signer as _;
let der: p256::ecdsa::DerSignature = key.0.sign(hash.as_str().as_bytes());
let sig_b64 = STANDARD.encode(der.as_bytes());
let pub_sec1 = key.verifying_key_sec1();
let err = verify_ecdsa_p256(&pub_sec1, &sig_b64, hash.as_str())
.expect_err("DER-encoded p256 sig MUST be rejected");
assert!(matches!(err, AcdpError::InvalidSignature(_)), "got {err:?}");
}
#[test]
fn acdp_signing_key_emits_correct_algorithm() {
let ed = AcdpSigningKey::Ed25519(SigningKey::generate());
let p2 = AcdpSigningKey::P256(P256SigningKey::generate());
assert_eq!(ed.algorithm(), "ed25519");
assert_eq!(p2.algorithm(), "ecdsa-p256");
let hash = ContentHash("sha256:".to_owned() + &"a".repeat(64));
let (alg_ed, _) = ed.sign_content_hash(&hash);
let (alg_p2, _) = p2.sign_content_hash(&hash);
assert_eq!(alg_ed, "ed25519");
assert_eq!(alg_p2, "ecdsa-p256");
}
const ED25519_TEST_SEED: [u8; 32] = [0u8; 32];
const ED25519_TEST_PUB_HEX: &str =
"3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29";
#[test]
fn sign_and_verify_ed25519_golden() {
use crate::crypto::verify::verify_ed25519;
let key = SigningKey::from_bytes(&ED25519_TEST_SEED);
let hash = ContentHash(
"sha256:f170150ddbf59d99794e7797824591b374d459782084597b644ecc57a41031b5".into(),
);
let sig_b64 = key.sign_content_hash(&hash);
assert_eq!(
sig_b64,
"ErkbV+FUdn49TgF3zJ3RBe3AmyGxLVAQdMjlhabUfM96qendmWwdVodX/SV3O3aKLypbUu6gmb5Npt3O/w7nDQ=="
);
let pub_bytes: [u8; 32] = hex::decode(ED25519_TEST_PUB_HEX)
.unwrap()
.try_into()
.unwrap();
verify_ed25519(&pub_bytes, &sig_b64, hash.as_str()).unwrap();
}
#[test]
fn seed_bytes_round_trip() {
let key = SigningKey::from_bytes(&ED25519_TEST_SEED);
assert_eq!(key.seed_bytes(), ED25519_TEST_SEED);
let rebuilt = SigningKey::from_bytes(&key.seed_bytes());
let hash = ContentHash(
"sha256:f170150ddbf59d99794e7797824591b374d459782084597b644ecc57a41031b5".into(),
);
assert_eq!(
key.sign_content_hash(&hash),
rebuilt.sign_content_hash(&hash),
"key reconstructed from seed_bytes must produce an identical signature"
);
}
#[test]
fn sign_string_verifies_directly() {
use crate::crypto::verify::verify_ed25519;
let key = SigningKey::from_bytes(&ED25519_TEST_SEED);
let signing_input = "acdp-registry-auth:v1:nonce-abc:\
did:web:agents.example.com:test-producer:\
registry.example.com:1748000000";
let sig_b64 = key.sign_string(signing_input);
assert_eq!(sig_b64.len(), 88);
let pub_bytes: [u8; 32] = hex::decode(ED25519_TEST_PUB_HEX)
.unwrap()
.try_into()
.unwrap();
verify_ed25519(&pub_bytes, &sig_b64, signing_input).unwrap();
verify_ed25519(&pub_bytes, &sig_b64, "different-input")
.expect_err("sign_string output MUST be specific to the signed input");
}
#[test]
fn p256_seed_bytes_round_trip() {
let seed: [u8; 32] =
hex::decode("c9afa9d845ba75166b5c215767b1d6934e50c3db36e89b127b8a622b120f6721")
.unwrap()
.try_into()
.unwrap();
let key = P256SigningKey::from_bytes(&seed).unwrap();
assert_eq!(key.seed_bytes(), seed);
let rebuilt = P256SigningKey::from_bytes(&key.seed_bytes()).unwrap();
let hash = ContentHash("sha256:".to_owned() + &"a".repeat(64));
assert_eq!(
key.sign_content_hash(&hash),
rebuilt.sign_content_hash(&hash),
"key reconstructed from seed_bytes must produce an identical signature"
);
}
#[test]
fn p256_sign_string_verifies_directly() {
use crate::crypto::verify::verify_ecdsa_p256;
let key = P256SigningKey::generate();
let signing_input = "acdp-registry-auth:v1:nonce-abc:\
did:web:agents.example.com:test-producer:\
registry.example.com:1748000000";
let sig_b64 = key.sign_string(signing_input);
assert_eq!(sig_b64.len(), 88);
let sec1 = key.verifying_key_sec1();
verify_ecdsa_p256(&sec1, &sig_b64, signing_input).unwrap();
verify_ecdsa_p256(&sec1, &sig_b64, "different-input")
.expect_err("sign_string output MUST be specific to the signed input");
}
#[test]
fn sign_and_verify_ecdsa_p256_golden() {
use crate::crypto::verify::verify_ecdsa_p256;
let mut seed = [0u8; 32];
seed[31] = 1; let key = P256SigningKey::from_bytes(&seed).unwrap();
let hash = ContentHash(
"sha256:f170150ddbf59d99794e7797824591b374d459782084597b644ecc57a41031b5".into(),
);
let sig_b64 = key.sign_content_hash(&hash);
assert_eq!(
sig_b64,
"O+b+E5OIecgwCnjDyTqsiwwy3VTdBHbVhiRR9k3FAPZHvLJ5dyYYVPPUWbl0dKDdgKMw2dWrnKWRANJVoS9vNw=="
);
let sec1_hex = hex::encode(key.verifying_key_sec1());
assert_eq!(
sec1_hex,
"046b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296\
4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5"
);
verify_ecdsa_p256(&key.verifying_key_sec1(), &sig_b64, hash.as_str()).unwrap();
}
}