Skip to content

Commit

Permalink
feat!: add 'handle' & 'team' to the client DPoP token and verify thes…
Browse files Browse the repository at this point in the history
…e in the ACME server & wire-server
  • Loading branch information
beltram committed Nov 23, 2023
1 parent 71a4f18 commit 337be81
Show file tree
Hide file tree
Showing 35 changed files with 954 additions and 453 deletions.
22 changes: 22 additions & 0 deletions Cargo.lock

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

6 changes: 1 addition & 5 deletions acme/src/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,7 @@ impl RustyAcme {
return Err(CertificateError::DisplayNameMismatch.into());
}

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;
let invalid_handle = cert_identity.handle != identifier.handle;
if invalid_handle {
return Err(CertificateError::HandleMismatch.into());
}
Expand Down
9 changes: 5 additions & 4 deletions acme/src/finalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,14 @@ impl RustyAcme {

// TODO: find a cleaner way to encode this reusing more x509-cert structs
fn csr_attributes(identifier: WireIdentifier) -> RustyAcmeResult<x509_cert::attr::Attributes> {
let gn = |n: String| -> RustyAcmeResult<x509_cert::ext::pkix::name::GeneralName> {
let ia5_str = x509_cert::der::asn1::Ia5String::new(&n)?;
fn gn(n: impl AsRef<str>) -> RustyAcmeResult<x509_cert::ext::pkix::name::GeneralName> {
let ia5_str = x509_cert::der::asn1::Ia5String::new(n.as_ref())?;
Ok(x509_cert::ext::pkix::name::GeneralName::UniformResourceIdentifier(
ia5_str,
))
};
let san = x509_cert::ext::pkix::SubjectAltName(vec![gn(identifier.client_id)?, gn(identifier.handle)?]);
}
let san =
x509_cert::ext::pkix::SubjectAltName(vec![gn(identifier.client_id)?, gn(identifier.handle.as_str())?]);
let san = x509_cert::attr::AttributeValue::new(x509_cert::der::Tag::OctetString, san.to_der()?)?;

let san_oid = oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME.to_der_vec()?;
Expand Down
13 changes: 9 additions & 4 deletions acme/src/identifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ pub enum AcmeIdentifier {
}

impl AcmeIdentifier {
pub fn try_new(display_name: String, domain: String, client_id: ClientId, handle: String) -> RustyAcmeResult<Self> {
pub fn try_new(
display_name: String,
domain: String,
client_id: ClientId,
handle: QualifiedHandle,
) -> RustyAcmeResult<Self> {
let client_id = client_id.to_uri();
let identifier = WireIdentifier {
display_name,
handle,
domain,
client_id,
handle,
};
let identifier = serde_json::to_string(&identifier)?;
Ok(Self::WireappId(identifier))
Expand All @@ -41,7 +46,7 @@ impl Default for AcmeIdentifier {
}
}

#[derive(Default, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct WireIdentifier {
#[serde(rename = "name")]
pub display_name: String,
Expand All @@ -50,5 +55,5 @@ pub struct WireIdentifier {
#[serde(rename = "client-id")]
pub client_id: String,
#[serde(rename = "handle")]
pub handle: String,
pub handle: QualifiedHandle,
}
14 changes: 4 additions & 10 deletions acme/src/identity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ mod thumbprint;
#[derive(Debug, Clone)]
pub struct WireIdentity {
pub client_id: String,
pub handle: String,
pub handle: QualifiedHandle,
pub display_name: String,
pub domain: String,
pub status: IdentityStatus,
Expand Down Expand Up @@ -118,7 +118,7 @@ fn try_extract_subject(cert: &x509_cert::TbsCertificate) -> RustyAcmeResult<(Str
}

/// extract Subject Alternative Name to pick client-id & display name
fn try_extract_san(cert: &x509_cert::TbsCertificate) -> RustyAcmeResult<(String, String)> {
fn try_extract_san(cert: &x509_cert::TbsCertificate) -> RustyAcmeResult<(String, QualifiedHandle)> {
let extensions = cert.extensions.as_ref().ok_or(CertificateError::InvalidFormat)?;

let san = extensions
Expand All @@ -141,13 +141,7 @@ fn try_extract_san(cert: &x509_cert::TbsCertificate) -> RustyAcmeResult<(String,
// 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();
} else if let Ok(h) = QualifiedHandle::try_from(name) {
handle = Some(h);
}
Ok(())
Expand Down Expand Up @@ -204,7 +198,7 @@ k9Jtg4ND98qu7qkUM3vtVVLiZkbCnRlFF04CIGCwhSo/78Kt8h6292SkT8c8eCS6

let expected_client_id = "yl-8A_wZSfaS2uV8VuMEBw:[email protected]";
assert_eq!(&identity.client_id, expected_client_id);
assert_eq!(&identity.handle, "alice_wire@wire.com");
assert_eq!(identity.handle.as_str(), "im:wireapp=%40alice_wire@wire.com");
assert_eq!(&identity.display_name, "Alice Smith");
assert_eq!(&identity.domain, "wire.com");
}
Expand Down
4 changes: 2 additions & 2 deletions acme/src/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ impl RustyAcme {
pub fn new_order_request(
display_name: &str,
client_id: ClientId,
handle: &str,
handle: &Handle,
expiry: core::time::Duration,
directory: &AcmeDirectory,
account: &AcmeAccount,
Expand All @@ -22,7 +22,7 @@ impl RustyAcme {
let acct_url = account.acct_url()?;

let domain = client_id.domain.clone();
let handle = format!("{}{}{handle}@{domain}", ClientId::URI_PREFIX, ClientId::HANDLE_PREFIX);
let handle = handle.to_qualified(&domain);
let identifiers = vec![AcmeIdentifier::try_new(
display_name.to_string(),
domain,
Expand Down
21 changes: 18 additions & 3 deletions cli/src/access_generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,28 @@ pub struct AccessGenerate {
/// base64Url encoded nonce generated by wire-server
///
/// e.g. 'WE88EvOBzbqGerznM+2P/AadVf7374y0cH19sDSZA2A'
#[arg(short = 'c', long)]
#[arg(long)]
nonce: String,
/// wire-server uri this token will be fetched from
///
/// e.g. 'https://wire.example.com/clients/token'
#[arg(short = 'c', long)]
#[arg(long)]
htu: String,
/// qualified wire client id
///
/// e.g. 'im:wireapp=ODM5NDJkOWRlYmI4NGNhZWIzNzdmM2JmNjYwNzJjNmI/[email protected]'
#[arg(short = 'i', long)]
client_id: String,
/// Wire handle
///
/// e.g. 'beltram_wire'
#[arg(long)]
handle: String,
/// Wire team the user belongs to
///
/// e.g. 'wire'
#[arg(short = 't', long)]
team: Option<String>,
/// client dpop & access token expiration in seconds
///
/// e.g. '300' for 5 minutes
Expand All @@ -54,15 +64,18 @@ impl AccessGenerate {
let challenge: AcmeNonce = self.challenge.into();
let htm = Htm::Post;
let htu: Htu = self.htu.as_str().try_into().unwrap();
let client_id = ClientId::try_from_uri(&self.client_id).expect("Invalid 'client_id'");
let handle = Handle::from(self.handle.clone()).to_qualified(&client_id.domain);

let dpop = Dpop {
challenge,
htm,
htu: htu.clone(),
handle: handle.clone(),
team: self.team.clone(),
extra_claims: None,
};
let nonce: BackendNonce = self.nonce.into();
let client_id = ClientId::try_from_uri(&self.client_id).expect("Invalid 'client_id'");
let expiry = core::time::Duration::from_secs(self.expiry);

let client_dpop_token =
Expand All @@ -76,6 +89,8 @@ impl AccessGenerate {
let access_token = RustyJwtTools::generate_access_token(
&client_dpop_token,
&client_id,
handle,
self.team.into(),
nonce,
htu,
htm,
Expand Down
21 changes: 14 additions & 7 deletions cli/src/access_verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,39 +12,44 @@ pub struct AccessVerify {
/// qualified wire client id
///
/// e.g. 'im:wireapp=ODM5NDJkOWRlYmI4NGNhZWIzNzdmM2JmNjYwNzJjNmI/[email protected]'
#[arg(short = 'i', long)]
#[arg(long)]
client_id: String,
/// qualified wire handle
///
/// e.g. 'beltram_wire'
#[arg(long)]
handle: String,
/// challenge (nonce) generated by acme server
///
/// e.g. 'okAJ33Ym/XS2qmmhhh7aWSbBlYy4Ttm1EysqW8I/9ng'
#[arg(short = 'c', long)]
#[arg(long)]
challenge: String,
/// maximum of clock skew in seconds allowed. Defaults to 360.
///
/// e.g. '360' (5 min)
#[arg(short = 'l', long, default_value = "360")]
#[arg(long, default_value = "360")]
leeway: u16,
/// access token maximum allowed expiration expressed as unix timestamp
///
/// e.g. '1701507459'
#[arg(short = 'e', long)]
#[arg(long)]
max_expiry: u64,
/// endpoint delivering the access-token on wire-server.
/// Should be configured in `provisioners[*].options.dpop.dpop-target` config key on the ACME server
///
/// e.g. 'https://wire.com/clients/123abef456/access-token'
#[arg(short = 'e', long)]
#[arg(long)]
issuer: String,
/// hash algorithm used to compute the JWK thumbprint. Supported values: ['SHA-256', 'SHA-384']
///
/// e.g. 'SHA-256'
#[arg(short = 'a', long)]
#[arg(long)]
hash_algorithm: HashAlgorithm,
/// Thumbprint of the dpop proof JWK
#[arg(long)]
kid: String,
/// path to file with wire-server's signature public key in PEM format
#[arg(short = 'k', long)]
#[arg(long)]
key: PathBuf,
/// version of wire-server http API
///
Expand All @@ -64,10 +69,12 @@ impl AccessVerify {
let challenge: AcmeNonce = self.challenge.into();
let (_, backend_pk) = parse_public_key_pem(read_file(Some(&self.key)).unwrap());
let issuer = self.issuer.as_str().try_into().expect("Invalid 'issuer'");
let handle = Handle::from(self.handle).to_qualified(&client_id.domain);

let verification = RustyJwtTools::verify_access_token(
&access_token,
&client_id,
&handle,
challenge,
self.leeway,
self.max_expiry,
Expand Down
Loading

0 comments on commit 337be81

Please sign in to comment.