Skip to content

feat(corim): add x5chain trust verification with PKIX and CRL support#276

Merged
yogeshbdeshpande merged 1 commit into
veraison:mainfrom
magickli1:feat/x5chain-trust-verify
Jun 30, 2026
Merged

feat(corim): add x5chain trust verification with PKIX and CRL support#276
yogeshbdeshpande merged 1 commit into
veraison:mainfrom
magickli1:feat/x5chain-trust-verify

Conversation

@magickli1

@magickli1 magickli1 commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds library support to verify CoRIMs signed with a COSE Sign1 x5chain header: core PKIX path validation per RFC 5280 §6 (signature chain, validity period, name & basic constraints, path-length), optional post-PKIX CRL revocation checks, then COSE signature verification with the PKIX-validated leaf public key.

Default CRL policy: strict. When TrustAnchors.CRLs is non-empty, verification uses CrlPolicyStrict by default (zero value; equivalent to OpenSSL CRL_CHECK_ALL). Callers must explicitly set CrlPolicyPermissive to skip in-chain issuers with no matching CRL. cocli: --crl-policy strict (default) or permissive.

This is not a full §6 implementation — certificate-policy processing is out of scope (see below).

Closes the gap for embedders that today must extract the leaf from x5chain and call SignedCorim.Verify(pk) manually. CLI exposure is tracked in cocli#59.

Motivation

Today callers must extract the leaf certificate from x5chain and call SignedCorim.Verify(pk) manually. This PR provides an end-to-end trust path for embedders that load trust material from files or construct a TrustAnchors value in memory.

New API

Symbol Purpose
TrustAnchors Trust anchors (*x509.CertPool), optional CRLs, CrlPolicy, optional CurrentTime (zero → time.Now())
CrlPolicy / CrlPolicyStrict / CrlPolicyPermissive CRL behaviour when CRLs is non-empty (default: strict)
LoadTrustAnchors(readFile, trustAnchorPaths, crlPaths) Load trust anchors and CRLs from DER or PEM files into TrustAnchors
SignedCorim.VerifyWithX5Chain(anchors) Leaf policy → PKIX → optional CRL → COSE verify

Naming aligns with COSE x5chain terminology and pairs with Verify(pk) (external key, no PKIX). LoadTrustAnchors uses a Load* name for file I/O and matches cocli --trust-anchors terminology.

Call SignedCorim.FromCOSE first. Key-based verification without PKIX remains SignedCorim.Verify.

Trust-anchor semantics

Aligned with maintainer feedback on cocli#59:

Input TrustAnchors.Pool Behaviour at verify time
trustAnchorPaths empty nil OS trust store
trustAnchorPaths non-empty non-nil pool with loaded anchors Only user-supplied anchors (override; no merge with system roots)

Callers may also construct TrustAnchors in memory (e.g. an empty non-nil pool for explicit-trust-only tests).

LoadTrustAnchors details: PEM trust-anchor bundles via x509.CertPool.AppendCertsFromPEM; PEM CRL files may contain multiple blocks; duplicate DER anchors across trustAnchorPaths are added once (PEM bundles rely on CertPool dedup). Load/parse errors use trust anchor wording (e.g. loading trust anchor from %s).

CRL policy

Default: CrlPolicyStrict (strict mode). No field assignment or CLI flag is required — the zero value and cocli default are both strict. Use CrlPolicyPermissive / --crl-policy=permissive only when partial CRL coverage is intentional.

Revocation is opt-in at the API level (CRLs empty → skip all revocation checks). When CRL material is supplied, behaviour is governed by CrlPolicy (zero value = CrlPolicyStrict):

TrustAnchors.CRLs CrlPolicy Behaviour
Empty (any) Skip all revocation checks
Non-empty CrlPolicyStrict (default) Post-PKIX checks equivalent to OpenSSL CRL_CHECK_ALL: for each (certificate, issuer) on the verified chain (excluding the trust anchor at the chain end), require at least one valid CRL in CRLs signed by that issuer; missing issuer CRL → fail (unable to get certificate CRL)
Non-empty CrlPolicyPermissive Same as strict when matching CRLs exist; skip revocation for in-chain issuers with no matching CRL

Shared rules (both policies, when CRLs is non-empty):

  • CRLs from issuers outside the verified chain are ignored
  • Matching CRLs exist but all invalid (expired / not yet valid) → fail
  • Multiple valid CRLs per issuer: check all; fail if serial appears on any valid CRL
  • Revoked serial on a valid matching CRL → fail

Strict vs permissive (quick reference)

Situation Strict Permissive
CRLs empty Skip revocation Skip revocation
In-chain issuer has no matching CRL Fail Skip that issuer
In-chain issuer has valid matching CRL, cert not revoked Pass Pass
Serial on valid matching CRL Fail Fail
Matching CRLs all expired / not yet valid Fail Fail

Verification order

  1. Require decoded COSE Sign1 message (FromCOSE); else no Sign1 message found
  2. Require x5chain header; else x5chain: header not set in CoRIM
  3. Leaf signing-certificate policy (validateLeafSigningCert): non-CA; digitalSignature when KeyUsage is present (certs without KeyUsage pass)
  4. PKIX path validation with ExtKeyUsageAny against anchors.Pool (system store when Pool == nil)
  5. Post-PKIX CRL checks when anchors.CRLs is non-empty (checkChainRevocation, per CrlPolicy above)
  6. COSE signature verification with the PKIX-validated leaf public key (not COSE kid)

When PKIX returns multiple valid chains, selectVerifiedChain picks the path with the most DER overlap with the presented x5chain (ties → first best-scoring chain).

PKIX errors

PKIX failures are wrapped as x5chain verification failed: %w, preserving standard library types (e.g. x509.UnknownAuthorityError). Callers should use errors.As rather than matching custom anchor-hint strings.

COSE signature failures after successful PKIX are wrapped as x5chain: COSE signature verification failed.

Usage example

signed := corim.NewSignedCorim()
if err := signed.FromCOSE(cbor); err != nil {
    // decode failed
}

anchors, err := corim.LoadTrustAnchors(os.ReadFile,
    []string{"path/to/trust-anchor.der"},
    []string{"path/to/issuer.crl"}, // optional; omit for no revocation
)
if err != nil {
    // failed to load trust material
}

// Default: CrlPolicyStrict (zero value)
// anchors.CrlPolicy = corim.CrlPolicyPermissive // optional override

if err := signed.VerifyWithX5Chain(anchors); err != nil {
    // PKIX, CRL, or COSE verify failed
    var unknownAuthority x509.UnknownAuthorityError
    if errors.As(err, &unknownAuthority) {
        // chain not anchored to a trusted root
    }
}

// System trust store: pass empty trustAnchorPaths
systemAnchors, err := corim.LoadTrustAnchors(os.ReadFile, nil, nil)
Scenario API / CLI
External public key signed.Verify(publicKey)
x5chain + explicit anchors LoadTrustAnchors(..., []string{...}, crlPaths) + VerifyWithX5Chain
x5chain + system anchors LoadTrustAnchors(..., nil, crlPaths)Pool == nil
Strict CRL (default) Non-empty CRLs, default / CrlPolicyStrict
Permissive CRL override anchors.CrlPolicy = corim.CrlPolicyPermissive
cocli (follow-up) cocli corim verify --file signed.cbor --trust-anchors anchor.der --crl issuer.crl --crl-policy strict|permissivecocli#59

Other changes

FromCOSE / extractX5Chain fixes

  • FromCOSE clears SigningCert / IntermediateCerts before decode; on any failure (including payload validation after headers parse), clears message, SigningCert, and IntermediateCerts
  • extractX5Chain assigns parsed certs directly (no longer via AddSigningCert / AddIntermediateCerts), so parse errors do not mutate object state
  • Direct extractX5Chain calls still preserve existing fields on parse failure
  • Clears stale certs when the header is absent or the chain shrinks on re-parse

Documentation & fixtures

  • Package examples (pkg.go.dev): ExampleSignedCorim_VerifyWithX5Chain, ExampleLoadTrustAnchors (x5chainExampleMeta helper avoids example name clashes)

  • Godoc on TrustAnchors, CrlPolicy, LoadTrustAnchors, and VerifyWithX5Chain

  • No root README.md change (usage in examples + godoc)

  • scripts/gen-certs.sh: PKIX extensions for test CA chain; regenerate command

  • Re-issued testdata/*.der (keys unchanged; verify needs .der only; .key for test signing):

    FORCE=1 make test-certs
    # or: scripts/gen-certs.sh regenerate

Out of scope

  • Certificate-policy processing (RFC 5280 §6 policy machinery: certificate policies, policy mappings, policy constraints, inhibit anyPolicy, explicit-policy). Go's crypto/x509 does not implement policy validation and neither does this PR.
  • OCSP / AIA fetching
  • kid vs x5chain leaf cross-check (COSE treats kid as a hint; use Verify for external-key verify)
  • IsCA check when loading trust files from disk
  • Exported chain-only verify without trust (Go exposes trust+signature via VerifyWithX5Chain only)
  • cocli wiring in this PR (cocli#59 follow-up exposes --trust-anchors, --crl, --crl-policy strict|permissive)

Revocation caveat: With CRLs empty, revocation is not checked. With CRLs non-empty and CrlPolicyPermissive, issuers without a matching CRL are skipped — both deviate from RFC 5280 §6.1.3(3)/§6.3 expectations for full revocation status determination. CrlPolicyStrict is the safer default when CRLs are supplied.

Test plan

  • make presubmit passes (coverage ≥84.4%, lint clean)
  • x509chain_test.go: VerifyWithX5Chain, LoadTrustAnchors (DER/PEM anchors and CRLs); helpers validateLeafSigningCert, checkChainRevocation, selectVerifiedChain
  • Happy path, untrusted anchor (explicit empty pool + errors.As for UnknownAuthorityError), wrong anchor via LoadTrustAnchors, expiry, revocation (leaf/intermediate), tampered payload, signing-key mismatch, CRL not-yet-valid
  • CrlPolicyStrict: missing issuer CRL fails
  • CrlPolicyPermissive: missing issuer CRL passes; revoked / all matching CRLs expired still fail
  • LoadTrustAnchors with empty trustAnchorPathsPool nil (system store at verify time); wrongTrustAnchorFails, emptyPathsUsesSystemStore
  • signedcorim_test.go: FromCOSE idempotent / shrink / absent header / stale intermediates / payload failure clears x5chain and message / extractX5Chain preserves state on error
  • VerifyWithX5Chain returns no Sign1 message found when FromCOSE was not called

@magickli1 magickli1 force-pushed the feat/x5chain-trust-verify branch 2 times, most recently from ac7a6c7 to ca7fa70 Compare June 23, 2026 02:21
@magickli1 magickli1 marked this pull request as ready for review June 23, 2026 02:21
@magickli1 magickli1 force-pushed the feat/x5chain-trust-verify branch 3 times, most recently from cb84ffc to 980f78f Compare June 24, 2026 06:44

@thomas-fossati thomas-fossati left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A most impressive contribution, I must say.

🚢 it!

@magickli1

magickli1 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Hi @setrofim @yogeshbdeshpande @DhanusML , this PR is ready for review,all checks are passing!
When you have a moment, please take a look and, if everything looks good, help merge it. Thanks!

@magickli1

Copy link
Copy Markdown
Contributor Author

Hi @thomas-fossati quick question on CRL policy for VerifyWithX5Chain:
When anchors.CRLs is non-empty, should an in-chain issuer with no matching (signature-valid) CRL be skipped (current fail-open behaviour), or should verification fail (closer to OpenSSL CRL_CHECK_ALL)?
Thanks!

@thomas-fossati

Copy link
Copy Markdown
Contributor

Hi @thomas-fossati quick question on CRL policy for VerifyWithX5Chain: When anchors.CRLs is non-empty, should an in-chain issuer with no matching (signature-valid) CRL be skipped (current fail-open behaviour), or should verification fail (closer to OpenSSL CRL_CHECK_ALL)? Thanks!

This is a policy decision that the stack should not make. Instead, we should set and document a reasonable default (fail-close) and allow users to override the behaviour.

@magickli1

magickli1 commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

Hi @thomas-fossati quick question on CRL policy for VerifyWithX5Chain: When anchors.CRLs is non-empty, should an in-chain issuer with no matching (signature-valid) CRL be skipped (current fail-open behaviour), or should verification fail (closer to OpenSSL CRL_CHECK_ALL)? Thanks!

This is a policy decision that the stack should not make. Instead, we should set and document a reasonable default (fail-close) and allow users to override the behaviour.

Thanks @thomas-fossati for the CRL-policy guidance — I've updated the implementation accordingly.

Default: strict mode (CrlPolicyStrict, zero value). When TrustAnchors.CRLs is non-empty, post-PKIX revocation follows OpenSSL CRL_CHECK_ALL semantics: every in-chain issuer must have at least one valid matching CRL. Callers who accept partial CRL coverage can explicitly opt into permissive mode via CrlPolicyPermissive

Strict vs permissive

Situation Strict (default) Permissive
CRLs empty Skip revocation Skip revocation
In-chain issuer has no matching CRL Fail Skip that issuer
Valid matching CRL, cert not revoked Pass Pass
Serial listed on a valid matching CRL Fail Fail
All matching CRLs expired / not yet valid Fail Fail

The only behavioural difference is the missing-issuer-CRL case: strict fails closed; permissive skips. Revoked serials and invalid CRLs still fail in both modes.

API

anchors, err := corim.LoadTrustAnchors(readFile, trustAnchorPaths, crlPaths)
// Default: anchors.CrlPolicy == corim.CrlPolicyStrict (zero value)
// anchors.CrlPolicy = corim.CrlPolicyPermissive // explicit override
err = signed.VerifyWithX5Chain(anchors)

Happy to adjust naming or defaults if you prefer something else.

@magickli1 magickli1 force-pushed the feat/x5chain-trust-verify branch 3 times, most recently from 305ebed to 566f74d Compare June 27, 2026 10:54
@thomas-fossati

Copy link
Copy Markdown
Contributor

Thanks, @magickli1! The update looks good to me. You have a linter check that has failed in corim/x509chain_test.go. You can probably fix it by passing a pointer to testPKI.

@magickli1 magickli1 force-pushed the feat/x5chain-trust-verify branch from 566f74d to 4c71ad9 Compare June 27, 2026 11:00
@magickli1

magickli1 commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, @magickli1! The update looks good to me. You have a linter check that has failed in corim/x509chain_test.go. You can probably fix it by passing a pointer to testPKI.

Sorry about that — I should have checked earlier. I've fixed it .

@magickli1

Copy link
Copy Markdown
Contributor Author

@thomas-fossati Could you help me to ping the other reviewers? I'll follow up with the cocli PR once this merges.

@yogeshbdeshpande

Copy link
Copy Markdown
Contributor

@magickli1 Thank you for the PR. I will review it by the end of the day today and provide you the review feedback!

magickli1 added a commit to magickli1/corim-rs that referenced this pull request Jun 29, 2026
CoRIM-rs already stored COSE x5chain headers but had no PKIX trust path.
Add end-to-end verification behind `feature = "openssl"`, aligned with
veraison/corim#276 (TrustAnchors, CRL strict/permissive, Go parity).

API: TrustAnchors, CrlPolicy, load_trust_anchors, verify_x5chain_ders,
SignedCorim::verify_with_x5chain. Order: leaf signing policy → PKIX
(explicit anchors or OS store; no merge) → optional CRL when material is
supplied → COSE verify via validated leaf key (no signature-validity).

Includes openssl X.509 helpers, CorimError variants, test PKI fixtures,
and inline tests. Default features unchanged (`default = []`).

Signed-off-by: magickli <lq1029901708@gmail.com>
Comment thread corim/signedcorim.go Outdated
Comment thread corim/signedcorim.go Outdated

@yogeshbdeshpande yogeshbdeshpande left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

started reviewing it now

Comment thread corim/x509chain.go Outdated
Comment thread corim/x509chain.go

@yogeshbdeshpande yogeshbdeshpande left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have left comments based on my review!

Please let me know if you want to discuss this during the Veraison Sync up today at 4PM UK time.

Add VerifyWithX5Chain and LoadTrustAnchors (PKIX + optional CRL, then COSE
verify). Empty CRL set skips revocation; CrlPolicyStrict requires nextUpdate.

Stricter x5chain decode (RFC 9052): reject empty arrays and multi-cert
elements. Leaf keyUsage optional; digitalSignature required when present.
README documents the new API.

Signed-off-by: magickli <lq1029901708@gmail.com>
@magickli1 magickli1 force-pushed the feat/x5chain-trust-verify branch from 4c71ad9 to 0eb3e6b Compare June 30, 2026 15:32
@magickli1

Copy link
Copy Markdown
Contributor Author

I have left comments based on my review!

Please let me know if you want to discuss this during the Veraison Sync up today at 4PM UK time.

Thanks for the review — I'll address your comments on the PR. I can't make today's sync (4 PM UK)。

@yogeshbdeshpande yogeshbdeshpande merged commit 9eeb486 into veraison:main Jun 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants