use crate::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_encoded_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_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 = crate::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_encoded_point(false).as_bytes(),
key.verifying_key_sec1().as_slice()
);
}
other => panic!("expected EcdsaP256, got {other:?}"),
}
}
#[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 = crate::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");
}
}