use std::{fmt::Debug, sync::Arc};
use cms::cert::x509::{
Certificate,
attr::AttributeValue,
der,
der::oid as const_oid,
ext::pkix::{SubjectAltName, name::GeneralName},
};
use ruma::{MatrixUri, OwnedUserId, UserId, matrix_uri::MatrixId};
use crate::{
olm::SignedJsonObject,
types::{Signature, X509Signature},
x509::{
errors::X509SignatureVerificationError,
raw_x509_signature::{RawX509Signature, RawX509SignatureAndFirstCertificate},
},
};
#[derive(Debug, Clone)]
pub(crate) struct X509Verifier {
x509_verify: Arc<dyn RawX509Verifier>,
}
impl X509Verifier {
pub(crate) fn new(x509_verify: Arc<dyn RawX509Verifier>) -> X509Verifier {
X509Verifier { x509_verify }
}
pub(crate) fn verify_signed_object(
&self,
user_id: &UserId,
signed_object: &(impl SignedJsonObject + Debug),
) -> bool {
let Some(this_user_sigs) = signed_object.signatures().get(user_id) else {
tracing::info!("X509: verify_signed_object(): no signatures on object");
return false;
};
let Ok(msg) = signed_object.to_canonical_json() else {
tracing::warn!("Unable to serialize object");
return false;
};
for sig in this_user_sigs.values().flatten() {
if let Signature::X509(sig) = sig
&& self
.verify_x509_signature(user_id, &msg, sig)
.inspect_err(|e| {
tracing::warn!(
"X509: verify_signed_object(): X509 signature failed verification: {e}"
)
})
.is_ok()
{
tracing::debug!("X509: verify_signed_object(): verified X509 signature");
return true;
}
}
false
}
pub(crate) fn verify_x509_signature(
&self,
user_id: &UserId,
message: &str,
sig: &X509Signature,
) -> Result<(), X509SignatureVerificationError> {
let res: RawX509SignatureAndFirstCertificate =
sig.try_into().map_err(X509SignatureVerificationError::RawSignatureParseError)?;
if !cert_contains_user_id_or_equivalent_email(user_id, &res.leaf_cert) {
tracing::warn!(?user_id, "Verifying certificate user ID or email failed");
return Err(X509SignatureVerificationError::BadUserIdOrEmail);
}
self.x509_verify.verify(message.as_bytes(), &res.raw_x509signature)
}
}
fn cert_contains_user_id_or_equivalent_email(user_id: &UserId, certificate: &Certificate) -> bool {
if let Some(certificate_user_id) = get_user_id_from_certificate(certificate) {
if certificate_user_id == user_id {
return true;
} else {
tracing::warn!(
"Certificate not valid for this user. \
Certificate user ID: {certificate_user_id}, \
User ID: {user_id}",
);
return false;
}
}
tracing::info!("Certificate subject does not contain a user ID. Checking for email address");
let Some(certificate_email) = get_email_address_from_certificate(certificate) else {
tracing::warn!("Certificate subject does not contain an email address");
return false;
};
let expected_email = map_user_id_to_email(user_id);
if certificate_email == expected_email {
true
} else {
tracing::warn!(
"Certificate not valid for this user. \
Certificate email: {certificate_email}, \
Expected email: {expected_email}, User ID: {user_id}",
);
false
}
}
pub trait RawX509Verifier: Debug + Send + Sync {
fn verify(
&self,
message: &[u8],
signature: &RawX509Signature,
) -> Result<(), X509SignatureVerificationError>;
}
fn map_user_id_to_email(user_id: &UserId) -> String {
format!("{}@{}", user_id.localpart(), user_id.server_name())
}
fn get_user_id_from_certificate(certificate: &Certificate) -> Option<OwnedUserId> {
let Ok(Some((_, san))) = certificate.tbs_certificate.get::<SubjectAltName>() else {
return None;
};
fn matrix_user_uri(alt_name: &GeneralName) -> Option<OwnedUserId> {
if let GeneralName::UniformResourceIdentifier(uri) = alt_name {
if let Ok(matrix_uri) = MatrixUri::parse(uri.as_str()) {
if let MatrixId::User(user_id) = matrix_uri.id() {
return Some(user_id.clone());
}
}
}
None
}
san.0.iter().find_map(matrix_user_uri)
}
fn get_email_address_from_certificate(certificate: &Certificate) -> Option<String> {
if let Ok(Some((_, san))) = certificate.tbs_certificate.get::<SubjectAltName>()
&& let Some(email) = san
.0
.into_iter()
.find_map(|n| if let GeneralName::Rfc822Name(email) = n { Some(email) } else { None })
{
return Some(email.as_str().to_owned());
}
let subject = &certificate.tbs_certificate.subject;
for atav in subject.0.iter().flat_map(|rdn| rdn.0.iter()) {
if atav.oid == const_oid::db::rfc3280::EMAIL_ADDRESS
&& let Some(e) = get_attribute_value_as_string(&atav.value)
{
return Some(e.to_owned());
}
}
None
}
fn get_attribute_value_as_string(value: &AttributeValue) -> Option<&str> {
use der::Tagged;
match value.tag() {
der::Tag::PrintableString => {
der::asn1::PrintableStringRef::try_from(value).ok().map(|s| s.as_str())
}
der::Tag::Utf8String => der::asn1::Utf8StringRef::try_from(value).ok().map(|s| s.as_str()),
der::Tag::Ia5String => der::asn1::Ia5StringRef::try_from(value).ok().map(|s| s.as_str()),
der::Tag::TeletexString => {
der::asn1::TeletexStringRef::try_from(value).ok().map(|s| s.as_str())
}
_ => None,
}
}
#[cfg(test)]
pub(crate) mod tests {
use cms::cert::x509::der::Decode;
use matrix_sdk_test::async_test;
use rcgen::generate_simple_self_signed;
use ruma::{DeviceKeyAlgorithm, DeviceKeyId, encryption::KeyUsage, user_id};
use vodozemac::Ed25519SecretKey;
use super::*;
use crate::{
types::{CrossSigningKey, SigningKeys},
x509::tests::{
cert_and_key_with_email_in_subject_alternate_name,
cert_and_key_with_email_in_subject_distinguished_name, cert_and_key_with_no_user_id,
cert_and_key_with_user_id_in_subject_alternate_name, create_rust_signer_and_verifier,
},
};
#[test]
fn test_can_extract_email_address_from_a_cert_sdn() {
let (cert, _) =
cert_and_key_with_email_in_subject_distinguished_name("myname@company.co.uk");
let email = get_email_address_from_certificate(&Certificate::from_der(cert.der()).unwrap())
.expect("Failed to get email address from cert");
assert_eq!(email, "myname@company.co.uk");
}
#[test]
fn test_can_extract_email_address_from_a_cert_san() {
let (cert, _) = cert_and_key_with_email_in_subject_alternate_name("myname@company.co.uk");
let email = get_email_address_from_certificate(&Certificate::from_der(cert.der()).unwrap())
.expect("Failed to get email address from cert");
assert_eq!(email, "myname@company.co.uk");
}
#[test]
fn test_can_extract_user_id_from_a_cert_san() {
let (cert, _) =
cert_and_key_with_user_id_in_subject_alternate_name("@myname:company.co.uk");
let user_id = get_user_id_from_certificate(&Certificate::from_der(cert.der()).unwrap())
.expect("Failed to get email address from cert");
assert_eq!(user_id, "@myname:company.co.uk");
}
#[test]
fn test_extract_email_address_from_a_cert_that_does_not_contain_one_returns_none() {
let cert = generate_simple_self_signed(&[]).expect("Failed to generate cert");
let email =
get_email_address_from_certificate(&Certificate::from_der(cert.cert.der()).unwrap());
assert!(email.is_none());
}
#[test]
fn test_extract_user_id_from_a_cert_that_does_not_contain_one_returns_none() {
let cert = generate_simple_self_signed(&[]).expect("Failed to generate cert");
let user_id =
get_user_id_from_certificate(&Certificate::from_der(cert.cert.der()).unwrap());
assert!(user_id.is_none());
}
#[async_test]
async fn test_can_verify_cert_containing_email_in_dn() {
let (cert, signing_key) =
cert_and_key_with_email_in_subject_distinguished_name("alice@localhost");
let (x509_signer, x509_verifier) = create_rust_signer_and_verifier(cert, signing_key);
let user_id = user_id!("@alice:localhost").to_owned();
let mut cross_signing_key = create_cross_signing_key(&user_id);
assert!(!x509_verifier.verify_signed_object(&user_id, &cross_signing_key));
x509_signer.sign_cross_signing_key(&user_id, &mut cross_signing_key).await.unwrap();
assert!(x509_verifier.verify_signed_object(&user_id, &cross_signing_key));
}
#[async_test]
async fn test_can_verify_cert_containing_email_in_san() {
let (cert, signing_key) =
cert_and_key_with_email_in_subject_alternate_name("alice@localhost");
let (x509_signer, x509_verifier) = create_rust_signer_and_verifier(cert, signing_key);
let user_id = user_id!("@alice:localhost").to_owned();
let mut cross_signing_key = create_cross_signing_key(&user_id);
x509_signer.sign_cross_signing_key(&user_id, &mut cross_signing_key).await.unwrap();
assert!(x509_verifier.verify_signed_object(&user_id, &cross_signing_key));
}
#[async_test]
async fn test_can_verify_cert_containing_username_in_san() {
let (cert, signing_key) =
cert_and_key_with_user_id_in_subject_alternate_name("@alice:localhost");
let (x509_signer, x509_verifier) = create_rust_signer_and_verifier(cert, signing_key);
let user_id = user_id!("@alice:localhost").to_owned();
let mut cross_signing_key = create_cross_signing_key(&user_id);
x509_signer.sign_cross_signing_key(&user_id, &mut cross_signing_key).await.unwrap();
assert!(x509_verifier.verify_signed_object(&user_id, &cross_signing_key));
}
#[async_test]
async fn test_verification_fails_if_dn_email_is_wrong() {
let (cert, signing_key) =
cert_and_key_with_email_in_subject_distinguished_name("bob@localhost");
let (x509_signer, x509_verifier) = create_rust_signer_and_verifier(cert, signing_key);
let user_id = user_id!("@alice:localhost").to_owned();
let mut cross_signing_key = create_cross_signing_key(&user_id);
x509_signer.sign_cross_signing_key(&user_id, &mut cross_signing_key).await.unwrap();
assert!(!x509_verifier.verify_signed_object(&user_id, &cross_signing_key));
}
#[async_test]
async fn test_verification_fails_if_san_email_is_wrong() {
let (cert, signing_key) =
cert_and_key_with_email_in_subject_alternate_name("bob@localhost");
let (x509_signer, x509_verifier) = create_rust_signer_and_verifier(cert, signing_key);
let user_id = user_id!("@alice:localhost").to_owned();
let mut cross_signing_key = create_cross_signing_key(&user_id);
x509_signer.sign_cross_signing_key(&user_id, &mut cross_signing_key).await.unwrap();
assert!(!x509_verifier.verify_signed_object(&user_id, &cross_signing_key));
}
#[async_test]
async fn test_verification_fails_if_cert_user_id_is_wrong() {
let (cert, signing_key) =
cert_and_key_with_user_id_in_subject_alternate_name("@bob:localhost");
let (x509_signer, x509_verifier) = create_rust_signer_and_verifier(cert, signing_key);
let user_id = user_id!("@alice:localhost").to_owned();
let mut cross_signing_key = create_cross_signing_key(&user_id);
x509_signer.sign_cross_signing_key(&user_id, &mut cross_signing_key).await.unwrap();
assert!(!x509_verifier.verify_signed_object(&user_id, &cross_signing_key));
}
#[async_test]
async fn test_verification_fails_if_cert_user_id_is_missing() {
let (cert, signing_key) = cert_and_key_with_no_user_id();
let (x509_signer, x509_verifier) = create_rust_signer_and_verifier(cert, signing_key);
let user_id = user_id!("@alice:localhost").to_owned();
let mut cross_signing_key = create_cross_signing_key(&user_id);
x509_signer.sign_cross_signing_key(&user_id, &mut cross_signing_key).await.unwrap();
assert!(!x509_verifier.verify_signed_object(&user_id, &cross_signing_key));
}
fn create_cross_signing_key(user_id: &UserId) -> CrossSigningKey {
let secret_key = Ed25519SecretKey::new();
let public_key = secret_key.public_key();
let keys = SigningKeys::from([(
DeviceKeyId::from_parts(
DeviceKeyAlgorithm::Ed25519,
public_key.to_base64().as_str().into(),
),
public_key.into(),
)]);
CrossSigningKey::new(user_id.to_owned(), vec![KeyUsage::Master], keys, Default::default())
}
}