Skip to content

Prefer embedded LTV revocation and handle large national CRLs#14

Merged
edgarsj merged 7 commits into
mainfrom
fix/embedded-ltv-revocation
Jun 25, 2026
Merged

Prefer embedded LTV revocation and handle large national CRLs#14
edgarsj merged 7 commits into
mainfrom
fix/embedded-ltv-revocation

Conversation

@edgarsj

@edgarsj edgarsj commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Problem

Verifying a real Latvian eID signature returned INDETERMINATE for certificate_not_revoked_at_signing_time:

CRL: ...LV_eID_ICA_2021_10084.crl: Failed to parse CRL data

Reproduction note

Only reproduces with asn1js@^3.0.9 (the latest within the existing ^3.0.6 range; npm install/npm ci land here). asn1js 3.0.9+ enforces DEFAULT_MAX_NODES = 10000, and the LV eID ICA CRL has ~13k entries, so new X509Crl() throws. The repo previously happened to resolve 3.0.7 (no cap), which masked it.

What changed

Revocation

  • parseCRL() falls back to asn1js.fromBER(..., { maxNodes }) + AsnParser.fromASN(CertificateList) + new X509Crl(certList) so national CRLs that exceed the node cap parse (bounded, still a DoS guard).
  • Lighter live OCSP: the issuer cert needed to build an OCSP request is now also recovered from the certs embedded in the signature's RevocationValues OCSP response, and issuer resolution prefers a candidate that actually signed the cert. The LV eID sample ships no certificateChain, so OCSP previously failed ("could not find issuer") and fell back to the ~668 KB CRL; it now answers via a small live OCSP query (verified end-to-end: method: "ocsp", not "crl").
  • Embedded XAdES RevocationValues are parsed and exposed on SignatureInfo.revocationValues (raw base64 DER) but their status is never trusted — a fresh live query is always made (see Security).

Security: embedded revocation status is not trusted (addresses review P1)

  • RevocationValues live in UnsignedSignatureProperties and are not covered by the signature. Trusting their good status to short-circuit the verdict would let a tampered container pass a revoked signer. The verdict therefore comes only from a live OCSP/CRL query; when revocation can't be checked the result is INDETERMINATE, never good. (The embedded issuer certificate is used only to address the live OCSP query, after verifying it actually signed the signer — a wrong/tampered cert yields unknown, not good.) Document integrity, signature, certificate validity, timestamp, and issuer-trust checks remain fully offline-capable.

XPath warning

  • Fixed the non-fatal InclusiveNamespaces "XPath evaluation failed" warning (was passing the namespace map as xpath.select's single arg; now uses xpath.useNamespaces).

Trusted list (Part B)

  • Compact bundle + bundled snapshot now carry a top-level bundleId (was null); refreshed the EU LOTL snapshot.
  • merge-forward: regeneration carries forward last-known-good services for unreachable territories so a transient failure can't silently drop a country.
  • native http(s) fetch fallback in the build tooling: some national endpoints (e.g. Estonia's sr.riik.ee) block undici's TLS/HTTP2 fingerprint (and Mozilla/* UAs) but accept Node's native client.

Deps

  • Declared asn1js, @peculiar/asn1-schema, @peculiar/asn1-x509 (were transitive).
  • .prettierignore excludes generated src/data/trusted-list.{ts,json} (also fixes a pre-existing npm run format:check failure).

Not done (with reason)

  • Full authentication of the embedded OCSP/CRL status (so it could replace the live query) — would need OCSP-response signature verification + responder chain + freshness, anchored to the trust list. Deferred; the live query is authoritative and now cheap.
  • Ukraine intentionally not added as a separate source; already present via the AdES LOTL.

Test plan

  • npm test147 passing / 24 suites, including:
    • parseCRL on the real >10k-entry LV eID CRL (committed public, no-personal-data fixture)
    • revocation safety: offline verifySignature(SampleFile)certificate_not_revoked_at_signing_time = indeterminate (not pass), other checks still pass
    • light OCSP path: with a mocked responder, revocation answers via method: "ocsp" and the signer CRL is never downloaded
  • npx tsc --noEmit clean, npm run build succeeds, npm run format:check clean

Changelog updated under [Unreleased]; version bump/tag left for the usual separate release step.

Fixes certificate_not_revoked_at_signing_time returning INDETERMINATE
("Failed to parse CRL data") for real Latvian eID signatures under
asn1js@^3.0.9, and makes the check offline and signing-time-correct.

- Revocation: evaluate "not revoked at signing time" from the signature's
  embedded XAdES RevocationValues (OCSP/CRL captured at signing time) before
  any live fetch; expose SignatureInfo.revocationValues and
  RevocationResult.fromEmbedded
- crl: parseCRL() raises asn1js's DEFAULT_MAX_NODES cap via a bounded
  fallback so national CRLs (~13k LV eID entries) parse
- xmlParser: fix InclusiveNamespaces "XPath evaluation failed" warning by
  using xpath.useNamespaces instead of misusing select()'s `single` arg
- trusted-list: add top-level bundleId; carry forward last-known-good
  services for unreachable territories; native http(s) fetch fallback for
  endpoints that block undici's fingerprint (e.g. EE sr.riik.ee); refresh
  bundled EU snapshot
- deps: declare asn1js, @peculiar/asn1-schema, @peculiar/asn1-x509
- prettier: ignore generated src/data/trusted-list.{ts,json}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fda1e9f552

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/core/revocation/check.ts Outdated
edgarsj added 2 commits June 25, 2026 23:39
Code review (P1): xades:RevocationValues live in UnsignedSignatureProperties
and are not covered by the signature, so short-circuiting the revocation
verdict to "good" from them let a tampered container pass a revoked signer
with no network. Authenticating them offline would require verifying the
OCSP/CRL signature and responder chain against a trusted issuer plus
freshness, which isn't generally possible offline (the issuer cert may not
be in the container).

- Remove the embedded-revocation short-circuit; the verdict now comes only
  from live OCSP/CRL. When revocation can't be checked it is INDETERMINATE,
  never "good"
- Drop RevocationCheckOptions.embeddedOCSP/embeddedCRL/atTime and
  RevocationResult.fromEmbedded; delete the embedded evaluator
- Still parse and expose SignatureInfo.revocationValues as RAW, explicitly
  unvalidated material for consumers doing their own LTV validation
- Add a regression test: offline revocation is indeterminate (not pass) while
  document/signature/certificate/timestamp/issuer checks still verify offline

The original INDETERMINATE-on-large-CRL bug stays fixed via the parseCRL
node-cap fallback on the live path (verified: live CRL returns "good").
When the container ships no certificate chain (as with the LV eID sample),
OCSP couldn't build a request ("could not find issuer") and fell back to
downloading the full ~668 KB CRL. The issuer cert is, however, bundled inside
the signature's RevocationValues OCSP response.

- extractCertsFromOCSPResponses(): recover certs embedded in OCSP responses
- resolveIssuerFromChain(): async issuer resolution that prefers a candidate
  whose key actually signed the cert (so a tampered same-name issuer can't
  misdirect the request); checkOCSP now uses it
- verification.ts feeds those embedded certs into the revocation chain

The embedded OCSP *status* is still not trusted — a fresh live OCSP query is
made. Verified end-to-end: revocation now answers via method "ocsp" instead of
"crl" for SampleFile, without downloading the CRL.
@edgarsj

edgarsj commented Jun 25, 2026

Copy link
Copy Markdown
Owner Author

@codex please re-review new code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ec009b2cec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/core/parser/signatureParser.ts Outdated
edgarsj added 2 commits June 26, 2026 01:01
Code review (P2): parseSignatureElement read RevocationValues,
CertificateValues, SigningTime and SignatureTimeStamp from the whole
document (xmlDoc), so in a multi-signature document every signature
inherited the first signature's embedded material — pointing the new OCSP
issuer resolution at another signature's certs.

Query these within the current signatureElement instead. No change for the
common one-signature-per-file case (the element is found either way); the
fix matters when parseSignatureElement is used on documents with multiple
ds:Signature elements.

Adds a real-DOM test parsing a two-signature document and asserting each
signature yields its own RevocationValues, CertificateValues and SigningTime.
@edgarsj

edgarsj commented Jun 25, 2026

Copy link
Copy Markdown
Owner Author

@codex please re-review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c68083f4cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/core/trustedlist/index.ts Outdated
@edgarsj edgarsj merged commit 6641369 into main Jun 25, 2026
3 checks passed
@edgarsj edgarsj deleted the fix/embedded-ltv-revocation branch June 25, 2026 22:53
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.

1 participant