Skip to content

Commit

Permalink
feat!:
Browse files Browse the repository at this point in the history
* handle is format changed from 'im:wireapp={input}' to 'im:wireapp=%40{input}@{domain}'
* WireIdentity contains JWK thumbprint of the certificate public key
* WireIdentity contains a validation status (Valid/Expired/Revoked)
  • Loading branch information
beltram committed Nov 16, 2023
1 parent 1699d0c commit 2160354
Show file tree
Hide file tree
Showing 15 changed files with 503 additions and 358 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions acme/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ p384 = "0.13"
pem = "3.0"
getrandom = { version = "0.2.8", features = ["js"] }

fluvio-wasm-timer = "0.2"

[dev-dependencies]
wasm-bindgen-test = "0.3"
hex = "0.4.3"
6 changes: 5 additions & 1 deletion acme/src/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ impl RustyAcme {
return Err(CertificateError::DisplayNameMismatch.into());
}

let invalid_handle = cert_identity.handle != identifier.handle.trim_start_matches(ClientId::URI_PREFIX);
let identifier_handle = identifier
.handle
.trim_start_matches(ClientId::URI_PREFIX)
.trim_start_matches(ClientId::HANDLE_PREFIX);
let invalid_handle = cert_identity.handle != identifier_handle;
if invalid_handle {
return Err(CertificateError::HandleMismatch.into());
}
Expand Down
3 changes: 3 additions & 0 deletions acme/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,7 @@ pub enum CertificateError {
/// X509 lacks required standard fields
#[error("X509 lacks required standard fields")]
InvalidFormat,
/// Advertised public key does not match algorithm
#[error("Advertised public key does not match algorithm")]
InvalidPublicKey,
}
89 changes: 74 additions & 15 deletions acme/src/identity.rs → acme/src/identity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,27 @@ use rusty_jwt_tools::prelude::*;
use crate::error::CertificateError;
use crate::prelude::*;

mod status;
mod thumbprint;

#[derive(Debug, Clone)]
pub struct WireIdentity {
pub client_id: String,
pub handle: String,
pub display_name: String,
pub domain: String,
pub status: IdentityStatus,
pub thumbprint: String,
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum IdentityStatus {
/// All is fine
Valid,
/// The Certificate is expired
Expired,
/// The Certificate is revoked
Revoked,
}

pub trait WireIdentityReader {
Expand All @@ -29,12 +44,16 @@ impl WireIdentityReader for x509_cert::Certificate {
fn extract_identity(&self) -> RustyAcmeResult<WireIdentity> {
let (client_id, handle) = try_extract_san(&self.tbs_certificate)?;
let (display_name, domain) = try_extract_subject(&self.tbs_certificate)?;
let status = status::extract_status(&self.tbs_certificate);
let thumbprint = thumbprint::try_compute_jwk_canonicalized_thumbprint(&self.tbs_certificate)?;

Ok(WireIdentity {
client_id,
handle,
display_name,
domain,
status,
thumbprint,
})
}

Expand Down Expand Up @@ -118,12 +137,16 @@ fn try_extract_san(cert: &x509_cert::TbsCertificate) -> RustyAcmeResult<(String,
_ => None,
})
.try_for_each(|name| -> RustyAcmeResult<()> {
// since both ClientId & handle are in the SAN we first try to parse the element as
// a ClientId (since it's the most characterizable) and else fallback to a handle
if let Ok(cid) = ClientId::try_from_uri(name) {
client_id = Some(cid.to_qualified());
} else if name.starts_with(ClientId::URI_PREFIX) {
let h = name
.strip_prefix(ClientId::URI_PREFIX)
.ok_or(RustyAcmeError::ImplementationError)?
.strip_prefix(ClientId::HANDLE_PREFIX)
.ok_or(RustyAcmeError::ImplementationError)?
.to_string();
handle = Some(h);
}
Expand All @@ -144,17 +167,33 @@ pub mod tests {
wasm_bindgen_test_configure!(run_in_browser);

const CERT: &str = r#"-----BEGIN CERTIFICATE-----
MIICDDCCAbOgAwIBAgIRAPByYiuFhbbYasW+GKz5FBkwCgYIKoZIzj0EAwIwLjEN
MAsGA1UEChMEd2lyZTEdMBsGA1UEAxMUd2lyZSBJbnRlcm1lZGlhdGUgQ0EwHhcN
MjMwNzMxMTQwMjA4WhcNMzMwNzI4MTQwMjA4WjApMREwDwYDVQQKEwh3aXJlLmNv
bTEUMBIGA1UEAxMLQWxpY2UgU21pdGgwKjAFBgMrZXADIQAF/hZvvmRkWMzqZ5jU
LnGKO+y8G/Vz+olfTknk7c/8IqOB5TCB4jAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0l
BAwwCgYIKwYBBQUHAwIwHQYDVR0OBBYEFGhAhRlgprn/FUxPfL+ehHvvAigpMB8G
A1UdIwQYMBaAFB81Yl+jcBh8rnCo9MJtkZ+2vq5YMFwGA1UdEQRVMFOGFWltOndp
cmVhcHA9YWxpY2Vfd2lyZYY6aW06d2lyZWFwcD1UNENveTR2ZFJ6aWFud2ZPZ1hw
bjZBL2EzMzhlOWVhOWU4N2ZlY0B3aXJlLmNvbTAdBgwrBgEEAYKkZMYoQAEEDTAL
AgEGBAR3aXJlBAAwCgYIKoZIzj0EAwIDRwAwRAIgCP+OnliYCy7PKs3rt+x4zUuF
e2grybnLl5fsak6lFPUCIE4T8ZMlKkOZ9xeYdTlrUPT67hc++ZRAtcU03Kqiz8sm
MIICGDCCAb+gAwIBAgIQHhoe3LLRoHP+EPY4KOTgATAKBggqhkjOPQQDAjAuMQ0w
CwYDVQQKEwR3aXJlMR0wGwYDVQQDExR3aXJlIEludGVybWVkaWF0ZSBDQTAeFw0y
MzExMTYxMDM3MjZaFw0zMzExMTMxMDM3MjZaMCkxETAPBgNVBAoTCHdpcmUuY29t
MRQwEgYDVQQDEwtBbGljZSBTbWl0aDAqMAUGAytlcAMhANmHK7rIOLVhj/vmKmK1
qei8Dor8Lu/FPOnXmKLZGKrfo4HyMIHvMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE
DDAKBggrBgEFBQcDAjAdBgNVHQ4EFgQUFlquvWRvc3MxFaLrNgzv+UdGoaswHwYD
VR0jBBgwFoAUz40pQ/qEp4eFDfctCF0jmJB+5xswaQYDVR0RBGIwYIYhaW06d2ly
ZWFwcD0lNDBhbGljZV93aXJlQHdpcmUuY29thjtpbTp3aXJlYXBwPXlsLThBX3da
U2ZhUzJ1VjhWdU1FQncvN2U3OTcyM2E4YmRjNjk0ZkB3aXJlLmNvbTAdBgwrBgEE
AYKkZMYoQAEEDTALAgEGBAR3aXJlBAAwCgYIKoZIzj0EAwIDRwAwRAIgRqbsOAF7
OseMTgkjrKe3UO/UjDUGzW+jlDWOGLZsh5ECIDdNastqkvwOGfbWaeh+IuM6/oBz
flIOs9TQGOVc0YL1
-----END CERTIFICATE-----"#;

const CERT_EXPIRED: &str = r#"-----BEGIN CERTIFICATE-----
MIICGDCCAb+gAwIBAgIQM1JQFaSAmNPtoyWrvmZNGjAKBggqhkjOPQQDAjAuMQ0w
CwYDVQQKEwR3aXJlMR0wGwYDVQQDExR3aXJlIEludGVybWVkaWF0ZSBDQTAeFw0y
MzExMTYxMDQ2MDVaFw0yMzExMTYxMTA2MDVaMCkxETAPBgNVBAoTCHdpcmUuY29t
MRQwEgYDVQQDEwtBbGljZSBTbWl0aDAqMAUGAytlcAMhAEJioXny0jRMd1GAo9aq
ywcUQBJwuc4ym1DxDBuTrFCzo4HyMIHvMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE
DDAKBggrBgEFBQcDAjAdBgNVHQ4EFgQU3OFsPDRVZrOLHbL7vGiVE9CzyKwwHwYD
VR0jBBgwFoAUusKuRvUWmJgzjSYJL3ndc8W2414waQYDVR0RBGIwYIYhaW06d2ly
ZWFwcD0lNDBhbGljZV93aXJlQHdpcmUuY29thjtpbTp3aXJlYXBwPXlhRld5M3Yt
UUZDZms0X2VkLW9fNEEvNGU4NTI0ZWY0ZTIzMDY4YkB3aXJlLmNvbTAdBgwrBgEE
AYKkZMYoQAEEDTALAgEGBAR3aXJlBAAwCgYIKoZIzj0EAwIDRwAwRAIgPA0RmEYk
k9Jtg4ND98qu7qkUM3vtVVLiZkbCnRlFF04CIGCwhSo/78Kt8h6292SkT8c8eCS6
4PmNd7NrZ71etdKR
-----END CERTIFICATE-----"#;

#[test]
Expand All @@ -163,9 +202,9 @@ e2grybnLl5fsak6lFPUCIE4T8ZMlKkOZ9xeYdTlrUPT67hc++ZRAtcU03Kqiz8sm
let cert_der = pem::parse(CERT).unwrap();
let identity = cert_der.contents().extract_identity().unwrap();

let expected_client_id = "T4Coy4vdRzianwfOgXpn6A:a338e9ea9e87fec@wire.com";
let expected_client_id = "yl-8A_wZSfaS2uV8VuMEBw:7e79723a8bdc694f@wire.com";
assert_eq!(&identity.client_id, expected_client_id);
assert_eq!(&identity.handle, "alice_wire");
assert_eq!(&identity.handle, "alice_wire@wire.com");
assert_eq!(&identity.display_name, "Alice Smith");
assert_eq!(&identity.domain, "wire.com");
}
Expand All @@ -175,7 +214,7 @@ e2grybnLl5fsak6lFPUCIE4T8ZMlKkOZ9xeYdTlrUPT67hc++ZRAtcU03Kqiz8sm
fn should_find_created_at_claim() {
let cert_der = pem::parse(CERT).unwrap();
let created_at = cert_der.contents().extract_created_at().unwrap();
assert_eq!(created_at, 1690812128);
assert_eq!(created_at, 1700131046);
}

#[test]
Expand All @@ -185,7 +224,27 @@ e2grybnLl5fsak6lFPUCIE4T8ZMlKkOZ9xeYdTlrUPT67hc++ZRAtcU03Kqiz8sm
let spki = cert_der.contents().extract_public_key().unwrap();
assert_eq!(
hex::encode(spki),
"05fe166fbe646458ccea6798d42e718a3becbc1bf573fa895f4e49e4edcffc22"
"d9872bbac838b5618ffbe62a62b5a9e8bc0e8afc2eefc53ce9d798a2d918aadf"
);
}

#[test]
#[wasm_bindgen_test]
fn should_have_valid_status() {
let cert_der = pem::parse(CERT).unwrap();
let identity = cert_der.contents().extract_identity().unwrap();
assert_eq!(&identity.status, &IdentityStatus::Valid);

let cert_der = pem::parse(CERT_EXPIRED).unwrap();
let identity = cert_der.contents().extract_identity().unwrap();
assert_eq!(&identity.status, &IdentityStatus::Expired);
}

#[test]
#[wasm_bindgen_test]
fn should_have_thumbprint() {
let cert_der = pem::parse(CERT).unwrap();
let identity = cert_der.contents().extract_identity().unwrap();
assert!(!identity.thumbprint.is_empty());
}
}
30 changes: 30 additions & 0 deletions acme/src/identity/status.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use super::IdentityStatus;

pub(crate) fn extract_status(cert: &x509_cert::TbsCertificate) -> IdentityStatus {
if is_revoked(cert) {
IdentityStatus::Revoked
} else if !is_time_valid(cert) {
IdentityStatus::Expired
} else {
IdentityStatus::Valid
}
}

fn is_time_valid(cert: &x509_cert::TbsCertificate) -> bool {
// 'not_before' < now < 'not_after'
let x509_cert::time::Validity { not_before, not_after } = cert.validity;

let now = fluvio_wasm_timer::SystemTime::now();
let Ok(now) = now.duration_since(fluvio_wasm_timer::UNIX_EPOCH) else {
return false;
};

let is_nbf = now >= not_before.to_unix_duration();
let is_naf = now < not_after.to_unix_duration();
is_nbf && is_naf
}

// TODO
fn is_revoked(_cert: &x509_cert::TbsCertificate) -> bool {
false
}
33 changes: 33 additions & 0 deletions acme/src/identity/thumbprint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use crate::{
error::CertificateError,
prelude::{RustyAcmeError, RustyAcmeResult},
};
use jwt_simple::prelude::*;
use rusty_jwt_tools::{
jwk::TryIntoJwk,
prelude::{HashAlgorithm, JwkThumbprint},
};
use x509_cert::spki::SubjectPublicKeyInfoOwned;

/// See: https://datatracker.ietf.org/doc/html/rfc8037#appendix-A.3
pub(crate) fn try_compute_jwk_canonicalized_thumbprint(cert: &x509_cert::TbsCertificate) -> RustyAcmeResult<String> {
let jwk = try_into_jwk(&cert.subject_public_key_info)?;
// Hash is always SHA-256
let thumbprint = JwkThumbprint::generate(&jwk, HashAlgorithm::SHA256)?;
Ok(thumbprint.kid)
}

fn try_into_jwk(spki: &SubjectPublicKeyInfoOwned) -> RustyAcmeResult<Jwk> {
let oid = oid_registry::Oid::new(std::borrow::Cow::Borrowed(spki.algorithm.oid.as_bytes()));

// cannot pattern match oid_registry::Oid because it contains a Cow<'_>
if oid == oid_registry::OID_SIG_ED25519 {
Ok(Ed25519PublicKey::from_bytes(spki.subject_public_key.raw_bytes())?.try_into_jwk()?)
} else if oid == oid_registry::OID_SIG_ECDSA_WITH_SHA256 {
Ok(ES256PublicKey::from_bytes(spki.subject_public_key.raw_bytes())?.try_into_jwk()?)
} else if oid == oid_registry::OID_SIG_ECDSA_WITH_SHA384 {
Ok(ES384PublicKey::from_bytes(spki.subject_public_key.raw_bytes())?.try_into_jwk()?)
} else {
Err(RustyAcmeError::InvalidCertificate(CertificateError::InvalidPublicKey))
}
}
2 changes: 1 addition & 1 deletion acme/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub mod prelude {
pub use error::{RustyAcmeError, RustyAcmeResult};
pub use finalize::AcmeFinalize;
pub use identifier::{AcmeIdentifier, WireIdentifier};
pub use identity::{WireIdentity, WireIdentityReader};
pub use identity::{IdentityStatus, WireIdentity, WireIdentityReader};
pub use jws::AcmeJws;
pub use order::AcmeOrder;

Expand Down
2 changes: 1 addition & 1 deletion acme/src/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ impl RustyAcme {
let acct_url = account.acct_url()?;

let domain = client_id.domain.clone();
let handle = format!("{}{handle}", ClientId::URI_PREFIX);
let handle = format!("{}{}{handle}@{domain}", ClientId::URI_PREFIX, ClientId::HANDLE_PREFIX);
let identifiers = vec![AcmeIdentifier::try_new(
display_name.to_string(),
domain,
Expand Down
Loading

0 comments on commit 2160354

Please sign in to comment.