trz_gateway_common/x509/
cert.rs

1use axum::http::StatusCode;
2use nameth::NamedEnumValues as _;
3use nameth::nameth;
4use openssl::error::ErrorStack;
5use openssl::pkey::PKey;
6use openssl::x509::X509;
7use openssl::x509::X509Builder;
8use openssl::x509::X509Extension;
9use openssl::x509::extension::BasicConstraints;
10use openssl::x509::extension::ExtendedKeyUsage;
11use openssl::x509::extension::KeyUsage;
12use openssl::x509::extension::SubjectAlternativeName;
13
14use super::common_fields::SetCommonFieldsError;
15use super::common_fields::set_akid;
16use super::common_fields::set_common_fields;
17use super::name::CertitficateName;
18use super::name::MakeNameError;
19use super::name::make_name;
20use super::validity::Validity;
21use crate::certificate_info::X509CertificateInfoRef;
22use crate::http_error::IsHttpError;
23
24/// Makes a leaf certificate.
25pub fn make_cert(
26    issuer: X509CertificateInfoRef,
27    name: CertitficateName,
28    validity: Validity,
29    public_key: &str,
30    extensions: Vec<X509Extension>,
31) -> Result<X509, MakeCertError> {
32    let mut builder = X509Builder::new().map_err(MakeCertError::NewBuilder)?;
33
34    let public_key =
35        PKey::public_key_from_pem(public_key.as_bytes()).map_err(MakeCertError::ParsePublicKey)?;
36    builder
37        .set_pubkey(&public_key)
38        .map_err(MakeCertError::SetPublicKey)?;
39
40    {
41        let name = make_name(name)?;
42        set_common_fields(
43            &mut builder,
44            issuer.certificate.subject_name(),
45            &name,
46            validity,
47        )?;
48    }
49
50    (|| {
51        let basic_constraints = BasicConstraints::new().critical().build()?;
52        builder.append_extension(basic_constraints)?;
53        Ok(())
54    })()
55    .map_err(MakeCertError::BasicConstraints)?;
56
57    (|| {
58        let key_usage = KeyUsage::new().critical().digital_signature().build()?;
59        builder.append_extension(key_usage)?;
60        Ok(())
61    })()
62    .map_err(MakeCertError::KeyUsage)?;
63
64    (|| {
65        let key_usage = ExtendedKeyUsage::new()
66            .critical()
67            .server_auth()
68            .client_auth()
69            .build()?;
70        builder.append_extension(key_usage)?;
71        Ok(())
72    })()
73    .map_err(MakeCertError::ExtendedKeyUsage)?;
74
75    set_akid(issuer.certificate, &mut builder).map_err(MakeCertError::AuthorityKeyIdentifier)?;
76
77    if let Some(common_name) = name.common_name {
78        (|| {
79            builder.append_extension(
80                SubjectAlternativeName::new()
81                    .dns(common_name)
82                    .build(&builder.x509v3_context(Some(issuer.certificate), None))?,
83            )?;
84            Ok(())
85        })()
86        .map_err(MakeCertError::SubjectAlternativeName)?;
87    }
88
89    for extension in extensions {
90        builder
91            .append_extension(extension)
92            .map_err(MakeCertError::AppendCustomExtension)?;
93    }
94
95    builder
96        .sign(issuer.private_key, openssl::hash::MessageDigest::sha256())
97        .map_err(MakeCertError::Sign)?;
98
99    let certificate = builder.build();
100
101    Ok(certificate)
102}
103
104#[nameth]
105#[derive(thiserror::Error, Debug)]
106pub enum MakeCertError {
107    #[error("[{n}] Failed to create a new X509 Certificate builder: {0}", n = self.name())]
108    NewBuilder(ErrorStack),
109
110    #[error("[{n}] Failed to parse PEM public key: {0}", n = self.name())]
111    ParsePublicKey(ErrorStack),
112
113    #[error("[{n}] Failed to set the public key: {0}", n = self.name())]
114    SetPublicKey(ErrorStack),
115
116    #[error("[{n}] {0}", n = self.name())]
117    MakeName(#[from] MakeNameError),
118
119    #[error("[{n}] {0}", n = self.name())]
120    SetCommonFieldsError(#[from] SetCommonFieldsError),
121
122    #[error("[{n}] Failed to set basic constraints: {0}", n = self.name())]
123    BasicConstraints(ErrorStack),
124
125    #[error("[{n}] Failed to set key usage: {0}", n = self.name())]
126    KeyUsage(ErrorStack),
127
128    #[error("[{n}] Failed to set extended key usage: {0}", n = self.name())]
129    ExtendedKeyUsage(ErrorStack),
130
131    #[error("[{n}] Failed to set AKID: {0}", n = self.name())]
132    AuthorityKeyIdentifier(ErrorStack),
133
134    #[error("[{n}] Failed to set subject alternative name: {0}", n = self.name())]
135    SubjectAlternativeName(ErrorStack),
136
137    #[error("[{n}] Failed add custom extension: {0}", n = self.name())]
138    AppendCustomExtension(ErrorStack),
139
140    #[error("[{n}] Failed to sign the certificate: {0}", n = self.name())]
141    Sign(ErrorStack),
142}
143
144impl IsHttpError for MakeCertError {
145    fn status_code(&self) -> StatusCode {
146        match self {
147            Self::ParsePublicKey { .. } => StatusCode::BAD_REQUEST,
148            Self::MakeName(error) => error.status_code(),
149            Self::SetCommonFieldsError(error) => error.status_code(),
150            Self::AppendCustomExtension { .. } => StatusCode::BAD_REQUEST,
151            Self::NewBuilder { .. }
152            | Self::SetPublicKey { .. }
153            | Self::BasicConstraints { .. }
154            | Self::KeyUsage { .. }
155            | Self::ExtendedKeyUsage { .. }
156            | Self::AuthorityKeyIdentifier { .. }
157            | Self::SubjectAlternativeName { .. }
158            | Self::Sign { .. } => StatusCode::INTERNAL_SERVER_ERROR,
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use std::error::Error;
166    use std::time::Duration;
167    use std::time::SystemTime;
168
169    use openssl::pkey::PKey;
170    use openssl::pkey::Private;
171    use openssl::pkey::Public;
172    use openssl::sign::Signer;
173    use openssl::sign::Verifier;
174    use openssl::stack::Stack;
175    use openssl::x509::X509;
176    use openssl::x509::X509Extension;
177    use openssl::x509::store::X509StoreBuilder;
178    use rustls::pki_types::CertificateDer;
179    use scopeguard::defer_on_unwind;
180
181    use super::super::name::CertitficateName;
182    use crate::certificate_info::CertificateInfo;
183    use crate::certificate_info::X509CertificateInfo;
184    use crate::certificate_info::X509CertificateInfoRef;
185    use crate::security_configuration::trusted_store::cache::CachedTrustedStoreConfig;
186    use crate::security_configuration::trusted_store::empty::EmptyTrustedStoreConfig;
187    use crate::x509::PemString as _;
188    use crate::x509::ca::MakeCaError;
189    use crate::x509::ca::make_ca;
190    use crate::x509::ca::make_intermediate;
191    use crate::x509::key::make_key;
192    use crate::x509::signed_extension::make_signed_extension;
193    use crate::x509::signed_extension::validate_signed_extension;
194    use crate::x509::validity::Validity;
195
196    const DATA: &str = "Hello, world! 😃";
197
198    #[test]
199    fn make_cert() -> Result<(), Box<dyn Error>> {
200        let ca = make_test_ca()?;
201        Ok({
202            let test_cert = make_test_cert(ca.as_ref())?;
203            let text = test_cert.certificate.to_text().pem_string()?;
204            let ca_text = ca.certificate.to_text().pem_string()?;
205            let _debug = scopeguard::guard_on_unwind((), |_| {
206                println!("CA is\n{ca_text}");
207                println!("Certificate is\n{text}");
208            });
209
210            assert!(text.contains("Signature Algorithm: ecdsa-with-SHA256"));
211            assert!(text.contains(
212                "Issuer: C=DE, ST=Bayern, L=Munich, O=Terrazzo, CN=Terrazzo Test Root CA"
213            ));
214            assert!(
215                text.contains("Subject: C=DE, ST=Bayern, L=Munich, O=Terrazzo, CN=Terrazzo Client")
216            );
217            assert!(text.contains("X509v3 Subject Key Identifier"));
218            assert!(text.contains("X509v3 Authority Key Identifier"));
219            // AuthorityKeyIdentifier.issuer(true)
220            assert!(
221                text.contains(
222                    "DirName:/C=DE/ST=Bayern/L=Munich/O=Terrazzo/CN=Terrazzo Test Root CA"
223                )
224            );
225            assert!(
226                !text
227                    .to_ascii_uppercase()
228                    .contains("DA:39:A3:EE:5E:6B:4B:0D:32:55:BF:EF:95:60:18:90:AF:D8:07:09")
229            );
230        })
231    }
232
233    #[test]
234    fn sign_payload() -> Result<(), Box<dyn Error>> {
235        let ca = make_test_ca()?;
236
237        Ok({
238            let test_cert = make_test_cert(ca.as_ref())?;
239
240            let signature = {
241                let mut signer = Signer::new_without_digest(&test_cert.private_key)?;
242                signer.update(DATA.as_bytes())?;
243                signer.sign_to_vec()?
244            };
245
246            assert!(validate_signature(
247                test_cert.certificate.public_key()?,
248                &signature
249            )?);
250
251            let test_cert2 = make_test_cert(ca.as_ref())?;
252            assert!(!validate_signature(
253                test_cert2.certificate.public_key()?,
254                &signature
255            )?);
256        })
257    }
258
259    fn validate_signature(
260        public_key: PKey<Public>,
261        signature: &[u8],
262    ) -> Result<bool, Box<dyn Error>> {
263        let mut verifier = Verifier::new_without_digest(&public_key)?;
264        verifier.update(DATA.as_bytes())?;
265        Ok(verifier.verify(&signature)?)
266    }
267
268    #[test]
269    fn signed_extension() -> Result<(), Box<dyn Error>> {
270        let test_case = SignedExtensionTestCase::new()?;
271        let extension = make_test_signed_extension(&test_case)?;
272        let certificate = make_test_cert_with_signed_extension(&test_case, extension)?;
273        validate_test_signed_extension(&test_case, certificate)?;
274        Ok(())
275    }
276
277    #[test]
278    fn signed_extension_wrong_name() -> Result<(), Box<dyn Error>> {
279        let test_case = SignedExtensionTestCase::new()?;
280        let extension = make_test_signed_extension(&SignedExtensionTestCase {
281            common_name: format!("NOT {}", test_case.common_name),
282            ..test_case.clone()
283        })?;
284        let certificate = make_test_cert_with_signed_extension(&test_case, extension)?;
285        let error = validate_test_signed_extension(&test_case, certificate).unwrap_err();
286        defer_on_unwind!(eprintln!("{error}"));
287        assert_eq!(
288            error.to_string(),
289            "[VerifySignature] [CertificatePropertiesMismatch] The signed extension content hash doesn't match: common_name was 'NOT With signed extension' expected 'With signed extension'"
290        );
291        Ok(())
292    }
293
294    #[test]
295    fn signed_extension_wrong_validity_from() -> Result<(), Box<dyn Error>> {
296        let test_case = SignedExtensionTestCase::new()?;
297        let extension = make_test_signed_extension(&SignedExtensionTestCase {
298            validity: Validity {
299                from: test_case.validity.from + Duration::from_secs(123),
300                ..test_case.validity
301            },
302            ..test_case.clone()
303        })?;
304        let certificate = make_test_cert_with_signed_extension(&test_case, extension)?;
305        let error = validate_test_signed_extension(&test_case, certificate).unwrap_err();
306        defer_on_unwind!(eprintln!("{error}"));
307        assert!(error.to_string().starts_with(
308            "[VerifySignature] [CertificatePropertiesMismatch] The signed extension content hash doesn't match: not_before was "));
309        Ok(())
310    }
311
312    #[test]
313    fn signed_extension_wrong_validity_to() -> Result<(), Box<dyn Error>> {
314        let test_case = SignedExtensionTestCase::new()?;
315        let extension = make_test_signed_extension(&SignedExtensionTestCase {
316            validity: Validity {
317                to: test_case.validity.to + Duration::from_secs(123),
318                ..test_case.validity
319            },
320            ..test_case.clone()
321        })?;
322        let certificate = make_test_cert_with_signed_extension(&test_case, extension)?;
323        let error = validate_test_signed_extension(&test_case, certificate).unwrap_err();
324        defer_on_unwind!(eprintln!("{error}"));
325        assert!(error.to_string().starts_with(
326            "[VerifySignature] [CertificatePropertiesMismatch] The signed extension content hash doesn't match: not_after was "));
327        Ok(())
328    }
329
330    #[test]
331    fn signed_extension_wrong_public_key() -> Result<(), Box<dyn Error>> {
332        let test_case = SignedExtensionTestCase::new()?;
333        let extension = make_test_signed_extension(&SignedExtensionTestCase {
334            public_key: make_key()?,
335            ..test_case.clone()
336        })?;
337        let certificate = make_test_cert_with_signed_extension(&test_case, extension)?;
338        let error = validate_test_signed_extension(&test_case, certificate).unwrap_err();
339        defer_on_unwind!(eprintln!("{error}"));
340        assert!(error.to_string().starts_with(
341            "[VerifySignature] [CertificatePropertiesMismatch] The signed extension content hash doesn't match: public_key was '"));
342        Ok(())
343    }
344
345    #[test]
346    fn signed_extension_wrong_signer() -> Result<(), Box<dyn Error>> {
347        let test_case = SignedExtensionTestCase::new()?;
348        for (t, common_name) in [(true, "Terrazzo Client"), (false, "NOT Terrazzo")] {
349            let signer = make_named_test_cert(
350                test_case.root.as_ref(),
351                CertitficateName {
352                    country: Some(['D', 'E']),
353                    state_or_province: Some("Bayern"),
354                    locality: Some("Munich"),
355                    organization: Some("Terrazzo"),
356                    common_name: Some(common_name),
357                },
358            )?;
359            let extension = make_test_signed_extension(&SignedExtensionTestCase {
360                signer,
361                ..test_case.clone()
362            })?;
363            let certificate = make_test_cert_with_signed_extension(&test_case, extension)?;
364            let result = validate_test_signed_extension(&test_case, certificate);
365            if t {
366                let () = result?;
367            } else {
368                let error = result.unwrap_err();
369                defer_on_unwind!(eprintln!("{error}"));
370                assert_eq!(
371                    error.to_string(),
372                    "[VerifySigner] [SignerCertificateNameMismatch] The signer certificate name was: NOT Terrazzo"
373                );
374            }
375        }
376        Ok(())
377    }
378
379    #[test]
380    fn signed_extension_untrusted_signer() -> Result<(), Box<dyn Error>> {
381        let test_case = SignedExtensionTestCase::new()?;
382        let wrong_signer = make_test_cert(make_test_ca()?.as_ref())?;
383        let extension = make_test_signed_extension(&SignedExtensionTestCase {
384            signer: wrong_signer,
385            ..test_case.clone()
386        })?;
387        let certificate = make_test_cert_with_signed_extension(&test_case, extension)?;
388        let error = validate_test_signed_extension(&test_case, certificate).unwrap_err();
389        defer_on_unwind!(eprintln!("{error}"));
390        assert!(
391            error
392                .to_string()
393                .starts_with("[VerifySigner] [VerifySignerChainError] [ValidationFailed] ")
394        );
395        Ok(())
396    }
397
398    #[test]
399    fn signed_extension_not_a_cert() -> Result<(), Box<dyn Error>> {
400        let error = validate_signed_extension(
401            &CertificateDer::from_slice("abcd".as_bytes()),
402            &EmptyTrustedStoreConfig,
403            "",
404        )
405        .unwrap_err();
406        defer_on_unwind!(eprintln!("{error}"));
407        assert!(
408            error
409                .to_string()
410                .starts_with("[X509Certificate] Failed to parse X509Certificate: ")
411        );
412        Ok(())
413    }
414
415    #[test]
416    fn signed_extension_missing() -> Result<(), Box<dyn Error>> {
417        let test_case = SignedExtensionTestCase::new()?;
418        let test_cert = make_test_cert(test_case.root.as_ref())?;
419        let error = validate_test_signed_extension(&test_case, test_cert.certificate).unwrap_err();
420        defer_on_unwind!(eprintln!("{error}"));
421        assert!(error.to_string().starts_with("[SignedExtensionNotFound]"));
422        Ok(())
423    }
424
425    #[test]
426    fn signed_extension_invalid_intermediate() -> Result<(), Box<dyn Error>> {
427        let test_case = SignedExtensionTestCase::new()?;
428        let intermediate = make_test_intermediate(test_case.root.as_ref())?;
429        let extension = make_test_signed_extension(&SignedExtensionTestCase {
430            intermediate,
431            ..test_case.clone()
432        })?;
433        let certificate = make_test_cert_with_signed_extension(&test_case, extension)?;
434        let error = validate_test_signed_extension(&test_case, certificate).unwrap_err();
435        defer_on_unwind!(eprintln!("{error}"));
436        assert!(
437            error.to_string().starts_with(
438                "[VerifySigner] [VerifySignerChainError] [ValidationFailed] The signer certificate is invalid"
439            )
440        );
441        Ok(())
442    }
443
444    #[derive(Clone)]
445    struct SignedExtensionTestCase {
446        root: X509CertificateInfo,
447        intermediate: X509CertificateInfo,
448        signer: X509CertificateInfo,
449        common_name: String,
450        validity: Validity,
451        public_key: PKey<Private>,
452    }
453
454    impl SignedExtensionTestCase {
455        fn new() -> Result<Self, Box<dyn Error>> {
456            let root = make_test_ca()?;
457            let intermediate = make_test_intermediate(root.as_ref())?;
458            let signer = make_test_cert(intermediate.as_ref())?;
459            let validity = Validity {
460                from: SystemTime::now(),
461                to: SystemTime::now() + Duration::from_secs(1) * 3600,
462            };
463            let public_key = make_key()?;
464            Ok(Self {
465                root,
466                intermediate,
467                signer,
468                common_name: "With signed extension".to_owned(),
469                validity,
470                public_key,
471            })
472        }
473    }
474
475    fn validate_test_signed_extension(
476        test_case: &SignedExtensionTestCase,
477        certificate: X509,
478    ) -> Result<(), Box<dyn Error>> {
479        let store = {
480            let mut builder = X509StoreBuilder::new()?;
481            builder.add_cert(test_case.root.certificate.to_owned())?;
482            builder.build()
483        };
484        let () = validate_signed_extension(
485            &CertificateDer::from_slice(&certificate.to_der()?),
486            &CachedTrustedStoreConfig::from(store),
487            "Terrazzo Client",
488        )?;
489        Ok(())
490    }
491
492    fn make_test_cert_with_signed_extension(
493        test_case: &SignedExtensionTestCase,
494        extension: X509Extension,
495    ) -> Result<X509, Box<dyn Error>> {
496        let certificate = super::make_cert(
497            test_case.intermediate.as_ref(),
498            CertitficateName {
499                country: Some(['D', 'E']),
500                state_or_province: Some("Bayern"),
501                locality: Some("Munich"),
502                organization: Some("Terrazzo"),
503                common_name: Some(&test_case.common_name),
504            },
505            test_case.validity,
506            &test_case.public_key.public_key_to_pem()?.pem_string()?,
507            vec![extension],
508        )?;
509        Ok(certificate)
510    }
511
512    fn make_test_signed_extension(
513        test_case: &SignedExtensionTestCase,
514    ) -> Result<X509Extension, Box<dyn Error>> {
515        let mut intermediates = Stack::new()?;
516        intermediates.push(test_case.intermediate.certificate.clone())?;
517        let extension = make_signed_extension(
518            &test_case.common_name,
519            test_case.validity,
520            &test_case.public_key.public_key_to_der()?,
521            Some(&intermediates),
522            test_case.signer.as_ref(),
523        )?;
524        Ok(extension)
525    }
526
527    fn make_test_cert(ca: X509CertificateInfoRef) -> Result<X509CertificateInfo, Box<dyn Error>> {
528        make_named_test_cert(
529            ca,
530            CertitficateName {
531                country: Some(['D', 'E']),
532                state_or_province: Some("Bayern"),
533                locality: Some("Munich"),
534                organization: Some("Terrazzo"),
535                common_name: Some("Terrazzo Client"),
536            },
537        )
538    }
539
540    fn make_named_test_cert(
541        ca: X509CertificateInfoRef,
542        name: CertitficateName,
543    ) -> Result<X509CertificateInfo, Box<dyn Error>> {
544        let private_key = make_key()?;
545        let public_key = private_key.public_key_to_pem().pem_string()?;
546        let certificate = super::make_cert(
547            ca,
548            name,
549            Validity {
550                from: SystemTime::now(),
551                to: SystemTime::now() + Duration::from_secs(1) * 3600,
552            },
553            &public_key,
554            vec![],
555        )?;
556        Ok(CertificateInfo {
557            certificate,
558            private_key,
559        })
560    }
561
562    fn make_test_ca() -> Result<X509CertificateInfo, MakeCaError> {
563        make_ca(
564            CertitficateName {
565                country: Some(['D', 'E']),
566                state_or_province: Some("Bayern"),
567                locality: Some("Munich"),
568                organization: Some("Terrazzo"),
569                common_name: Some("Terrazzo Test Root CA"),
570            },
571            Validity {
572                from: SystemTime::now(),
573                to: SystemTime::now() + Duration::from_secs(1) * 3600,
574            },
575        )
576    }
577
578    fn make_test_intermediate(
579        root: X509CertificateInfoRef,
580    ) -> Result<X509CertificateInfo, MakeCaError> {
581        make_intermediate(
582            root,
583            CertitficateName {
584                common_name: Some("Terrazzo Test intermediate"),
585                ..CertitficateName::default()
586            },
587            Validity {
588                from: SystemTime::now(),
589                to: SystemTime::now() + Duration::from_secs(1) * 3600,
590            },
591        )
592    }
593}