use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use serde::{Deserialize, Serialize};
use crate::keys::{KeriDecodeError, KeriPublicKey};
use crate::state::KeyState;
use crate::types::CesrKey;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kty")]
pub enum PublicKeyJwk {
#[serde(rename = "OKP")]
Okp {
kid: String,
crv: String,
x: String,
},
#[serde(rename = "EC")]
Ec {
kid: String,
crv: String,
x: String,
y: String,
},
}
impl PublicKeyJwk {
pub fn from_key(key: &KeriPublicKey, kid: &str) -> Result<Self, KeriDecodeError> {
match key {
KeriPublicKey::Ed25519 { key: raw, .. } => Ok(Self::Okp {
kid: kid.to_string(),
crv: "Ed25519".to_string(),
x: URL_SAFE_NO_PAD.encode(raw),
}),
KeriPublicKey::P256 {
key: compressed, ..
} => {
use p256::elliptic_curve::sec1::ToEncodedPoint;
let pk = p256::PublicKey::from_sec1_bytes(compressed).map_err(|e| {
KeriDecodeError::DecodeError(format!("P-256 point decode failed: {e}"))
})?;
let uncompressed = pk.to_encoded_point(false);
let x = uncompressed.x().ok_or_else(|| {
KeriDecodeError::DecodeError("P-256 point has no x-coordinate".to_string())
})?;
let y = uncompressed.y().ok_or_else(|| {
KeriDecodeError::DecodeError("P-256 point has no y-coordinate".to_string())
})?;
Ok(Self::Ec {
kid: kid.to_string(),
crv: "P-256".to_string(),
x: URL_SAFE_NO_PAD.encode(x),
y: URL_SAFE_NO_PAD.encode(y),
})
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VerificationMethod {
pub id: String,
#[serde(rename = "type")]
pub type_: String,
pub controller: String,
#[serde(rename = "publicKeyJwk")]
pub public_key_jwk: PublicKeyJwk,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DidWebsDocument {
pub id: String,
#[serde(rename = "verificationMethod")]
pub verification_method: Vec<VerificationMethod>,
pub service: Vec<serde_json::Value>,
#[serde(rename = "alsoKnownAs")]
pub also_known_as: Vec<String>,
}
impl DidWebsDocument {
pub fn from_key_state(state: &KeyState, domain: &str) -> Result<Self, KeriDecodeError> {
let aid = state.prefix.as_str();
let id = format!("did:webs:{domain}:{aid}");
let mut verification_method = Vec::with_capacity(state.current_keys.len());
for cesr_key in &state.current_keys {
verification_method.push(verification_method_for(cesr_key, &id)?);
}
Ok(Self {
id,
verification_method,
service: Vec::new(),
also_known_as: vec![format!("did:keri:{aid}")],
})
}
}
fn verification_method_for(
cesr_key: &CesrKey,
did: &str,
) -> Result<VerificationMethod, KeriDecodeError> {
let kid = cesr_key.as_str();
let key = KeriPublicKey::parse(kid)?;
Ok(VerificationMethod {
id: format!("#{kid}"),
type_: "JsonWebKey".to_string(),
controller: did.to_string(),
public_key_jwk: PublicKeyJwk::from_key(&key, kid)?,
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::types::{Prefix, Said, Threshold};
fn ed25519_state(aid: &str, key_cesr: &str) -> KeyState {
KeyState::from_inception(
Prefix::new_unchecked(aid.to_string()),
vec![CesrKey::new_unchecked(key_cesr.to_string())],
vec![Said::new_unchecked("ENext0".to_string())],
Threshold::Simple(1),
Threshold::Simple(1),
Said::new_unchecked(aid.to_string()),
vec![],
Threshold::Simple(0),
vec![],
)
}
fn ed25519_cesr(raw: &[u8; 32]) -> String {
KeriPublicKey::ed25519(raw).unwrap().to_qb64().unwrap()
}
fn p256_cesr() -> (String, [u8; 33]) {
use p256::elliptic_curve::sec1::ToEncodedPoint;
let sk = p256::SecretKey::from_slice(&[
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
0x1d, 0x1e, 0x1f, 0x20,
])
.unwrap();
let pt = sk.public_key().to_encoded_point(true);
let mut compressed = [0u8; 33];
compressed.copy_from_slice(pt.as_bytes());
let cesr = KeriPublicKey::P256 {
key: compressed,
transferable: true,
}
.to_qb64()
.unwrap();
(cesr, compressed)
}
#[test]
fn document_has_canonical_field_order() {
let key = ed25519_cesr(&[3u8; 32]);
let state = ed25519_state("EAid000000000000000000000000000000000000000", &key);
let doc = DidWebsDocument::from_key_state(&state, "example.com").unwrap();
let json = serde_json::to_value(&doc).unwrap();
let keys: Vec<&str> = json
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect();
assert_eq!(
keys,
vec!["id", "verificationMethod", "service", "alsoKnownAs"]
);
}
#[test]
fn ed25519_verification_method_matches_reference_shape() {
let key = ed25519_cesr(&[7u8; 32]);
let state = ed25519_state("EAid000000000000000000000000000000000000000", &key);
let doc = DidWebsDocument::from_key_state(&state, "example.com").unwrap();
assert_eq!(
doc.id,
"did:webs:example.com:EAid000000000000000000000000000000000000000"
);
assert_eq!(
doc.also_known_as,
vec!["did:keri:EAid000000000000000000000000000000000000000"]
);
assert!(doc.service.is_empty());
let vm = &doc.verification_method[0];
assert_eq!(vm.id, format!("#{key}"));
assert_eq!(vm.type_, "JsonWebKey");
assert_eq!(vm.controller, doc.id);
match &vm.public_key_jwk {
PublicKeyJwk::Okp { kid, crv, x } => {
assert_eq!(kid, &key);
assert_eq!(crv, "Ed25519");
assert_eq!(x, &URL_SAFE_NO_PAD.encode([7u8; 32]));
}
other => panic!("expected OKP JWK, got {other:?}"),
}
}
#[test]
fn ed25519_jwk_serializes_kty_first() {
let key = ed25519_cesr(&[1u8; 32]);
let state = ed25519_state("EAid000000000000000000000000000000000000000", &key);
let doc = DidWebsDocument::from_key_state(&state, "example.com").unwrap();
let jwk = serde_json::to_value(&doc.verification_method[0].public_key_jwk).unwrap();
let labels: Vec<&str> = jwk
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect();
assert_eq!(labels, vec!["kty", "kid", "crv", "x"]);
assert_eq!(jwk["kty"], "OKP");
assert_eq!(jwk["crv"], "Ed25519");
}
#[test]
fn p256_verification_method_emits_ec_jwk_with_x_and_y() {
let (key, compressed) = p256_cesr();
let state = ed25519_state("EAidP256000000000000000000000000000000000000", &key);
let doc = DidWebsDocument::from_key_state(&state, "example.com").unwrap();
let vm = &doc.verification_method[0];
assert_eq!(vm.id, format!("#{key}"));
match &vm.public_key_jwk {
PublicKeyJwk::Ec { kid, crv, x, y } => {
assert_eq!(kid, &key);
assert_eq!(crv, "P-256");
assert_eq!(x, &URL_SAFE_NO_PAD.encode(&compressed[1..33]));
assert_eq!(URL_SAFE_NO_PAD.decode(y).unwrap().len(), 32);
}
other => panic!("expected EC JWK, got {other:?}"),
}
}
#[test]
fn multisig_emits_one_method_per_key() {
let k1 = ed25519_cesr(&[1u8; 32]);
let k2 = ed25519_cesr(&[2u8; 32]);
let mut state = ed25519_state("EAid000000000000000000000000000000000000000", &k1);
state.current_keys.push(CesrKey::new_unchecked(k2.clone()));
let doc = DidWebsDocument::from_key_state(&state, "example.com").unwrap();
assert_eq!(doc.verification_method.len(), 2);
assert_eq!(doc.verification_method[0].id, format!("#{k1}"));
assert_eq!(doc.verification_method[1].id, format!("#{k2}"));
}
#[test]
fn domain_with_port_and_path_is_preserved() {
let key = ed25519_cesr(&[5u8; 32]);
let state = ed25519_state("EAid000000000000000000000000000000000000000", &key);
let doc = DidWebsDocument::from_key_state(&state, "example.com%3A3901:dids").unwrap();
assert_eq!(
doc.id,
"did:webs:example.com%3A3901:dids:EAid000000000000000000000000000000000000000"
);
assert_eq!(doc.verification_method[0].controller, doc.id);
}
#[test]
fn undecodable_key_is_rejected_at_the_boundary() {
let mut state = ed25519_state("EAid000000000000000000000000000000000000000", "Dvalid");
state.current_keys = vec![CesrKey::new_unchecked("Xnot-a-verkey".to_string())];
assert!(DidWebsDocument::from_key_state(&state, "example.com").is_err());
}
#[test]
fn document_round_trips_through_json() {
let key = ed25519_cesr(&[9u8; 32]);
let state = ed25519_state("EAid000000000000000000000000000000000000000", &key);
let doc = DidWebsDocument::from_key_state(&state, "example.com").unwrap();
let wire = serde_json::to_string(&doc).unwrap();
let parsed: DidWebsDocument = serde_json::from_str(&wire).unwrap();
assert_eq!(parsed, doc);
}
}