Skip to content

fix: harden core parsing and identity selection#107

Merged
danielewood merged 7 commits intodevelopfrom
fix/core-parsing-identity
Mar 2, 2026
Merged

fix: harden core parsing and identity selection#107
danielewood merged 7 commits intodevelopfrom
fix/core-parsing-identity

Conversation

@danielewood
Copy link
Copy Markdown
Collaborator

Summary

  • Harden PEM and DER parsing across inspect/convert/container flows: scan all relevant PEM blocks, preserve valid certificates when malformed PEM blocks are present, and accept DER private-key inputs where key-only behavior is valid.
  • Preserve JKS private key entry pairing by decoding key entries with alias + chain metadata and selecting leaf/extra certificates from the matching entry during container parsing.
  • Fix identity and issuer correctness gaps by improving cert dedup identity when AKI is missing, validating CA cert/key match before CSR signing, and selecting OCSP issuers via signature-valid matching with AKI/SKI preference.
  • Expand behavior-focused edge-case tests for malformed inputs, DER key paths, JKS pairing, issuer selection, and CA mismatch validation while keeping assertions non-tautological.

Closes #92
Closes #93
Closes #94
Closes #102

Copilot AI review requested due to automatic review settings March 1, 2026 22:51
@danielewood danielewood changed the base branch from main to develop March 1, 2026 22:54
Copy link
Copy Markdown

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

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: 64ed695793

ℹ️ 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 internal/certstore/container.go
Comment thread internal/certstore/container.go Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens certificate/key/container parsing and selection logic across the library and CLI to avoid silent data loss and incorrect identity/issuer selection, and adds targeted edge-case tests to lock in the behavior.

Changes:

  • Make PEM parsing resilient to malformed blocks; expand PEM/DER private-key detection and scanning of PEM bundles.
  • Preserve JKS private-key entry alias/chain pairing through decoding and container selection.
  • Improve correctness checks for CA cert/key mismatch (CSR signing) and OCSP issuer auto-selection; add regression tests.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
sign.go Rejects CSR signing when CA certificate and CA key don’t match.
sign_test.go Adds test covering CA cert/key mismatch fast-fail behavior.
jks.go Adds DecodeJKSKeyEntries to preserve alias→key→chain pairing; keeps legacy DecodeJKS wrapper.
jks_test.go Adds alias/chain pairing regression test for JKS key entries.
internal/certstore/container.go Updates JKS selection to use paired key-entry chains; adds DER private-key parsing.
internal/certstore/container_test.go Adds DER private-key parse test and JKS pairing selection test; adjusts unparseable inputs coverage.
certkit.go Makes PEM cert parsing skip malformed blocks; makes PEM key/CSR parsers scan bundles; adds issuer auto-selection helper.
certkit_test.go Adds tests for malformed+valid PEM preservation, key/CSR scanning, issuer selection, and updated error-path expectations.
internal/inspect.go Inspects all PEM keys (not just first block); adds DER PKCS#1/SEC1 private key detection.
internal/inspect_test.go Adds coverage for malformed cert blocks coexisting with valid certs; adds DER RSA key inspection test.
internal/certstore/memstore.go Changes cert dedup identity to fall back to issuer+serial when AKI is missing.
internal/certstore/memstore_test.go Adds test ensuring missing-AKI certs dedup by issuer+serial rather than serial alone.
cmd/certkit/ocsp.go Improves issuer parsing (PEM or DER) and issuer auto-selection via signature-valid matching.
CHANGELOG.md Records behavior changes/fixes and new edge-case test coverage under Unreleased.

Comment thread certkit.go Outdated
Comment thread certkit.go
Comment thread internal/certstore/memstore.go
Comment thread internal/certstore/container.go Outdated
Comment thread internal/certstore/container.go
Comment thread certkit.go
@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@danielewood
Copy link
Copy Markdown
Collaborator Author

Addressed reviewer feedback in fe88d6e:\n\n- : JKS selection now prefers the first key entry with a certificate chain, keeps key/cert pairing safe (no unrelated trusted cert fallback), and includes trusted JKS certs in when a key-entry leaf is used.\n- : removed error-string control flow in , added explicit skip logging for malformed/failed key and cert parse paths, and made continue scanning past malformed CSR blocks.\n- + : introduced sentinel and updated test assertions to .\n- : aligned SQLite certificate identity keying with missing-AKI issuer+serial dedup semantics to avoid dropped certs on persistence; added regression coverage in .\n- and : added/updated regression tests for malformed CSR scanning, table-driven issuer selection scenarios, JKS trusted-extra retention, and chained-entry selection in multi-entry JKS.\n\nAlso ran no commit to main........................................................Passed
branch name..............................................................Passed
goimports................................................................Passed
go fix...................................................................Passed
go vet...................................................................Passed
golangci-lint............................................................Passed
wasm.....................................................................Passed
go build.................................................................Passed
go test..................................................................Passed
govulncheck..............................................................Passed
gendocs..................................................................Passed
prettier.................................................................Passed
vitest...................................................................Passed
wrangler build...........................................................Passed
markdownlint.............................................................Passed successfully before commit.

@danielewood
Copy link
Copy Markdown
Collaborator Author

Correction to my prior note (shell escaped formatting issue). Addressed reviewer feedback in fe88d6e:\n\n- internal/certstore/container.go: JKS selection now prefers the first key entry with a certificate chain, keeps key/cert pairing safe (no unrelated trusted cert fallback), and includes trusted JKS certs in extra cert output when a key-entry leaf is used.\n- certkit.go: removed error-string control flow in ParsePEMPrivateKeyWithPasswords, added explicit slog.Debug skip logging for malformed/failed key and cert parse paths, and made ParsePEMCertificateRequest continue scanning past malformed CSR blocks.\n- sign.go + sign_test.go: introduced sentinel ErrCAKeyMismatch and updated test assertions to errors.Is.\n- internal/certstore/sqlite.go: aligned SQLite certificate identity keying with missing-AKI issuer+serial dedup semantics to avoid dropped certs on persistence; added regression coverage in internal/certstore/sqlite_test.go.\n- certkit_test.go and internal/certstore/container_test.go: added/updated regression tests for malformed CSR scanning, table-driven issuer selection scenarios, JKS trusted-extra retention, and chained-entry selection in multi-entry JKS.\n\nAlso ran pre-commit run --all-files successfully before commit.

Copy link
Copy Markdown

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

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: fe88d6ebdf

ℹ️ 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 internal/inspect.go Outdated
// Try private key
if key, err := certkit.ParsePEMPrivateKeyWithPasswords(data, passwords); err == nil {
results = append(results, inspectKey(key))
if keys, err := certkit.ParsePEMPrivateKeys(data, passwords); err == nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve valid key results when one PEM key block fails

inspectPEMData now gates key inspection on ParsePEMPrivateKeys(data, passwords) succeeding, but ParsePEMPrivateKeys is fail-fast for any malformed/unsupported key block. In PEM bundles that contain at least one valid private key plus one bad key block (for example a corrupted or unsupported key later in the file), this drops all private_key results instead of reporting the valid key(s), which is a regression for inspect output robustness.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch — fixed in c093758. inspectPEMData now scans PEM key blocks one-by-one and calls ParsePEMPrivateKeyWithPasswords per block, logging and skipping malformed key blocks instead of dropping all key results. Added regression coverage in TestInspectFile_PEMMalformedKeyAndValidKey to ensure valid keys are still reported when a malformed key block appears in the same bundle.

@danielewood
Copy link
Copy Markdown
Collaborator Author

Follow-up: all review feedback has been addressed and fixes pushed. Please take another look when you can.

Copy link
Copy Markdown

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

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: c093758328

ℹ️ 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 certkit.go Outdated
Comment on lines +659 to +660
if err := cert.CheckSignatureFrom(candidate); err == nil {
return candidate
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep issuer DN match when auto-selecting OCSP issuer

The fallback branch in SelectIssuerCertificate accepts any candidate where CheckSignatureFrom succeeds, even if cert.RawIssuer does not match the candidate subject; CheckSignatureFrom only validates signature/key usage and does not enforce issuer-name equality. In bundles containing two issuer certs that reuse the same signing key but have different subject encodings/names (common with cross-signing or reissued intermediates), this can select the wrong issuer cert, causing OCSP requests to be built with the wrong issuer-name hash and fail with unknown/no match despite having a valid issuer in ExtraCerts.

Useful? React with 👍 / 👎.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@claude

This comment has been minimized.

@danielewood
Copy link
Copy Markdown
Collaborator Author

Completed another review-feedback sweep and addressed the new actionable items from comments after c093758.\n\nChanges pushed in 46c2132:\n- Added before all error-path branches in so skipped trusted/key-entry parse failures are observable (ERR-5).\n- Tightened to require issuer DN match + signature validity (removed fallback that could pick a DN-mismatched issuer with reused key material), and added debug logging for rejected candidates.\n- Consolidated duplicate CSR scan tests into a single table-driven test () to satisfy T-12 while preserving coverage.\n- Updated under Unreleased/Fixed for this behavior change set.\n\nValidation: no commit to main........................................................Passed
branch name..............................................................Passed
goimports................................................................Passed
go fix...................................................................Passed
go vet...................................................................Passed
golangci-lint............................................................Passed
wasm.....................................................................Passed
go build.................................................................Passed
go test..................................................................Passed
govulncheck..............................................................Passed
gendocs..................................................................Passed
prettier.................................................................Passed
vitest...................................................................Passed
wrangler build...........................................................Passed
markdownlint.............................................................Passed passed before commit.

@danielewood
Copy link
Copy Markdown
Collaborator Author

Correction (previous comment had shell-escaped formatting glitches).\n\nCompleted another review-feedback sweep and addressed the new actionable items from comments after c093758.\n\nChanges pushed in 46c2132:\n- Added slog debug logging before all error-path continue branches in DecodeJKSKeyEntries so skipped trusted/key-entry parse failures are observable (ERR-5).\n- Tightened SelectIssuerCertificate to require issuer DN match plus signature validity, and removed the fallback that could pick a DN-mismatched issuer with reused key material.\n- Consolidated duplicate CSR scan tests into a single table-driven test: TestParsePEMCertificateRequest_SkipsBadBlocksBeforeValidCSR (T-12).\n- Updated CHANGELOG.md under Unreleased/Fixed for this follow-up behavior change.\n\nValidation: pre-commit run --all-files passed before commit.

@claude

This comment has been minimized.

@danielewood
Copy link
Copy Markdown
Collaborator Author

Completed another feedback sweep and found actionable new feedback after 46c2132. Addressed both ERR-5 items from the latest review note in 50c7a56:\n\n- Added before skipping non-private-key JKS aliases in \n- Added before breaking on malformed PKCS#8 key payloads in \n- Updated under Unreleased/Fixed for PR #107\n\nValidation: no commit to main........................................................Passed
branch name..............................................................Passed
goimports................................................................Passed
go fix...................................................................Passed
go vet...................................................................Passed
golangci-lint............................................................Passed
wasm.....................................................................Passed
go build.................................................................Passed
go test..................................................................Passed
govulncheck..............................................................Passed
gendocs..................................................................Passed
prettier.................................................................Passed
vitest...................................................................Passed
wrangler build...........................................................Passed
markdownlint.............................................................Passed passed locally.

@danielewood
Copy link
Copy Markdown
Collaborator Author

Correction to my prior comment (shell interpolation mangled formatting).

Completed another feedback sweep and found actionable new feedback after 46c2132. Addressed both ERR-5 items from the latest review note in 50c7a56:

  • Added slog.Debug before skipping non-private-key JKS aliases in DecodeJKSKeyEntries
  • Added slog.Debug before breaking on malformed PKCS#8 key payloads in DecodeJKSKeyEntries
  • Updated CHANGELOG.md under Unreleased/Fixed for PR fix: harden core parsing and identity selection #107

Validation: pre-commit run --all-files passed locally.

@danielewood danielewood merged commit 7ec9a4b into develop Mar 2, 2026
@danielewood danielewood deleted the fix/core-parsing-identity branch March 2, 2026 00:52
Copy link
Copy Markdown

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

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: 920843de4c

ℹ️ 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 certkit.go
Comment on lines +165 to +169
for {
var block *pem.Block
block, rest = pem.Decode(rest)
if block == nil {
break
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop scanning after first key block in password parser

ParsePEMPrivateKeyWithPasswords now keeps scanning all key PEM blocks after a failed parse/decrypt attempt, so a file with an encrypted first key and an unencrypted later key will return the later key when the provided password is wrong for the first block. This changes failure into silent key substitution, and callers that expect one key file to map to one signer (for example CSR generation via internal/csr.go) can proceed with an unintended key instead of surfacing a decryption error.

Useful? React with 👍 / 👎.

@claude
Copy link
Copy Markdown

claude bot commented Mar 2, 2026

Code review

Two ERR-5 violations found in new code — missing slog.Debug before continue in type-filter loops.

Per CLAUDE.md ERR-5: "Use continue in loops only with a slog.Debug explaining why."

This PR already adds slog.Debug for equivalent type-filter continue cases in ParsePEMCertificates (fixed during this PR) and DecodeJKSKeyEntries, but two spots were missed.


Issue 1 — certkit.go: ParsePEMPrivateKey and ParsePEMPrivateKeyWithPasswords

Both functions silently skip non-key PEM blocks without a debug log:

certkit/certkit.go

Lines 113 to 119 in 920843d

break
}
if !keyBlockTypes[block.Type] {
continue
}
singlePEM := pem.EncodeToMemory(block)

certkit/certkit.go

Lines 169 to 175 in 920843d

break
}
if !keyBlockTypes[block.Type] {
continue
}
singlePEM := pem.EncodeToMemory(block)

Suggested fix for both:

if !keyBlockTypes[block.Type] {
    slog.Debug("skipping non-key PEM block", "block_type", block.Type)
    continue
}

Issue 2 — internal/inspect.go: inspectPEMData

Same pattern — silently skips non-key PEM blocks in the new key-scanning loop:

certkit/internal/inspect.go

Lines 103 to 109 in 920843d

break
}
if !isPrivateKeyPEMBlockType(block.Type) {
continue
}
key, err := certkit.ParsePEMPrivateKeyWithPasswords(pem.EncodeToMemory(block), passwords)

Suggested fix:

if !isPrivateKeyPEMBlockType(block.Type) {
    slog.Debug("skipping non-key PEM block during inspect", "block_type", block.Type)
    continue
}

@claude
Copy link
Copy Markdown

claude bot commented Mar 2, 2026

Code review

One category of ERR-5 (MUST) violations found in certkit.go: five continue statements that lack the required slog.Debug call. Every other continue in the same functions consistently includes slog.Debug, making these look like oversights.


ERR-5 violations

Rule: ERR-5 (MUST)Use continue in loops only with a slog.Debug explaining why.

1. ParsePEMPrivateKey — type-filter guard (certkit.go ~L115)

certkit/certkit.go

Lines 113 to 118 in 920843d

break
}
if !keyBlockTypes[block.Type] {
continue
}

Missing slog.Debug before continue. All other continue statements in the file include one.

Fix:

if !keyBlockTypes[block.Type] {
    slog.Debug("skipping non-key PEM block", "block_type", block.Type)
    continue
}

2. ParsePEMPrivateKeyWithPasswords — type-filter guard (certkit.go ~L171)

certkit/certkit.go

Lines 169 to 174 in 920843d

break
}
if !keyBlockTypes[block.Type] {
continue
}

Same pattern as above. This function has five other continue statements — all with slog.Debug. This one is the outlier.

Fix:

if !keyBlockTypes[block.Type] {
    slog.Debug("skipping non-key PEM block", "block_type", block.Type)
    continue
}

3. ParsePEMCertificateRequest — block-type guard (certkit.go ~L311)

certkit/certkit.go

Lines 309 to 314 in 920843d

break
}
if block.Type != "CERTIFICATE REQUEST" && block.Type != "NEW CERTIFICATE REQUEST" {
continue
}
csr, err := x509.ParseCertificateRequest(block.Bytes)

The immediately following continue (for malformed DER) correctly includes slog.Debug("skipping malformed certificate request PEM block", ...). This guard should too.

Fix:

if block.Type != "CERTIFICATE REQUEST" && block.Type != "NEW CERTIFICATE REQUEST" {
    slog.Debug("skipping non-CSR PEM block", "block_type", block.Type)
    continue
}

4. SelectIssuerCertificate — nil guard and DN-mismatch guard (certkit.go ~L635–639)

certkit/certkit.go

Lines 633 to 644 in 920843d

var fallback *x509.Certificate
for _, candidate := range candidates {
if candidate == nil {
continue
}
if !bytes.Equal(cert.RawIssuer, candidate.RawSubject) {
continue
}
if err := cert.CheckSignatureFrom(candidate); err != nil {
slog.Debug("skipping candidate with invalid issuer signature", "error", err)
continue
}

The third continue (signature failure, ~L641) correctly includes slog.Debug("skipping candidate with invalid issuer signature", ...). The nil-guard and DN-mismatch guards on lines 635–639 do not.

Fix:

if candidate == nil {
    slog.Debug("skipping nil issuer candidate")
    continue
}
if !bytes.Equal(cert.RawIssuer, candidate.RawSubject) {
    slog.Debug("skipping issuer candidate with mismatched subject DN", "candidate_subject", candidate.Subject.String())
    continue
}

No bugs found.

danielewood added a commit that referenced this pull request Mar 2, 2026
* fix(wasm): harden ingestion and require explicit unverified export (#105)

* fix(wasm): harden ingest and make unverified export explicit

* fix: address PR 105 reliability review feedback

* fix(web): enforce upstream timeout through body reads

* fix: address latest PR 105 review feedback

* fix(crl): clarify read errors and harden size-limit coverage

* fix: harden core parsing and identity selection (#107)

* fix: harden parsing and issuer/key selection correctness

* docs(changelog): reference PR for unreleased parsing fixes

* fix: prevent parser fallback and JKS identity regressions

* fix(inspect): preserve valid keys when PEM bundle has malformed blocks

* fix: address remaining PR 107 review feedback

* fix(jks): surface skipped-entry reasons in debug logs

* fix(scan): harden scan boundaries and export text summary (#106)

* fix(scan): keep traversal bounded and restore export summaries

* fix(scan): fail fast on walker processing errors

* fix(scan): use typed max-size errors in read paths

* fix(scan): reject invalid export formats consistently

* fix(scan): keep export destination off stdout

* fix(network): harden SSRF fetch validation and apply default connect timeout (#108)

* fix(network): harden revocation fetch SSRF checks and connect timeout defaults

* fix(network): propagate SSRF validation deadlines and unblock inspect AIA opt-in

* fix(bundle): restore private-network opt-in for AIA chain fetches

* fix(network): address remaining PR feedback for inspect AIA handling

* fix(wasm): keep AIA resolution working without DNS lookups

* fix(cli): normalize validation exits, JSON schema, and export defaults (#109)

* fix(cli): align validation exits and secure export defaults

* fix(cli): address PR #109 review feedback

* fix(tests): align PR #109 follow-up feedback

* fix(security): remediate current code scanning alerts

* build(hooks): consume shared develop branch-name exemption

* fix: address PR 110 review feedback
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants