use serde_json::Value;
use thiserror::Error;
use crate::keys::{
base64url_decode, base64url_encode, decode_signature_bytes, encode_signature_bytes,
};
use crate::PublicKeyMaterial;
#[derive(Debug, Error)]
pub enum VerificationMethodError {
#[error("Missing verification method type")]
MissingType,
#[error("Unsupported verification method type: {0}")]
UnsupportedType(String),
#[error("Missing key material")]
MissingKeyMaterial,
#[error("Invalid key material")]
InvalidKeyMaterial,
#[error("Signature encoding error")]
SignatureEncoding,
}
#[derive(Debug, Clone)]
pub struct VerificationMethod {
pub id: String,
pub method_type: String,
pub public_key: PublicKeyMaterial,
}
impl VerificationMethod {
pub fn verify_signature(
&self,
content: &[u8],
signature: &str,
) -> Result<(), VerificationMethodError> {
let signature_bytes = decode_signature_bytes(signature)
.map_err(|_| VerificationMethodError::SignatureEncoding)?;
self.public_key
.verify_message(content, &signature_bytes)
.map_err(|_| VerificationMethodError::SignatureEncoding)
}
pub fn encode_signature(
&self,
signature_bytes: &[u8],
) -> Result<String, VerificationMethodError> {
match self.public_key {
PublicKeyMaterial::X25519(_) => Err(VerificationMethodError::UnsupportedType(
self.method_type.clone(),
)),
_ => Ok(encode_signature_bytes(signature_bytes)),
}
}
}
pub fn create_verification_method(
method_dict: &Value,
) -> Result<VerificationMethod, VerificationMethodError> {
let object = method_dict
.as_object()
.ok_or(VerificationMethodError::InvalidKeyMaterial)?;
let method_type = object
.get("type")
.and_then(Value::as_str)
.ok_or(VerificationMethodError::MissingType)?;
let id = object
.get("id")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
let public_key = extract_public_key(method_dict)?;
Ok(VerificationMethod {
id,
method_type: method_type.to_string(),
public_key,
})
}
pub fn extract_public_key(
method_dict: &Value,
) -> Result<PublicKeyMaterial, VerificationMethodError> {
let object = method_dict
.as_object()
.ok_or(VerificationMethodError::InvalidKeyMaterial)?;
let method_type = object
.get("type")
.and_then(Value::as_str)
.ok_or(VerificationMethodError::MissingType)?;
match method_type {
"EcdsaSecp256k1VerificationKey2019" => extract_secp256k1_key(object),
"EcdsaSecp256r1VerificationKey2019" => extract_secp256r1_key(object),
"Ed25519VerificationKey2018" | "Ed25519VerificationKey2020" | "Multikey" => {
extract_ed25519_key(object)
}
"X25519KeyAgreementKey2019" => extract_x25519_key(object),
"JsonWebKey2020" => extract_jwk_key(object),
other => Err(VerificationMethodError::UnsupportedType(other.to_string())),
}
}
fn extract_secp256k1_key(
object: &serde_json::Map<String, Value>,
) -> Result<PublicKeyMaterial, VerificationMethodError> {
if let Some(jwk) = object.get("publicKeyJwk") {
return extract_ec_key_from_jwk(jwk, "secp256k1").map(PublicKeyMaterial::Secp256k1);
}
if let Some(multibase) = object.get("publicKeyMultibase").and_then(Value::as_str) {
let bytes = bs58::decode(multibase.trim_start_matches('z'))
.into_vec()
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)?;
let key = k256::ecdsa::VerifyingKey::from_sec1_bytes(&bytes)
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)?;
return Ok(PublicKeyMaterial::Secp256k1(key));
}
Err(VerificationMethodError::MissingKeyMaterial)
}
fn extract_secp256r1_key(
object: &serde_json::Map<String, Value>,
) -> Result<PublicKeyMaterial, VerificationMethodError> {
if let Some(jwk) = object.get("publicKeyJwk") {
return extract_p256_key_from_jwk(jwk, "P-256").map(PublicKeyMaterial::Secp256r1);
}
if let Some(multibase) = object.get("publicKeyMultibase").and_then(Value::as_str) {
let bytes = bs58::decode(multibase.trim_start_matches('z'))
.into_vec()
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)?;
let key = p256::ecdsa::VerifyingKey::from_sec1_bytes(&bytes)
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)?;
return Ok(PublicKeyMaterial::Secp256r1(key));
}
Err(VerificationMethodError::MissingKeyMaterial)
}
fn extract_jwk_key(
object: &serde_json::Map<String, Value>,
) -> Result<PublicKeyMaterial, VerificationMethodError> {
let material_count = ["publicKeyJwk", "publicKeyMultibase", "publicKeyBase58"]
.into_iter()
.filter(|field| object.contains_key(*field))
.count();
if material_count != 1 {
return Err(VerificationMethodError::InvalidKeyMaterial);
}
let jwk = object
.get("publicKeyJwk")
.ok_or(VerificationMethodError::MissingKeyMaterial)?;
if jwk
.as_object()
.is_some_and(|object| object.contains_key("d"))
{
return Err(VerificationMethodError::InvalidKeyMaterial);
}
let kty = jwk
.get("kty")
.and_then(Value::as_str)
.ok_or(VerificationMethodError::InvalidKeyMaterial)?;
let crv = jwk
.get("crv")
.and_then(Value::as_str)
.ok_or(VerificationMethodError::InvalidKeyMaterial)?;
match (kty, crv) {
("EC", "secp256k1") => extract_ec_key_from_jwk(jwk, crv).map(PublicKeyMaterial::Secp256k1),
("EC", "P-256") => extract_p256_key_from_jwk(jwk, crv).map(PublicKeyMaterial::Secp256r1),
("OKP", "Ed25519") => extract_okp_key_from_jwk(jwk, crv).and_then(|bytes| {
ed25519_dalek::VerifyingKey::from_bytes(&bytes)
.map(PublicKeyMaterial::Ed25519)
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)
}),
("OKP", "X25519") => extract_okp_key_from_jwk(jwk, crv).map(PublicKeyMaterial::X25519),
_ => Err(VerificationMethodError::UnsupportedType(crv.to_string())),
}
}
fn extract_okp_key_from_jwk(
jwk: &Value,
expected_curve: &str,
) -> Result<[u8; 32], VerificationMethodError> {
let object = jwk
.as_object()
.ok_or(VerificationMethodError::InvalidKeyMaterial)?;
if object.get("kty").and_then(Value::as_str) != Some("OKP")
|| object.get("crv").and_then(Value::as_str) != Some(expected_curve)
{
return Err(VerificationMethodError::InvalidKeyMaterial);
}
let encoded = object
.get("x")
.and_then(Value::as_str)
.filter(|value| {
!value.is_empty()
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
})
.ok_or(VerificationMethodError::InvalidKeyMaterial)?;
let bytes =
base64url_decode(encoded).map_err(|_| VerificationMethodError::InvalidKeyMaterial)?;
if bytes.len() != 32 || base64url_encode(&bytes) != encoded {
return Err(VerificationMethodError::InvalidKeyMaterial);
}
bytes
.try_into()
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)
}
fn extract_ed25519_key(
object: &serde_json::Map<String, Value>,
) -> Result<PublicKeyMaterial, VerificationMethodError> {
if let Some(jwk) = object.get("publicKeyJwk") {
let x = jwk
.get("x")
.and_then(Value::as_str)
.ok_or(VerificationMethodError::InvalidKeyMaterial)?;
let bytes = base64url_decode(x).map_err(|_| VerificationMethodError::InvalidKeyMaterial)?;
let key = ed25519_dalek::VerifyingKey::from_bytes(
&bytes
.try_into()
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)?,
)
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)?;
return Ok(PublicKeyMaterial::Ed25519(key));
}
if let Some(multibase) = object.get("publicKeyMultibase").and_then(Value::as_str) {
let mut bytes = bs58::decode(multibase.trim_start_matches('z'))
.into_vec()
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)?;
if bytes.len() == 34 && bytes.starts_with(&[0xed, 0x01]) {
bytes = bytes[2..].to_vec();
}
let key = ed25519_dalek::VerifyingKey::from_bytes(
&bytes
.try_into()
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)?,
)
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)?;
return Ok(PublicKeyMaterial::Ed25519(key));
}
if let Some(base58_key) = object.get("publicKeyBase58").and_then(Value::as_str) {
let bytes = bs58::decode(base58_key)
.into_vec()
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)?;
let key = ed25519_dalek::VerifyingKey::from_bytes(
&bytes
.try_into()
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)?,
)
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)?;
return Ok(PublicKeyMaterial::Ed25519(key));
}
Err(VerificationMethodError::MissingKeyMaterial)
}
fn extract_x25519_key(
object: &serde_json::Map<String, Value>,
) -> Result<PublicKeyMaterial, VerificationMethodError> {
let multibase = object
.get("publicKeyMultibase")
.and_then(Value::as_str)
.ok_or(VerificationMethodError::MissingKeyMaterial)?;
let mut bytes = bs58::decode(multibase.trim_start_matches('z'))
.into_vec()
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)?;
if bytes.len() == 34 && bytes.starts_with(&[0xec, 0x01]) {
bytes = bytes[2..].to_vec();
}
let bytes: [u8; 32] = bytes
.try_into()
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)?;
Ok(PublicKeyMaterial::X25519(bytes))
}
fn extract_ec_key_from_jwk(
jwk: &Value,
expected_curve: &str,
) -> Result<k256::ecdsa::VerifyingKey, VerificationMethodError> {
let object = jwk
.as_object()
.ok_or(VerificationMethodError::InvalidKeyMaterial)?;
if object.get("kty").and_then(Value::as_str) != Some("EC") {
return Err(VerificationMethodError::InvalidKeyMaterial);
}
if object.get("crv").and_then(Value::as_str) != Some(expected_curve) {
return Err(VerificationMethodError::InvalidKeyMaterial);
}
let x = decode_coordinate(object.get("x").and_then(Value::as_str))?;
let y = decode_coordinate(object.get("y").and_then(Value::as_str))?;
let encoded = k256::EncodedPoint::from_affine_coordinates((&x).into(), (&y).into(), false);
k256::ecdsa::VerifyingKey::from_encoded_point(&encoded)
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)
}
fn extract_p256_key_from_jwk(
jwk: &Value,
expected_curve: &str,
) -> Result<p256::ecdsa::VerifyingKey, VerificationMethodError> {
let object = jwk
.as_object()
.ok_or(VerificationMethodError::InvalidKeyMaterial)?;
if object.get("kty").and_then(Value::as_str) != Some("EC") {
return Err(VerificationMethodError::InvalidKeyMaterial);
}
if object.get("crv").and_then(Value::as_str) != Some(expected_curve) {
return Err(VerificationMethodError::InvalidKeyMaterial);
}
let x = decode_coordinate(object.get("x").and_then(Value::as_str))?;
let y = decode_coordinate(object.get("y").and_then(Value::as_str))?;
let encoded = p256::EncodedPoint::from_affine_coordinates((&x).into(), (&y).into(), false);
p256::ecdsa::VerifyingKey::from_encoded_point(&encoded)
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)
}
fn decode_coordinate(value: Option<&str>) -> Result<[u8; 32], VerificationMethodError> {
let bytes = base64url_decode(value.ok_or(VerificationMethodError::InvalidKeyMaterial)?)
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)?;
bytes
.try_into()
.map_err(|_| VerificationMethodError::InvalidKeyMaterial)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn extracts_ed25519_from_json_web_key_2020_okp_jwk() {
let expected = ed25519_dalek::SigningKey::from_bytes(&[7_u8; 32]).verifying_key();
let encoded = base64url_encode(expected.as_bytes());
let method = json!({
"id": "did:wba:example.test:alice#device-sign",
"type": "JsonWebKey2020",
"controller": "did:wba:example.test:alice",
"publicKeyJwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": encoded,
},
});
assert!(matches!(
extract_public_key(&method).unwrap(),
PublicKeyMaterial::Ed25519(key) if key == expected
));
}
#[test]
fn extracts_x25519_from_json_web_key_2020_okp_jwk() {
let encoded = base64url_encode(&[9_u8; 32]);
let method = json!({
"id": "did:wba:example.test:alice#device-e2ee",
"type": "JsonWebKey2020",
"controller": "did:wba:example.test:alice",
"publicKeyJwk": {
"kty": "OKP",
"crv": "X25519",
"x": encoded,
},
});
assert!(matches!(
extract_public_key(&method).unwrap(),
PublicKeyMaterial::X25519(bytes) if bytes == [9_u8; 32]
));
}
#[test]
fn rejects_private_or_ambiguous_json_web_key_2020_okp_jwk() {
let encoded = base64url_encode(&[11_u8; 32]);
for method in [
json!({
"type": "JsonWebKey2020",
"publicKeyJwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": encoded,
"d": encoded,
},
}),
json!({
"type": "JsonWebKey2020",
"publicKeyJwk": {
"kty": "OKP",
"crv": "X25519",
"x": encoded,
"d": encoded,
},
}),
json!({
"type": "JsonWebKey2020",
"publicKeyJwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": encoded,
},
"publicKeyMultibase": "zambiguous",
}),
] {
assert!(matches!(
extract_public_key(&method),
Err(VerificationMethodError::InvalidKeyMaterial)
));
}
}
#[test]
fn x25519_json_web_key_2020_cannot_verify_signatures() {
let method = json!({
"id": "did:wba:example.test:alice#device-e2ee",
"type": "JsonWebKey2020",
"publicKeyJwk": {
"kty": "OKP",
"crv": "X25519",
"x": base64url_encode(&[13_u8; 32]),
},
});
let method = create_verification_method(&method).unwrap();
assert!(method.verify_signature(b"message", "AA").is_err());
}
}