◐ Off-By-One · answer catalog

go-x509-path-validation-name-constraints-policy-mapping-differential

2 answer(s)gogo1.26gogo1.26

Problem class: go-x509-path-validation-name-constraints-policy-mapping-differential

📦 Source in repository (JSON)

Answer 1

The solution is complete and verified. Full document is at ~/rfc5280/SOLUTION.md; here it is:


RFC 5280 Path Validation from Scratch + crypto/x509 Differential Harness

Problem class: go-x509-path-validation-name-constraints-policy-mapping-differential Environment: Go 1.26 (linux/amd64) · module rfc5280 · working tree ~/rfc5280


1. Root-cause analysis

The task is not "fix one bug" — it is "build a second, independent implementation of the RFC 5280 §6.1 path-validation algorithm and find out where it legitimately diverges from crypto/x509.Verify." The hard part is that six independent subsystems interact, and each has a known failure mode that produces a silently wrong verdict rather than a crash:

Subsystem Classic failure mode Normative anchor
ASN.1 decoding Using crypto/x509 structs, or mis-tagging the PKIX IMPLICIT module RFC 5280 App. A.2 (PKIX1Implicit88)
Signature Unmarshalling an EC point as if it were a SEQUENCE RFC 5480 §2.2
Basic constraints / path length Counting self-issued CAs, or counting the leaf RFC 5280 §6.1.4(g)–(h)
Name constraints Empty permitted intersection treated as "unconstrained" RFC 5280 §, §6.1.4(g)
Policy processing Missing valid_policy_tree, mapping/inhibit variables RFC 5280 §6.1.3(d)–(f), §6.1.4(b),(h)–(j)
Revocation Not threading the validation instant into CRL windows; no delta/indirect RFC 5280 §6.3–§6.3.4

The root cause of divergence is almost never the engine — it is one of four cases:

  1. A real engine defect that the differential matrix exposes (we found three; see §2).
  2. A crypto/x509 gap: it does not check revocation at all, does not implement directoryName name constraints, has no clock-skew knob, and treats the AKI as a path-building hint rather than a hard chaining check.
  3. A policy wording difference: crypto/x509 since Go 1.20 implements the RFC 9618 corrections to §6.1.2/§6.1.3/§6.1.4/§6.1.5; the engine ports those corrections, but any pre-9618 implementation would diverge.
  4. Local policy: revocation fail-open vs fail-closed and clock-skew tolerance are inputs, not RFC constants.

The deliverable therefore has to (a) implement the algorithm, and (b) adjudicate each divergence with the normative text and a minimal reproduction, instead of declaring the stdlib the winner by default.


2. Exact fix — the implementation

All code lives under ~/rfc5280. It uses only encoding/asn1 for decoding and crypto/{rsa,ecdsa,ed25519,sha1,sha256,sha512} for signatures. crypto/x509 is imported only in the harness, for certificate generation and for the second opinion.

rfc5280/
├── go.mod                     module rfc5280 (go 1.26)
├── cert.go        (769 L)     hand-written ASN.1 parser: certs, extensions, SPKI, signatures, DN compare
├── nameconstraints.go (355 L) DNS/IP/email/URI/directoryName subtree matching + intersection
├── policy.go      (343 L)     valid_policy_tree, policy mapping, explicit/inhibit variables (RFC 9618)
├── crl.go         (281 L)     CRL/delta-CRL parse, signature + window checks, indirect entry issuers
├── verify.go      (360 L)     RFC 5280 §6.1.1–§6.1.6 state machine + path building
├── pkits_policy_test.go       external NIST PKITS policy-vector check
├── nameconstraints_test.go    matching unit tests
└── harness/
    ├── build.go   (242 L)      crypto/x509 certificate & CRL generation helpers
    ├── cases.go   (673 L)      91-case matrix + combinatorial DNS matrix
    └── main.go    (198 L)      JSONL differential reporter

2.1 Raw decoding (the first real bug)

crypto/x509's parser is cryptobyte-based and cannot be reused. The replacement tbsCertificate mirrors the wire format. The first defect the matrix exposed: an EC public key is a raw point, not a SEQUENCE, so it must go through ecdsa.ParseUncompressedPublicKey with the curve taken from the algorithm parameters:

case info.Algorithm.Algorithm.Equal(oidECDSA):
    var curveOID asn1.ObjectIdentifier
    if _, err := asn1.Unmarshal(info.Algorithm.Parameters.FullBytes, &curveOID); err != nil {
        return nil, fmt.Errorf("rfc5280: ECDSA parameters: %w", err)
    }
    curve := curveForOID(curveOID)          // prime256v1 / secp384r1 / secp521r1
    return ecdsa.ParseUncompressedPublicKey(curve, der)

2.2 Name constraints — PKIX uses the IMPLICIT module

NameConstraints is defined in PKIX1Implicit88, so permittedSubtrees [0] is an implicit tag whose content is the concatenated GeneralSubtree SEQUENCEs — there is no extra wrapper. Parsing it as an explicit wrapper shifts every field by one and yields tags don't match (16 vs class:2 tag:2). The fix iterates raw TLVs directly:

for _, field := range raw {           // raw = fields of NameConstraints SEQUENCE
    rest := field.Bytes               // content of [0] / [1]
    for len(rest) > 0 {
        var s asn1.RawValue
        rest, err = asn1.Unmarshal(rest, &s)
        gs, err := parseGeneralSubtree(s.FullBytes)
        ...
    }
}

Subtree semantics implemented exactly:

2.3 The empty-intersection bug (a genuine engine defect)

RFC 5280 §6.1.4(g) says the name-constraints variable is the intersection of the current and the certificate's permittedSubtrees. Permitted sets example.com and other.com intersect to the empty set — and an empty set means nothing is permitted, which must reject. A naive representation cannot distinguish "no permitted DNS constraint" (no restriction) from "empty permitted DNS set" (reject everything), and the engine initially accepted matrix_dns_*_intersection_miss. Fix: carry an explicit PermittedTypes set through intersection:

type NameConstraints struct {
    Permitted      []GeneralSubtree
    Excluded       []GeneralSubtree
    PermittedTypes map[GeneralNameType]bool // name forms that carry a permitted set
}

// intersection marks a form as constrained if either side constrained it, even
// when the resulting subtree list is empty.
out.PermittedTypes[t] = true
...
// during check:
any := nc.PermittedTypes[n.t]          // not "len(list)>0"

2.4 Policy processing

policy.go is a faithful valid_policy_tree with RFC 9618 corrections: a graph of nodes keyed by validPolicy, an expected set per node, parentIndex for expected_policy_set lookup, prune(), deleteLeaf(), and the six state variables (valid_policy_tree, explicit_policy, policy_mapping, inhibit_any_policy, plus the user-initial-policy-set). It enforces that mapping to/from anyPolicy is invalid and applies requireExplicitPolicy/inhibitPolicyMapping/inhibitAnyPolicy, including the "present and zero" distinction.

2.5 Revocation

crl.go parses basic, delta (deltaCRLIndicator) and indirect CRLs, verifies the CRL signature and the thisUpdate/nextUpdate window, and applies the §5.3.3 rule that a certificateIssuer CRL entry extension governs this entry and all following entries. reasonCode == 8 (removeFromCRL) in a delta un-revokes. One subtle defect the matrix found: ValidatePath passed opts.Revocation through unchanged, so CheckRevoked used time.Now() instead of the validation instant and rejected the whole path because every CRL looked expired. Fix:

revOpts := opts.Revocation
revOpts.Time = now
if err := CheckRevoked(cert, workingIssuerCert(chain, m-i, anchor), revOpts); err != nil { ... }

2.6 The rest of §6.1

verify.go implements signature → validity → revocation → issuer-name → name constraints, then §6.1.4 preparation: cA=TRUE, keyCertSign, self-issued-aware path length, constraint intersection, AKI/SKI chaining, EKU intersection with anyExtendedKeyUsage nesting, and a DFS path builder for Verify.


3. Verification

All commands run from ~/rfc5280.

$ go vet ./...                 # clean
$ go test ./...                # ok rfc5280
$ go run ./harness             # writes report.jsonl to stdout + file

3.1 External verification: NIST PKITS policy vectors

pkits_policy_test.go drives the engine's policy state machine with the same 88 NIST PKITS policy vectors used by crypto/x509/pkits_test.go (sections 4.8.x–4.12.x: same-policy, no-policy, anyPolicy, requireExplicitPolicy, policy mapping, inhibitPolicyMapping, inhibitAnyPolicy, including the self-issued variants):

$ go test -run TestPKITSPolicyVectors -v
    pkits_policy_test.go:94: ran 88 PKITS policy vectors, 0 mismatches
ok      rfc5280 0.024s

This is independent of crypto/x509's verifier output — it is the NIST expected boolean.

3.2 Built-in unit tests

$ go test ./...
ok      rfc5280 0.018s

Covers DNS leading-dot/wildcard/case/trailing-dot, email mailbox-vs-host, URI host-only extraction, directoryName prefix matching, IPv4 containment, and the empty-permitted-intersection rejection.

3.3 Differential harness

go run ./harness generates 91 hierarchies (basic constraints, path length, KU, EKU nesting, DNS/IP/email/URI/directoryName constraints, a 42-case combinatorial DNS matrix, policy mapping/inhibit, validity, AKI/SKI, basic/delta/missing CRLs), validates each with both engines, and emits one JSON object per line. Current result:

91 cases, 6 divergences; report written to report.jsonl

Every divergence is adjudicated with the responsible clause and a minimal reproduction. None is resolved by saying "the stdlib is right":

Case Engine stdlib Adjudication (abridged)
nc_dirname_permitted_match accept reject Engine processes directoryName subtrees per §/§6.1.3(e); crypto/x509 implements only DNS/IP/email/URI and rejects the critical extension as unhandled. Engine is RFC-conformant.
validity_clock_skew_expired_within_skew accept reject §6.1.1 makes "current time" an input but defines no skew tolerance. Engine passed ClockSkew=2h; stdlib has no knob. Both verdicts conformant under different local policy.
aki_ski_mismatch reject accept Engine treats AKI/SKI equality as a hard chaining guard (§). stdlib falls back to issuer-name + signature (§6.1.3(d) only requires name equality); AKI consistency is a SHOULD.
crl_revoked_leaf reject accept Engine performs §6.3.3 CRL checking and finds the serial; crypto/x509.Verify performs no revocation checking. Engine is RFC-conformant.
crl_missing_for_revocation_check reject accept §6.3 defines no default when revocation is required; engine fails closed. stdlib does not check at all.
crl_delta_revocation reject accept Engine applies the §6.3.4 delta CRL (deltaCRLIndicator == base CRL number); stdlib has no delta support. Engine is RFC-conformant.

All other 85 cases — including the 42-pair DNS-constraint matrix, the path-length/KU/EKU cases, the policy mapping/inhibit cases, the IP/email/URI cases and the two intermediate-intersection cases — agree with crypto/x509. Sample report lines:

{"case":"matrix_dns_example.com_www.example.com_permitted","clause":"RFC 5280 §<ip-address> / §6.1.3(e)","engine_verdict":"accept","stdlib_verdict":"accept","divergence":false}
{"case":"crl_delta_revocation","clause":"RFC 5280 §6.3.4","engine_verdict":"reject","stdlib_verdict":"accept","divergence":true,"adjudication":"Engine applies the delta CRL (RFC 5280 §6.3.4) and rejects the leaf. crypto/x509 has no delta-CRL support. Engine verdict is RFC-conformant.","repro":"base CRL number 10 with no entries + delta CRL with deltaCRLIndicator=10 revoking the leaf serial"}

3.4 Reproduce from scratch

mkdir -p ~/rfc5280/harness && cd ~/rfc5280
cat > go.mod <<'EOF'
module rfc5280
go 1.26
EOF
# copy cert.go nameconstraints.go policy.go crl.go verify.go and the test files from this solution
# copy harness/build.go harness/cases.go harness/main.go
go vet ./... && go test ./... && go run ./harness

4. Conclusion

The engine is a from-scratch RFC 5280 §6.1 validator whose only dependencies are encoding/asn1 and raw crypto primitives. It passes all 88 NIST PKITS policy vectors and agrees with crypto/x509.Verify on 85 of 91 differential cases. The six divergences are not defects: four expose documented crypto/x509 gaps (directoryName constraints, revocation, delta CRLs, no clock skew), one is a permissible local-policy choice (AKI as a hard guard), and all are adjudicated against the normative text with a minimal reproduction rather than by deferring to the standard library.

Evidence & signatures

# Evidence
- Problem class: go-x509-path-validation-name-constraints-policy-mapping-differential
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-25T22:16:25.888Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement RFC 5280 certificate path validation from scratch in Go using only raw crypto primitives, never crypto/x509 for validation logic, covering basic constraints, key usage, extended key usage with anyExtendedKeyUsage nesting, name constraints (permitted and excluded across DNS, IP, email, URI and directoryName with correct subtree matching), certificate policy processing with explicit and inhibit policy mapping plus policy constraints, path length and AKI/SKI chaining, validity windows with clock skew, and CRL-based revocation including indirect and delta CRLs. Build a differential harness that generates a large matrix of well-formed, adversarial and edge-case CA hierarchies and certificates, validates every case with both your engine and Go's crypto/x509.Verify, and emits a machine-readable JSONL report enumerating each case, both verdicts, and the responsible spec clause for every divergence, with each divergence adjudicated by citing the normative text and a minimal reproduction rather than by deferring to the stdlib.", "environment": "go1.26", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-x509-path-validation-name-constraints-policy-mapping-differential", "provider": "openrouter", "solved_at": "2026-09-25T22:16:25.888Z", "version": "1.26"}

Answer 2

The solution is complete and verified. Full document is at ~/rfc5280/SOLUTION.md; here it is:


RFC 5280 Path Validation from Scratch + crypto/x509 Differential Harness

Problem class: go-x509-path-validation-name-constraints-policy-mapping-differential Environment: Go 1.26 (linux/amd64) · module rfc5280 · working tree ~/rfc5280


1. Root-cause analysis

The task is not "fix one bug" — it is "build a second, independent implementation of the RFC 5280 §6.1 path-validation algorithm and find out where it legitimately diverges from crypto/x509.Verify." The hard part is that six independent subsystems interact, and each has a known failure mode that produces a silently wrong verdict rather than a crash:

Subsystem Classic failure mode Normative anchor
ASN.1 decoding Using crypto/x509 structs, or mis-tagging the PKIX IMPLICIT module RFC 5280 App. A.2 (PKIX1Implicit88)
Signature Unmarshalling an EC point as if it were a SEQUENCE RFC 5480 §2.2
Basic constraints / path length Counting self-issued CAs, or counting the leaf RFC 5280 §6.1.4(g)–(h)
Name constraints Empty permitted intersection treated as "unconstrained" RFC 5280 §, §6.1.4(g)
Policy processing Missing valid_policy_tree, mapping/inhibit variables RFC 5280 §6.1.3(d)–(f), §6.1.4(b),(h)–(j)
Revocation Not threading the validation instant into CRL windows; no delta/indirect RFC 5280 §6.3–§6.3.4

The root cause of divergence is almost never the engine — it is one of four cases:

  1. A real engine defect that the differential matrix exposes (we found three; see §2).
  2. A crypto/x509 gap: it does not check revocation at all, does not implement directoryName name constraints, has no clock-skew knob, and treats the AKI as a path-building hint rather than a hard chaining check.
  3. A policy wording difference: crypto/x509 since Go 1.20 implements the RFC 9618 corrections to §6.1.2/§6.1.3/§6.1.4/§6.1.5; the engine ports those corrections, but any pre-9618 implementation would diverge.
  4. Local policy: revocation fail-open vs fail-closed and clock-skew tolerance are inputs, not RFC constants.

The deliverable therefore has to (a) implement the algorithm, and (b) adjudicate each divergence with the normative text and a minimal reproduction, instead of declaring the stdlib the winner by default.


2. Exact fix — the implementation

All code lives under ~/rfc5280. It uses only encoding/asn1 for decoding and crypto/{rsa,ecdsa,ed25519,sha1,sha256,sha512} for signatures. crypto/x509 is imported only in the harness, for certificate generation and for the second opinion.

rfc5280/
├── go.mod                     module rfc5280 (go 1.26)
├── cert.go        (769 L)     hand-written ASN.1 parser: certs, extensions, SPKI, signatures, DN compare
├── nameconstraints.go (355 L) DNS/IP/email/URI/directoryName subtree matching + intersection
├── policy.go      (343 L)     valid_policy_tree, policy mapping, explicit/inhibit variables (RFC 9618)
├── crl.go         (281 L)     CRL/delta-CRL parse, signature + window checks, indirect entry issuers
├── verify.go      (360 L)     RFC 5280 §6.1.1–§6.1.6 state machine + path building
├── pkits_policy_test.go       external NIST PKITS policy-vector check
├── nameconstraints_test.go    matching unit tests
└── harness/
    ├── build.go   (242 L)      crypto/x509 certificate & CRL generation helpers
    ├── cases.go   (673 L)      91-case matrix + combinatorial DNS matrix
    └── main.go    (198 L)      JSONL differential reporter

2.1 Raw decoding (the first real bug)

crypto/x509's parser is cryptobyte-based and cannot be reused. The replacement tbsCertificate mirrors the wire format. The first defect the matrix exposed: an EC public key is a raw point, not a SEQUENCE, so it must go through ecdsa.ParseUncompressedPublicKey with the curve taken from the algorithm parameters:

case info.Algorithm.Algorithm.Equal(oidECDSA):
    var curveOID asn1.ObjectIdentifier
    if _, err := asn1.Unmarshal(info.Algorithm.Parameters.FullBytes, &curveOID); err != nil {
        return nil, fmt.Errorf("rfc5280: ECDSA parameters: %w", err)
    }
    curve := curveForOID(curveOID)          // prime256v1 / secp384r1 / secp521r1
    return ecdsa.ParseUncompressedPublicKey(curve, der)

2.2 Name constraints — PKIX uses the IMPLICIT module

NameConstraints is defined in PKIX1Implicit88, so permittedSubtrees [0] is an implicit tag whose content is the concatenated GeneralSubtree SEQUENCEs — there is no extra wrapper. Parsing it as an explicit wrapper shifts every field by one and yields tags don't match (16 vs class:2 tag:2). The fix iterates raw TLVs directly:

for _, field := range raw {           // raw = fields of NameConstraints SEQUENCE
    rest := field.Bytes               // content of [0] / [1]
    for len(rest) > 0 {
        var s asn1.RawValue
        rest, err = asn1.Unmarshal(rest, &s)
        gs, err := parseGeneralSubtree(s.FullBytes)
        ...
    }
}

Subtree semantics implemented exactly:

2.3 The empty-intersection bug (a genuine engine defect)

RFC 5280 §6.1.4(g) says the name-constraints variable is the intersection of the current and the certificate's permittedSubtrees. Permitted sets example.com and other.com intersect to the empty set — and an empty set means nothing is permitted, which must reject. A naive representation cannot distinguish "no permitted DNS constraint" (no restriction) from "empty permitted DNS set" (reject everything), and the engine initially accepted matrix_dns_*_intersection_miss. Fix: carry an explicit PermittedTypes set through intersection:

type NameConstraints struct {
    Permitted      []GeneralSubtree
    Excluded       []GeneralSubtree
    PermittedTypes map[GeneralNameType]bool // name forms that carry a permitted set
}

// intersection marks a form as constrained if either side constrained it, even
// when the resulting subtree list is empty.
out.PermittedTypes[t] = true
...
// during check:
any := nc.PermittedTypes[n.t]          // not "len(list)>0"

2.4 Policy processing

policy.go is a faithful valid_policy_tree with RFC 9618 corrections: a graph of nodes keyed by validPolicy, an expected set per node, parentIndex for expected_policy_set lookup, prune(), deleteLeaf(), and the six state variables (valid_policy_tree, explicit_policy, policy_mapping, inhibit_any_policy, plus the user-initial-policy-set). It enforces that mapping to/from anyPolicy is invalid and applies requireExplicitPolicy/inhibitPolicyMapping/inhibitAnyPolicy, including the "present and zero" distinction.

2.5 Revocation

crl.go parses basic, delta (deltaCRLIndicator) and indirect CRLs, verifies the CRL signature and the thisUpdate/nextUpdate window, and applies the §5.3.3 rule that a certificateIssuer CRL entry extension governs this entry and all following entries. reasonCode == 8 (removeFromCRL) in a delta un-revokes. One subtle defect the matrix found: ValidatePath passed opts.Revocation through unchanged, so CheckRevoked used time.Now() instead of the validation instant and rejected the whole path because every CRL looked expired. Fix:

revOpts := opts.Revocation
revOpts.Time = now
if err := CheckRevoked(cert, workingIssuerCert(chain, m-i, anchor), revOpts); err != nil { ... }

2.6 The rest of §6.1

verify.go implements signature → validity → revocation → issuer-name → name constraints, then §6.1.4 preparation: cA=TRUE, keyCertSign, self-issued-aware path length, constraint intersection, AKI/SKI chaining, EKU intersection with anyExtendedKeyUsage nesting, and a DFS path builder for Verify.


3. Verification

All commands run from ~/rfc5280.

$ go vet ./...                 # clean
$ go test ./...                # ok rfc5280
$ go run ./harness             # writes report.jsonl to stdout + file

3.1 External verification: NIST PKITS policy vectors

pkits_policy_test.go drives the engine's policy state machine with the same 88 NIST PKITS policy vectors used by crypto/x509/pkits_test.go (sections 4.8.x–4.12.x: same-policy, no-policy, anyPolicy, requireExplicitPolicy, policy mapping, inhibitPolicyMapping, inhibitAnyPolicy, including the self-issued variants):

$ go test -run TestPKITSPolicyVectors -v
    pkits_policy_test.go:94: ran 88 PKITS policy vectors, 0 mismatches
ok      rfc5280 0.024s

This is independent of crypto/x509's verifier output — it is the NIST expected boolean.

3.2 Built-in unit tests

$ go test ./...
ok      rfc5280 0.018s

Covers DNS leading-dot/wildcard/case/trailing-dot, email mailbox-vs-host, URI host-only extraction, directoryName prefix matching, IPv4 containment, and the empty-permitted-intersection rejection.

3.3 Differential harness

go run ./harness generates 91 hierarchies (basic constraints, path length, KU, EKU nesting, DNS/IP/email/URI/directoryName constraints, a 42-case combinatorial DNS matrix, policy mapping/inhibit, validity, AKI/SKI, basic/delta/missing CRLs), validates each with both engines, and emits one JSON object per line. Current result:

91 cases, 6 divergences; report written to report.jsonl

Every divergence is adjudicated with the responsible clause and a minimal reproduction. None is resolved by saying "the stdlib is right":

Case Engine stdlib Adjudication (abridged)
nc_dirname_permitted_match accept reject Engine processes directoryName subtrees per §/§6.1.3(e); crypto/x509 implements only DNS/IP/email/URI and rejects the critical extension as unhandled. Engine is RFC-conformant.
validity_clock_skew_expired_within_skew accept reject §6.1.1 makes "current time" an input but defines no skew tolerance. Engine passed ClockSkew=2h; stdlib has no knob. Both verdicts conformant under different local policy.
aki_ski_mismatch reject accept Engine treats AKI/SKI equality as a hard chaining guard (§). stdlib falls back to issuer-name + signature (§6.1.3(d) only requires name equality); AKI consistency is a SHOULD.
crl_revoked_leaf reject accept Engine performs §6.3.3 CRL checking and finds the serial; crypto/x509.Verify performs no revocation checking. Engine is RFC-conformant.
crl_missing_for_revocation_check reject accept §6.3 defines no default when revocation is required; engine fails closed. stdlib does not check at all.
crl_delta_revocation reject accept Engine applies the §6.3.4 delta CRL (deltaCRLIndicator == base CRL number); stdlib has no delta support. Engine is RFC-conformant.

All other 85 cases — including the 42-pair DNS-constraint matrix, the path-length/KU/EKU cases, the policy mapping/inhibit cases, the IP/email/URI cases and the two intermediate-intersection cases — agree with crypto/x509. Sample report lines:

{"case":"matrix_dns_example.com_www.example.com_permitted","clause":"RFC 5280 §<ip-address> / §6.1.3(e)","engine_verdict":"accept","stdlib_verdict":"accept","divergence":false}
{"case":"crl_delta_revocation","clause":"RFC 5280 §6.3.4","engine_verdict":"reject","stdlib_verdict":"accept","divergence":true,"adjudication":"Engine applies the delta CRL (RFC 5280 §6.3.4) and rejects the leaf. crypto/x509 has no delta-CRL support. Engine verdict is RFC-conformant.","repro":"base CRL number 10 with no entries + delta CRL with deltaCRLIndicator=10 revoking the leaf serial"}

3.4 Reproduce from scratch

mkdir -p ~/rfc5280/harness && cd ~/rfc5280
cat > go.mod <<'EOF'
module rfc5280
go 1.26
EOF
# copy cert.go nameconstraints.go policy.go crl.go verify.go and the test files from this solution
# copy harness/build.go harness/cases.go harness/main.go
go vet ./... && go test ./... && go run ./harness

4. Conclusion

The engine is a from-scratch RFC 5280 §6.1 validator whose only dependencies are encoding/asn1 and raw crypto primitives. It passes all 88 NIST PKITS policy vectors and agrees with crypto/x509.Verify on 85 of 91 differential cases. The six divergences are not defects: four expose documented crypto/x509 gaps (directoryName constraints, revocation, delta CRLs, no clock skew), one is a permissible local-policy choice (AKI as a hard guard), and all are adjudicated against the normative text with a minimal reproduction rather than by deferring to the standard library.

Evidence & signatures

# Evidence
- Problem class: go-x509-path-validation-name-constraints-policy-mapping-differential
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-25T22:16:25.888Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement RFC 5280 certificate path validation from scratch in Go using only raw crypto primitives, never crypto/x509 for validation logic, covering basic constraints, key usage, extended key usage with anyExtendedKeyUsage nesting, name constraints (permitted and excluded across DNS, IP, email, URI and directoryName with correct subtree matching), certificate policy processing with explicit and inhibit policy mapping plus policy constraints, path length and AKI/SKI chaining, validity windows with clock skew, and CRL-based revocation including indirect and delta CRLs. Build a differential harness that generates a large matrix of well-formed, adversarial and edge-case CA hierarchies and certificates, validates every case with both your engine and Go's crypto/x509.Verify, and emits a machine-readable JSONL report enumerating each case, both verdicts, and the responsible spec clause for every divergence, with each divergence adjudicated by citing the normative text and a minimal reproduction rather than by deferring to the stdlib.", "environment": "go1.26", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-x509-path-validation-name-constraints-policy-mapping-differential", "provider": "openrouter", "solved_at": "2026-09-25T22:16:25.888Z", "version": "1.26"}
Generated from the verified corpus · MIT licensedBack to the catalog