use std::{pin::Pin, sync::Arc, time::Duration};
use ruma::{DeviceKeyId, UserId, canonical_json::to_canonical_value};
use thiserror::Error;
use tracing::info;
use crate::{
SignatureError,
olm::utility::to_signable_json,
types::{CrossSigningKey, Signature, Signatures, X509_SIGNATURE_ALGORITHM},
x509::raw_x509_signature::RawX509Signature,
};
#[derive(Debug, Clone)]
pub(crate) struct X509Signer {
x509_sign: Arc<dyn RawX509Signer>,
}
impl X509Signer {
pub(crate) fn new(x509_sign: Arc<dyn RawX509Signer>) -> Self {
Self { x509_sign }
}
pub(crate) fn raw(&self) -> Arc<dyn RawX509Signer> {
self.x509_sign.clone()
}
pub(crate) async fn sign_cross_signing_key(
&self,
signing_user_id: &UserId,
cross_signing_key: &mut CrossSigningKey,
) -> Result<(), SignatureError> {
let json = to_signable_json(to_canonical_value(&cross_signing_key)?)?;
let signing_result = self.x509_sign.sign(json.into_bytes()).await;
let (authority_key_identifier, signature) = signing_result?
.into_x509_signature()
.map_err(|e| SignatureError::X509SigningError(e.into()))?;
cross_signing_key.signatures.add_signature(
signing_user_id.to_owned(),
DeviceKeyId::from_parts(X509_SIGNATURE_ALGORITHM.into(), &authority_key_identifier),
signature,
);
Ok(())
}
pub fn has_later_expiry_than(&self, user_id: &UserId, signatures: &Signatures) -> bool {
let Some(this_user_sigs) = signatures.get(user_id) else {
info!("X509: has_later_expiry_than(): no signatures on object");
return true;
};
let Ok(signer_validity_not_after) = self.x509_sign.validity_not_after() else {
info!("X509: has_later_expiry_than(): signer has invalid validity period");
return false;
};
for sig in this_user_sigs.values() {
if let Ok(Signature::X509(sig)) = sig {
let data: cms::signed_data::SignedData =
match sig.get_signature().content.decode_as() {
Ok(res) => res,
Err(e) => {
tracing::warn!(
"X509: has_later_expiry_than(): unable to parse X509 signature: {}",
e
);
continue;
}
};
let Some(certificate_set) = data.certificates else {
tracing::warn!("X509: has_later_expiry_than(): no certificates found");
continue;
};
let Some(validity_not_after) = certificate_set
.0
.iter()
.filter_map(|cert| match cert {
cms::cert::CertificateChoices::Certificate(c) => {
Some(c.tbs_certificate.validity.not_after.to_unix_duration())
}
_ => None,
})
.min()
else {
tracing::warn!("X509: has_later_expiry_than(): no certificates found");
continue;
};
if validity_not_after >= signer_validity_not_after {
return false;
}
}
}
true
}
}
pub trait RawX509Signer: std::fmt::Debug + Send + Sync {
#[cfg(not(target_family = "wasm"))]
fn sign(
&self,
message: Vec<u8>,
) -> Pin<Box<dyn Future<Output = Result<RawX509Signature, SignatureError>> + Send>>;
#[cfg(target_family = "wasm")]
fn sign(
&self,
message: Vec<u8>,
) -> Pin<Box<dyn Future<Output = Result<RawX509Signature, SignatureError>>>>;
fn validity_not_after(&self) -> Result<Duration, ValidityError>;
}
#[derive(Error, Debug)]
#[error("Failed to get X.509 certificate validity period")]
pub struct ValidityError;
#[cfg(test)]
mod tests {
use std::sync::Arc;
use assert_matches::assert_matches;
use matrix_sdk_test::async_test;
use rcgen::{CertificateParams, KeyPair};
use ruma::{DeviceKeyAlgorithm, DeviceKeyId, encryption::KeyUsage, user_id};
use vodozemac::Ed25519SecretKey;
use crate::{
types::{CrossSigningKey, Signature, SigningKeys},
x509::{
X509Signer, rust_raw_x509_signer::RustRawX509Signer,
tests::subject_key_identifier_extension,
},
};
#[async_test]
async fn test_can_sign() {
let x509_signer = {
let rust_raw_x509_signer =
RustRawX509Signer::new_from_pem_data(TEST_CERT_CHAIN, TEST_CERT_KEY).unwrap();
X509Signer::new(Arc::new(rust_raw_x509_signer))
};
let user_id = user_id!("@vdh-x509test:sw1v.org").to_owned();
let mut cross_signing_key = {
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.clone(), vec![KeyUsage::Master], keys, Default::default())
};
x509_signer.sign_cross_signing_key(&user_id, &mut cross_signing_key).await.unwrap();
let self_sigs = cross_signing_key.signatures.get(&user_id).unwrap();
assert_matches!(
self_sigs
.get(&DeviceKeyId::from_parts(
"io.element.x509".into(),
"2F6Rmhfww1sT23VCfSE3mt8+lhE".into()
))
.unwrap(),
Ok(Signature::X509(_))
);
}
#[async_test]
async fn test_can_compare_validity() {
let signing_key = KeyPair::from_pem(TEST_CERT_KEY).unwrap();
let mut cert_params = CertificateParams::default();
cert_params.use_authority_key_identifier_extension = true;
cert_params.custom_extensions.push(subject_key_identifier_extension(&signing_key));
let (x509_signer_old, x509_signer_current, x509_signer_new) =
crate::x509::tests::signers_with_different_validity();
let user_id = user_id!("@user:localhost");
let signatures = {
let mut cross_signing_key = {
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(),
)
};
x509_signer_current
.sign_cross_signing_key(user_id, &mut cross_signing_key)
.await
.unwrap();
cross_signing_key.signatures
};
assert!(!x509_signer_old.has_later_expiry_than(user_id, &signatures));
assert!(!x509_signer_current.has_later_expiry_than(user_id, &signatures));
assert!(x509_signer_new.has_later_expiry_than(user_id, &signatures));
}
const TEST_CERT_CHAIN: &str = "
-----BEGIN CERTIFICATE-----
MIIEMDCCAhigAwIBAgIBATANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQGEwJVSzEP
MA0GA1UECAwGTG9uZG9uMRMwEQYDVQQKDAplbGVtZW50LmlvMRgwFgYDVQQDDA9p
bnRlcm1lZGlhdGUtY2EwHhcNMjYwNjExMjI1MjMyWhcNMjcwNjExMjI1MjMyWjAm
MSQwIgYJKoZIhvcNAQkBFhV2ZGgteDUwOXRlc3RAc3cxdi5vcmcwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCQW4+eDT5UlecWnOeLjzMIcQ/sdRb20uPF
iiRNmzS7kvWIxsTLi2t8pmhQsxqI3CdIWyWfEHy56Np+4HCVCYHoKEc9H/Zm2W2N
63YFTZol7VKVYRAHPHdNh06y6wb1LuzTds5uSpEsvRhqrq14scYCQiFVstdtq5Pc
+F5DpLnGpo1GvzJiF1T5WpNgk9EqcP9Yso4rpwzv1TOsNqX91Tej2tsAZvRbkBms
ILyoFMe3FBwZYGy3nd56Mz/LgxJ4FZEbzHeHQ2i3jpeiz7DESW5VGFUHwg1FdWAG
tTzET+eoVDRC9YzRR+Ka8yQru0kFaLAhAUjvyoMo1Nb8C1IoSTtVAgMBAAGjQjBA
MB0GA1UdDgQWBBSMiQQcHg6Ixffg8lknAK1wY42fjDAfBgNVHSMEGDAWgBR0DWpq
YxJI0M81uFSlqoYvckCA3jANBgkqhkiG9w0BAQsFAAOCAgEArMHX8k+xYTUVAOSR
Vzt8aMMC/LSwN0zQq/+CLX3AuhE94j+2cRM3TF5jpzeRmTObpaK88qVN5CrrK5IC
ddMvMAW+eAUyJc7aXX1tPWD0YRcWlplR0CVm6RY/VIwsVlWvhIX6fYpAJBMoYqr4
tCYcDjA1/FNkuUQnBNnxnp0I3HQuSa1UBCCHZ/00kbVl331XY5bxZrk1OSUWP9TF
pHdW1Fgjqu0EZccjEYK7F1I+weTfkl/uOp6QUl7cmhiLnMsd0QwKUdFStOy1BYX8
tbkMN44diYC9hRUhP9gsud12dBeF4EKl66y43zgUNPAzD3MMmU/xndWIgFs+/PnK
vlbtSMl7wdTr0bh+c3srvY4uuft2Uy0PH9mi0Sffy6LKqg5FUT6kdPtQscZYamb0
TfJ4xjFPII7H6mkvnH/Nm0RWTvsChLeyrPQ8kl328NW0NWlitBZ0k3jiQj0Aqfim
uZwS3f03oqA/254nZU9K6SC5Nvq0L7g71XGK6ZwMVOK3Vw3TV6XLq+nO3FilSrk3
a48QHPLwSRAs7dMIWztnycB/hyMp7daocSieza74AxSwZv6KS6ilEBFv3z9OjTS0
Z5Zy87N5SmP89OfE92IxNQ+tYlnk2THqZSBbSj5hqdHOUVWCzbvHxFEKGSzK3+B5
vDQS6Fr6qeVx8k6TzrvYTf9u61w=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFcDCCA1igAwIBAgIBCDANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJVSzEP
MA0GA1UECAwGTG9uZG9uMRMwEQYDVQQKDAplbGVtZW50LmlvMRAwDgYDVQQDDAd0
ZXN0LWNhMB4XDTI2MDYwNTE1MDIxNloXDTI3MDYwNTE1MDIxNlowTTELMAkGA1UE
BhMCVUsxDzANBgNVBAgMBkxvbmRvbjETMBEGA1UECgwKZWxlbWVudC5pbzEYMBYG
A1UEAwwPaW50ZXJtZWRpYXRlLWNhMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
CgKCAgEAt+6qbrTcH+TiHxJl7pJjIajUXvu0nYwnfqDQcaZRY0qlqPuHccBudNfk
FMoD6fchj5OvyQsZLP8UnirLxVgf6GgTLZ2JK4tTpaCFWv45ukHVYeY/r+c0fOuy
zUw4H5LljJ4ikPI3w7vncSQTOCYHpDGVhW8Mh4mAug7ODlL3RZVSsRxuikDhLQoR
OS2pRDwTFll1CYE8+MTkxPcGrnOFQMqB9290af0PnkS64BO0nPUZYqsPELkcPkGU
lc0mTWYunvfG21cU7TnPGRto4nj5l4xZBGpRZSEf3L3yh+4Chi35Rq1hWHlQ8ito
X7n1vNELkHShkQcSmALEmlspGPt6Mhjdb3Xj1N3wNlftXe7g2tFEcnpXu6EwNJYP
mljN1qEA/37xBz9JyZzeahPvOg/hF/+EX3/FcnXLDAYCbFnx/44Ljnr7MHqjQwfH
B7Ndcp22OHC6fwAEjuTPTQlbMmxrxXETKsQwg7aVIPs743R6RJeBQbkUP3hm/7G0
uwoB2kphdjvGHYJtDguNXHuiXz/Pn6F+D6m3j/SDF4uAfnzxKDDVURPlQg2Wr2od
ez0aklHgJ9ZUAfpk3C6f1VGUmxpjVhp6nPsQ5ZgGFHNOjWZuV2+cwxahpYG6KzVD
mO/brmSc76ibzRwapIOLSjb+plbp8VGrBElIhvqRFyZmTOcGdLkCAwEAAaNjMGEw
HQYDVR0OBBYEFHQNampjEkjQzzW4VKWqhi9yQIDeMB8GA1UdIwQYMBaAFNhekZoX
8MNbE9t1Qn0hN5rfPpYRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGG
MA0GCSqGSIb3DQEBCwUAA4ICAQA1X27xupoF0brABykMbCE4Q1QQ2u3oLUOoX0vP
EVBjGC7tlIe6O1+s8lCSSM6pAly61kMl2rIXE7GzPMLVNjg67etXfNbb8KXPqiKi
bPPLQ6Pk5/RJd3D8DUO4gKgEKtkBZK30JzKVsl5eLgxT1GVnDXgV9jNLqXuNgreZ
Ba4BYjdtR2pMbVlW0ZEda7m4N5CDh8rfJQqnZeudhzqjecXV5bxxvKotvOEgsYml
WZ4yxAwuJhlb+I/Rmc+hocsRBx8v8LIIQh/biUMaZnoCqj8d9qkpFmf9nWhXQRbJ
Sqz2ad08ZXxMY+vXHQ3mR1H4cYm5jOWuyXWSULo/WT+OrGPFrH1/E3IjhvVaal6m
c4IJQZCI7iHhyeHLxjaVmctgwV2yMUZnCuBXrv0yHkkDyr1D6OBaHF/0dYSd/rym
ZDo1S4QcKmoJJRP0nDdq7zMoLneu7Ytmn3PZ8qiAUnh5vlaQCg3SHkdQ0berp7yU
+0tFAPc7c4+yad+YBxLi+Zqplqw9Ra810Nn+coBH54+DOytWQQdK4kx0pXc+2MYR
sykQE11ItydeFcAu2Rtp6dw5G9DNOIQJWYeGOECx6phCZYrZVEPZoeF0dnsEcdP8
F5aVeO+O1gffTCxzhLTqMXNvm86oP6QC71gC1w6eM4uuGubc56Dm4hxVywwRUGgJ
tsmVjQ==
-----END CERTIFICATE-----
";
const TEST_CERT_KEY: &str = "
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCQW4+eDT5UlecW
nOeLjzMIcQ/sdRb20uPFiiRNmzS7kvWIxsTLi2t8pmhQsxqI3CdIWyWfEHy56Np+
4HCVCYHoKEc9H/Zm2W2N63YFTZol7VKVYRAHPHdNh06y6wb1LuzTds5uSpEsvRhq
rq14scYCQiFVstdtq5Pc+F5DpLnGpo1GvzJiF1T5WpNgk9EqcP9Yso4rpwzv1TOs
NqX91Tej2tsAZvRbkBmsILyoFMe3FBwZYGy3nd56Mz/LgxJ4FZEbzHeHQ2i3jpei
z7DESW5VGFUHwg1FdWAGtTzET+eoVDRC9YzRR+Ka8yQru0kFaLAhAUjvyoMo1Nb8
C1IoSTtVAgMBAAECggEAEOKg2SYn6wlwsxitzcl1ePCOogQtKDhRO6c1qV007Rba
wQGs/bkEXNtzGrNkcGs97gz5SNKLIEzQF+SlTo2C5Dan5IqzTeLzWVUYJDUoSXTp
wr7Meugz9T3VMwDiOrYfLfn42fY/ZmoE69+cO2Ch7lwxXX6Si8m0vTVRA10GfOmL
2xcaXTN5EcDT4aA9IuEEzs3bGd1j4+WufDtPVV5zZXQLPXzxMBqegphnqW36ACs4
14a6HaWOfy+ggy52xxx92TeHO5Sf6MX7o2BIIEp2UXGEl8Nur7UTifZpiiqfGbxj
z2AD69Q8+Oh6qiM49JNKUJ1FKpeHrUtI/qdZpKAvuQKBgQDI+7wLvMAfQ+xRUv4u
EHhWh5uK5PTf9YojfPqGsLls5xzHlwIa2CYNbWFk7/grFL2lT0uv3beoVcxSQC8+
ITMxrNl2OnqSwIiLF3rAIEQn/XfeiU/cdq1wakLEBdeNxCY3G4p7vk85s+KAkoH/
ebs2ChuXnuTQGQf32S4GGdQU+QKBgQC3365jChmHK0LW8pJyjJIJrWjvQ9dLXb30
j6dnCWPd6M0dK5+NfumOPMvw15cu6tkjs7IMwxFcnSN84h1BKe79PGSTaLl1JaOC
wo24ZGqTZZEELkehSWqc6isBce5aVInXUzBH9MSvxvLdXlW+zaItH/PvSRwO2sUS
kSY0sF0cPQKBgQCE/odt4OXlCoZLPjbydnWbFLspit47gPh7CU2iaTkaRki2DkgX
SWbMxc+IAn9eyqe/xxwXcQkB/FxrJQvd+gwtV+rCoGnRyFPSbqQMlI1lRQXYHVba
VTHpzHcHzbHYnq6HEtNtlP5J+a3tVIVvb7chSEj/6OYSii3KpU0ePmMnyQKBgGpp
oVrf9XYsqzoKmIaCo+HF4fzWnjqXvd9TY+ZVoN5EZLCFFomk8TXIKZ7wpiYY9CGd
VWXdXqbiqi8UDSoxQoZ79Rj6epo5di+uuKYGN0emeA6bWgkVnAXD36+uZ+sPEdbz
5fU+yrWPxe4nMiiCiWDkJSBOh1ZxdawRJLNJfhlhAoGBAMF2/htOA9Zucf23I/7X
xvt0tTxmh11PlmWJqAdP4w8Pi0xM6LgXpsnU8tFhK/ouHsgshcrDfZpugE7j6ZLv
xJoRUZyn77BUtdyTi/hbMsK5v9GzujTmK2hXYH+mamYRStsIZWFB1haAGT7W6njW
oJQP+ghjUYgZHGzfyheUm0L6
-----END PRIVATE KEY-----
";
}