Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

3 changes: 2 additions & 1 deletion crypto-ffi/bindings/js/packages/browser/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ export async function setup() {
async authenticate(
_idp: string,
_keyAuth: string,
_acmeAud: string
_acmeAud: string,
_acquisition_snapshot: Uint8Array
) {
return "dummy-id-token";
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ class MockPkiEnvironmentHooks : PkiEnvironmentHooks {
override suspend fun authenticate(
idp: String,
keyAuth: String,
acmeAud: String
acmeAud: String,
acquisitionSnapshot: ByteArray
): String {
return "mock-id-token"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -930,7 +930,8 @@ final class WireCoreCryptoTests: XCTestCase {
func authenticate(
idp: String,
keyAuth: String,
acmeAud: String
acmeAud: String,
acquisitionSnapshot: Data
) async -> String {
return "mock-id-token"
}
Expand Down
39 changes: 38 additions & 1 deletion crypto-ffi/src/e2ei/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::sync::Arc;

use async_lock::Mutex;
use jwt_simple::prelude::{ES256KeyPair, ES384KeyPair, ES512KeyPair, Ed25519KeyPair};
use wire_e2e_identity::{HashAlgorithm, JwsAlgorithm};
use wire_e2e_identity::{HashAlgorithm, JwsAlgorithm, acquisition::states};
use x509_cert::der::Encode as _;

use crate::{Ciphersuite as FfiCiphersuite, ClientId, CoreCryptoError, CoreCryptoResult, Credential, PkiEnvironment};
Expand Down Expand Up @@ -50,6 +50,17 @@ impl TryFrom<FfiCiphersuite> for JwsAlgorithm {
}
}

impl From<JwsAlgorithm> for FfiCiphersuite {
fn from(value: JwsAlgorithm) -> Self {
match value {
JwsAlgorithm::Ed25519 => FfiCiphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519,
JwsAlgorithm::P256 => FfiCiphersuite::MLS_128_DHKEMP256_AES128GCM_SHA256_P256,
JwsAlgorithm::P384 => FfiCiphersuite::MLS_256_DHKEMP384_AES256GCM_SHA384_P384,
JwsAlgorithm::P521 => FfiCiphersuite::MLS_256_DHKEMP521_AES256GCM_SHA512_P521,
}
}
}

/// Configuration for an X509 credential acquisition flow.
#[derive(Debug, Clone, uniffi::Record)]
pub struct X509CredentialAcquisitionConfiguration {
Expand Down Expand Up @@ -106,6 +117,7 @@ pub struct X509CredentialAcquisition {

enum AcquisitionState {
Initialized(Box<wire_e2e_identity::X509CredentialAcquisition>),
DpopChallengeCompleted(Box<wire_e2e_identity::X509CredentialAcquisition<states::DpopChallengeCompleted>>),
InProgress,
Finalized,
}
Expand Down Expand Up @@ -167,6 +179,23 @@ impl X509CredentialAcquisition {
})
}

/// Deserialize a credential acquisition flow.
#[uniffi::constructor(name = "fromBytes")]
pub fn from_bytes(pki_environment: Arc<PkiEnvironment>, bytes: &[u8]) -> CoreCryptoResult<Self> {
let snapshot = wire_e2e_identity::X509CredentialAcquisition::<states::DpopChallengeCompleted>::deserialize(
Arc::new(pki_environment.as_ref().clone().into()),
bytes,
)
.map_err(CoreCryptoError::generic())?;

let ciphersuite: FfiCiphersuite = snapshot.sign_alg().into();

Ok(Self {
state: Mutex::new(AcquisitionState::DpopChallengeCompleted(snapshot.into())),
ciphersuite,
})
}

/// Complete the DPoP and OIDC challenges and return the acquired X509 credential.
pub async fn finalize(&self) -> CoreCryptoResult<Credential> {
let state = {
Expand All @@ -186,6 +215,14 @@ impl X509CredentialAcquisition {
.map_err(|err| CoreCryptoError::E2ei {
e2ei_error: err.to_string(),
}),
AcquisitionState::DpopChallengeCompleted(inner) => {
inner
.complete_oidc_challenge()
.await
.map_err(|err| CoreCryptoError::E2ei {
e2ei_error: err.to_string(),
})
}
AcquisitionState::InProgress => {
return Err(CoreCryptoError::ad_hoc(
"x509 credential acquisition is already in progress",
Expand Down
7 changes: 6 additions & 1 deletion crypto-ffi/src/pki_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ pub trait PkiEnvironmentHooks: Send + Sync {
idp: String,
key_auth: String,
acme_aud: String,
acquisition_snapshot: Vec<u8>,
) -> Result<String, PkiEnvironmentHooksError>;

/// Get a nonce from the backend.
Expand Down Expand Up @@ -205,8 +206,12 @@ impl pki_env::hooks::PkiEnvironmentHooks for PkiEnvironmentHooksShim {
idp: String,
key_auth: String,
acme_aud: String,
acquisition_snapshot: Vec<u8>,
) -> Result<String, pki_env::hooks::PkiEnvironmentHooksError> {
self.0.authenticate(idp, key_auth, acme_aud).await.map_err(Into::into)
self.0
.authenticate(idp, key_auth, acme_aud, acquisition_snapshot)
.await
.map_err(Into::into)
}

async fn get_backend_nonce(&self) -> Result<String, pki_env::hooks::PkiEnvironmentHooksError> {
Expand Down
1 change: 1 addition & 0 deletions crypto/src/test_utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ impl PkiEnvironmentHooks for DummyPkiEnvironmentHooks {
_idp: String,
_key_auth: String,
_acme_aud: String,
_acquisition_snapshot: Vec<u8>,
) -> Result<String, PkiEnvironmentHooksError> {
Ok("dummy-id-token".to_string())
}
Expand Down
2 changes: 1 addition & 1 deletion e2e-identity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jwt-simple.workspace = true
derive_more = { workspace = true, features = ["deref", "from", "into"] }
url.workspace = true
zeroize.workspace = true
uuid.workspace = true
uuid = { workspace = true, features = ["serde"] }
x509-cert = { workspace = true, features = ["builder", "hazmat"] }
time = { workspace = true, features = [
"serde",
Expand Down
18 changes: 15 additions & 3 deletions e2e-identity/src/acquisition/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,22 @@ mod dpop_challenge;
mod error;
mod initial;
mod oidc_challenge;
mod serialization;

pub mod identity;
pub mod thumbprint;

#[derive(Debug)]
pub use error::Error as AcquisitionError;
pub use serialization::ClientIdDef;

#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct X509CredentialConfiguration {
pub acme_url: String,
pub idp_url: String,
pub sign_alg: JwsAlgorithm,
pub hash_alg: HashAlgorithm,
pub display_name: String,
#[serde(with = "ClientIdDef")]
pub client_id: ClientId,
pub handle: String,
pub domain: String,
Expand All @@ -38,10 +43,10 @@ pub struct X509CredentialConfiguration {
pub mod states {
use crate::acme::{AcmeAccount, AcmeChallenge, AcmeOrder};

#[derive(Debug)]
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Initialized;

#[derive(Debug)]
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct DpopChallengeCompleted {
pub nonce: String,
pub acme_account: AcmeAccount,
Expand Down Expand Up @@ -86,8 +91,10 @@ pub mod states {
/// .complete_dpop_challenge().await?
/// .complete_oidc_challenge().await?;
/// ```
#[derive(serde::Serialize)]
pub struct X509CredentialAcquisition<T: std::fmt::Debug = states::Initialized> {
/// A reference to the PKI environment that stores trust anchors.
#[serde(skip)]
pki_env: Arc<PkiEnvironment>,
/// The configuration used for acquisition.
config: X509CredentialConfiguration,
Expand All @@ -114,6 +121,11 @@ fn get_header(resp: &HttpResponse, header: &'static str) -> Result<String> {
}

impl<T: std::fmt::Debug> X509CredentialAcquisition<T> {
/// The signing algorithm used for certificate acquisition.
pub fn sign_alg(&self) -> JwsAlgorithm {
self.config.sign_alg
}

/// Send an HTTP request to the ACME server and return the result in the form of a
/// pair (nonce, deserialized JSON response). The nonce is returned so it can be
/// used by the caller to construct the body of the next ACME request.
Expand Down
3 changes: 2 additions & 1 deletion e2e-identity/src/acquisition/oidc_challenge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ impl X509CredentialAcquisition<states::DpopChallengeCompleted> {
let oidc_challenge_token = &self.data.oidc_challenge.token;
let thumbprint = JwkThumbprint::generate(&self.acme_jwk, self.config.hash_alg)?.kid;
let key_auth = format!("{oidc_challenge_token}.{thumbprint}");
let snapshot = serde_json::to_vec(&self)?;

let url = &self.data.oidc_challenge.url;
let id_token = hooks
.authenticate(self.config.idp_url.clone(), key_auth, url.to_string())
.authenticate(self.config.idp_url.clone(), key_auth, url.to_string(), snapshot)
.await?;

let oidc_challenge_request = RustyAcme::oidc_chall_request(
Expand Down
143 changes: 143 additions & 0 deletions e2e-identity/src/acquisition/serialization.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
use std::sync::Arc;

use jwt_simple::prelude::Jwk;
use rusty_jwt_tools::prelude::{ClientId, Pem};
use uuid::Uuid;

use super::{Result, X509CredentialAcquisition, X509CredentialConfiguration, states};
use crate::pki_env::PkiEnvironment;

#[derive(serde::Serialize, serde::Deserialize)]
#[serde(remote = "ClientId")]
pub struct ClientIdDef {
/// base64url encoded UUIDv4 unique user identifier
pub user_id: Uuid,
/// the device id assigned by the backend in hex
pub device_id: u64,
/// the backend domain of the client
pub domain: String,
}

#[derive(serde::Deserialize, serde::Serialize)]
struct X509CredentialAcquisitionSerialisationHelper<T: std::fmt::Debug> {
config: X509CredentialConfiguration,
sign_kp: Pem,
acme_kp: Pem,
acme_jwk: Jwk,
data: T,
}

impl X509CredentialAcquisition<states::DpopChallengeCompleted> {
pub fn deserialize(pki_env: Arc<PkiEnvironment>, bytes: &[u8]) -> Result<Self> {
let helper: X509CredentialAcquisitionSerialisationHelper<states::DpopChallengeCompleted> =
serde_json::from_slice(bytes)?;

Ok(Self {
pki_env,
config: helper.config,
sign_kp: helper.sign_kp,
acme_kp: helper.acme_kp,
acme_jwk: helper.acme_jwk,
data: helper.data,
})
}
}

#[cfg(test)]
mod tests {
use core_crypto_keystore::{ConnectionType, Database, DatabaseKey};
use rusty_jwt_tools::prelude::{HashAlgorithm, JwsAlgorithm};

use super::*;
use crate::{
acme::{AcmeAccount, AcmeChallenge, AcmeOrder},
pki_env::hooks::{HttpHeader, HttpMethod, HttpResponse, PkiEnvironmentHooks, PkiEnvironmentHooksError},
};

#[derive(Debug)]
struct UnusedPkiEnvironmentHooks;

#[async_trait::async_trait]
impl PkiEnvironmentHooks for UnusedPkiEnvironmentHooks {
async fn http_request(
&self,
_method: HttpMethod,
_url: String,
_headers: Vec<HttpHeader>,
_body: Vec<u8>,
) -> std::result::Result<HttpResponse, PkiEnvironmentHooksError> {
unreachable!("serialization round-trip should not perform HTTP requests")
}

async fn authenticate(
&self,
_idp: String,
_key_auth: String,
_acme_aud: String,
_acquisition_snapshot: Vec<u8>,
) -> std::result::Result<String, PkiEnvironmentHooksError> {
unreachable!("serialization round-trip should not authenticate")
}

async fn get_backend_nonce(&self) -> std::result::Result<String, PkiEnvironmentHooksError> {
unreachable!("serialization round-trip should not request backend nonces")
}

async fn fetch_backend_access_token(
&self,
_dpop: String,
) -> std::result::Result<String, PkiEnvironmentHooksError> {
unreachable!("serialization round-trip should not fetch backend access tokens")
}
}

#[tokio::test]
async fn can_serialize_and_deserialize_dpop_challenge_completed_acquisition() {
let pki_env = Arc::new(
PkiEnvironment::new(
Arc::new(UnusedPkiEnvironmentHooks),
Database::open(ConnectionType::InMemory, &DatabaseKey::generate())
.await
.unwrap(),
)
.await
.unwrap(),
);
let client_id = ClientId::try_new(Uuid::new_v4().to_string(), 1, "wire.example").unwrap();
let config = X509CredentialConfiguration {
acme_url: "acme.example".into(),
idp_url: "idp.example".into(),
sign_alg: JwsAlgorithm::P256,
hash_alg: HashAlgorithm::SHA256,
display_name: "Alice".into(),
client_id,
handle: "alice".into(),
domain: "wire.example".into(),
team: Some("team".into()),
validity_period: std::time::Duration::from_secs(3600),
};
let initialized = X509CredentialAcquisition::try_new(pki_env.clone(), config).unwrap();
let acquisition = X509CredentialAcquisition::<states::DpopChallengeCompleted> {
pki_env: initialized.pki_env,
config: initialized.config,
sign_kp: initialized.sign_kp,
acme_kp: initialized.acme_kp,
acme_jwk: initialized.acme_jwk,
data: states::DpopChallengeCompleted {
nonce: "acme-nonce".into(),
acme_account: AcmeAccount::default(),
order: AcmeOrder::default(),
oidc_challenge: AcmeChallenge::new_user(),
},
};

let serialized = serde_json::to_vec(&acquisition).unwrap();
let deserialized =
X509CredentialAcquisition::<states::DpopChallengeCompleted>::deserialize(pki_env, &serialized).unwrap();

assert_eq!(
serde_json::to_value(&acquisition).unwrap(),
serde_json::to_value(&deserialized).unwrap()
);
}
}
1 change: 1 addition & 0 deletions e2e-identity/src/pki_env/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ pub trait PkiEnvironmentHooks: std::fmt::Debug + Send + Sync {
idp: String,
key_auth: String,
acme_aud: String,
acquisition_snapshot: Vec<u8>,
) -> Result<String, PkiEnvironmentHooksError>;

/// Get a nonce from the backend
Expand Down
1 change: 1 addition & 0 deletions e2e-identity/tests/utils/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ impl PkiEnvironmentHooks for TestPkiEnvironmentHooks {
idp: String,
key_auth: String,
acme_aud: String,
_acquisition_snapshot: Vec<u8>,
) -> Result<String, PkiEnvironmentHooksError> {
let oauth_cfg = OauthCfg {
client_id: "wireapp".to_string(),
Expand Down
Loading