use anyhow::{anyhow, Result};
use base64::engine::{general_purpose::GeneralPurpose, DecodePaddingMode, GeneralPurposeConfig};
use base64::{alphabet, Engine};
const ED25519_MULTICODEC: [u8; 2] = [0xed, 0x01];
const B64URL: GeneralPurpose = GeneralPurpose::new(
&alphabet::URL_SAFE,
GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
);
pub fn did_key_from_ed25519(public_key: &[u8; 32]) -> String {
let mut prefixed = Vec::with_capacity(2 + 32);
prefixed.extend_from_slice(&ED25519_MULTICODEC);
prefixed.extend_from_slice(public_key);
let encoded = multibase::encode(multibase::Base::Base58Btc, &prefixed);
format!("did:key:{encoded}")
}
pub fn ed25519_from_did_key(did: &str) -> Result<[u8; 32]> {
let multibase_part = did
.strip_prefix("did:key:")
.ok_or_else(|| anyhow!("not a did:key URI"))?;
let (_, decoded) =
multibase::decode(multibase_part).map_err(|e| anyhow!("multibase decode failed: {e}"))?;
if decoded.len() != 34
|| decoded[0] != ED25519_MULTICODEC[0]
|| decoded[1] != ED25519_MULTICODEC[1]
{
return Err(anyhow!("not an Ed25519 did:key (wrong prefix or length)"));
}
let mut key = [0u8; 32];
key.copy_from_slice(&decoded[2..34]);
Ok(key)
}
pub fn did_jwk_from_ed25519(public_key: &[u8; 32]) -> String {
let b64url_nopad = GeneralPurpose::new(
&alphabet::URL_SAFE,
GeneralPurposeConfig::new().with_encode_padding(false),
);
let x = b64url_nopad.encode(public_key);
let jwk_json = format!("{{\"crv\":\"Ed25519\",\"kty\":\"OKP\",\"x\":\"{x}\"}}");
let b64url_pad = GeneralPurpose::new(&alphabet::URL_SAFE, GeneralPurposeConfig::new());
let enc = b64url_pad.encode(jwk_json.as_bytes());
format!("did:jwk:{enc}")
}
pub fn ed25519_from_did_jwk(did: &str) -> Result<[u8; 32]> {
let encoded = did
.strip_prefix("did:jwk:")
.ok_or_else(|| anyhow!("not a did:jwk URI"))?;
let json = B64URL
.decode(encoded)
.map_err(|e| anyhow!("did:jwk base64url decode failed: {e}"))?;
let jwk: serde_json::Value =
serde_json::from_slice(&json).map_err(|e| anyhow!("did:jwk JWK parse failed: {e}"))?;
if jwk.get("kty").and_then(|v| v.as_str()) != Some("OKP") {
return Err(anyhow!("did:jwk is not an OKP key"));
}
if jwk.get("crv").and_then(|v| v.as_str()) != Some("Ed25519") {
return Err(anyhow!("did:jwk is not an Ed25519 key"));
}
let x = jwk
.get("x")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("did:jwk JWK has no x parameter"))?;
let key_bytes = B64URL
.decode(x)
.map_err(|e| anyhow!("did:jwk x base64url decode failed: {e}"))?;
let key: [u8; 32] = key_bytes
.as_slice()
.try_into()
.map_err(|_| anyhow!("did:jwk x is not a 32-byte Ed25519 key"))?;
Ok(key)
}
pub fn did_web_from_domain(domain: &str, path: Option<&[&str]>) -> String {
let encoded_domain = domain.replace(':', "%3A");
match path {
Some(segments) if !segments.is_empty() => {
let path_str = segments.join(":");
format!("did:web:{encoded_domain}:{path_str}")
}
_ => format!("did:web:{encoded_domain}"),
}
}
pub fn did_web_to_url(did: &str) -> Result<String> {
let domain_path = did
.strip_prefix("did:web:")
.ok_or_else(|| anyhow!("not a did:web URI"))?;
let parts: Vec<&str> = domain_path.split(':').collect();
let domain = parts[0].replace("%3A", ":");
if parts.len() == 1 {
Ok(format!("https://{domain}/.well-known/did.json"))
} else {
let path = parts[1..].join("/");
Ok(format!("https://{domain}/{path}/did.json"))
}
}
pub fn create_did_document(did: &str, public_key: &[u8; 32]) -> serde_json::Value {
let key_multibase = {
let mut prefixed = Vec::with_capacity(34);
prefixed.extend_from_slice(&ED25519_MULTICODEC);
prefixed.extend_from_slice(public_key);
multibase::encode(multibase::Base::Base58Btc, &prefixed)
};
let key_id = format!("{did}#key-0");
serde_json::json!({
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/multikey/v1"
],
"id": did,
"verificationMethod": [{
"id": &key_id,
"type": "Multikey",
"controller": did,
"publicKeyMultibase": key_multibase
}],
"authentication": [&key_id],
"assertionMethod": [&key_id]
})
}
pub fn ed25519_from_did_document(doc: &serde_json::Value) -> Result<[u8; 32]> {
let methods = doc
.get("verificationMethod")
.and_then(|v| v.as_array())
.ok_or_else(|| anyhow!("no verificationMethod in DID document"))?;
let method = methods
.first()
.ok_or_else(|| anyhow!("empty verificationMethod array"))?;
let multibase_key = method
.get("publicKeyMultibase")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("no publicKeyMultibase in verification method"))?;
let (_, decoded) =
multibase::decode(multibase_key).map_err(|e| anyhow!("multibase decode failed: {e}"))?;
if decoded.len() != 34
|| decoded[0] != ED25519_MULTICODEC[0]
|| decoded[1] != ED25519_MULTICODEC[1]
{
return Err(anyhow!("not an Ed25519 key (wrong multicodec prefix)"));
}
let mut key = [0u8; 32];
key.copy_from_slice(&decoded[2..34]);
Ok(key)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn did_key_roundtrip() {
let pk = [0xABu8; 32];
let did = did_key_from_ed25519(&pk);
assert!(did.starts_with("did:key:z"));
let recovered = ed25519_from_did_key(&did).unwrap();
assert_eq!(recovered, pk);
}
#[test]
fn reject_bad_prefix() {
assert!(ed25519_from_did_key("did:web:example.com").is_err());
}
#[test]
fn did_jwk_from_ed25519_roundtrips() {
let pk = [0x42u8; 32];
let did = did_jwk_from_ed25519(&pk);
assert!(did.starts_with("did:jwk:"));
let recovered = ed25519_from_did_jwk(&did).unwrap();
assert_eq!(recovered, pk);
}
#[test]
fn did_jwk_resolves_vector_issuer() {
let issuer = "did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IkE2RUh2X1BPRUw0ZGNOMFk1MHZBbVdmazFqQ2JwUTFmSGR5R1pCSlZNYmcifQ==";
let expected = "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8";
let key = ed25519_from_did_jwk(issuer).unwrap();
let hex: String = key.iter().map(|b| format!("{b:02x}")).collect();
assert_eq!(hex, expected);
}
#[test]
fn did_web_simple() {
let did = did_web_from_domain("example.com", None);
assert_eq!(did, "did:web:example.com");
let url = did_web_to_url(&did).unwrap();
assert_eq!(url, "https://example.com/.well-known/did.json");
}
#[test]
fn did_web_with_path() {
let did = did_web_from_domain("example.com", Some(&["users", "alice"]));
assert_eq!(did, "did:web:example.com:users:alice");
let url = did_web_to_url(&did).unwrap();
assert_eq!(url, "https://example.com/users/alice/did.json");
}
#[test]
fn did_web_with_port() {
let did = did_web_from_domain("localhost%3A8080", None);
assert_eq!(did, "did:web:localhost%3A8080");
let url = did_web_to_url(&did).unwrap();
assert_eq!(url, "https://localhost:8080/.well-known/did.json");
}
#[test]
fn did_document_roundtrip() {
let pk = [0xCDu8; 32];
let did = did_web_from_domain("example.com", None);
let doc = create_did_document(&did, &pk);
let recovered = ed25519_from_did_document(&doc).unwrap();
assert_eq!(recovered, pk);
assert_eq!(doc["id"], "did:web:example.com");
}
}