Skip to main content

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        let test_cert = make_test_cert(ca.as_ref())?;
202        let text = test_cert.certificate.to_text().pem_string()?;
203        let ca_text = ca.certificate.to_text().pem_string()?;
204        let _debug = scopeguard::guard_on_unwind((), |_| {
205            println!("CA is\n{ca_text}");
206            println!("Certificate is\n{text}");
207        });
208
209        assert!(text.contains("Signature Algorithm: ecdsa-with-SHA256"));
210        assert!(
211            text.contains(
212                "Issuer: C=DE, ST=Bayern, L=Munich, O=Terrazzo, CN=Terrazzo Test Root CA"
213            )
214        );
215        assert!(
216            text.contains("Subject: C=DE, ST=Bayern, L=Munich, O=Terrazzo, CN=Terrazzo Client")
217        );
218        assert!(text.contains("X509v3 Subject Key Identifier"));
219        assert!(text.contains("X509v3 Authority Key Identifier"));
220        // AuthorityKeyIdentifier.issuer(true)
221        assert!(
222            text.contains("DirName:/C=DE/ST=Bayern/L=Munich/O=Terrazzo/CN=Terrazzo Test Root CA")
223        );
224        assert!(
225            !text
226                .to_ascii_uppercase()
227                .contains("DA:39:A3:EE:5E:6B:4B:0D:32:55:BF:EF:95:60:18:90:AF:D8:07:09")
228        );
229        Ok(())
230    }
231
232    #[test]
233    fn sign_payload() -> Result<(), Box<dyn Error>> {
234        let ca = make_test_ca()?;
235
236        let test_cert = make_test_cert(ca.as_ref())?;
237
238        let signature = {
239            let mut signer = Signer::new_without_digest(&test_cert.private_key)?;
240            signer.update(DATA.as_bytes())?;
241            signer.sign_to_vec()?
242        };
243
244        assert!(validate_signature(
245            test_cert.certificate.public_key()?,
246            &signature
247        )?);
248
249        let test_cert2 = make_test_cert(ca.as_ref())?;
250        assert!(!validate_signature(
251            test_cert2.certificate.public_key()?,
252            &signature
253        )?);
254        Ok(())
255    }
256
257    fn validate_signature(
258        public_key: PKey<Public>,
259        signature: &[u8],
260    ) -> Result<bool, Box<dyn Error>> {
261        let mut verifier = Verifier::new_without_digest(&public_key)?;
262        verifier.update(DATA.as_bytes())?;
263        Ok(verifier.verify(signature)?)
264    }
265
266    #[test]
267    fn signed_extension() -> Result<(), Box<dyn Error>> {
268        let test_case = SignedExtensionTestCase::new()?;
269        let extension = make_test_signed_extension(&test_case)?;
270        let certificate = make_test_cert_with_signed_extension(&test_case, extension)?;
271        validate_test_signed_extension(&test_case, certificate)?;
272        Ok(())
273    }
274
275    #[test]
276    fn signed_extension_wrong_name() -> Result<(), Box<dyn Error>> {
277        let test_case = SignedExtensionTestCase::new()?;
278        let extension = make_test_signed_extension(&SignedExtensionTestCase {
279            common_name: format!("NOT {}", test_case.common_name),
280            ..test_case.clone()
281        })?;
282        let certificate = make_test_cert_with_signed_extension(&test_case, extension)?;
283        let error = validate_test_signed_extension(&test_case, certificate).unwrap_err();
284        defer_on_unwind!(eprintln!("{error}"));
285        assert_eq!(
286            error.to_string(),
287            "[VerifySignature] [CertificatePropertiesMismatch] The signed extension content hash doesn't match: common_name was 'NOT With signed extension' expected 'With signed extension'"
288        );
289        Ok(())
290    }
291
292    #[test]
293    fn signed_extension_wrong_validity_from() -> Result<(), Box<dyn Error>> {
294        let test_case = SignedExtensionTestCase::new()?;
295        let extension = make_test_signed_extension(&SignedExtensionTestCase {
296            validity: Validity {
297                from: test_case.validity.from + Duration::from_secs(123),
298                ..test_case.validity
299            },
300            ..test_case.clone()
301        })?;
302        let certificate = make_test_cert_with_signed_extension(&test_case, extension)?;
303        let error = validate_test_signed_extension(&test_case, certificate).unwrap_err();
304        defer_on_unwind!(eprintln!("{error}"));
305        assert!(error.to_string().starts_with(
306            "[VerifySignature] [CertificatePropertiesMismatch] The signed extension content hash doesn't match: not_before was "));
307        Ok(())
308    }
309
310    #[test]
311    fn signed_extension_wrong_validity_to() -> Result<(), Box<dyn Error>> {
312        let test_case = SignedExtensionTestCase::new()?;
313        let extension = make_test_signed_extension(&SignedExtensionTestCase {
314            validity: Validity {
315                to: test_case.validity.to + Duration::from_secs(123),
316                ..test_case.validity
317            },
318            ..test_case.clone()
319        })?;
320        let certificate = make_test_cert_with_signed_extension(&test_case, extension)?;
321        let error = validate_test_signed_extension(&test_case, certificate).unwrap_err();
322        defer_on_unwind!(eprintln!("{error}"));
323        assert!(error.to_string().starts_with(
324            "[VerifySignature] [CertificatePropertiesMismatch] The signed extension content hash doesn't match: not_after was "));
325        Ok(())
326    }
327
328    #[test]
329    fn signed_extension_wrong_public_key() -> Result<(), Box<dyn Error>> {
330        let test_case = SignedExtensionTestCase::new()?;
331        let extension = make_test_signed_extension(&SignedExtensionTestCase {
332            public_key: make_key()?,
333            ..test_case.clone()
334        })?;
335        let certificate = make_test_cert_with_signed_extension(&test_case, extension)?;
336        let error = validate_test_signed_extension(&test_case, certificate).unwrap_err();
337        defer_on_unwind!(eprintln!("{error}"));
338        assert!(error.to_string().starts_with(
339            "[VerifySignature] [CertificatePropertiesMismatch] The signed extension content hash doesn't match: public_key was '"));
340        Ok(())
341    }
342
343    #[test]
344    fn signed_extension_wrong_signer() -> Result<(), Box<dyn Error>> {
345        let test_case = SignedExtensionTestCase::new()?;
346        for (t, common_name) in [(true, "Terrazzo Client"), (false, "NOT Terrazzo")] {
347            let signer = make_named_test_cert(
348                test_case.root.as_ref(),
349                CertitficateName {
350                    country: Some(['D', 'E']),
351                    state_or_province: Some("Bayern"),
352                    locality: Some("Munich"),
353                    organization: Some("Terrazzo"),
354                    common_name: Some(common_name),
355                },
356            )?;
357            let extension = make_test_signed_extension(&SignedExtensionTestCase {
358                signer,
359                ..test_case.clone()
360            })?;
361            let certificate = make_test_cert_with_signed_extension(&test_case, extension)?;
362            let result = validate_test_signed_extension(&test_case, certificate);
363            if t {
364                let () = result?;
365            } else {
366                let error = result.unwrap_err();
367                defer_on_unwind!(eprintln!("{error}"));
368                assert_eq!(
369                    error.to_string(),
370                    "[VerifySigner] [SignerCertificateNameMismatch] The signer certificate name was: NOT Terrazzo"
371                );
372            }
373        }
374        Ok(())
375    }
376
377    #[test]
378    fn signed_extension_untrusted_signer() -> Result<(), Box<dyn Error>> {
379        let test_case = SignedExtensionTestCase::new()?;
380        let wrong_signer = make_test_cert(make_test_ca()?.as_ref())?;
381        let extension = make_test_signed_extension(&SignedExtensionTestCase {
382            signer: wrong_signer,
383            ..test_case.clone()
384        })?;
385        let certificate = make_test_cert_with_signed_extension(&test_case, extension)?;
386        let error = validate_test_signed_extension(&test_case, certificate).unwrap_err();
387        defer_on_unwind!(eprintln!("{error}"));
388        assert!(
389            error
390                .to_string()
391                .starts_with("[VerifySigner] [VerifySignerChainError] [ValidationFailed] ")
392        );
393        Ok(())
394    }
395
396    #[test]
397    fn signed_extension_not_a_cert() -> Result<(), Box<dyn Error>> {
398        let error = validate_signed_extension(
399            &CertificateDer::from_slice("abcd".as_bytes()),
400            &EmptyTrustedStoreConfig,
401            "",
402        )
403        .unwrap_err();
404        defer_on_unwind!(eprintln!("{error}"));
405        assert!(
406            error
407                .to_string()
408                .starts_with("[X509Certificate] Failed to parse X509Certificate: ")
409        );
410        Ok(())
411    }
412
413    #[test]
414    fn signed_extension_missing() -> Result<(), Box<dyn Error>> {
415        let test_case = SignedExtensionTestCase::new()?;
416        let test_cert = make_test_cert(test_case.root.as_ref())?;
417        let error = validate_test_signed_extension(&test_case, test_cert.certificate).unwrap_err();
418        defer_on_unwind!(eprintln!("{error}"));
419        assert!(error.to_string().starts_with("[SignedExtensionNotFound]"));
420        Ok(())
421    }
422
423    #[test]
424    fn signed_extension_invalid_intermediate() -> Result<(), Box<dyn Error>> {
425        let test_case = SignedExtensionTestCase::new()?;
426        let intermediate = make_test_intermediate(test_case.root.as_ref())?;
427        let extension = make_test_signed_extension(&SignedExtensionTestCase {
428            intermediate,
429            ..test_case.clone()
430        })?;
431        let certificate = make_test_cert_with_signed_extension(&test_case, extension)?;
432        let error = validate_test_signed_extension(&test_case, certificate).unwrap_err();
433        defer_on_unwind!(eprintln!("{error}"));
434        assert!(
435            error.to_string().starts_with(
436                "[VerifySigner] [VerifySignerChainError] [ValidationFailed] The signer certificate is invalid"
437            )
438        );
439        Ok(())
440    }
441
442    #[derive(Clone)]
443    struct SignedExtensionTestCase {
444        root: X509CertificateInfo,
445        intermediate: X509CertificateInfo,
446        signer: X509CertificateInfo,
447        common_name: String,
448        validity: Validity,
449        public_key: PKey<Private>,
450    }
451
452    impl SignedExtensionTestCase {
453        fn new() -> Result<Self, Box<dyn Error>> {
454            let root = make_test_ca()?;
455            let intermediate = make_test_intermediate(root.as_ref())?;
456            let signer = make_test_cert(intermediate.as_ref())?;
457            let validity = Validity {
458                from: SystemTime::now(),
459                to: SystemTime::now() + Duration::from_secs(1) * 3600,
460            };
461            let public_key = make_key()?;
462            Ok(Self {
463                root,
464                intermediate,
465                signer,
466                common_name: "With signed extension".to_owned(),
467                validity,
468                public_key,
469            })
470        }
471    }
472
473    fn validate_test_signed_extension(
474        test_case: &SignedExtensionTestCase,
475        certificate: X509,
476    ) -> Result<(), Box<dyn Error>> {
477        let store = {
478            let mut builder = X509StoreBuilder::new()?;
479            builder.add_cert(test_case.root.certificate.to_owned())?;
480            builder.build()
481        };
482        let () = validate_signed_extension(
483            &CertificateDer::from_slice(&certificate.to_der()?),
484            &CachedTrustedStoreConfig::from(store),
485            "Terrazzo Client",
486        )?;
487        Ok(())
488    }
489
490    fn make_test_cert_with_signed_extension(
491        test_case: &SignedExtensionTestCase,
492        extension: X509Extension,
493    ) -> Result<X509, Box<dyn Error>> {
494        let certificate = super::make_cert(
495            test_case.intermediate.as_ref(),
496            CertitficateName {
497                country: Some(['D', 'E']),
498                state_or_province: Some("Bayern"),
499                locality: Some("Munich"),
500                organization: Some("Terrazzo"),
501                common_name: Some(&test_case.common_name),
502            },
503            test_case.validity,
504            &test_case.public_key.public_key_to_pem()?.pem_string()?,
505            vec![extension],
506        )?;
507        Ok(certificate)
508    }
509
510    fn make_test_signed_extension(
511        test_case: &SignedExtensionTestCase,
512    ) -> Result<X509Extension, Box<dyn Error>> {
513        let mut intermediates = Stack::new()?;
514        intermediates.push(test_case.intermediate.certificate.clone())?;
515        let extension = make_signed_extension(
516            &test_case.common_name,
517            test_case.validity,
518            &test_case.public_key.public_key_to_der()?,
519            Some(&intermediates),
520            test_case.signer.as_ref(),
521        )?;
522        Ok(extension)
523    }
524
525    fn make_test_cert(ca: X509CertificateInfoRef) -> Result<X509CertificateInfo, Box<dyn Error>> {
526        make_named_test_cert(
527            ca,
528            CertitficateName {
529                country: Some(['D', 'E']),
530                state_or_province: Some("Bayern"),
531                locality: Some("Munich"),
532                organization: Some("Terrazzo"),
533                common_name: Some("Terrazzo Client"),
534            },
535        )
536    }
537
538    fn make_named_test_cert(
539        ca: X509CertificateInfoRef,
540        name: CertitficateName,
541    ) -> Result<X509CertificateInfo, Box<dyn Error>> {
542        let private_key = make_key()?;
543        let public_key = private_key.public_key_to_pem().pem_string()?;
544        let certificate = super::make_cert(
545            ca,
546            name,
547            Validity {
548                from: SystemTime::now(),
549                to: SystemTime::now() + Duration::from_secs(1) * 3600,
550            },
551            &public_key,
552            vec![],
553        )?;
554        Ok(CertificateInfo {
555            certificate,
556            private_key,
557        })
558    }
559
560    fn make_test_ca() -> Result<X509CertificateInfo, MakeCaError> {
561        make_ca(
562            CertitficateName {
563                country: Some(['D', 'E']),
564                state_or_province: Some("Bayern"),
565                locality: Some("Munich"),
566                organization: Some("Terrazzo"),
567                common_name: Some("Terrazzo Test Root CA"),
568            },
569            Validity {
570                from: SystemTime::now(),
571                to: SystemTime::now() + Duration::from_secs(1) * 3600,
572            },
573        )
574    }
575
576    fn make_test_intermediate(
577        root: X509CertificateInfoRef,
578    ) -> Result<X509CertificateInfo, MakeCaError> {
579        make_intermediate(
580            root,
581            CertitficateName {
582                common_name: Some("Terrazzo Test intermediate"),
583                ..CertitficateName::default()
584            },
585            Validity {
586                from: SystemTime::now(),
587                to: SystemTime::now() + Duration::from_secs(1) * 3600,
588            },
589        )
590    }
591}