Skip to content

Commit 0b79ebf

Browse files
JstatiaCopilot
andcommitted
Add Rust example programs for DID:x509, CWT claims, and certificate trust
Add three new example programs to the native Rust workspace: - did/x509/examples/did_x509_basics.rs: Demonstrates parsing, building, validating, and resolving DID:x509 identifiers with ephemeral CA + leaf certificate chains created via rcgen. - signing/headers/examples/cwt_claims_basics.rs: Shows CWT claims builder fluent API, CBOR serialization/deserialization roundtrip, header label constants, and minimal SCITT claims patterns. - extension_packs/certificates/examples/certificate_trust_validation.rs: Demonstrates X.509 certificate trust validation pipeline including COSE_Sign1 message construction with x5chain header, trust pack configuration, and custom trust plan building with fluent extensions. All examples compile and run successfully. Each uses only existing dev-dependencies (rcgen, hex, sha2) and produces visible stdout output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8f3e6a8 commit 0b79ebf

File tree

6 files changed

+416
-2
lines changed

6 files changed

+416
-2
lines changed

native/rust/did/x509/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,8 @@ hex = "0.4"
2020
sha2.workspace = true
2121
openssl = { workspace = true }
2222

23-
[lints.rust]
23+
[[example]]
24+
name = "did_x509_basics"
25+
26+
[lints.rust]
2427
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage,coverage_nightly)'] }
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//! DID:x509 basics — parse, build, validate, and resolve workflows.
5+
//!
6+
//! Run with:
7+
//! cargo run --example did_x509_basics -p did_x509
8+
9+
use std::borrow::Cow;
10+
11+
use did_x509::{
12+
DidX509Builder, DidX509Parser, DidX509Policy, DidX509Resolver, DidX509Validator, SanType,
13+
};
14+
use rcgen::{BasicConstraints, CertificateParams, IsCa, Issuer, KeyPair};
15+
use sha2::{Digest, Sha256};
16+
17+
fn main() {
18+
// ── 1. Generate an ephemeral CA + leaf certificate chain ──────────
19+
println!("=== Step 1: Create ephemeral certificate chain ===\n");
20+
21+
let (ca_der, leaf_der) = create_cert_chain();
22+
let chain: Vec<&[u8]> = vec![leaf_der.as_slice(), ca_der.as_slice()];
23+
24+
let ca_thumbprint = hex::encode(Sha256::digest(&ca_der));
25+
println!(" CA thumbprint (SHA-256): {}", ca_thumbprint);
26+
27+
// ── 2. Build a DID:x509 identifier from the chain ────────────────
28+
println!("\n=== Step 2: Build DID:x509 identifiers ===\n");
29+
30+
// Build with an EKU policy (code-signing OID 1.3.6.1.5.5.7.3.3)
31+
let eku_policy = DidX509Policy::Eku(vec![Cow::Borrowed("1.3.6.1.5.5.7.3.3")]);
32+
let did_eku = DidX509Builder::build_sha256(&ca_der, &[eku_policy.clone()])
33+
.expect("build EKU DID");
34+
println!(" DID (EKU): {}", did_eku);
35+
36+
// Build with a subject policy
37+
let subject_policy = DidX509Policy::Subject(vec![
38+
("CN".to_string(), "Example Leaf".to_string()),
39+
]);
40+
let did_subject = DidX509Builder::build_sha256(&ca_der, &[subject_policy.clone()])
41+
.expect("build subject DID");
42+
println!(" DID (Subject): {}", did_subject);
43+
44+
// Build with a SAN policy
45+
let san_policy = DidX509Policy::San(SanType::Dns, "leaf.example.com".to_string());
46+
let did_san = DidX509Builder::build_sha256(&ca_der, &[san_policy.clone()])
47+
.expect("build SAN DID");
48+
println!(" DID (SAN): {}", did_san);
49+
50+
// ── 3. Parse DID:x509 identifiers back into components ───────────
51+
println!("\n=== Step 3: Parse DID:x509 identifiers ===\n");
52+
53+
let parsed = DidX509Parser::parse(&did_eku).expect("parse DID");
54+
println!(" Hash algorithm: {}", parsed.hash_algorithm);
55+
println!(" CA fingerprint hex: {}", parsed.ca_fingerprint_hex);
56+
println!(" Has EKU policy: {}", parsed.has_eku_policy());
57+
println!(" Has subject policy: {}", parsed.has_subject_policy());
58+
59+
if let Some(eku_oids) = parsed.get_eku_policy() {
60+
println!(" EKU OIDs: {:?}", eku_oids);
61+
}
62+
63+
// ── 4. Validate DID against the certificate chain ────────────────
64+
println!("\n=== Step 4: Validate DID against certificate chain ===\n");
65+
66+
// Validate the SAN-based DID (leaf cert has SAN: dns:leaf.example.com)
67+
let result = DidX509Validator::validate(&did_san, &chain).expect("validate DID");
68+
println!(" DID (SAN) valid: {}", result.is_valid);
69+
println!(" Matched CA index: {:?}", result.matched_ca_index);
70+
71+
// Validate subject-based DID (leaf cert has CN=Example Leaf)
72+
let result = DidX509Validator::validate(&did_subject, &chain).expect("validate subject DID");
73+
println!(" DID (Subject) valid: {}", result.is_valid);
74+
75+
// Demonstrate a failing validation with a wrong subject
76+
let wrong_subject = DidX509Policy::Subject(vec![
77+
("CN".to_string(), "Wrong Name".to_string()),
78+
]);
79+
let did_wrong = DidX509Builder::build_sha256(&ca_der, &[wrong_subject])
80+
.expect("build wrong DID");
81+
let result = DidX509Validator::validate(&did_wrong, &chain).expect("validate wrong DID");
82+
println!(" DID (wrong CN) valid: {} (expected false)", result.is_valid);
83+
if !result.errors.is_empty() {
84+
println!(" Validation errors: {:?}", result.errors);
85+
}
86+
87+
// ── 5. Resolve DID to a DID Document ─────────────────────────────
88+
println!("\n=== Step 5: Resolve DID to DID Document ===\n");
89+
90+
let doc = DidX509Resolver::resolve(&did_san, &chain).expect("resolve DID");
91+
let doc_json = doc.to_json(true).expect("serialize DID Document");
92+
println!("{}", doc_json);
93+
94+
println!("\n=== All steps completed successfully! ===");
95+
}
96+
97+
/// Create an ephemeral CA and leaf certificate chain using rcgen.
98+
/// Returns (ca_der, leaf_der) — both DER-encoded.
99+
fn create_cert_chain() -> (Vec<u8>, Vec<u8>) {
100+
// CA certificate
101+
let mut ca_params = CertificateParams::new(vec!["Example CA".to_string()]).unwrap();
102+
ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
103+
ca_params
104+
.distinguished_name
105+
.push(rcgen::DnType::CommonName, "Example CA");
106+
ca_params
107+
.distinguished_name
108+
.push(rcgen::DnType::OrganizationName, "Example Org");
109+
110+
let ca_key = KeyPair::generate().unwrap();
111+
let ca_cert = ca_params.self_signed(&ca_key).unwrap();
112+
let ca_der = ca_cert.der().to_vec();
113+
114+
// Create an Issuer from the CA params + key for signing the leaf.
115+
// Note: Issuer::new consumes the params, so we rebuild them.
116+
let mut ca_issuer_params = CertificateParams::new(vec!["Example CA".to_string()]).unwrap();
117+
ca_issuer_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
118+
ca_issuer_params
119+
.distinguished_name
120+
.push(rcgen::DnType::CommonName, "Example CA");
121+
ca_issuer_params
122+
.distinguished_name
123+
.push(rcgen::DnType::OrganizationName, "Example Org");
124+
let ca_issuer = Issuer::new(ca_issuer_params, ca_key);
125+
126+
// Leaf certificate signed by CA
127+
let mut leaf_params = CertificateParams::new(vec!["leaf.example.com".to_string()]).unwrap();
128+
leaf_params.is_ca = IsCa::NoCa;
129+
leaf_params
130+
.distinguished_name
131+
.push(rcgen::DnType::CommonName, "Example Leaf");
132+
leaf_params
133+
.distinguished_name
134+
.push(rcgen::DnType::OrganizationName, "Example Org");
135+
// Add code-signing EKU
136+
leaf_params.extended_key_usages = vec![rcgen::ExtendedKeyUsagePurpose::CodeSigning];
137+
138+
let leaf_key = KeyPair::generate().unwrap();
139+
let leaf_cert = leaf_params.signed_by(&leaf_key, &ca_issuer).unwrap();
140+
let leaf_der = leaf_cert.der().to_vec();
141+
142+
(ca_der, leaf_der)
143+
}

native/rust/extension_packs/certificates/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,8 @@ cbor_primitives_everparse = { path = "../../primitives/cbor/everparse" }
3535
cose_sign1_crypto_openssl = { path = "../../primitives/crypto/openssl" }
3636
openssl = { workspace = true }
3737

38+
[[example]]
39+
name = "certificate_trust_validation"
40+
3841
[lints.rust]
3942
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//! Certificate-based trust validation — create an ephemeral certificate chain,
5+
//! construct a COSE_Sign1 message with an embedded x5chain header, then validate
6+
//! using the X.509 certificate trust pack.
7+
//!
8+
//! Run with:
9+
//! cargo run --example certificate_trust_validation -p cose_sign1_certificates
10+
11+
use std::sync::Arc;
12+
13+
use cbor_primitives::{CborEncoder, CborProvider};
14+
use cbor_primitives_everparse::EverParseCborProvider;
15+
use cose_sign1_certificates::validation::pack::{
16+
CertificateTrustOptions, X509CertificateTrustPack,
17+
};
18+
use cose_sign1_validation::fluent::*;
19+
use cose_sign1_validation_primitives::CoseHeaderLocation;
20+
21+
fn main() {
22+
// ── 1. Generate an ephemeral self-signed certificate ─────────────
23+
println!("=== Step 1: Generate ephemeral certificate ===\n");
24+
25+
let rcgen::CertifiedKey { cert, .. } =
26+
rcgen::generate_simple_self_signed(vec!["example-leaf".to_string()])
27+
.expect("rcgen failed");
28+
let leaf_der = cert.der().to_vec();
29+
println!(" Leaf cert DER size: {} bytes", leaf_der.len());
30+
31+
// ── 2. Build a minimal COSE_Sign1 with x5chain header ───────────
32+
println!("\n=== Step 2: Build COSE_Sign1 with x5chain ===\n");
33+
34+
let payload = b"Hello, COSE world!";
35+
let cose_bytes = build_cose_sign1_with_x5chain(&leaf_der, payload);
36+
println!(" COSE message size: {} bytes", cose_bytes.len());
37+
println!(" Payload: {:?}", std::str::from_utf8(payload).unwrap());
38+
39+
// ── 3. Set up the certificate trust pack ─────────────────────────
40+
println!("\n=== Step 3: Configure certificate trust pack ===\n");
41+
42+
// For this example, treat the embedded x5chain as trusted.
43+
// In production, configure actual trust roots and revocation checks.
44+
let cert_pack = Arc::new(X509CertificateTrustPack::new(CertificateTrustOptions {
45+
trust_embedded_chain_as_trusted: true,
46+
..Default::default()
47+
}));
48+
println!(" Trust pack: embedded x5chain treated as trusted");
49+
50+
let trust_packs: Vec<Arc<dyn CoseSign1TrustPack>> = vec![cert_pack];
51+
52+
// ── 4. Build a validator with bypass trust + signature bypass ─────
53+
// (We bypass the actual crypto check because the COSE message's
54+
// signature is a dummy — in a real scenario the signing service
55+
// would produce a valid signature.)
56+
println!("\n=== Step 4: Validate with trust bypass ===\n");
57+
58+
let validator = CoseSign1Validator::new(trust_packs.clone()).with_options(|o| {
59+
o.certificate_header_location = CoseHeaderLocation::Any;
60+
o.trust_evaluation_options.bypass_trust = true;
61+
});
62+
63+
let result = validator
64+
.validate_bytes(
65+
EverParseCborProvider,
66+
Arc::from(cose_bytes.clone().into_boxed_slice()),
67+
)
68+
.expect("validation pipeline error");
69+
70+
println!(" resolution: {:?}", result.resolution.kind);
71+
println!(" trust: {:?}", result.trust.kind);
72+
println!(" signature: {:?}", result.signature.kind);
73+
println!(" overall: {:?}", result.overall.kind);
74+
75+
// ── 5. Demonstrate custom trust plan ─────────────────────────────
76+
println!("\n=== Step 5: Custom trust plan (advanced) ===\n");
77+
78+
use cose_sign1_certificates::validation::fluent_ext::PrimarySigningKeyScopeRulesExt;
79+
80+
let cert_pack2 = Arc::new(X509CertificateTrustPack::new(CertificateTrustOptions {
81+
trust_embedded_chain_as_trusted: true,
82+
..Default::default()
83+
}));
84+
let packs: Vec<Arc<dyn CoseSign1TrustPack>> = vec![cert_pack2];
85+
86+
let plan = TrustPlanBuilder::new(packs)
87+
.for_primary_signing_key(|key| {
88+
key.require_x509_chain_trusted()
89+
.and()
90+
.require_signing_certificate_present()
91+
})
92+
.compile()
93+
.expect("plan compile");
94+
95+
let validator2 = CoseSign1Validator::new(plan).with_options(|o| {
96+
o.certificate_header_location = CoseHeaderLocation::Any;
97+
});
98+
99+
let result2 = validator2
100+
.validate_bytes(
101+
EverParseCborProvider,
102+
Arc::from(cose_bytes.into_boxed_slice()),
103+
)
104+
.expect("validation pipeline error");
105+
106+
println!(" resolution: {:?}", result2.resolution.kind);
107+
println!(" trust: {:?}", result2.trust.kind);
108+
println!(" signature: {:?}", result2.signature.kind);
109+
println!(" overall: {:?}", result2.overall.kind);
110+
111+
// Print failures if any
112+
let stages = [
113+
("resolution", &result2.resolution),
114+
("trust", &result2.trust),
115+
("signature", &result2.signature),
116+
("overall", &result2.overall),
117+
];
118+
for (name, stage) in stages {
119+
if !stage.failures.is_empty() {
120+
println!("\n {} failures:", name);
121+
for f in &stage.failures {
122+
println!(" - {}", f.message);
123+
}
124+
}
125+
}
126+
127+
println!("\n=== Example completed! ===");
128+
}
129+
130+
/// Build a minimal COSE_Sign1 byte sequence with an embedded x5chain header.
131+
///
132+
/// The message structure is:
133+
/// [protected_headers_bstr, unprotected_headers_map, payload_bstr, signature_bstr]
134+
///
135+
/// Protected headers contain:
136+
/// { 1 (alg): -7 (ES256), 33 (x5chain): bstr(cert_der) }
137+
fn build_cose_sign1_with_x5chain(leaf_der: &[u8], payload: &[u8]) -> Vec<u8> {
138+
let p = EverParseCborProvider;
139+
let mut enc = p.encoder();
140+
141+
// COSE_Sign1 is a 4-element CBOR array
142+
enc.encode_array(4).unwrap();
143+
144+
// Protected headers: CBOR bstr wrapping a CBOR map
145+
let mut hdr_enc = p.encoder();
146+
hdr_enc.encode_map(2).unwrap();
147+
hdr_enc.encode_i64(1).unwrap(); // label: alg
148+
hdr_enc.encode_i64(-7).unwrap(); // value: ES256
149+
hdr_enc.encode_i64(33).unwrap(); // label: x5chain
150+
hdr_enc.encode_bstr(leaf_der).unwrap();
151+
let protected_bytes = hdr_enc.into_bytes();
152+
enc.encode_bstr(&protected_bytes).unwrap();
153+
154+
// Unprotected headers: empty map
155+
enc.encode_map(0).unwrap();
156+
157+
// Payload: embedded byte string
158+
enc.encode_bstr(payload).unwrap();
159+
160+
// Signature: dummy (not cryptographically valid)
161+
enc.encode_bstr(b"example-signature-placeholder").unwrap();
162+
163+
enc.into_bytes()
164+
}

native/rust/signing/headers/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,8 @@ did_x509 = { path = "../../did/x509" }
1717
[dev-dependencies]
1818
cbor_primitives_everparse = { path = "../../primitives/cbor/everparse" }
1919

20-
[lints.rust]
20+
[[example]]
21+
name = "cwt_claims_basics"
22+
23+
[lints.rust]
2124
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage,coverage_nightly)'] }

0 commit comments

Comments
 (0)