wire_e2e_identity/
pki_env_hooks.rs

1//! PKI Environment API Hooks
2use std::fmt;
3
4/// An http method
5#[derive(Debug)]
6pub enum HttpMethod {
7    /// GET
8    Get,
9    /// POST
10    Post,
11    /// PUT
12    Put,
13    /// DELETE
14    Delete,
15    /// PATCH
16    Patch,
17    /// HEAD
18    Head,
19}
20
21/// An http header
22#[derive(Debug)]
23pub struct HttpHeader {
24    /// header name
25    pub name: String,
26    /// header value
27    pub value: String,
28}
29
30/// An HTTP Response
31pub struct HttpResponse {
32    /// Response status code
33    pub status: u16,
34    /// Response Header
35    pub headers: Vec<HttpHeader>,
36    /// Response Body
37    pub body: Vec<u8>,
38}
39
40impl HttpResponse {
41    /// Deserialize the body of the response into a JSON value.
42    pub fn json(&self) -> Result<serde_json::Value, serde_json::Error> {
43        serde_json::from_slice(&self.body)
44    }
45
46    /// Return the value of the first header with the given name.
47    pub fn first_header(&self, name: &str) -> Option<String> {
48        self.headers
49            .iter()
50            .find_map(|h| (h.name == name).then(|| h.value.clone()))
51    }
52}
53
54impl std::fmt::Debug for HttpResponse {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        if f.alternate() {
57            writeln!(f, "BEGIN HttpResponse")?;
58            writeln!(f, "Status: {}", self.status)?;
59            for header in &self.headers {
60                writeln!(f, "{}: {}", header.name, header.value)?;
61            }
62            writeln!(f)?;
63            match std::str::from_utf8(&self.body) {
64                Ok(body) => match serde_json::from_str::<serde_json::Value>(body) {
65                    Ok(body) => writeln!(f, "{body:#}")?,
66                    Err(_) => writeln!(f, "{body}")?,
67                },
68                Err(_) => writeln!(f, "{:#?}", self.body)?,
69            }
70            writeln!(f, "END HttpResponse")
71        } else {
72            f.debug_struct("HttpResponse")
73                .field("status", &self.status)
74                .field("headers", &self.headers)
75                .field("body", &self.body)
76                .finish()
77        }
78    }
79}
80
81/// Error type for PKI environment hooks
82#[derive(Debug, thiserror::Error, derive_more::From)]
83#[error("reason: {reason}")]
84pub struct PkiEnvironmentHooksError {
85    /// the error reason
86    pub reason: String,
87}
88
89/// The PKI Environment Hooks used for external calls during e2e enrollment flow.
90/// When communicating with the Identity Provider (IDP)  and Wire server,
91/// CoreCrypto delegates to the client app by calling the relevant methods.
92///
93/// Client App                 CoreCrypto                     Acme                     IDP
94///    |                           |                          |                        |
95///    | X509CredentialAcquisition().finalize()               |                        |
96///    |-------------------------->|                          |                        |
97///    |                           | GET acme/root.pem        |                        |
98///    |                           |------------------------> |                        |
99///    |                           | 200 OK                   |                        |
100///    |                           |<------------------------ |                        |
101///    | authenticate()            |                          |                        |
102///    |<--------------------------|                          |                        |
103///    |                           | Authentication flow      |                        |
104///    | ----------------------------------------------------------------------------> |
105///    |<----------------------------------------------------------------------------- |
106///    | return Success [PkiEnvironmentHooks.authenticate()]  |                        |
107///    |<--------------------------|                          |                        |
108///    |                           |  (excluded several calls for brevity)             |
109///    | return Success(Credential) [X509CredentialAcquisition().finalize()]           |
110///    |<--------------------------|                          |                        |
111#[cfg_attr(target_os = "unknown", async_trait::async_trait(?Send))]
112#[cfg_attr(not(target_os = "unknown"), async_trait::async_trait)]
113pub trait PkiEnvironmentHooks: std::fmt::Debug + Send + Sync {
114    /// Make an HTTP request
115    /// Used for requests to ACME servers, CRL distributors etc.
116    async fn http_request(
117        &self,
118        method: HttpMethod,
119        url: String,
120        headers: Vec<HttpHeader>,
121        body: Vec<u8>,
122    ) -> Result<HttpResponse, PkiEnvironmentHooksError>;
123
124    /// Authenticate with the user's identity provider (IdP)
125    ///
126    /// The implementation should perform an [authentication using the authorization code flow]
127    /// (<https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth>) with the PKCE
128    /// (<https://www.rfc-editor.org/rfc/rfc7636>) extension. As part of the authorization
129    /// request, the implementation should specify `key_auth` and `acme_aud` claims, along with
130    /// their values, in the `claims` parameter. This is to instruct the IdP to add the `key_auth`
131    /// and `acme_aud` claims to the ID token that will be returned as part of the access token.
132    ///
133    /// Once the authentication is completed successfully, the implementation should request
134    /// an access token from the IdP, extract the ID token from it and return it to the caller.
135    async fn authenticate(
136        &self,
137        idp: String,
138        key_auth: String,
139        acme_aud: String,
140    ) -> Result<String, PkiEnvironmentHooksError>;
141
142    /// Get a nonce from the backend
143    async fn get_backend_nonce(&self) -> Result<String, PkiEnvironmentHooksError>;
144
145    /// Fetch an access token to be used for the DPoP challenge (`wire-dpop-01`)
146    ///
147    /// The implementation should take the provided DPoP token (`dpop`) and make a request to the
148    /// backend to obtain an access token, which should be returned to the caller.
149    async fn fetch_backend_access_token(&self, dpop: String) -> Result<String, PkiEnvironmentHooksError>;
150}