use std::fmt;
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use jsonwebtoken::jwk::Jwk;
use jsonwebtoken::{Algorithm, EncodingKey};
use secrecy::{ExposeSecret, SecretString};
use serde::Deserialize;
use serde::de::IgnoredAny;
pub const DID_WEB_PREFIX: &str = "did:web:";
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum HolderError {
#[error("the holder identifier is not a did:web DID")]
Did,
#[error("the key id is not a DID URL of the holder's DID with a fragment")]
KeyId,
#[error("the holder key is not an EC private key in PKCS#8 PEM")]
Pem(#[source] jsonwebtoken::errors::Error),
#[error("the holder key is on neither P-256 (ES256) nor P-384 (ES384)")]
Curve(#[source] jsonwebtoken::errors::Error),
#[error("credential {index} names no input descriptor")]
Descriptor {
index: usize,
},
#[error("credential {index} is not a JWT-encoded credential")]
NotAJwt {
index: usize,
},
#[error("credential {index} carries no vc claim")]
NotACredential {
index: usize,
},
#[error("credential {index} is not issued to the holder")]
OtherSubject {
index: usize,
},
#[error("the holder holds no credential")]
NoCredential,
#[error("credentials {first} and {second} answer the same input descriptor")]
SameDescriptor {
first: usize,
second: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Did(String);
impl Did {
pub fn new(text: impl Into<String>) -> Result<Self, HolderError> {
let text = text.into();
let id = text.strip_prefix(DID_WEB_PREFIX).ok_or(HolderError::Did)?;
if id.is_empty() || !id.split(':').all(segment) {
return Err(HolderError::Did);
}
Ok(Self(text))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn document_path(&self) -> String {
let path: Vec<&str> = self.0.split(':').skip(3).collect();
if path.is_empty() {
String::from("/.well-known/did.json")
} else {
format!("/{}/did.json", path.join("/"))
}
}
}
impl fmt::Display for Did {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
fn segment(segment: &str) -> bool {
let bytes = segment.as_bytes();
let mut index = 0;
while let Some(&byte) = bytes.get(index) {
if byte == b'%' {
let encoded = bytes
.get(index.saturating_add(1)..index.saturating_add(3))
.is_some_and(|hex| hex.iter().all(u8::is_ascii_hexdigit));
if !encoded {
return false;
}
index = index.saturating_add(3);
} else if byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_') {
index = index.saturating_add(1);
} else {
return false;
}
}
!bytes.is_empty()
}
pub struct HolderKey {
kid: String,
algorithm: Algorithm,
private: EncodingKey,
public: Jwk,
}
impl HolderKey {
pub fn from_pem(pem: &SecretString, kid: &str, did: &Did) -> Result<Self, HolderError> {
let fragment = kid
.strip_prefix(did.as_str())
.and_then(|rest| rest.strip_prefix('#'))
.ok_or(HolderError::KeyId)?;
if fragment.is_empty()
|| fragment
.chars()
.any(|c| c == '#' || c.is_whitespace() || c.is_control())
{
return Err(HolderError::KeyId);
}
let private =
EncodingKey::from_ec_pem(pem.expose_secret().as_bytes()).map_err(HolderError::Pem)?;
let (algorithm, public) = match Jwk::from_encoding_key(&private, Algorithm::ES256) {
Ok(public) => (Algorithm::ES256, public),
Err(_not_p256) => (
Algorithm::ES384,
Jwk::from_encoding_key(&private, Algorithm::ES384).map_err(HolderError::Curve)?,
),
};
Ok(Self {
kid: kid.to_owned(),
algorithm,
private,
public,
})
}
#[must_use]
pub fn public(&self) -> &Jwk {
&self.public
}
#[must_use]
pub fn kid(&self) -> &str {
&self.kid
}
#[must_use]
pub fn algorithm(&self) -> Algorithm {
self.algorithm
}
#[must_use]
pub fn algorithm_name(&self) -> &'static str {
if self.algorithm == Algorithm::ES256 {
"ES256"
} else {
"ES384"
}
}
pub(super) fn private(&self) -> &EncodingKey {
&self.private
}
}
impl fmt::Debug for HolderKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HolderKey")
.field("kid", &self.kid)
.field("algorithm", &self.algorithm)
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct HeldCredential {
descriptor: String,
jwt: SecretString,
expires: Option<i64>,
}
impl HeldCredential {
#[must_use]
pub fn descriptor(&self) -> &str {
&self.descriptor
}
#[must_use]
pub fn expires(&self) -> Option<i64> {
self.expires
}
pub(super) fn jwt(&self) -> &SecretString {
&self.jwt
}
}
impl fmt::Debug for HeldCredential {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HeldCredential")
.field("descriptor", &self.descriptor)
.finish_non_exhaustive()
}
}
#[derive(Deserialize)]
struct CredentialClaims {
sub: Option<String>,
exp: Option<i64>,
vc: Option<IgnoredAny>,
}
#[derive(Debug)]
pub struct Holder {
did: Did,
key: HolderKey,
credentials: Vec<HeldCredential>,
}
impl Holder {
pub fn new(
did: Did,
key: HolderKey,
credentials: Vec<(String, SecretString)>,
) -> Result<Self, HolderError> {
if !key
.kid()
.strip_prefix(did.as_str())
.is_some_and(|rest| rest.starts_with('#'))
{
return Err(HolderError::KeyId);
}
if credentials.is_empty() {
return Err(HolderError::NoCredential);
}
let mut held: Vec<HeldCredential> = Vec::with_capacity(credentials.len());
for (index, (descriptor, jwt)) in credentials.into_iter().enumerate() {
if descriptor.trim().is_empty() {
return Err(HolderError::Descriptor { index });
}
if let Some(first) = held.iter().position(|held| held.descriptor == descriptor) {
return Err(HolderError::SameDescriptor {
first,
second: index,
});
}
let claims = claims(jwt.expose_secret()).ok_or(HolderError::NotAJwt { index })?;
if claims.vc.is_none() {
return Err(HolderError::NotACredential { index });
}
if claims.sub.as_deref() != Some(did.as_str()) {
return Err(HolderError::OtherSubject { index });
}
held.push(HeldCredential {
descriptor,
jwt,
expires: claims.exp,
});
}
Ok(Self {
did,
key,
credentials: held,
})
}
#[must_use]
pub fn did(&self) -> &Did {
&self.did
}
#[must_use]
pub fn key(&self) -> &HolderKey {
&self.key
}
#[must_use]
pub fn credentials(&self) -> &[HeldCredential] {
&self.credentials
}
}
fn claims(jwt: &str) -> Option<CredentialClaims> {
let mut parts = jwt.split('.');
let (Some(header), Some(payload), Some(signature), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return None;
};
if header.is_empty() || signature.is_empty() {
return None;
}
let payload = URL_SAFE_NO_PAD.decode(payload).ok()?;
serde_json::from_slice(&payload).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_did_web_identifier_is_read() {
for text in [
"did:web:example.org",
"did:web:example.org%3A8443",
"did:web:example.org:org:ura-0001",
] {
assert!(Did::new(text).is_ok(), "{text}");
}
}
#[test]
fn anything_else_is_refused() {
for text in [
"did:web:",
"did:key:z6Mk",
"did:web:exa mple.org",
"did:web:example.org::x",
"did:web:example.org%3",
"did:web:example.org/path",
"https://example.org",
] {
assert!(matches!(Did::new(text), Err(HolderError::Did)), "{text}");
}
}
#[test]
fn a_compact_jws_is_read_for_its_claims() {
let payload = URL_SAFE_NO_PAD.encode(br#"{"sub":"did:web:example.org","vc":{}}"#);
let read = claims(&format!("e30.{payload}.c2ln")).expect("claims");
assert_eq!(read.sub.as_deref(), Some("did:web:example.org"));
assert!(read.vc.is_some());
assert!(claims("e30.e30").is_none());
assert!(claims("e30.e30.c2ln.x").is_none());
assert!(claims("e30.!!.c2ln").is_none());
}
}