wire_e2e_identity/acme/
finalize.rs

1use base64::Engine;
2use jwt_simple::prelude::*;
3use rusty_jwt_tools::prelude::*;
4use x509_cert::der::Encode;
5
6use crate::acme::{
7    identifier::CanonicalIdentifier,
8    order::{AcmeOrderError, AcmeOrderStatus},
9    prelude::*,
10};
11
12impl RustyAcme {
13    /// see [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4)
14    pub fn finalize_req(
15        order: &AcmeOrder,
16        account: &AcmeAccount,
17        alg: JwsAlgorithm,
18        acme_kp: &Pem,
19        signing_kp: &Pem,
20        previous_nonce: String,
21    ) -> RustyAcmeResult<AcmeJws> {
22        // Extract the account URL from previous response which created a new account
23        let acct_url = account.acct_url()?;
24        order.verify()?;
25        let csr = Self::generate_csr(alg, order.try_get_coalesce_identifier()?, signing_kp)?;
26        let payload = AcmeFinalizeRequest { csr };
27        let req = AcmeJws::new(
28            alg,
29            previous_nonce,
30            &order.finalize,
31            Some(&acct_url),
32            Some(payload),
33            acme_kp,
34        )?;
35        Ok(req)
36    }
37
38    fn generate_csr(alg: JwsAlgorithm, identifier: CanonicalIdentifier, kp: &Pem) -> RustyAcmeResult<String> {
39        let algorithm = Self::csr_alg(alg)?;
40        let cert_info = x509_cert::request::CertReqInfo {
41            version: x509_cert::request::Version::V1,
42            subject: Self::csr_subject(&identifier)?,
43            public_key: Self::csr_spki(alg, kp)?,
44            attributes: Self::csr_attributes(identifier)?,
45        };
46        let signature = Self::csr_signature(alg, kp, &cert_info)?;
47
48        let csr = x509_cert::request::CertReq {
49            info: cert_info,
50            algorithm,
51            signature,
52        };
53        let csr = csr.to_der()?;
54        let csr = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(csr);
55        Ok(csr)
56    }
57
58    fn csr_alg(alg: JwsAlgorithm) -> RustyAcmeResult<x509_cert::spki::AlgorithmIdentifierOwned> {
59        let oid = match alg {
60            JwsAlgorithm::Ed25519 => const_oid::db::rfc8410::ID_ED_25519,
61            JwsAlgorithm::P256 => const_oid::db::rfc5912::ECDSA_WITH_SHA_256,
62            JwsAlgorithm::P384 => const_oid::db::rfc5912::ECDSA_WITH_SHA_384,
63            JwsAlgorithm::P521 => const_oid::db::rfc5912::ECDSA_WITH_SHA_512,
64        };
65        Self::into_asn1_alg(oid, None)
66    }
67
68    fn csr_subject(identifier: &CanonicalIdentifier) -> RustyAcmeResult<x509_cert::name::DistinguishedName> {
69        let dn_domain_oid = const_oid::db::rfc4519::ORGANIZATION_NAME;
70        let dn_domain_value =
71            x509_cert::attr::AttributeValue::new(x509_cert::der::Tag::Utf8String, identifier.domain.as_bytes())?;
72        let dn_domain = x509_cert::attr::AttributeTypeAndValue {
73            oid: dn_domain_oid,
74            value: dn_domain_value,
75        };
76
77        // TODO: temporarily using a custom OIDC for carrying the display name without having it listed as a DNS SAN.
78        // reusing LDAP's OID for display_name see http://oid-info.com/get/2.16.840.1.113730.3.1.241
79        let dn_display_name_oid = const_oid::ObjectIdentifier::new("2.16.840.1.113730.3.1.241")?;
80        // let dn_display_name_oid = asn1_rs::oid!(2.16.840 .1 .113730 .3 .1 .241).as_bytes().try_into()?;
81        let dn_display_name_value =
82            x509_cert::attr::AttributeValue::new(x509_cert::der::Tag::Utf8String, identifier.display_name.as_bytes())?;
83        let dn_display_name = x509_cert::attr::AttributeTypeAndValue {
84            oid: dn_display_name_oid,
85            value: dn_display_name_value,
86        };
87
88        let domain = x509_cert::name::RelativeDistinguishedName(vec![dn_domain].try_into()?);
89        let display_name = x509_cert::name::RelativeDistinguishedName(vec![dn_display_name].try_into()?);
90        let subject = x509_cert::name::DistinguishedName::from(vec![domain, display_name]);
91        Ok(subject)
92    }
93
94    fn csr_spki(alg: JwsAlgorithm, kp: &Pem) -> RustyAcmeResult<x509_cert::spki::SubjectPublicKeyInfoOwned> {
95        let (pk, algorithm) = match alg {
96            JwsAlgorithm::Ed25519 => {
97                let pk = Ed25519KeyPair::from_pem(kp.as_str())?.public_key().to_bytes();
98                // see https://www.rfc-editor.org/rfc/rfc8410#section-3
99                let alg = Self::into_asn1_alg(const_oid::db::rfc8410::ID_ED_25519, None)?;
100                (pk, alg)
101            }
102            JwsAlgorithm::P256 => {
103                let kp = ES256KeyPair::from_pem(kp.as_str())?;
104                let pk = kp.public_key().public_key().to_bytes_uncompressed();
105
106                // see https://www.rfc-editor.org/rfc/rfc3279#section-2.3.5
107                let alg = Self::into_asn1_alg(
108                    const_oid::db::rfc5912::ID_EC_PUBLIC_KEY,
109                    Some(const_oid::db::rfc5912::SECP_256_R_1),
110                )?;
111                (pk, alg)
112            }
113            JwsAlgorithm::P384 => {
114                let kp = ES384KeyPair::from_pem(kp.as_str())?;
115                let pk = kp.public_key().public_key().to_bytes_uncompressed();
116
117                // see https://www.rfc-editor.org/rfc/rfc3279#section-2.3.5
118                let alg = Self::into_asn1_alg(
119                    const_oid::db::rfc5912::ID_EC_PUBLIC_KEY,
120                    Some(const_oid::db::rfc5912::SECP_384_R_1),
121                )?;
122                (pk, alg)
123            }
124            JwsAlgorithm::P521 => {
125                let kp = ES512KeyPair::from_pem(kp.as_str())?;
126                let pk = kp.public_key().public_key().to_bytes_uncompressed();
127
128                // see https://www.rfc-editor.org/rfc/rfc3279#section-2.3.5
129                let alg = Self::into_asn1_alg(
130                    const_oid::db::rfc5912::ID_EC_PUBLIC_KEY,
131                    Some(const_oid::db::rfc5912::SECP_521_R_1),
132                )?;
133                (pk, alg)
134            }
135        };
136        let subject_public_key = x509_cert::der::asn1::BitString::new(0, pk)?;
137        Ok(x509_cert::spki::SubjectPublicKeyInfoOwned {
138            algorithm,
139            subject_public_key,
140        })
141    }
142
143    // TODO: find a cleaner way to encode this reusing more x509-cert structs
144    fn csr_attributes(identifier: CanonicalIdentifier) -> RustyAcmeResult<x509_cert::attr::Attributes> {
145        fn gn(n: impl AsRef<str>) -> RustyAcmeResult<x509_cert::ext::pkix::name::GeneralName> {
146            let ia5_str = x509_cert::der::asn1::Ia5String::new(n.as_ref())?;
147            Ok(x509_cert::ext::pkix::name::GeneralName::UniformResourceIdentifier(
148                ia5_str,
149            ))
150        }
151        let san =
152            x509_cert::ext::pkix::SubjectAltName(vec![gn(identifier.client_id)?, gn(identifier.handle.as_str())?]);
153        let san = x509_cert::attr::AttributeValue::new(x509_cert::der::Tag::OctetString, san.to_der()?)?;
154
155        let san_oid = const_oid::db::rfc5280::ID_CE_SUBJECT_ALT_NAME.to_der()?;
156        let san = [san_oid, san.to_der()?].concat();
157        let san = x509_cert::attr::AttributeValue::new(x509_cert::der::Tag::Sequence, san)?;
158        let san = x509_cert::attr::AttributeValue::new(x509_cert::der::Tag::Sequence, san.to_der()?)?;
159
160        let attributes = vec![x509_cert::attr::Attribute {
161            oid: const_oid::db::rfc5912::ID_EXTENSION_REQ,
162            values: vec![san].try_into()?,
163        }];
164        Ok(attributes.try_into()?)
165    }
166
167    fn csr_signature(
168        alg: JwsAlgorithm,
169        kp: &Pem,
170        cert_info: &x509_cert::request::CertReqInfo,
171    ) -> RustyAcmeResult<x509_cert::der::asn1::BitString> {
172        use signature::Signer as _;
173        let cert_data = cert_info.to_der()?;
174
175        let signature = match alg {
176            JwsAlgorithm::Ed25519 => {
177                let kp = Ed25519KeyPair::from_pem(kp.as_str())?;
178                let signature = kp.key_pair().as_ref().sign(&cert_data);
179                x509_cert::der::asn1::BitString::new(0, signature.to_vec())?
180            }
181            JwsAlgorithm::P256 => {
182                let kp = ES256KeyPair::from_pem(kp.as_str())?;
183                let sk: &p256::ecdsa::SigningKey = kp.key_pair().as_ref();
184                let signature: p256::ecdsa::DerSignature = sk.try_sign(&cert_data)?;
185                x509_cert::der::asn1::BitString::new(0, signature.to_der()?)?
186            }
187            JwsAlgorithm::P384 => {
188                let kp = ES384KeyPair::from_pem(kp.as_str())?;
189                let sk: &p384::ecdsa::SigningKey = kp.key_pair().as_ref();
190                let signature: p384::ecdsa::DerSignature = sk.try_sign(&cert_data)?;
191                x509_cert::der::asn1::BitString::new(0, signature.to_der()?)?
192            }
193            JwsAlgorithm::P521 => {
194                let kp = ES512KeyPair::from_pem(kp.as_str())?;
195                let sk: ecdsa::SigningKey<p521::NistP521> = kp.key_pair().as_ref().clone();
196                let sk = p521::ecdsa::SigningKey::from(sk);
197
198                let signature: p521::ecdsa::DerSignature = sk.try_sign(&cert_data)?.to_der();
199                x509_cert::der::asn1::BitString::new(0, signature.to_der()?)?
200            }
201        };
202        Ok(signature)
203    }
204
205    fn into_asn1_alg(
206        oid: const_oid::ObjectIdentifier,
207        oid_parameter: Option<const_oid::ObjectIdentifier>,
208    ) -> RustyAcmeResult<x509_cert::spki::AlgorithmIdentifierOwned> {
209        let alg = x509_cert::spki::AlgorithmIdentifierOwned {
210            oid,
211            parameters: oid_parameter.map(Into::into),
212        };
213        Ok(alg)
214    }
215
216    /// see [RFC 8555 Section 7.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.4)
217    pub fn finalize_response(response: serde_json::Value) -> RustyAcmeResult<AcmeFinalize> {
218        let finalize = serde_json::from_value::<AcmeFinalize>(response)?;
219        Ok(finalize)
220    }
221}
222
223#[derive(Debug, thiserror::Error)]
224#[error(transparent)]
225pub struct AcmeFinalizeError(#[from] AcmeOrderError);
226
227#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
228#[cfg_attr(test, derive(Clone))]
229#[serde(rename_all = "camelCase")]
230struct AcmeFinalizeRequest {
231    /// Certificate Signing Request in DER format
232    csr: String,
233}
234
235#[derive(Debug, serde::Serialize, serde::Deserialize)]
236#[cfg_attr(test, derive(Clone))]
237#[serde(rename_all = "camelCase")]
238pub struct AcmeFinalize {
239    pub certificate: url::Url,
240    #[serde(flatten)]
241    pub order: AcmeOrder,
242}
243
244impl AcmeFinalize {
245    pub fn verify(&self) -> RustyAcmeResult<()> {
246        match self.order.status {
247            AcmeOrderStatus::Valid => {}
248            AcmeOrderStatus::Pending | AcmeOrderStatus::Processing | AcmeOrderStatus::Ready => {
249                return Err(RustyAcmeError::ClientImplementationError(
250                    "finalize is not supposed to be 'pending | processing | ready' at this point. \
251                    It means you have forgotten previous steps",
252                ));
253            }
254            AcmeOrderStatus::Invalid => return Err(AcmeFinalizeError(AcmeOrderError::Invalid))?,
255        }
256        self.order.verify().map_err(|e| match e {
257            RustyAcmeError::OrderError(e) => RustyAcmeError::FinalizeError(AcmeFinalizeError(e)),
258            _ => e,
259        })?;
260        Ok(())
261    }
262}
263
264#[cfg(test)]
265impl Default for AcmeFinalize {
266    fn default() -> Self {
267        Self {
268            certificate: "https://acme-server/acme/wire-acme/certificate/poWXmZGdL5d5qlvHMHRC19w2O9s96fvz"
269                .parse()
270                .unwrap(),
271            order: AcmeOrder {
272                status: AcmeOrderStatus::Valid,
273                ..Default::default()
274            },
275        }
276    }
277}
278
279#[cfg(test)]
280pub mod tests {
281    use serde_json::json;
282    use wasm_bindgen_test::*;
283
284    use super::*;
285
286    wasm_bindgen_test_configure!(run_in_browser);
287
288    mod json {
289        use super::*;
290
291        #[test]
292        #[wasm_bindgen_test]
293        fn can_deserialize_sample_response() {
294            let rfc_sample = json!({
295                "status": "valid",
296                "expires": "2016-01-20T14:09:07.99Z",
297                "notBefore": "2016-01-01T00:00:00Z",
298                "notAfter": "2016-01-08T00:00:00Z",
299                "identifiers": [
300                    { "type": "wireapp-user", "value": "www.example.org" },
301                    { "type": "wireapp-device", "value": "example.org" }
302                ],
303                "authorizations": [
304                    "https://example.com/acme/authz/PAniVnsZcis",
305                    "https://example.com/acme/authz/r4HqLzrSrpI"
306                ],
307                "finalize": "https://example.com/acme/order/TOlocE8rfgo/finalize",
308                "certificate": "https://example.com/acme/cert/mAt3xBGaobw"
309            });
310            assert!(serde_json::from_value::<AcmeFinalize>(rfc_sample).is_ok());
311        }
312    }
313
314    mod verify {
315        use super::*;
316
317        #[test]
318        #[wasm_bindgen_test]
319        fn should_succeed_when_valid() {
320            let finalize = AcmeFinalize {
321                order: AcmeOrder {
322                    status: AcmeOrderStatus::Valid,
323                    ..Default::default()
324                },
325                ..Default::default()
326            };
327            assert!(finalize.verify().is_ok());
328        }
329
330        #[test]
331        #[wasm_bindgen_test]
332        fn should_fail_when_expired() {
333            // just make sure we delegate to order.verify()
334            let yesterday = time::OffsetDateTime::now_utc() - time::Duration::days(1);
335            let finalize = AcmeFinalize {
336                order: AcmeOrder {
337                    status: AcmeOrderStatus::Valid,
338                    expires: Some(yesterday),
339                    ..Default::default()
340                },
341                ..Default::default()
342            };
343            assert!(matches!(
344                finalize.verify().unwrap_err(),
345                RustyAcmeError::FinalizeError(AcmeFinalizeError(AcmeOrderError::Expired))
346            ));
347        }
348
349        #[test]
350        #[wasm_bindgen_test]
351        fn should_fail_when_status_not_valid() {
352            for status in [
353                AcmeOrderStatus::Pending,
354                AcmeOrderStatus::Processing,
355                AcmeOrderStatus::Ready,
356            ] {
357                let finalize = AcmeFinalize {
358                    order: AcmeOrder {
359                        status,
360                        ..Default::default()
361                    },
362                    ..Default::default()
363                };
364                assert!(matches!(
365                    finalize.verify().unwrap_err(),
366                    RustyAcmeError::ClientImplementationError(_),
367                ));
368            }
369        }
370
371        #[test]
372        #[wasm_bindgen_test]
373        fn should_fail_when_status_invalid() {
374            let finalize = AcmeFinalize {
375                order: AcmeOrder {
376                    status: AcmeOrderStatus::Invalid,
377                    ..Default::default()
378                },
379                ..Default::default()
380            };
381            assert!(matches!(
382                finalize.verify().unwrap_err(),
383                RustyAcmeError::FinalizeError(AcmeFinalizeError(AcmeOrderError::Invalid)),
384            ));
385        }
386    }
387}