Skip to content

fix(core): accept pathlen:0 on the issuing CA - #45

Merged
ajile-in merged 2 commits into
mainfrom
fix/pathlen-off-by-one
Nov 4, 2026
Merged

ajile-in merged 2 commits into
mainfrom
fix/pathlen-off-by-one

Conversation

@ajile-in

@ajile-in ajile-in commented Nov 4, 2026 •

Copy link
Copy Markdown

Fixes #44.

Bug

verifyCertificateChain compared pathLenConstraint against the certificate's chain index:

bcExt.pathLength < i

The leaf is at index 0 and does not count towards the limit (RFC 5280 §4.2.1.9), so an issuing CA at index 1 with pathlen:0 was always rejected:

Certificate 1 basic constraints: path length constraint violated

pathlen:0 on the issuing CA is the normal way for a CA to say "I issue only end-entity certificates", so this rejected common real-world chains.

Fix

bcExt.pathLength < i - 1

An issuer at index i has i - 1 intermediates below it. Genuine violations are still rejected.

Tests

Three static fixtures in typescript/shared-test-data/ (P-256, 10-year validity), plus typescript/test/pathlen.test.ts:

Fixture Chain Expected
pathlen0-chain.pem root → issuing CA (pathlen:0) → leaf resolves
pathlen-boundary-chain.pem root → int1 (pathlen:1) → int2 (pathlen:0) → leaf resolves
pathlen-violation-chain.pem root (pathlen:0) → intermediate → leaf still rejected

The first two reproduce the reported error on @didx509/core 0.1.2 and pass with the fix.

Second fix: predicate encoding

The native parity suite caught a masked divergence: with the chain no longer failing early, …::san:email:user@example.com resolved even though the spec and the C++ reference require reserved characters to be percent-encoded. decodeScalar() now enforces the reference grammar (unreserved characters or %XX, valid UTF-8) for subject, SAN, EKU and Fulcio-issuer values. DIDs produced with pctEncode are unaffected.

npm run parity:report was regenerated: 16 vectors move from unexpected-failure to document-mismatch (they now resolve; the rest is the known JWK key_ops/ext divergence), the unencoded-predicate vector stops diverging, and the total stays at 112 known divergences with no new risky entries.

Release

Bumps core to 0.1.3. @didx509/credo does not need a release: the published 0.1.2 depends on ^0.1.2, which matches 0.1.3.

Verification

  • tsc --noEmit, ESLint (no errors), 149/149 tests, tsup build
  • npm pack --dry-run: didx509-core-0.1.3.tgz, 19 files
  • REQUIRE_CORE=true native parity suite: 222/222
  • New fixtures reproduce the error when run against the published 0.1.2 tarball

verifyCertificateChain compared pathLenConstraint against the certificate's
chain index, but the leaf does not count towards the limit (RFC 5280
4.2.1.9). An issuing CA at index 1 with pathlen:0 was therefore rejected as
'Certificate 1 basic constraints: path length constraint violated', even
though pathlen:0 is the common way for a CA to allow only end-entity
certificates.

Compare against i - 1 instead. Genuine violations are still rejected.

Fixtures: root -> issuing CA (pathlen:0) -> leaf; a four-cert chain at the
boundary (root -> int1 pathlen:1 -> int2 pathlen:0 -> leaf); and a real
violation (root pathlen:0 -> intermediate -> leaf).

Verified against @didx509/core 0.1.2 (the new fixtures reproduce the report)
and with the fix: 144/144 tests, typecheck, lint, build, npm pack.

Fixes #44

Signed-off-by: Ajay Jadhav <ajay@ayanworks.com>
The path-length fix in the previous commit lets chains resolve that used to
fail early, which exposed a masked divergence in the parity suite:
`san:email:user@example.com` was accepted, while the specification and the
C++ reference resolver reject it (reserved characters must be escaped).

Add decodeScalar(), which enforces the reference resolver's grammar: only
unreserved characters (A-Za-z0-9-._~) and %XX escapes, non-empty, and the
decoded value must be valid UTF-8. Use it for subject, SAN, EKU and
Fulcio-issuer predicate values instead of decodeURIComponent, which accepts
raw reserved characters.

Regenerate the parity report: 16 vectors move from 'unexpected-failure' to
'document-mismatch' (they now resolve; the remaining difference is the known
JWK key_ops/ext divergence), and predicate-value-must-be-percent-encoded
stops diverging. Total known divergences stay at 112, with no new risky
entries.

Verified: 149/149 tests, typecheck, lint, build; REQUIRE_CORE=true parity
suite 222/222.

Signed-off-by: Ajay Jadhav <ajay@ayanworks.com>
@ajile-in
ajile-in merged commit e9f2ba7 into main Nov 4, 2026
8 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.

core: path length check rejects valid chains (issuing CA with pathlen:0)

1 participant