use std::fmt;
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
use base64::Engine as _;
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use sha2::{Digest, Sha256};
use crate::client::{Client, ClientAuth};
use crate::events::ClientAuthFailure;
use crate::server::ClientCredential;
use crate::token::Confirmation;
pub const TLS_CLIENT_AUTH: &str = "tls_client_auth";
pub const SELF_SIGNED_TLS_CLIENT_AUTH: &str = "self_signed_tls_client_auth";
pub const TLS_CLIENT_AUTH_SUBJECT_DN: &str = "tls_client_auth_subject_dn";
pub const TLS_CLIENT_AUTH_SAN_DNS: &str = "tls_client_auth_san_dns";
pub const TLS_CLIENT_AUTH_SAN_URI: &str = "tls_client_auth_san_uri";
pub const TLS_CLIENT_AUTH_SAN_IP: &str = "tls_client_auth_san_ip";
pub const TLS_CLIENT_AUTH_SAN_EMAIL: &str = "tls_client_auth_san_email";
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct CertificateThumbprint([u8; 32]);
impl CertificateThumbprint {
pub fn from_der(der: &[u8]) -> Self {
CertificateThumbprint(Sha256::digest(der).into())
}
pub fn from_pem(pem: &str) -> Result<Self, MtlsRegistrationError> {
let body = match pem.find("-----BEGIN CERTIFICATE-----") {
Some(start) => {
let after = &pem[start + "-----BEGIN CERTIFICATE-----".len()..];
match after.find("-----END CERTIFICATE-----") {
Some(end) => &after[..end],
None => return Err(MtlsRegistrationError::MalformedCertificate),
}
}
None => return Err(MtlsRegistrationError::MalformedCertificate),
};
let compact: String = body.chars().filter(|c| !c.is_ascii_whitespace()).collect();
let der = STANDARD
.decode(compact.as_bytes())
.map_err(|_| MtlsRegistrationError::MalformedCertificate)?;
Ok(CertificateThumbprint::from_der(&der))
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn to_base64url(&self) -> String {
URL_SAFE_NO_PAD.encode(self.0)
}
pub fn from_base64url(text: &str) -> Result<Self, MtlsRegistrationError> {
let bytes = URL_SAFE_NO_PAD
.decode(text.as_bytes())
.map_err(|_| MtlsRegistrationError::MalformedThumbprint)?;
let fixed: [u8; 32] = bytes
.try_into()
.map_err(|_| MtlsRegistrationError::MalformedThumbprint)?;
Ok(CertificateThumbprint(fixed))
}
}
impl fmt::Display for CertificateThumbprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_base64url())
}
}
impl fmt::Debug for CertificateThumbprint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "CertificateThumbprint({})", self.to_base64url())
}
}
impl Serialize for CertificateThumbprint {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_base64url())
}
}
impl<'de> Deserialize<'de> for CertificateThumbprint {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = String::deserialize(deserializer)?;
CertificateThumbprint::from_base64url(&text).map_err(D::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientCertificate<'a> {
thumbprint: CertificateThumbprint,
subject_dn: Option<&'a str>,
san_dns: &'a [&'a str],
san_uri: &'a [&'a str],
san_ip: &'a [&'a str],
san_email: &'a [&'a str],
}
impl<'a> ClientCertificate<'a> {
pub fn from_der(der: &[u8]) -> Self {
ClientCertificate::from_thumbprint(CertificateThumbprint::from_der(der))
}
pub fn from_thumbprint(thumbprint: CertificateThumbprint) -> Self {
ClientCertificate {
thumbprint,
subject_dn: None,
san_dns: &[],
san_uri: &[],
san_ip: &[],
san_email: &[],
}
}
pub fn with_subject_dn(mut self, dn: &'a str) -> Self {
self.subject_dn = Some(dn);
self
}
pub fn with_san_dns(mut self, entries: &'a [&'a str]) -> Self {
self.san_dns = entries;
self
}
pub fn with_san_uri(mut self, entries: &'a [&'a str]) -> Self {
self.san_uri = entries;
self
}
pub fn with_san_ip(mut self, entries: &'a [&'a str]) -> Self {
self.san_ip = entries;
self
}
pub fn with_san_email(mut self, entries: &'a [&'a str]) -> Self {
self.san_email = entries;
self
}
pub fn thumbprint(&self) -> &CertificateThumbprint {
&self.thumbprint
}
fn satisfies(&self, expected: &ExpectedSubject) -> bool {
match expected {
ExpectedSubject::SubjectDn(dn) => self.subject_dn == Some(dn.as_str()),
ExpectedSubject::SanDns(v) => self.san_dns.contains(&v.as_str()),
ExpectedSubject::SanUri(v) => self.san_uri.contains(&v.as_str()),
ExpectedSubject::SanIp(v) => self.san_ip.contains(&v.as_str()),
ExpectedSubject::SanEmail(v) => self.san_email.contains(&v.as_str()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExpectedSubject {
SubjectDn(String),
SanDns(String),
SanUri(String),
SanIp(String),
SanEmail(String),
}
impl ExpectedSubject {
pub fn from_registration_parameters<'a, I>(parameters: I) -> Result<Self, MtlsRegistrationError>
where
I: IntoIterator<Item = (&'a str, &'a str)>,
{
let mut found: Option<ExpectedSubject> = None;
for (name, value) in parameters {
let candidate = match name {
TLS_CLIENT_AUTH_SUBJECT_DN => ExpectedSubject::SubjectDn(value.to_string()),
TLS_CLIENT_AUTH_SAN_DNS => ExpectedSubject::SanDns(value.to_string()),
TLS_CLIENT_AUTH_SAN_URI => ExpectedSubject::SanUri(value.to_string()),
TLS_CLIENT_AUTH_SAN_IP => ExpectedSubject::SanIp(value.to_string()),
TLS_CLIENT_AUTH_SAN_EMAIL => ExpectedSubject::SanEmail(value.to_string()),
_ => continue,
};
if value.is_empty() {
return Err(MtlsRegistrationError::EmptySubjectValue);
}
if found.is_some() {
return Err(MtlsRegistrationError::MoreThanOneSubjectValue);
}
found = Some(candidate);
}
found.ok_or(MtlsRegistrationError::NoSubjectValue)
}
pub fn parameter_name(&self) -> &'static str {
match self {
ExpectedSubject::SubjectDn(_) => TLS_CLIENT_AUTH_SUBJECT_DN,
ExpectedSubject::SanDns(_) => TLS_CLIENT_AUTH_SAN_DNS,
ExpectedSubject::SanUri(_) => TLS_CLIENT_AUTH_SAN_URI,
ExpectedSubject::SanIp(_) => TLS_CLIENT_AUTH_SAN_IP,
ExpectedSubject::SanEmail(_) => TLS_CLIENT_AUTH_SAN_EMAIL,
}
}
pub fn value(&self) -> &str {
match self {
ExpectedSubject::SubjectDn(v)
| ExpectedSubject::SanDns(v)
| ExpectedSubject::SanUri(v)
| ExpectedSubject::SanIp(v)
| ExpectedSubject::SanEmail(v) => v,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegisteredCertificates(Vec<CertificateThumbprint>);
impl RegisteredCertificates {
pub fn from_thumbprints(
thumbprints: Vec<CertificateThumbprint>,
) -> Result<Self, MtlsRegistrationError> {
if thumbprints.is_empty() {
return Err(MtlsRegistrationError::NoCertificates);
}
Ok(RegisteredCertificates(thumbprints))
}
pub fn from_der_certificates<'a, I>(certificates: I) -> Result<Self, MtlsRegistrationError>
where
I: IntoIterator<Item = &'a [u8]>,
{
RegisteredCertificates::from_thumbprints(
certificates
.into_iter()
.map(CertificateThumbprint::from_der)
.collect(),
)
}
pub fn from_jwks(jwks: &str) -> Result<Self, MtlsRegistrationError> {
let document: serde_json::Value =
serde_json::from_str(jwks).map_err(|_| MtlsRegistrationError::MalformedJwks)?;
let keys = document
.get("keys")
.and_then(|k| k.as_array())
.ok_or(MtlsRegistrationError::MalformedJwks)?;
let mut thumbprints = Vec::with_capacity(keys.len());
for key in keys {
let chain = match key.get("x5c").and_then(|c| c.as_array()) {
Some(chain) => chain,
None => continue,
};
let leaf = chain
.first()
.and_then(|c| c.as_str())
.ok_or(MtlsRegistrationError::MalformedJwks)?;
let der = STANDARD
.decode(leaf.as_bytes())
.map_err(|_| MtlsRegistrationError::MalformedCertificate)?;
thumbprints.push(CertificateThumbprint::from_der(&der));
}
if thumbprints.is_empty() {
return Err(MtlsRegistrationError::NoCertificateInJwks);
}
Ok(RegisteredCertificates(thumbprints))
}
pub fn thumbprints(&self) -> &[CertificateThumbprint] {
&self.0
}
pub fn contains(&self, thumbprint: &CertificateThumbprint) -> bool {
self.0.contains(thumbprint)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum MtlsClientRegistration {
TlsClientAuth(ExpectedSubject),
SelfSignedTlsClientAuth(RegisteredCertificates),
}
impl MtlsClientRegistration {
pub fn method_name(&self) -> &'static str {
match self {
MtlsClientRegistration::TlsClientAuth(_) => TLS_CLIENT_AUTH,
MtlsClientRegistration::SelfSignedTlsClientAuth(_) => SELF_SIGNED_TLS_CLIENT_AUTH,
}
}
pub fn accepts(&self, certificate: &ClientCertificate<'_>) -> bool {
match self {
MtlsClientRegistration::TlsClientAuth(expected) => certificate.satisfies(expected),
MtlsClientRegistration::SelfSignedTlsClientAuth(registered) => {
registered.contains(certificate.thumbprint())
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum MtlsRegistrationError {
NoSubjectValue,
MoreThanOneSubjectValue,
EmptySubjectValue,
MalformedJwks,
NoCertificateInJwks,
NoCertificates,
MalformedCertificate,
MalformedThumbprint,
}
impl fmt::Display for MtlsRegistrationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
MtlsRegistrationError::NoSubjectValue => {
"tls_client_auth requires one of the RFC 8705 s2.1.1 subject parameters"
}
MtlsRegistrationError::MoreThanOneSubjectValue => {
"RFC 8705 s2.1.2 permits exactly one tls_client_auth subject parameter"
}
MtlsRegistrationError::EmptySubjectValue => {
"a tls_client_auth subject parameter was empty, which no certificate can match"
}
MtlsRegistrationError::MalformedJwks => "the JWK Set could not be parsed",
MtlsRegistrationError::NoCertificateInJwks => {
"the JWK Set carries no x5c certificate to match against"
}
MtlsRegistrationError::NoCertificates => {
"a certificate registration must name at least one certificate"
}
MtlsRegistrationError::MalformedCertificate => {
"the certificate is not DER or PEM-wrapped DER"
}
MtlsRegistrationError::MalformedThumbprint => {
"an x5t#S256 value is base64url of exactly 32 bytes"
}
};
f.write_str(text)
}
}
impl std::error::Error for MtlsRegistrationError {}
impl Confirmation {
pub fn confirms_certificate(&self, der: &[u8]) -> bool {
match self.certificate_thumbprint() {
Some(bound) => *bound == CertificateThumbprint::from_der(der),
None => false,
}
}
pub fn certificate_thumbprint(&self) -> Option<&CertificateThumbprint> {
self.x5t_s256.as_ref()
}
pub fn for_certificate(certificate: &ClientCertificate<'_>) -> Self {
Confirmation {
#[cfg(feature = "dpop")]
jkt: None,
x5t_s256: Some(*certificate.thumbprint()),
}
}
}
pub(crate) fn verify_certificate(
client: &Client,
cred: &ClientCredential<'_>,
) -> Result<(), ClientAuthFailure> {
let registration = match &client.auth {
ClientAuth::Mtls { registration } => registration,
_ => return Err(ClientAuthFailure::SecretMismatch),
};
if cred.client_secret.is_some() {
return Err(ClientAuthFailure::SecretMismatch);
}
let certificate = cred
.certificate
.ok_or(ClientAuthFailure::NoCertificatePresented)?;
if registration.accepts(certificate) {
Ok(())
} else {
Err(ClientAuthFailure::CertificateMismatch)
}
}
#[cfg(test)]
#[path = "tests/mtls.rs"]
mod tests;