use acdp_primitives::error::AcdpError;
const MULTICODEC_ED25519: [u8; 2] = [0xed, 0x01];
const MULTICODEC_P256: [u8; 2] = [0x80, 0x24];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DidKeyMaterial {
Ed25519([u8; 32]),
EcdsaP256([u8; 33]),
}
impl DidKeyMaterial {
pub fn algorithm(&self) -> &'static str {
match self {
Self::Ed25519(_) => "ed25519",
Self::EcdsaP256(_) => "ecdsa-p256",
}
}
}
pub fn resolve_did_key(did: &str) -> Result<DidKeyMaterial, AcdpError> {
let msi = did
.strip_prefix("did:key:")
.ok_or_else(|| AcdpError::KeyResolution(format!("not a did:key DID: {did}")))?;
decode_multibase_key(msi)
}
fn decode_multibase_key(msi: &str) -> Result<DidKeyMaterial, AcdpError> {
let rest = msi.strip_prefix('z').ok_or_else(|| {
AcdpError::KeyResolution(format!(
"did:key requires the 'z' (base58btc) multibase prefix, got '{msi}'"
))
})?;
let decoded = bs58::decode(rest)
.into_vec()
.map_err(|e| AcdpError::KeyResolution(format!("did:key base58 decode: {e}")))?;
match decoded.get(0..2) {
Some(p) if p == MULTICODEC_ED25519 => {
let key: [u8; 32] = decoded[2..].try_into().map_err(|_| {
AcdpError::KeyResolution(format!(
"did:key ed25519 key must be 32 bytes after the multicodec prefix, got {}",
decoded.len().saturating_sub(2)
))
})?;
Ok(DidKeyMaterial::Ed25519(key))
}
Some(p) if p == MULTICODEC_P256 => {
let key: [u8; 33] = decoded[2..].try_into().map_err(|_| {
AcdpError::KeyResolution(format!(
"did:key p256 key must be a 33-byte SEC1-compressed point after the \
multicodec prefix, got {}",
decoded.len().saturating_sub(2)
))
})?;
if !matches!(key[0], 0x02 | 0x03) {
return Err(AcdpError::KeyResolution(
"did:key p256 key must be SEC1-compressed (leading 0x02/0x03)".into(),
));
}
Ok(DidKeyMaterial::EcdsaP256(key))
}
_ => Err(AcdpError::KeyResolution(
"did:key multicodec prefix is neither ed25519-pub (0xed 0x01) \
nor p256-pub (0x80 0x24)"
.into(),
)),
}
}
pub fn resolve_did_key_url(key_id: &str) -> Result<DidKeyMaterial, AcdpError> {
let (did_part, fragment) = key_id
.split_once('#')
.ok_or_else(|| AcdpError::KeyResolution(format!("key_id '{key_id}' has no '#fragment'")))?;
let msi = did_part
.strip_prefix("did:key:")
.ok_or_else(|| AcdpError::KeyResolution(format!("not a did:key DID URL: {key_id}")))?;
if fragment != msi {
return Err(AcdpError::KeyResolution(format!(
"did:key fragment '#{fragment}' must equal the method-specific identifier \
'{msi}' (the did:key document's only verification method is the key itself)"
)));
}
decode_multibase_key(msi)
}
pub fn did_key_from_ed25519(public_key: &[u8; 32]) -> String {
let mut prefixed = Vec::with_capacity(2 + 32);
prefixed.extend_from_slice(&MULTICODEC_ED25519);
prefixed.extend_from_slice(public_key);
format!("did:key:z{}", bs58::encode(&prefixed).into_string())
}
pub fn did_key_from_p256_sec1(sec1: &[u8]) -> Result<String, AcdpError> {
let vk = p256::ecdsa::VerifyingKey::from_sec1_bytes(sec1)
.map_err(|e| AcdpError::KeyResolution(format!("p256 SEC1 parse: {e}")))?;
let compressed = vk.to_sec1_point(true);
let mut prefixed = Vec::with_capacity(2 + 33);
prefixed.extend_from_slice(&MULTICODEC_P256);
prefixed.extend_from_slice(compressed.as_bytes());
Ok(format!(
"did:key:z{}",
bs58::encode(&prefixed).into_string()
))
}
pub fn did_key_document(did: &str) -> Result<crate::DidDocument, AcdpError> {
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
let material = resolve_did_key(did)?;
let vm_id = did_key_url(did)?;
let (method_type, public_key_jwk, public_key_multibase) = match material {
DidKeyMaterial::Ed25519(_) => (
"Multikey",
None,
Some(did.trim_start_matches("did:key:").to_string()),
),
DidKeyMaterial::EcdsaP256(compressed) => {
let vk = p256::ecdsa::VerifyingKey::from_sec1_bytes(&compressed)
.map_err(|e| AcdpError::KeyResolution(format!("did:key p256 point: {e}")))?;
let point = vk.to_sec1_point(false);
let bytes = point.as_bytes();
(
"JsonWebKey2020",
Some(serde_json::json!({
"kty": "EC",
"crv": "P-256",
"x": URL_SAFE_NO_PAD.encode(&bytes[1..33]),
"y": URL_SAFE_NO_PAD.encode(&bytes[33..65]),
})),
None,
)
}
};
Ok(crate::DidDocument {
id: did.to_string(),
verification_methods: vec![crate::VerificationMethod {
id: vm_id.clone(),
method_type: method_type.to_string(),
controller: did.to_string(),
public_key_jwk,
public_key_multibase,
}],
assertion_method: vec![crate::AssertionMethodRef::Id(vm_id)],
services: Vec::new(),
})
}
pub fn did_key_url(did: &str) -> Result<String, AcdpError> {
let msi = did
.strip_prefix("did:key:")
.ok_or_else(|| AcdpError::KeyResolution(format!("not a did:key DID: {did}")))?;
Ok(format!("{did}#{msi}"))
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_PUB_HEX: &str = "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29";
fn test_pub() -> [u8; 32] {
hex::decode(TEST_PUB_HEX).unwrap().try_into().unwrap()
}
#[test]
fn ed25519_round_trip() {
let did = did_key_from_ed25519(&test_pub());
assert!(did.starts_with("did:key:z"));
match resolve_did_key(&did).unwrap() {
DidKeyMaterial::Ed25519(k) => assert_eq!(k, test_pub()),
other => panic!("expected Ed25519, got {other:?}"),
}
}
#[test]
fn p256_round_trip_compresses_uncompressed_input() {
let key = acdp_crypto::sign::P256SigningKey::generate();
let did = did_key_from_p256_sec1(&key.verifying_key_sec1()).unwrap();
match resolve_did_key(&did).unwrap() {
DidKeyMaterial::EcdsaP256(k) => {
assert_eq!(k.len(), 33);
assert!(matches!(k[0], 0x02 | 0x03));
let vk = p256::ecdsa::VerifyingKey::from_sec1_bytes(&k).unwrap();
assert_eq!(
vk.to_sec1_point(false).as_bytes(),
key.verifying_key_sec1().as_slice()
);
}
other => panic!("expected EcdsaP256, got {other:?}"),
}
}
#[test]
fn did_key_document_exposes_the_key_to_document_verifiers() {
let did = did_key_from_ed25519(&test_pub());
let doc = did_key_document(&did).unwrap();
assert_eq!(doc.id, did);
let url = did_key_url(&did).unwrap();
let (_, frag) = url.split_once('#').unwrap();
let vm = doc.find_by_fragment(frag).expect("the key itself");
assert_eq!(vm.ed25519_public_key_bytes().unwrap(), test_pub());
assert_eq!(vm.declared_algorithm(), Some("ed25519"));
assert!(doc.is_assertion_method(&url));
let key = acdp_crypto::sign::P256SigningKey::generate();
let did = did_key_from_p256_sec1(&key.verifying_key_sec1()).unwrap();
let doc = did_key_document(&did).unwrap();
let url = did_key_url(&did).unwrap();
let (_, frag) = url.split_once('#').unwrap();
let vm = doc.find_by_fragment(frag).unwrap();
assert_eq!(
vm.ecdsa_p256_public_key_sec1().unwrap(),
key.verifying_key_sec1().to_vec()
);
assert_eq!(vm.declared_algorithm(), Some("ecdsa-p256"));
assert!(doc.is_assertion_method(&url));
assert!(matches!(
did_key_document("did:web:example.com"),
Err(AcdpError::KeyResolution(_))
));
}
#[test]
fn key_url_fragment_must_equal_msi() {
let did = did_key_from_ed25519(&test_pub());
let url = did_key_url(&did).unwrap();
resolve_did_key_url(&url).unwrap();
let bad = format!("{did}#key-1");
let err = resolve_did_key_url(&bad).unwrap_err();
assert!(matches!(err, AcdpError::KeyResolution(_)), "got {err:?}");
let err = resolve_did_key_url(&did).unwrap_err();
assert!(matches!(err, AcdpError::KeyResolution(_)), "got {err:?}");
}
#[test]
fn rejects_unknown_multicodec() {
let mut prefixed = vec![0xe7, 0x01];
prefixed.extend_from_slice(&[0u8; 33]);
let did = format!("did:key:z{}", bs58::encode(&prefixed).into_string());
let err = resolve_did_key(&did).unwrap_err();
assert!(matches!(err, AcdpError::KeyResolution(_)), "got {err:?}");
}
#[test]
fn rejects_non_z_multibase_and_garbage() {
for bad in ["did:key:uAAAA", "did:key:z!!!not-base58!!!", "did:web:x"] {
assert!(
resolve_did_key(bad).is_err(),
"'{bad}' must fail did:key resolution"
);
}
}
#[test]
fn rejects_wrong_length_ed25519() {
let mut prefixed = MULTICODEC_ED25519.to_vec();
prefixed.extend_from_slice(&[0u8; 31]); let did = format!("did:key:z{}", bs58::encode(&prefixed).into_string());
assert!(resolve_did_key(&did).is_err());
}
#[test]
fn rejects_uncompressed_p256_payload() {
let key = acdp_crypto::sign::P256SigningKey::generate();
let mut prefixed = MULTICODEC_P256.to_vec();
prefixed.extend_from_slice(&key.verifying_key_sec1());
let did = format!("did:key:z{}", bs58::encode(&prefixed).into_string());
assert!(resolve_did_key(&did).is_err());
}
#[test]
fn algorithm_strings() {
assert_eq!(DidKeyMaterial::Ed25519([0; 32]).algorithm(), "ed25519");
assert_eq!(DidKeyMaterial::EcdsaP256([2; 33]).algorithm(), "ecdsa-p256");
}
}