wire_e2e_identity/acme/
chall.rs

1use rusty_jwt_tools::prelude::*;
2
3use crate::acme::prelude::*;
4
5impl RustyAcme {
6    /// client id challenge request to `POST /acme/challenge/{token}`
7    /// see [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1)
8    pub fn dpop_chall_request(
9        access_token: String,
10        dpop_chall: AcmeChallenge,
11        account: &AcmeAccount,
12        alg: JwsAlgorithm,
13        kp: &Pem,
14        previous_nonce: String,
15    ) -> RustyAcmeResult<AcmeJws> {
16        // Extract the account URL from previous response which created a new account
17        let acct_url = account.acct_url()?;
18
19        let payload = Some(serde_json::json!({
20            "access_token": access_token,
21        }));
22
23        let req = AcmeJws::new(alg, previous_nonce, &dpop_chall.url, Some(&acct_url), payload, kp)?;
24        Ok(req)
25    }
26
27    /// oidc challenge request to `POST /acme/challenge/{token}`
28    /// see [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1)
29    #[allow(clippy::too_many_arguments)]
30    pub fn oidc_chall_request(
31        id_token: String,
32        oidc_chall: AcmeChallenge,
33        account: &AcmeAccount,
34        alg: JwsAlgorithm,
35        kp: &Pem,
36        previous_nonce: String,
37    ) -> RustyAcmeResult<AcmeJws> {
38        // Extract the account URL from previous response which created a new account
39        let acct_url = account.acct_url()?;
40        let payload = Some(serde_json::json!({
41            "id_token": id_token,
42        }));
43        let req = AcmeJws::new(alg, previous_nonce, &oidc_chall.url, Some(&acct_url), payload, kp)?;
44        Ok(req)
45    }
46
47    /// 18. parse the response from `POST /acme/challenge/{token}` [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1)
48    pub fn new_chall_response(response: serde_json::Value) -> RustyAcmeResult<AcmeChallenge> {
49        let chall = serde_json::from_value::<AcmeChallenge>(response)?;
50        match chall.status {
51            Some(AcmeChallengeStatus::Valid) => {}
52            Some(AcmeChallengeStatus::Processing) => return Err(AcmeChallError::Processing)?,
53            Some(AcmeChallengeStatus::Invalid) => return Err(AcmeChallError::Invalid)?,
54            Some(AcmeChallengeStatus::Pending) => {
55                return Err(RustyAcmeError::ClientImplementationError(
56                    "a challenge is not supposed to be pending at this point. \
57                    It must either be 'valid' or 'processing'.",
58                ));
59            }
60            None => {
61                return Err(RustyAcmeError::ClientImplementationError(
62                    "at this point a challenge is supposed to have a status",
63                ));
64            }
65        }
66        Ok(chall)
67    }
68}
69
70#[derive(Debug, thiserror::Error)]
71pub enum AcmeChallError {
72    /// This challenge is invalid
73    #[error("This challenge is invalid")]
74    Invalid,
75    /// This challenge is being processed, retry later
76    #[error("This challenge is being processed, retry later")]
77    Processing,
78}
79
80/// For creating a challenge
81/// see [RFC 8555 Section 7.5.1](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.5.1)
82#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
83#[serde(rename_all = "camelCase")]
84pub struct AcmeChallenge {
85    #[serde(rename = "type")]
86    /// Should be `wire-http-01` or `wire-oidc-01`
87    pub typ: AcmeChallengeType,
88    /// URL to call for the acme server to complete the challenge
89    pub url: url::Url,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    /// Should be `valid`
92    pub status: Option<AcmeChallengeStatus>,
93    /// The acme challenge value to store in the Dpop token
94    pub token: String,
95    /// Non-standard, Wire specific claim. Indicates the consumer from where it should get the challenge
96    /// proof. Either from wire-server "/access-token" endpoint in case of a DPoP challenge, or from
97    /// an OAuth token endpoint for an OIDC challenge
98    pub target: url::Url,
99}
100
101#[cfg(test)]
102impl AcmeChallenge {
103    pub fn new_device() -> Self {
104        Self {
105            status: None,
106            typ: AcmeChallengeType::WireDpop01,
107            url: "ttps://stepca/acme/wire/challenge/EitdRA8gzxuRCrHlppZJfQsB8Hjsklpj/DaugXj4rBw04OfjyWfucICoaOAGGzXFQ"
108                .parse()
109                .unwrap(),
110            token: "DGyRejmCefe7v4NfDGDKfA".to_string(),
111            target: "http://wire.com:21893/clients/aeddd6d37af25726/access-token"
112                .parse()
113                .unwrap(),
114        }
115    }
116
117    pub fn new_user() -> Self {
118        Self {
119            status: None,
120            typ: AcmeChallengeType::WireOidc01,
121            url: "https://stepca/acme/wire/challenge/EitdRA8gzxuRCrHlppZJfQsB8Hjsklpj/47eOxmrLEJR3aJl7X0hpnH4y0rU8uRo2"
122                .parse()
123                .unwrap(),
124            token: "4xQIED9iPLQo1fkPLBq1znAniwvcVsxQ".to_string(),
125            target: "http://keycloak:15170/realms/master".parse().unwrap(),
126        }
127    }
128}
129
130/// see [RFC 8555 Section 7.1.6](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.1.6)
131#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
132#[serde(rename_all = "lowercase")]
133pub enum AcmeChallengeStatus {
134    Pending,
135    Processing,
136    Valid,
137    Invalid,
138}
139
140#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
141pub enum AcmeChallengeType {
142    #[serde(rename = "http-01")]
143    Http01,
144    #[serde(rename = "dns-01")]
145    Dns01,
146    #[serde(rename = "tls-alpn-01")]
147    TlsAlpn01,
148    /// Custom type for clientId challenge
149    #[serde(rename = "wire-dpop-01")]
150    WireDpop01,
151    /// Custom type for handle + display name challenge
152    #[serde(rename = "wire-oidc-01")]
153    WireOidc01,
154}
155
156#[cfg(test)]
157pub mod tests {
158    use serde_json::json;
159    use wasm_bindgen_test::*;
160
161    use super::*;
162
163    wasm_bindgen_test_configure!(run_in_browser);
164
165    #[test]
166    #[wasm_bindgen_test]
167    fn can_deserialize_rfc_sample_response() {
168        // http challenge
169        // see https://www.rfc-editor.org/rfc/rfc8555.html#section-8.3
170        let rfc_sample = json!({
171            "type": "http-01",
172            "url": "https://example.com/acme/chall/prV_B7yEyA4",
173            "status": "pending",
174            "token": "LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0",
175            "target": "https://example.com/target"
176        });
177        assert!(serde_json::from_value::<AcmeChallenge>(rfc_sample).is_ok());
178
179        // dns challenge
180        // see https://www.rfc-editor.org/rfc/rfc8555.html#section-8.4
181        let rfc_sample = json!({
182            "type": "dns-01",
183            "url": "https://example.com/acme/chall/Rg5dV14Gh1Q",
184            "status": "pending",
185            "token": "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA",
186            "target": "https://example.com/target"
187        });
188        assert!(serde_json::from_value::<AcmeChallenge>(rfc_sample).is_ok());
189    }
190
191    #[test]
192    #[wasm_bindgen_test]
193    fn chall_type_should_deserialize_as_expected() {
194        use serde_json::from_value as deser;
195        assert_eq!(
196            deser::<AcmeChallengeType>(json!("http-01")).unwrap(),
197            AcmeChallengeType::Http01
198        );
199        assert_eq!(
200            deser::<AcmeChallengeType>(json!("dns-01")).unwrap(),
201            AcmeChallengeType::Dns01
202        );
203        assert_eq!(
204            deser::<AcmeChallengeType>(json!("tls-alpn-01")).unwrap(),
205            AcmeChallengeType::TlsAlpn01
206        );
207        assert_eq!(
208            deser::<AcmeChallengeType>(json!("wire-dpop-01")).unwrap(),
209            AcmeChallengeType::WireDpop01
210        );
211        assert_eq!(
212            deser::<AcmeChallengeType>(json!("wire-oidc-01")).unwrap(),
213            AcmeChallengeType::WireOidc01
214        );
215        assert!(deser::<AcmeChallengeType>(json!("Http-01")).is_err());
216        assert!(deser::<AcmeChallengeType>(json!("http01")).is_err());
217    }
218}