use core::fmt;
use std::error::Error;
use std::sync::Arc;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use url::Url;
use super::{DereferenceError, Resolver};
use crate::did::{Did, DidError, DidUrl, VerificationRelationship};
use crate::jose::{CompactJws, Jwk, base64url};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum KeyReference {
DidUrl(DidUrl),
Jwk(Box<Jwk>),
X509Chain(Vec<Vec<u8>>),
Jwks {
issuer: Url,
kid: String,
},
Jku {
url: Url,
kid: Option<String>,
},
}
impl KeyReference {
pub fn from_jws(jws: &CompactJws, iss: Option<&str>) -> Result<Self, KeyReferenceError> {
let header = jws.header();
let mut kid_error = None;
if let Some(kid) = header.kid.as_deref() {
match Self::from_kid(kid, iss)? {
Ok(reference) => return Ok(reference),
Err(source) => {
kid_error = Some(KeyReferenceError::InvalidKid {
kid: kid.to_owned(),
source,
});
}
}
}
if let Some(jwk) = &header.jwk {
if jwk.is_private() {
return Err(KeyReferenceError::InvalidJwk(
"the `jwk` header holds a private key".to_owned(),
));
}
return Ok(Self::Jwk(Box::new(jwk.clone())));
}
if let Some(x5c) = header.additional.get("x5c") {
return x5c_chain(x5c).map(Self::X509Chain);
}
if let Some(jku) = header.additional.get("jku") {
let url = jku
.as_str()
.ok_or_else(|| KeyReferenceError::InvalidUrl {
parameter: "jku",
value: jku.to_string(),
reason: "not a string".to_owned(),
})
.and_then(|jku| parse_url("jku", jku))?;
return Ok(Self::Jku {
url,
kid: header.kid.clone(),
});
}
Err(kid_error.unwrap_or(KeyReferenceError::Missing))
}
fn from_kid(kid: &str, iss: Option<&str>) -> Result<Result<Self, DidError>, KeyReferenceError> {
let not_did_url = match DidUrl::parse(kid) {
Ok(url) => return did_url(url).map(Ok),
Err(error) => error,
};
if kid.starts_with('#') {
let did = iss
.and_then(|iss| Did::parse(iss).ok())
.ok_or_else(|| KeyReferenceError::RelativeKidWithoutIssuer(kid.to_owned()))?;
let url = did
.join(kid)
.map_err(|source| KeyReferenceError::InvalidKid {
kid: kid.to_owned(),
source,
})?;
return did_url(url).map(Ok);
}
if let Some(issuer) = iss
.and_then(|iss| Url::parse(iss).ok())
.filter(|issuer| issuer.scheme() == "https")
{
return Ok(Ok(Self::Jwks {
issuer,
kid: kid.to_owned(),
}));
}
Ok(Err(not_did_url))
}
pub fn binds_to(&self, party: Option<&str>, key: &Jwk) -> bool {
match (self, party) {
(Self::DidUrl(url), Some(party)) => {
Did::parse(party).is_ok_and(|did| did == *url.did())
}
(Self::Jwk(_), None) => true,
(Self::Jwk(_), Some(party)) => is_key_identifier(party, key),
(Self::Jwks { issuer, .. }, Some(party)) => {
Url::parse(party).is_ok_and(|party| party == *issuer)
}
_ => false,
}
}
}
impl fmt::Display for KeyReference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DidUrl(url) => fmt::Display::fmt(url, f),
Self::Jwk(jwk) => match jwk.thumbprint_sha256() {
Ok(thumbprint) => write!(f, "JWK {thumbprint}"),
Err(_) => f.write_str("JWK"),
},
Self::X509Chain(chain) => write!(f, "`x5c` chain of {} certificates", chain.len()),
Self::Jwks { issuer, kid } => write!(f, "`{kid}` of issuer `{issuer}`"),
Self::Jku {
url,
kid: Some(kid),
} => write!(f, "`{kid}` of `{url}`"),
Self::Jku { url, kid: None } => write!(f, "the key of `{url}`"),
}
}
}
fn did_url(url: DidUrl) -> Result<KeyReference, KeyReferenceError> {
if url.path().is_some() || url.query().is_some() {
return Err(KeyReferenceError::KidWithParameters(url));
}
Ok(KeyReference::DidUrl(url))
}
fn x5c_chain(x5c: &serde_json::Value) -> Result<Vec<Vec<u8>>, KeyReferenceError> {
let invalid = |reason: &str| KeyReferenceError::InvalidX5c(reason.to_owned());
let certificates = x5c.as_array().ok_or_else(|| invalid("not an array"))?;
if certificates.is_empty() {
return Err(invalid("the chain is empty"));
}
certificates
.iter()
.map(|certificate| {
let certificate = certificate
.as_str()
.ok_or_else(|| invalid("a certificate is not a string"))?;
STANDARD
.decode(certificate)
.map_err(|_| invalid("a certificate is not base64"))
})
.collect()
}
fn parse_url(parameter: &'static str, value: &str) -> Result<Url, KeyReferenceError> {
Url::parse(value).map_err(|e| KeyReferenceError::InvalidUrl {
parameter,
value: value.to_owned(),
reason: e.to_string(),
})
}
fn is_key_identifier(party: &str, key: &Jwk) -> bool {
let Ok(thumbprint) = key.thumbprint_sha256() else {
return false;
};
if party == thumbprint {
return true;
}
Did::parse(party)
.ok()
.filter(|did| did.method() == "jwk")
.and_then(|did| base64url::decode(did.method_specific_id()).ok())
.and_then(|json| serde_json::from_slice::<Jwk>(&json).ok())
.and_then(|jwk| jwk.thumbprint_sha256().ok())
.is_some_and(|named| named == thumbprint)
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum KeyReferenceError {
#[error("the token names no key: no `kid`, `jwk`, `x5c` or `jku`")]
Missing,
#[error("`kid` {0:?} is relative and `iss` is not a DID")]
RelativeKidWithoutIssuer(String),
#[error("`kid` {kid:?} is not a DID URL: {source}")]
InvalidKid {
kid: String,
#[source]
source: DidError,
},
#[error("`kid` `{0}` carries a path or DID parameters")]
KidWithParameters(DidUrl),
#[error("invalid `jwk`: {0}")]
InvalidJwk(String),
#[error("invalid `x5c`: {0}")]
InvalidX5c(String),
#[error("`{parameter}` {value:?} is not a URL: {reason}")]
InvalidUrl {
parameter: &'static str,
value: String,
reason: String,
},
#[error("`cnf` names its key only by `{0}`, which is not supported")]
UnsupportedConfirmation(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum KeyRole {
Issuer,
Subject,
}
impl KeyRole {
pub fn relationship(self) -> VerificationRelationship {
match self {
Self::Issuer => VerificationRelationship::AssertionMethod,
Self::Subject => VerificationRelationship::Authentication,
}
}
}
#[async_trait::async_trait]
pub trait KeyResolver: Send + Sync {
async fn resolve_key(
&self,
key: &KeyReference,
role: KeyRole,
) -> Result<Jwk, KeyResolutionError>;
}
macro_rules! forward_key_resolver {
($($ty:ty),* $(,)?) => {$(
#[async_trait::async_trait]
impl<T: KeyResolver + ?Sized> KeyResolver for $ty {
async fn resolve_key(
&self,
key: &KeyReference,
role: KeyRole,
) -> Result<Jwk, KeyResolutionError> {
(**self).resolve_key(key, role).await
}
}
)*};
}
forward_key_resolver!(&T, Box<T>, Arc<T>);
#[async_trait::async_trait]
impl KeyResolver for Resolver {
async fn resolve_key(
&self,
key: &KeyReference,
role: KeyRole,
) -> Result<Jwk, KeyResolutionError> {
match key {
KeyReference::DidUrl(url) => self
.resolve_public_key(url, Some(role.relationship()))
.await
.map_err(KeyResolutionError::from),
other => Err(KeyResolutionError::unsupported(other)),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct JwkResolver;
#[async_trait::async_trait]
impl KeyResolver for JwkResolver {
async fn resolve_key(&self, key: &KeyReference, _: KeyRole) -> Result<Jwk, KeyResolutionError> {
match key {
KeyReference::Jwk(jwk) => Ok(jwk.public()),
other => Err(KeyResolutionError::unsupported(other)),
}
}
}
#[derive(Clone, Default)]
pub struct CompositeKeyResolver {
resolvers: Vec<Arc<dyn KeyResolver>>,
}
impl CompositeKeyResolver {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with(mut self, resolver: impl KeyResolver + 'static) -> Self {
self.resolvers.push(Arc::new(resolver));
self
}
#[must_use]
pub fn with_shared(mut self, resolver: Arc<dyn KeyResolver>) -> Self {
self.resolvers.push(resolver);
self
}
pub fn len(&self) -> usize {
self.resolvers.len()
}
pub fn is_empty(&self) -> bool {
self.resolvers.is_empty()
}
}
impl fmt::Debug for CompositeKeyResolver {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CompositeKeyResolver")
.field("resolvers", &self.resolvers.len())
.finish()
}
}
#[async_trait::async_trait]
impl KeyResolver for CompositeKeyResolver {
async fn resolve_key(
&self,
key: &KeyReference,
role: KeyRole,
) -> Result<Jwk, KeyResolutionError> {
for resolver in &self.resolvers {
match resolver.resolve_key(key, role).await {
Err(KeyResolutionError::Unsupported(_)) => {}
answer => return answer,
}
}
Err(KeyResolutionError::unsupported(key))
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum KeyResolutionError {
Unsupported(Box<KeyReference>),
Failed(Box<dyn Error + Send + Sync + 'static>),
}
impl KeyResolutionError {
pub fn unsupported(key: &KeyReference) -> Self {
Self::Unsupported(Box::new(key.clone()))
}
pub fn new<E: Error + Send + Sync + 'static>(error: E) -> Self {
Self::Failed(Box::new(error))
}
pub fn msg<M: fmt::Display>(message: M) -> Self {
Self::Failed(message.to_string().into())
}
pub fn is_unsupported(&self) -> bool {
matches!(self, Self::Unsupported(_))
}
pub fn inner(&self) -> Option<&(dyn Error + Send + Sync + 'static)> {
match self {
Self::Failed(error) => Some(&**error),
Self::Unsupported(_) => None,
}
}
}
impl fmt::Display for KeyResolutionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unsupported(reference) => write!(f, "no resolver handles {reference}"),
Self::Failed(error) => fmt::Display::fmt(error, f),
}
}
}
impl Error for KeyResolutionError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Failed(error) => error.source(),
Self::Unsupported(_) => None,
}
}
}
impl From<DereferenceError> for KeyResolutionError {
fn from(error: DereferenceError) -> Self {
Self::new(error)
}
}