cosmian_kms_crypto 5.24.0

Cosmian KMS Crypto - cryptographic operations and algorithms
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
use std::path::PathBuf;

use cosmian_logger::warn;
use ini::Ini;
use openssl::{
    asn1::{Asn1Object, Asn1OctetString},
    nid::Nid,
    x509::{
        X509Extension, X509v3Context,
        extension::{
            AuthorityKeyIdentifier, BasicConstraints, ExtendedKeyUsage, KeyUsage,
            SubjectAlternativeName, SubjectKeyIdentifier,
        },
    },
};

use crate::{crypto_bail, error::CryptoError};

/// X509 Extension section parser.
/// The expected format of file for extensions be like:
///
/// ```cnf
/// [ v3_ca ]
/// basicConstraints=CA:TRUE,pathlen:0
/// keyUsage=keyCertSign,digitalSignature
/// extendedKeyUsage=emailProtection
/// crlDistributionPoints=URI:http://cse.example.com/crl.pem
/// ```
///
/// Warning: this naïve parser doesn't parse long form and internal links, such as:
///
/// ```cnf
/// [ v3_ca ]
/// basicConstraints=critical,@bs_section
///
/// [bs_section]
/// CA=true
/// pathlen=1
/// ```
pub fn parse_v3_ca_from_file(
    extension_file: &PathBuf,
    x509_context: &X509v3Context,
) -> Result<Vec<X509Extension>, CryptoError> {
    let conf = Ini::load_from_file(extension_file).map_err(|e| {
        CryptoError::NotSupported(format!(
            "cannot read x509 extension file: `{}`. Reason: {e}",
            extension_file.display()
        ))
    })?;
    parse_v3_ca(&conf, x509_context)
}

pub fn parse_v3_ca_from_str(
    conf: &str,
    x509_context: &X509v3Context,
) -> Result<Vec<X509Extension>, CryptoError> {
    let conf = Ini::load_from_str(conf).map_err(|e| {
        CryptoError::NotSupported(format!(
            "cannot read x509 extension str: `{conf:?}`.\nReason: {e}"
        ))
    })?;
    parse_v3_ca(&conf, x509_context)
}

pub fn parse_v3_ca(
    conf: &Ini,
    x509_context: &X509v3Context,
) -> Result<Vec<X509Extension>, CryptoError> {
    let v3_ca = conf.section(Some("v3_ca")).ok_or_else(|| {
        CryptoError::NotSupported(
            "unable to find `v3_ca` parag from X.509 extension content".to_owned(),
        )
    })?;

    let mut extensions = Vec::new();
    for (key, value) in v3_ca {
        match key {
            "subjectKeyIdentifier" => {
                if value.contains("critical") {
                    extensions.push(SubjectKeyIdentifier::new().critical().build(x509_context)?);
                } else {
                    extensions.push(SubjectKeyIdentifier::new().build(x509_context)?);
                }
            }
            "keyUsage" => {
                let mut ku = KeyUsage::new();
                for value in value.trim().split(',') {
                    match value {
                        "critical" => ku.critical(),
                        "digitalSignature" => ku.digital_signature(),
                        "nonRepudiation" => ku.non_repudiation(),
                        "keyEncipherment" => ku.key_encipherment(),
                        "dataEncipherment" => ku.data_encipherment(),
                        "keyAgreement" => ku.key_agreement(),
                        "keyCertSign" => ku.key_cert_sign(),
                        "crlSign" => ku.crl_sign(),
                        "encipherOnly" => ku.encipher_only(),
                        "decipherOnly" => ku.decipher_only(),
                        _ => {
                            crypto_bail!("not supported `keyUsage` extension's value: `{value}`");
                        }
                    };
                }
                extensions.push(ku.build()?);
            }
            "subjectAltName" => {
                let mut san = SubjectAlternativeName::new();
                value.trim().split(',').try_for_each(|value| {
                    match value {
                        "critical" => san.critical(),
                        // `email:my@example.com`
                        _ if value.starts_with("email") => san.email(colon_split(value, "email")?),
                        // `URI:http://my.example.com/`
                        _ if value.starts_with("URI") => san.uri(colon_split(value, "URI")?),
                        // `DNS:mail.example.com`
                        _ if value.starts_with("DNS") => san.dns(colon_split(value, "DNS")?),
                        // `IP:192.168.1.1`
                        // `IP:13::16`
                        _ if value.starts_with("IP") => san.ip(colon_split(value, "IP")?),
                        // `RID:1.2.3.4`
                        _ if value.starts_with("RID") => san.rid(colon_split(value, "RID")?),
                        // "otherName" => {
                        //     // otherName:1.2.3.4
                        //     let other_name_value = colon_split(value, "otherName")?;
                        //     // must encode content to DER ASN.1 ...
                        //     san.other_name2(oid, content)
                        // }
                        _ => {
                            crypto_bail!(
                                "not supported `subjectAltName` extension's value: {value}"
                            );
                        }
                    };
                    Ok::<_, CryptoError>(())
                })?;
                extensions.push(san.build(x509_context)?);
            }
            "privateKeyUsagePeriod" => {
                #[expect(deprecated)]
                extensions.push(X509Extension::new_nid(
                    None,
                    Some(x509_context),
                    Nid::PRIVATE_KEY_USAGE_PERIOD,
                    value,
                )?);
            }
            "issuerAltName" => {
                #[expect(deprecated)]
                extensions.push(X509Extension::new_nid(
                    None,
                    Some(x509_context),
                    Nid::ISSUER_ALT_NAME,
                    value,
                )?);
            }
            "basicConstraints" => {
                let mut bc = BasicConstraints::new();
                value.trim().split(',').try_for_each(|value| {
                    match value {
                        "critical" => bc.critical(),
                        "CA:true" | "CA:TRUE" => bc.ca(),
                        _ if value.starts_with("pathlen") => {
                            let pathlen =
                                colon_split(value, "pathlen")?.parse::<u32>().map_err(|e| {
                                    CryptoError::NotSupported(format!(
                                        "unable to convert Basic Constraints pathlen to `u32` \
                                         value: `{value}`. Reason: {e}"
                                    ))
                                })?;
                            bc.pathlen(pathlen)
                        }
                        _ => {
                            warn!("ignored `basicConstraints` extension's value: {value}");
                            &mut bc
                        }
                    };
                    Ok::<_, CryptoError>(())
                })?;
                extensions.push(bc.build()?);
            }
            "nameConstraints" => {
                #[expect(deprecated)]
                extensions.push(X509Extension::new_nid(
                    None,
                    Some(x509_context),
                    Nid::NAME_CONSTRAINTS,
                    value,
                )?);
            }
            "crlDistributionPoints" => {
                #[expect(deprecated)]
                extensions.push(X509Extension::new_nid(
                    None,
                    Some(x509_context),
                    Nid::CRL_DISTRIBUTION_POINTS,
                    value,
                )?);
            }
            "certificatePolicies" => {
                extensions.push(build_certificate_policies_extension(value)?);
            }
            "extendedKeyUsage" => {
                let mut eku = ExtendedKeyUsage::new();
                value.trim().split(',').try_for_each(|value| {
                    match value {
                        "critical" => eku.critical(),
                        "serverAuth" => eku.server_auth(),
                        "clientAuth" => eku.client_auth(),
                        "codeSigning" => eku.code_signing(),
                        "emailProtection" => eku.email_protection(),
                        "timeStamping" => eku.time_stamping(),
                        "OCSPSigning" => eku.other("OCSPSigning"),
                        "ipsecIKE" => eku.other("ipsecIKE"),
                        "msCodeInd" => eku.ms_code_ind(),
                        "msCodeCom" => eku.ms_code_com(),
                        "msCTLSign" => eku.ms_ctl_sign(),
                        "msEFS" => eku.ms_efs(),
                        "nsSGC" => eku.ns_sgc(),
                        "msSGC" => eku.ms_sgc(),
                        _ => {
                            crypto_bail!(
                                "not supported `extendedKeyUsage` extension's value: {value}"
                            );
                        }
                    };
                    Ok::<_, CryptoError>(())
                })?;
                extensions.push(eku.build()?);
            }
            "authorityKeyIdentifier" => {
                let mut aki = AuthorityKeyIdentifier::new();
                value.trim().split(',').try_for_each(|value| {
                    match value {
                        "critical" => aki.critical(),
                        "issuer:always" => aki.issuer(true),
                        "issuer" => aki.issuer(false),
                        "keyid:always" => aki.keyid(true),
                        "keyid" => aki.keyid(false),
                        _ => {
                            crypto_bail!(
                                "not supported `authorityKeyIdentifier` extension's value: {value}"
                            );
                        }
                    };
                    Ok::<_, CryptoError>(())
                })?;
                extensions.push(aki.build(x509_context)?);
            }
            "authorityInfoAccess" => {
                // OID 1.3.6.1.5.5.7.1.1 — Authority Information Access
                // Value format: "OCSP;URI:http://ocsp.example.com/,caIssuers;URI:http://ca.example.com/ca.crt"
                #[expect(deprecated)]
                extensions.push(X509Extension::new_nid(
                    None,
                    Some(x509_context),
                    Nid::INFO_ACCESS,
                    value,
                )?);
            }
            "noRevAvail" => {
                // id-ce-noRevAvail (RFC 9608 §2), OID { id-ce 56 } = 2.5.29.56.
                // OpenSSL does not register this OID in its extension table, so we build
                // the extension directly using new_from_der().
                // Criticality MUST be FALSE; DER encoding is '0500'H (NULL).
                let oid = Asn1Object::from_str("2.5.29.56")?;
                let val = Asn1OctetString::new_from_bytes(&[0x05, 0x00])?;
                extensions.push(X509Extension::new_from_der(
                    oid.as_ref(),
                    false,
                    val.as_ref(),
                )?);
            }
            _ => {
                return Err(CryptoError::Default(format!(
                    "`{key}` is not a valid X.509 extension key property"
                )));
            }
        }
    }

    Ok(extensions)
}

/// Build a `certificatePolicies` X.509v3 extension (OID 2.5.29.32) from an OpenSSL
/// `x509v3_config`-style value string, **without** requiring an OpenSSL config database.
///
/// OpenSSL's built-in `certificatePolicies` parser (`r2i_certpol`) requires `ctx->db` to be
/// non-NULL even for bare OIDs, because it calls `policy_section()` internally. The x509v3
/// context created by `X509Builder::x509v3_context` is always constructed without a config
/// database in our call-site, so delegating to `X509Extension::new_nid` with `None` always
/// triggers "no config database". This function parses the value natively and emits the
/// correct DER without any OpenSSL conf dependency.
///
/// # Supported syntax
///
/// ```text
/// certificatePolicies = [critical,] OID [, CPS:url | CPS.N:url] [, OID ...]
/// ```
///
/// * `critical` – marks the extension as critical (may appear anywhere before the first OID).
/// * `OID` – a dotted-decimal policy OID, e.g. `1.3.6.1.4.1.16376.9.1.1.1.0`.
/// * `CPS:url` – inline CPS (Certification Practice Statement) qualifier.
/// * `CPS.N:url` – same with a numeric suffix (section-style numbering accepted by OpenSSL
///   config files); the `.N` part is ignored and treated like plain `CPS:`.
///
/// # Errors
///
/// Returns an error for unknown qualifier tokens or malformed OID notation.
fn build_certificate_policies_extension(value: &str) -> Result<X509Extension, CryptoError> {
    let mut critical = false;
    // Each entry: (policy_oid_dotted, cps_urls)
    let mut policies: Vec<(String, Vec<String>)> = Vec::new();

    for token in value.split(',') {
        let token = token.trim();
        if token.eq_ignore_ascii_case("critical") {
            critical = true;
        } else if is_dotted_oid(token) {
            policies.push((token.to_owned(), Vec::new()));
        } else if let Some(url) = strip_cps_qualifier(token) {
            let last = policies.last_mut().ok_or_else(|| {
                CryptoError::NotSupported(
                    "certificatePolicies: CPS qualifier appears before any policy OID".to_owned(),
                )
            })?;
            last.1.push(url.to_owned());
        } else {
            return Err(CryptoError::NotSupported(format!(
                "certificatePolicies: unsupported token `{token}`. \
                 Accepted: `critical`, a dotted OID, `CPS:url`, or `CPS.N:url`"
            )));
        }
    }

    if policies.is_empty() {
        return Err(CryptoError::NotSupported(
            "certificatePolicies: at least one policy OID is required".to_owned(),
        ));
    }

    // Encode SEQUENCE OF PolicyInformation
    let mut policies_content = Vec::new();
    // id-qt-cps OID bytes (1.3.6.1.5.5.7.2.1) – pre-encoded for efficiency
    let cps_oid_der = der_encode_oid("1.3.6.1.5.5.7.2.1")?;

    for (oid_str, cps_urls) in &policies {
        let oid_der = der_encode_oid(oid_str)?;

        let policy_info_inner = if cps_urls.is_empty() {
            oid_der
        } else {
            let mut qualifiers_content = Vec::new();
            for url in cps_urls {
                // PolicyQualifierInfo ::= SEQUENCE { policyQualifierId OID, qualifier IA5String }
                let ia5_der = der_encode_ia5string(url);
                let pqi_inner: Vec<u8> = [cps_oid_der.as_slice(), ia5_der.as_slice()].concat();
                qualifiers_content.extend_from_slice(&der_encode_sequence(&pqi_inner));
            }
            // PolicyInformation ::= SEQUENCE { policyIdentifier OID, policyQualifiers SEQUENCE }
            [
                oid_der.as_slice(),
                der_encode_sequence(&qualifiers_content).as_slice(),
            ]
            .concat()
        };
        policies_content.extend_from_slice(&der_encode_sequence(&policy_info_inner));
    }

    // extnValue OCTET STRING wraps the DER of SEQUENCE OF PolicyInformation
    let ext_value_der = der_encode_sequence(&policies_content);
    let ext_oid = Asn1Object::from_str("2.5.29.32")?;
    let octet_string = Asn1OctetString::new_from_bytes(&ext_value_der)?;
    X509Extension::new_from_der(ext_oid.as_ref(), critical, octet_string.as_ref())
        .map_err(CryptoError::from)
}

/// Returns `true` when `s` looks like a dotted-decimal OID
/// (starts with a digit, contains only ASCII digits and `.`).
fn is_dotted_oid(s: &str) -> bool {
    !s.is_empty()
        && s.starts_with(|c: char| c.is_ascii_digit())
        && s.chars().all(|c| c.is_ascii_digit() || c == '.')
}

/// If `token` is `CPS:url` or `CPS.N:url` (section-style numbering), returns the URL part.
fn strip_cps_qualifier(token: &str) -> Option<&str> {
    let rest = token.strip_prefix("CPS")?;
    // "CPS:url" — direct
    if let Some(url) = rest.strip_prefix(':') {
        return Some(url);
    }
    // "CPS.N:url" — skip the numeric ".N" suffix
    let after_dot = rest.strip_prefix('.')?;
    let colon = after_dot.find(':')?;
    // Verify the part before ':' is all digits (the index N)
    if after_dot[..colon].chars().all(|c| c.is_ascii_digit()) {
        return after_dot.get(colon + 1..);
    }
    None
}

// ── Minimal DER encoding helpers ─────────────────────────────────────────────

/// Encode a DER length field (short-form or two-octet long-form).
///
/// Certificate-extension values are always < 64 KiB. The `if` branches
/// guarantee the casts are always in range; the allows suppress the
/// false-positive truncation and silent-conversion lints.
#[allow(clippy::cast_possible_truncation, clippy::as_conversions)]
fn der_encode_length(len: usize) -> Vec<u8> {
    if len < 128 {
        vec![len as u8]
    } else if len < 256 {
        vec![0x81, len as u8]
    } else {
        vec![0x82, (len >> 8) as u8, (len & 0xFF) as u8]
    }
}

/// Wrap `content` in a DER SEQUENCE (tag `0x30`).
fn der_encode_sequence(content: &[u8]) -> Vec<u8> {
    let mut out = vec![0x30];
    out.extend_from_slice(&der_encode_length(content.len()));
    out.extend_from_slice(content);
    out
}

/// Encode a dotted-decimal OID string as a DER OID TLV (tag `0x06`).
fn der_encode_oid(oid_str: &str) -> Result<Vec<u8>, CryptoError> {
    let components: Vec<u64> = oid_str
        .split('.')
        .map(str::parse::<u64>)
        .collect::<Result<Vec<_>, _>>()
        .map_err(|e| CryptoError::NotSupported(format!("invalid OID `{oid_str}`: {e}")))?;

    // Split off the first two arcs — both are required by the OID grammar.
    let (first, remainder) = components.split_first().ok_or_else(|| {
        CryptoError::NotSupported(format!("OID `{oid_str}` must have at least two components"))
    })?;
    let (second, rest) = remainder.split_first().ok_or_else(|| {
        CryptoError::NotSupported(format!("OID `{oid_str}` must have at least two components"))
    })?;

    let mut content = Vec::new();
    // First two arcs are merged: value = 40 * first + second
    content.extend_from_slice(&der_encode_base128(first * 40 + second));
    for &arc in rest {
        content.extend_from_slice(&der_encode_base128(arc));
    }

    let mut out = vec![0x06]; // OID tag
    out.extend_from_slice(&der_encode_length(content.len()));
    out.extend_from_slice(&content);
    Ok(out)
}

/// Encode `n` in base-128 big-endian (DER OID arc encoding).
/// `n & 0x7F` is always ≤ 127 and fits in `u8`; the allow suppresses the
/// false-positive silent-conversion lint.
#[allow(clippy::cast_possible_truncation, clippy::as_conversions)]
fn der_encode_base128(mut n: u64) -> Vec<u8> {
    if n == 0 {
        return vec![0x00];
    }
    let mut bytes = Vec::new();
    while n > 0 {
        bytes.push((n & 0x7F) as u8);
        n >>= 7;
    }
    bytes.reverse();
    // Set the high bit on every byte except the last
    if let Some((_, all_but_last)) = bytes.split_last_mut() {
        for b in all_but_last {
            *b |= 0x80;
        }
    }
    bytes
}

/// Encode a string as a DER `IA5String` (tag `0x16`).
fn der_encode_ia5string(s: &str) -> Vec<u8> {
    let bytes = s.as_bytes();
    let mut out = vec![0x16]; // IA5String tag
    out.extend_from_slice(&der_encode_length(bytes.len()));
    out.extend_from_slice(bytes);
    out
}

/// Within a value, there can be properties (ie: `email:test@example.com`).
/// This function extracts the property value, by splitting the input `value` (ie: `prop_name:val`) and returning `val`
fn colon_split<'a>(value: &'a str, property_name: &str) -> Result<&'a str, CryptoError> {
    let (_, val) = value
        .split_once(&format!("{property_name}:"))
        .ok_or_else(|| {
            CryptoError::NotSupported(format!(
                "unable to parse `{property_name}` from value: `{value}`"
            ))
        })?;
    Ok(val)
}

#[expect(clippy::unwrap_used)]
#[cfg(test)]
mod tests {
    use cosmian_logger::{info, log_init};
    use ini::Ini;
    use openssl::{
        conf::{Conf, ConfMethod},
        x509::X509,
    };
    use x509_parser::{
        der_parser::oid,
        extensions::{ParsedExtension, X509ExtensionParser},
        nom::Parser as _,
        prelude::*,
    };

    use super::{colon_split, is_dotted_oid, parse_v3_ca_from_str, strip_cps_qualifier};

    #[test]
    fn test_split() {
        let split = colon_split("email:dummy@gmail.com", "email").unwrap();
        assert_eq!(split, "dummy@gmail.com");
        colon_split("email:dummy@gmail.com", "emails").unwrap_err();
    }

    #[test]
    fn test_parse_ext_file() {
        log_init(option_env!("RUST_LOG"));

        let ext_file = r"[ v3_ca ]
basicConstraints=CA:TRUE,pathlen:0
keyUsage=keyCertSign,digitalSignature
extendedKeyUsage=emailProtection
crlDistributionPoints=URI:http://cse.example.com/crl.pem";

        let mut x509_builder = X509::builder().unwrap();
        let x509_context = x509_builder.x509v3_context(None, None);

        let parsed_exts = parse_v3_ca_from_str(ext_file, &x509_context).unwrap();
        assert_eq!(parsed_exts.len(), 4);

        let parsed_exts_der = parsed_exts
            .iter()
            .map(|x| x.to_der().unwrap())
            .collect::<Vec<_>>();

        let exts_with_x509_parser = parsed_exts_der
            .iter()
            .map(|x| X509ExtensionParser::new().parse(x).unwrap().1)
            .collect::<Vec<_>>();

        parsed_exts
            .into_iter()
            .try_for_each(|extension| x509_builder.append_extension(extension))
            .unwrap();

        let x509 = x509_builder.build();
        let crl_distribution_point = x509.as_ref().crl_distribution_points().unwrap();
        let stack = crl_distribution_point
            .iter()
            .next()
            .unwrap()
            .distpoint()
            .unwrap()
            .fullname()
            .unwrap();
        assert_eq!(
            stack.get(0).unwrap().uri(),
            Some("http://cse.example.com/crl.pem")
        );

        let cert_as_txt = x509.as_ref().to_text().unwrap();
        let cert = String::from_utf8_lossy(&cert_as_txt);

        let cert_ = r"            X509v3 Basic Constraints: 
                CA:TRUE, pathlen:0
            X509v3 Key Usage: 
                Digital Signature, Certificate Sign
            X509v3 Extended Key Usage: 
                E-mail Protection
            X509v3 CRL Distribution Points: 
                Full Name:
                  URI:http://cse.example.com/crl.pem
    Signature Algorithm: NULL
    Signature Value:

";
        assert_eq!(
            cert.split_once("X509v3 extensions:\n")
                .unwrap()
                .1
                .replace('\n', ""),
            cert_.replace('\n', "")
        );

        for ext in &exts_with_x509_parser {
            info!("\n\next: {:?}", ext);
            info!("value is: {:?}", String::from_utf8(ext.value.to_vec()));
        }

        // BasicConstraints
        let bc = exts_with_x509_parser
            .iter()
            .find(|x| x.oid == oid!(2.5.29.19))
            .unwrap();
        assert!(!bc.critical);
        assert_eq!(
            bc.parsed_extension(),
            &ParsedExtension::BasicConstraints(BasicConstraints {
                ca: true,
                path_len_constraint: Some(0)
            })
        );

        // KeyUsage
        let ku: &X509Extension<'_> = exts_with_x509_parser
            .iter()
            .find(|x| x.oid == oid!(2.5.29.15))
            .unwrap();
        assert!(!ku.critical);
        assert_eq!(
            ku.parsed_extension(),
            &ParsedExtension::KeyUsage(KeyUsage { flags: 33 })
        );

        // ExtendedKeyUsage
        let eku: &X509Extension<'_> = exts_with_x509_parser
            .iter()
            .find(|x| x.oid == oid!(2.5.29.37))
            .unwrap();
        assert!(!eku.critical);
        assert_eq!(
            eku.parsed_extension(),
            &ParsedExtension::ExtendedKeyUsage(ExtendedKeyUsage {
                any: false,
                server_auth: false,
                client_auth: false,
                code_signing: false,
                email_protection: true,
                time_stamping: false,
                ocsp_signing: false,
                other: vec![]
            })
        );

        // CRLDistributionPoints
        let crl_dp: &X509Extension<'_> = exts_with_x509_parser
            .iter()
            .find(|x| x.oid == oid!(2.5.29.31))
            .unwrap();
        assert!(!crl_dp.critical);
        assert_eq!(
            crl_dp.parsed_extension(),
            &ParsedExtension::CRLDistributionPoints(CRLDistributionPoints {
                points: vec![CRLDistributionPoint {
                    distribution_point: Some(DistributionPointName::FullName(vec![
                        GeneralName::URI("http://cse.example.com/crl.pem")
                    ])),
                    reasons: None,
                    crl_issuer: None
                }]
            })
        );
    }

    /// Following documentation, these are used extensions by Gmail:
    /// - Key Usage (required, critical)
    /// - Extended Key Usage (required, either)
    /// - Basic Constraints (required, critical)
    /// - Certificate Policies (optional)
    /// - CRL Distribution Points (required)
    ///
    /// see: <https://support.google.com/a/answer/7300887?fl=1&sjid=2466928410660190479-NA#zippy=%2Croot-ca%2Cintermediate-ca-certificates-other-than-from-issuing-intermediate-ca%2Cintermediate-ca-certificate-that-issues-the-end-entity>
    #[test]
    fn test_parse_extensions_gmail() {
        log_init(option_env!("RUST_LOG"));

        let ext_file = r"[ v3_ca ]
basicConstraints=critical,CA:TRUE,pathlen:0
keyUsage=critical,keyCertSign,digitalSignature
extendedKeyUsage=emailProtection
crlDistributionPoints=URI:http://cse.example.com/crl.pem
certificatePolicies=2.5.29.32
";

        let conf = Conf::new(ConfMethod::default()).unwrap();
        let mut x509_builder = X509::builder().unwrap();
        let x509_context = x509_builder.x509v3_context(None, Some(conf.as_ref()));

        let parsed_exts = parse_v3_ca_from_str(ext_file, &x509_context).unwrap();
        assert_eq!(parsed_exts.len(), 5);

        let parsed_exts_der = parsed_exts
            .iter()
            .map(|x| x.to_der().unwrap())
            .collect::<Vec<_>>();

        let exts_with_x509_parser = parsed_exts_der
            .iter()
            .map(|x| X509ExtensionParser::new().parse(x).unwrap().1)
            .collect::<Vec<_>>();

        parsed_exts
            .into_iter()
            .try_for_each(|extension| x509_builder.append_extension(extension))
            .unwrap();

        for ext in &exts_with_x509_parser {
            info!("\n\next: {:?}", ext);
            info!("value is: {:?}", String::from_utf8(ext.value.to_vec()));
        }

        // BasicConstraints
        let bc = exts_with_x509_parser
            .iter()
            .find(|x| x.oid == oid!(2.5.29.19))
            .unwrap();
        assert!(bc.critical);
        assert_eq!(
            bc.parsed_extension(),
            &ParsedExtension::BasicConstraints(BasicConstraints {
                ca: true,
                path_len_constraint: Some(0)
            })
        );

        // KeyUsage
        let ku: &X509Extension<'_> = exts_with_x509_parser
            .iter()
            .find(|x| x.oid == oid!(2.5.29.15))
            .unwrap();
        assert!(ku.critical);
        assert_eq!(
            ku.parsed_extension(),
            &ParsedExtension::KeyUsage(KeyUsage { flags: 33 })
        );

        // ExtendedKeyUsage
        let eku: &X509Extension<'_> = exts_with_x509_parser
            .iter()
            .find(|x| x.oid == oid!(2.5.29.37))
            .unwrap();
        assert!(!eku.critical);
        assert_eq!(
            eku.parsed_extension(),
            &ParsedExtension::ExtendedKeyUsage(ExtendedKeyUsage {
                any: false,
                server_auth: false,
                client_auth: false,
                code_signing: false,
                email_protection: true,
                time_stamping: false,
                ocsp_signing: false,
                other: vec![]
            })
        );

        // CRLDistributionPoints
        let crl_dp: &X509Extension<'_> = exts_with_x509_parser
            .iter()
            .find(|x| x.oid == oid!(2.5.29.31))
            .unwrap();
        assert!(!crl_dp.critical);
        assert_eq!(
            crl_dp.parsed_extension(),
            &ParsedExtension::CRLDistributionPoints(CRLDistributionPoints {
                points: vec![CRLDistributionPoint {
                    distribution_point: Some(DistributionPointName::FullName(vec![
                        GeneralName::URI("http://cse.example.com/crl.pem")
                    ])),
                    reasons: None,
                    crl_issuer: None
                }]
            })
        );

        // CertificatePolicies
        let cert_policies: &X509Extension<'_> = exts_with_x509_parser
            .iter()
            .find(|x| x.oid == oid!(2.5.29.32))
            .unwrap();
        assert!(!cert_policies.critical);
        assert_eq!(
            cert_policies.parsed_extension(),
            &ParsedExtension::CertificatePolicies(vec![PolicyInformation {
                policy_id: oid!(2.5.29.32),
                policy_qualifiers: None
            }])
        );
    }

    /// Non-regression: the "OK" CNF (without CPS qualifier) must parse successfully.
    #[test]
    fn test_certificate_policies_cnf_ok() {
        log_init(option_env!("RUST_LOG"));

        let cnf_path = concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/../../.mise/scripts/test/certificatePolicies/v3_ca_ok.cnf"
        );
        let cnf_content = std::fs::read_to_string(cnf_path).unwrap();

        let builder = X509::builder().unwrap();
        let ctx = builder.x509v3_context(None, None);
        let result = parse_v3_ca_from_str(&cnf_content, &ctx);
        assert!(
            result.is_ok(),
            "v3_ca_ok.cnf should parse successfully, got: {:?}",
            result.err()
        );
        // Should produce 3 extensions: basicConstraints, subjectKeyIdentifier, keyUsage
        assert_eq!(result.unwrap().len(), 3);
    }

    /// Non-regression: the "KO" CNF (with CPS.1:url qualifier) now SUCCEEDS after the fix.
    ///
    /// Before the fix this test asserted `result.is_err()` (the old code used
    /// `X509Extension::new_nid` which requires an OpenSSL config database).
    /// After the fix (`build_certificate_policies_extension` native DER encoder),
    /// parsing succeeds — proving the fix works.
    #[test]
    fn test_certificate_policies_cnf_ko_now_succeeds_with_fix() {
        log_init(option_env!("RUST_LOG"));

        let cnf_path = concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/../../.mise/scripts/test/certificatePolicies/v3_ca_ko.cnf"
        );
        let cnf_content = std::fs::read_to_string(cnf_path).unwrap();

        let builder = X509::builder().unwrap();
        let ctx = builder.x509v3_context(None, None);
        let result = parse_v3_ca_from_str(&cnf_content, &ctx);

        // NEW BEHAVIOR (after fix): parsing succeeds
        assert!(
            result.is_ok(),
            "v3_ca_ko.cnf should now SUCCEED with the fix, got: {:?}",
            result.err()
        );
        // Should produce 4 extensions: basicConstraints, subjectKeyIdentifier, keyUsage, certificatePolicies
        assert_eq!(result.unwrap().len(), 4);
    }

    /// Verify that `certificatePolicies` with a CPS qualifier (both `CPS:url` and the
    /// section-numbering variant `CPS.N:url`) is correctly parsed and encoded **without**
    /// an OpenSSL config database — which is the real-world scenario in the KMS server.
    ///
    /// Regression test for the customer bug where
    /// `certificatePolicies=1.3.6.1.4.1.16376.9.1.1.1.0,CPS.1:http://...` triggered
    /// `error:11000088:X509 V3 routines:do_ext_nconf:no config database`.
    #[test]
    #[allow(
        clippy::items_after_statements,
        clippy::panic,
        clippy::indexing_slicing
    )]
    fn test_certificate_policies_with_cps_qualifier() {
        const CPS_URL: &str = "http://pkipubpc.cnp.recouv/pc_acoss_reseau_des_urssaf_v4.pdf";
        const POLICY_OID: &str = "1.3.6.1.4.1.16376.9.1.1.1.0";
        const ID_QT_CPS: &str = "1.3.6.1.5.5.7.2.1";

        log_init(option_env!("RUST_LOG"));

        // — Test 1: bare OID, no conf database (the context has no conf).
        let ext_bare = format!("[ v3_ca ]\ncertificatePolicies={POLICY_OID}\n");
        let builder = X509::builder().unwrap();
        // Deliberately no Conf — mirrors the production path in apply_user_extensions.
        let ctx = builder.x509v3_context(None, None);
        let exts = parse_v3_ca_from_str(&ext_bare, &ctx).unwrap();
        assert_eq!(exts.len(), 1);
        let der = exts[0].to_der().unwrap();
        let (_, parsed) = X509ExtensionParser::new().parse(&der).unwrap();
        assert!(!parsed.critical);
        assert!(
            matches!(
                parsed.parsed_extension(),
                ParsedExtension::CertificatePolicies(_)
            ),
            "expected CertificatePolicies, got {:?}",
            parsed.parsed_extension()
        );

        // — Test 2: CPS.N:url (section-numbering variant used by the customer PKI).
        let ext_cps_numbered =
            format!("[ v3_ca ]\ncertificatePolicies={POLICY_OID},CPS.1:{CPS_URL}\n");
        let ctx2 = builder.x509v3_context(None, None);
        let exts2 = parse_v3_ca_from_str(&ext_cps_numbered, &ctx2).unwrap();
        assert_eq!(exts2.len(), 1);
        let der2 = exts2[0].to_der().unwrap();
        let (_, parsed2) = X509ExtensionParser::new().parse(&der2).unwrap();
        assert!(!parsed2.critical);
        let policies2 = match parsed2.parsed_extension() {
            ParsedExtension::CertificatePolicies(p) => p,
            other => panic!("expected CertificatePolicies, got {other:?}"),
        };
        assert_eq!(policies2.len(), 1);
        assert_eq!(policies2[0].policy_id.to_id_string(), POLICY_OID);
        let qualifiers2 = policies2[0].policy_qualifiers.as_ref().unwrap();
        assert_eq!(qualifiers2.len(), 1);
        // OID 1.3.6.1.5.5.7.2.1 = id-qt-cps
        assert_eq!(qualifiers2[0].policy_qualifier_id.to_id_string(), ID_QT_CPS);
        // qualifier bytes are DER IA5String (0x16 len bytes): verify URL is embedded
        let q2 = qualifiers2[0].qualifier;
        assert!(q2.len() >= 2 && q2[0] == 0x16, "expected IA5String tag");
        let url2 = std::str::from_utf8(&q2[2..]).unwrap();
        assert_eq!(url2, CPS_URL);

        // — Test 3: plain CPS:url (without numbered suffix).
        let ext_cps_plain = format!("[ v3_ca ]\ncertificatePolicies={POLICY_OID},CPS:{CPS_URL}\n");
        let ctx3 = builder.x509v3_context(None, None);
        let exts3 = parse_v3_ca_from_str(&ext_cps_plain, &ctx3).unwrap();
        assert_eq!(exts3.len(), 1);
        let der3 = exts3[0].to_der().unwrap();
        let (_, parsed3) = X509ExtensionParser::new().parse(&der3).unwrap();
        let policies3 = match parsed3.parsed_extension() {
            ParsedExtension::CertificatePolicies(p) => p,
            other => panic!("expected CertificatePolicies, got {other:?}"),
        };
        let qualifiers3 = policies3[0].policy_qualifiers.as_ref().unwrap();
        let q3 = qualifiers3[0].qualifier;
        assert!(q3.len() >= 2 && q3[0] == 0x16, "expected IA5String tag");
        let url3 = std::str::from_utf8(&q3[2..]).unwrap();
        assert_eq!(url3, CPS_URL, "CPS URL mismatch in plain CPS: form");

        // — Test 4: critical flag.
        let ext_critical =
            format!("[ v3_ca ]\ncertificatePolicies=critical,{POLICY_OID},CPS:{CPS_URL}\n");
        let ctx4 = builder.x509v3_context(None, None);
        let exts4 = parse_v3_ca_from_str(&ext_critical, &ctx4).unwrap();
        let der4 = exts4[0].to_der().unwrap();
        let (_, parsed4) = X509ExtensionParser::new().parse(&der4).unwrap();
        assert!(parsed4.critical, "expected critical extension");
    }

    #[test]
    fn test_is_dotted_oid() {
        assert!(is_dotted_oid("1.3.6.1.4.1.16376.9.1.1.1.0"));
        assert!(is_dotted_oid("2.5.29.32"));
        assert!(!is_dotted_oid("critical"));
        assert!(!is_dotted_oid("CPS:url"));
        assert!(!is_dotted_oid(""));
    }

    #[test]
    fn test_strip_cps_qualifier() {
        assert_eq!(
            strip_cps_qualifier("CPS:http://example.com"),
            Some("http://example.com")
        );
        assert_eq!(
            strip_cps_qualifier("CPS.1:http://example.com"),
            Some("http://example.com")
        );
        assert_eq!(
            strip_cps_qualifier("CPS.42:http://example.com"),
            Some("http://example.com")
        );
        assert_eq!(strip_cps_qualifier("critical"), None);
        assert_eq!(strip_cps_qualifier("1.2.3.4"), None);
    }

    /// Demonstrates that the **old** code path — `X509Extension::new_nid` with `None` as
    /// config — fails for `certificatePolicies` values that contain a `CPS.N:url` qualifier.
    ///
    /// This mirrors the customer error:
    ///   `error:11000088:X509 V3 routines:do_ext_nconf:no config database`
    ///
    /// The new implementation (`build_certificate_policies_extension`) replaces this
    /// error-prone call with a hand-crafted DER encoder that requires no OpenSSL config.
    #[test]
    #[allow(deprecated, clippy::expect_used)]
    fn test_old_new_nid_fails_for_cps_syntax() {
        use openssl::{nid::Nid, x509::X509Extension as OpenSslX509Extension};

        // Read the customer's CNF fixture that triggered the production bug.
        let cnf_path = concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/../../.mise/scripts/test/certificatePolicies/v3_ca_ko.cnf"
        );
        let cnf_content =
            std::fs::read_to_string(cnf_path).expect("v3_ca_ko.cnf should be readable");
        let ini = Ini::load_from_str(&cnf_content).expect("v3_ca_ko.cnf should be valid INI");
        let cps_value = ini
            .section(Some("v3_ca"))
            .and_then(|s| s.get("certificatePolicies"))
            .expect("certificatePolicies key missing from [v3_ca]");

        let builder = X509::builder().unwrap();
        // Deliberately no Conf — mirrors the production path in apply_user_extensions.
        let ctx = builder.x509v3_context(None, None);

        // The old API cannot resolve CPS.1:url without a config database; it must return an error.
        let result =
            OpenSslX509Extension::new_nid(None, Some(&ctx), Nid::CERTIFICATE_POLICIES, cps_value);
        assert!(
            result.is_err(),
            "Expected X509Extension::new_nid to fail for CPS.1:url syntax without conf, but it succeeded"
        );
    }
}