Skip to content

add support for pkcs11 v3.2 ml-kem - #131

Draft
Nicolas-Peiffer wants to merge 69 commits into
masterfrom
pkcs11-v3.2-ml-kem
Draft

add support for pkcs11 v3.2 ml-kem#131
Nicolas-Peiffer wants to merge 69 commits into
masterfrom
pkcs11-v3.2-ml-kem

Conversation

@Nicolas-Peiffer

Copy link
Copy Markdown
Contributor
  • README: add support for new PQC crypto algorithms
  • new integration testing with SoftHSMv3
  • rename config file to crypto11.config.json
  • add support for ML-KEM
  • various golang linting

Signed-off-by: Nicolas-Peiffer <102670102+Nicolas-Peiffer@users.noreply.github.com>
Signed-off-by: Nicolas-Peiffer <102670102+Nicolas-Peiffer@users.noreply.github.com>
this is temporary we wait on the PR to be merged
…tAccessSameLibraryTwice uses FindKeys; TestNoLogin cleans up unexpected key and skips; TestInvalidPinDoesntDestroyLibrary skips gracefully when tokens absent
…oken2" for TestInvalidPinDoesntDestroyLibrary
Algorithm support tables covering RSA, ECDSA, DSA, ML-KEM (all three security levels), AES, DES3, certificates, HMAC, and RNG — with which operations each supports
ML-KEM noted as requiring PKCS#11 v3.2 and SoftHSMv3
New SoftHSMv3 testing section explaining the automated SOFTHSM3_MODULE workflow
SoftHSMv2 section updated with a markdown link and a note that ML-KEM tests auto-skip on v2
aead.go (HIGH): copy the HSM-generated GCM IV back into the caller's
nonce slice whenever UseGCMIVFromHSM is set. The previous code only
length-checked it, so the caller never learned the actual IV — making
the ciphertext undecryptable or, worse, enabling GCM nonce reuse.

common.go (MEDIUM): fix out-of-bounds read in bytesToUlong. The prior
unsafe dereference always read sizeof(CK_ULONG) bytes regardless of
slice length, reading past the end of shorter attributes like a 4-byte
CKA_PARAMETER_SET. Copy into a zero-padded [sizeof_ulong]byte first.

crypto11.go (MEDIUM):
- Zero object handles to CK_INVALID_HANDLE after a successful Delete
  so any subsequent use fails loudly instead of silently targeting a
  recycled handle on a different key.
- Make Close idempotent with sync.Once; a second Close previously
  triggered a refcount panic inside moduleCtx.Close.
- Key the module cache by absolute path before the lookup, not after,
  so two spellings of the same library share one C_Initialize call.
- Wipe the transient PIN []byte with pkcs11.Wipe immediately after
  Login and clear cfg.Pin on the retained copy; take a defensive
  shallow copy of *Config at Configure entry so callers are never
  affected by our default-filling or PIN clearing.

mlkem.go (LOW): bounds-check the GetAttributeValue result in Bytes()
before indexing; zero pubKeyHandle after Delete; reject unknown
paramSet values with a clear error before generating a key pair.
Nicolas-Peiffer and others added 8 commits June 17, 2026 17:54
Add the shared CI standard used across pkcs11-go / crypto11 / gose:
CodeQL, govulncheck, Gitleaks, OpenSSF Scorecard, dependency review,
golangci-lint and grouped Dependabot, plus .golangci.yml.

Resolve the local `replace => ../pkcs11-go` directive via the committed
vendor/ tree (GOFLAGS=-mod=vendor), dropping the previous checkout-and-
copy-sibling steps. CI keeps the SoftHSM2 test job.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…x URL

Align with the shared env var convention across the four repos:
- SOFTHSM3_MODULE → PKCS11_MODULE
- SOFTHSM3_PIN    → PKCS11_PIN

Add a skip-all guard in TestMain: when neither PKCS11_MODULE nor
crypto11.config.json is present (CI without a provisioned HSM), exit 0
instead of failing every test with CKR_GENERAL_ERROR.

Drop the softhsm2-util pre-init from ci.yml — TestMain now bootstraps
tokens entirely via the PKCS#11 API when PKCS11_MODULE is set, so no
external tool is needed. ML-KEM and v3.2 tests self-skip on SoftHSM2.

Fix SoftHSMv3 repository URL: pqctoday/softhsmv3 →
pqctoday-org/pqctoday-hsm.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Separate the go directive (minimum consumer requirement, no patch pin)
from the toolchain directive (maintainer build toolchain), following
Go 1.21+ semantics. Closes #137.

Remove the hand-written min(a, b int) helper — the Go 1.21 builtin
covers this generically.

Document the two-directive pattern in README.
makeKeyPair, makePrivateKey and makeRSAKeyPair return an error for any
CKA_KEY_TYPE this package cannot represent, but the loops that call them
only skipped errNoCkaID and errNoPublicHalf. Every other error aborted the
whole walk, so a single object of an unknown type made FindAllKeyPairs,
FindAllKeys, FindPrivateKeysWithAttributes, FindKeyRSAPairsWithAttributes
and FindAllPairedCertificates fail outright with "unsupported key type: %X",
hiding every other key on the token.

v2 turns that six-year-old report into a regression we ship ourselves: an
ML-KEM private key is a CKO_PRIVATE_KEY whose type none of those finders
understand, so as soon as a user generates an ML-KEM key pair, enumerating
the rest of that token stops working.

Wrap the "unsupported key type" errors in a new errUnsupportedKeyType
sentinel and skip it in the enumeration loops, next to errNoCkaID and
errNoPublicHalf. FindKeysWithAttributes does the same for symmetric keys
with no matching Cipher, and makeRSAKeyPair's "not an RSA key pair" wraps
the sentinel too, so an RSA-only finder walking a mixed token no longer
trips over the first EC key it meets.

Singular lookups (FindKeyPair, FindKey, FindPrivateKey) now report "not
found or is empty" instead of "unsupported key type" for such keys, which
matches how they already behave when the public half is missing.

Adds a regression test that puts an ML-KEM key pair and an RSA key pair on
one token and checks all four enumerators still succeed.

Reported by @Knacktus in #68 back in 2020, and independently run into by
@droppingin, who cross-referenced it from #103 while fixing the Windows
CK_ULONG conversions. Thank you both — the report was accurate the whole
time, it just took us until the ML-KEM work to feel the pain ourselves.

Fixes #68
Refs #103

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nicolas-Peiffer and others added 2 commits August 3, 2026 11:53
Requires pkcs11-go v1.1.0-rc1, which exports the CK_ULONG codec this package
had been carrying a broken copy of.

CK_ULONG is a C unsigned long: 8 bytes under LP64 (Linux, macOS), 4 under
Windows' LLP64 model and on 32-bit targets. Go's uint is 8 bytes everywhere.
bytesToUlong reinterpreted the address of its buffer as a Go uint, which
always reads 8 bytes, so on Windows it read 4 bytes past a 4-byte CK_ULONG
buffer and returned whatever sat behind it in the top half of the value.
Every CK_ULONG attribute was affected — CKA_KEY_TYPE, CKA_MODULUS_BITS,
CKA_VALUE_LEN — which is how this first surfaced, as FindAllKeyPairs failing
under SoftHSM2 on Windows. The v2 security audit fixed the neighbouring
out-of-bounds read on short attributes (a 4-byte CKA_PARAMETER_SET) by
copying into a sizeof(CK_ULONG) buffer first, but kept the same width
assumption when reading that buffer back, so on Windows the read only moved
from overrunning the caller's slice to overrunning our own buffer.

Rather than fix the copy here, delete it. Three changes, all of them moving
C-ABI knowledge down into the one package that legitimately holds the
PKCS#11 headers:

1. ulongToBytes / bytesToUlong are gone; call sites use
   pkcs11.ULongToBytes / pkcs11.BytesToULong, which size the conversion from
   the C type and carry the tests. common.go held the only import "C" in
   crypto11, so the package no longer contains any cgo of its own (it still
   builds with cgo, through the binding).

2. signPSS builds its parameter with NewMechanismWithParams and
   NewPSSParams instead of concatenating three CK_ULONGs by hand, so the
   binding marshals the real CK_RSA_PKCS_PSS_PARAMS struct. This is what the
   "TODO this is pretty horrible, maybe the PKCS#11 wrapper could be
   improved to help us out here" comment was waiting for; OAEP and GCM
   parameters were already built this way. concat had no other caller and is
   removed with it.

3. FindRSAPrivateKeysWithAttributes gets the same skip the other finders
   got in f2bc2f6 — it walks every CKO_PRIVATE_KEY on the token, so one EC
   or ML-KEM key used to abort the whole enumeration. makeRSAPrivateKey's
   error for a non-RSA key also wrapped a nil err, rendering as
   "not an RSA key type: %!w(<nil>)" and matching no sentinel at all; it now
   reports the key type it found and wraps errUnsupportedKeyType.

Reported by @droppingin in #103, with the analysis, the C data-model
references and a SoftHSM2-for-Windows reproduction that made this easy to
confirm. That PR sized the encoding by the magnitude of the value rather
than by the platform's CK_ULONG, which would have shortened
CK_MAC_GENERAL_PARAMS and CK_RSA_PKCS_PSS_PARAMS on LP64; the binding sizes
them from the C type instead. Thanks also to @Knacktus, whose #68 covers the
enumeration failure this surfaced through.

Refs #68, #103#103

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
signPSS returned errUnsupportedRSAOptions for rsa.PSSSaltLengthAuto, behind
a TODO saying the biggest possible salt could in principle be worked out
from the key. It can, and crypto/rsa already does it:

  (modulus_bits - 1 + 7)/8 - hash_length - 2

For a 4096-bit modulus and SHA-256 that is (4096-1+7)/8 - 32 - 2 = 478.

The salt is the largest the encoded message can carry, so callers that pass
the zero-valued PSSOptions.SaltLength — the crypto.Signer default, and what
several callers hand us without thinking about salts at all — now get a
signature rather than an error. Verifiers accept it either way:
PSSSaltLengthAuto recovers the salt length from the encoding.

The calculation moves into maxPSSSaltLength so it can be tested without a
token, and differs from #96 in one respect. That PR computes the length as
uint(k.N.BitLen()-1+7)/8 - 2 - hLen, all unsigned; a modulus too small for
the chosen hash (SHA-512 at 521 bits or below) makes that subtraction wrap
and yields a salt length near 2^64 instead of an error. Doing the
arithmetic in int and reporting rsa.ErrMessageTooLong on a negative result
matches what crypto/rsa returns for the same case.

errUnsupportedRSAOptions is still returned for PSSSaltLengthAuto when the
key's public half is not an *rsa.PublicKey, since there is no modulus to
size the salt from. Both doc comments that described Auto as unsupported are
updated.

testRsaSigningPSS now runs each hash against both Auto and EqualsHash, and
TestMaxPSSSaltLength covers the helper against the crypto/rsa expression for
1024- through 4096-bit keys across SHA-1..SHA-512, round-trips a signature
through VerifyPSS with Auto, and pins the two error paths. It skips the
4096-bit key under -short, where generating it dominates the runtime.

Note that a token is free to reject a salt length it does not implement;
SoftHSMv3 does not advertise CKM_RSA_PKCS_PSS at all, so TestHardRSA's PSS
subtests skip there and this path is covered natively.

Fixes the TODO with the calculation from #96 by @maraino, who also supplied
the worked 4096/SHA-256 example above and the crypto/rsa change it follows
(https://golang.org/cl/302230).

Refs #96#96

Co-Authored-By: Mariano Cano <mariano.cano@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nicolas-Peiffer and others added 2 commits August 3, 2026 17:36
Context keeps its *pool.ResourcePool unexported, along with every counter the
pool maintains, so there is no way to see how the session pool is coping —
issue #119 wanted them for metrics.

The pool is vendored under internal/pool, so its types cannot appear in the
public API at all: callers outside this module cannot name them. That rules
out returning *pool.ResourcePool or forwarding its accessors, and leaves the
second of the two shapes proposed in #119 — a struct of our own:

  func (c *Context) PoolStats() PoolStats

PoolStats mirrors all nine of the pool's accessors (Capacity, Available,
Active, InUse, MaxCapacity, WaitCount, WaitTime, IdleTimeout, IdleClosed) as
a plain value with no reference back to the pool, so it can be kept, copied
and marshalled. Durations marshal to JSON as nanoseconds, matching the
pool's own StatsJSON, which is what the first shape in #119 would have
returned.

#119 asked whether reading stats might cause unnecessary communication with
the token. It does not: every field is an atomic counter the pool already
maintains in memory, so PoolStats makes no PKCS#11 call and takes no session
from the pool. It is therefore safe on a metrics scrape path, concurrently
with any other operation, and on a closed Context, where it reports the pool
as it was torn down — Capacity zero, MaxCapacity intact. The fields are read
one at a time rather than under a lock, so a snapshot taken under load is
individually accurate but not internally consistent: Available and InUse
need not add up to Capacity. All of this is in the doc comments, since none
of it is guessable from the field names.

Two fields need explaining rather than hiding. Capacity is one below the
effective MaxSessions, because crypto11 holds a session back to keep the
login state alive. IdleTimeout and IdleClosed are always zero, since
Configure builds the pool with no idle timeout and nothing else changes it;
they are reported anyway so the struct stays a faithful mirror if that ever
becomes configurable, with a comment saying so.

A nil pool returns the zero PoolStats rather than panicking. That is only
reachable for a Context that did not come from Configure.

Tests: TestPoolStatsFields drives all nine fields off a pool of fake
resources, including a blocking Get to move WaitCount and WaitTime, so the
mapping is covered without a token. TestPoolStatsMarshalsToJSON pins the
nanosecond encoding, TestPoolStatsNoPool the zero-value Context.
TestContextPoolStats then checks a real token: the counters before, from
inside a withSession callback (InUse 1, Available down one, Active at least
one), after the session is returned, and after Close.

Proposed by @eriklupander in #119, including the PoolStats shape this
follows and the question about talking to the device.

Refs #119#119

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ming

Some tokens return CKA_VALUE in a fixed-size buffer, null-padded past the
end of the certificate it holds. x509.ParseCertificate rejects the padding
as trailing data, so FindCertificate — and FindAllPairedCertificates, which
reaches the same code through makeKeyPair — failed outright on those tokens.

DER is self-delimiting, so the certificate's own length header says where it
ends. findCertificate now hands the attribute to parseCertificateValue,
which reads one ASN.1 element with asn1.Unmarshal into an asn1.RawValue,
parses RawValue.FullBytes, and requires whatever follows to be null bytes.

This differs from #106, which trims the buffer with bytes.Trim(raw, "\x00")
before parsing. Content-based trimming cannot tell padding from certificate:
the last byte of a certificate is the last byte of its signature, which is
effectively random, so roughly one certificate in 256 legitimately ends in a
null byte. Generating self-signed ECDSA certificates until one does — 81
attempts — and running both versions against it:

  baseline (no trim), unpadded:  parses OK
  #106, unpadded:                x509: malformed certificate
  #106, padded:                  x509: malformed certificate
  length-delimited, either way:  parses OK

So that one-in-256 breaks on tokens that pad, which the change was written
for, and also breaks on tokens that do not, which worked before. bytes.Trim
is bidirectional besides, so leading null bytes are stripped too: the buffer
no longer starts with the certificate, and reinterpreting it silently is
worse than reporting it. Trailing bytes that are not null are likewise an
error here rather than something to discard — a truncated or misreported
attribute should surface, not be papered over.

Certificate parsing moves out of findCertificate so it can be tested without
a token. TestParseCertificateValue covers plain DER, null-padded DER, and a
certificate whose signature ends in a null byte both unpadded and padded —
the case #106 gets wrong, generated by looping until one turns up rather
than hardcoding a fixture. TestParseCertificateValueRejectsGarbage pins the
error paths: non-null trailing data, leading null bytes, nil input, and a
certificate truncated in half then null-padded back to its original length,
which is precisely the corruption a padding-tolerant parser must not accept.

Reported by @donachan-tesla in #106, with the null-padded tokens that make
this reachable at all.

Refs #106#106

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The four FindPrivateKey* methods have had no direct test coverage since
they landed in #115 — the only exercise was one FindRSAPrivateKey subtest
in rsa_test.go, which touches neither attribute matching nor argument
validation.

Port the tests proposed in PR #111, adapted to this
branch's cryptoki binding:

  - TestFindKeysRequiresIdOrLabel gains assertions that FindPrivateKey
    and FindPrivateKeys reject a nil id *and* nil label.
  - TestFindingPrivateKeysWithAttributes covers matching on CkaLabel
    (single and shared) and filtering on CkaKeyType.

Also add TestFindingPrivateKeyNotFound, which is not from #111. It pins
the current not-found behaviour, where the singular lookups return an
error while the plural ones return an empty slice. Note that this
contradicts the doc comments on FindPrivateKey and
FindPrivateKeyWithAttributes, which still promise "or nil if it cannot
be found"; the test documents what the code does today rather than
asserting it is correct.

The tests are unchanged in intent from mandelsoft's original, which
proposed them alongside a PrivateKey implementation that #115 has since
superseded.

#111

Co-Authored-By: Uwe Krueger <553075+mandelsoft@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r name

Issue #112 asked for the generic FindKeyPair* family to return
SignerDecrypter so existing key pairs could decrypt. PR #115 answered it
with a parallel RSA-specific API instead of a breaking signature change,
but never said so in the docs, so the issue stayed open for 19 months.
Close the gap without touching the generic signatures:

- Rename FindKeyRSAPairsWithAttributes to FindRSAKeyPairsWithAttributes.
  Every sibling is FindRSA*; this one was the odd one out, which made the
  RSA finders hard to grep for. Breaking rename, so v2 or never.
- Add FindAllRSAKeyPairs, the decryption-capable counterpart of
  FindAllKeyPairs. "Give me everything on this token I can decrypt with"
  had no one-call form.
- Cross-reference the two families: Signer and the FindKeyPair finders now
  point at SignerDecrypter and the FindRSA* finders, and SignerDecrypter
  points back at how to obtain one. Also fixes "crypto.Signer of
  SignerDecrypter" -> "or" in two rsa.go doc comments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nicolas-Peiffer and others added 2 commits August 4, 2026 12:01
Five assertions in keys_test.go counted every matching object on the
token rather than the ones the test created, so they only passed against
a token that started empty. On a persistent token — CloudHSM, a shared
dev HSM, or a rerun after a crash left keys behind — they failed:

  - TestFindingKeysWithAttributes, searching on CkaValueLen 16 and 32
  - TestFindingKeyPairsWithAttributes, searching on CkaKeyType CKK_RSA
  - TestFindingPrivateKeysWithAttributes, the same search
  - TestFindingAllKeys and TestFindingAllKeyPairs

Take the fix proposed in PR #79, by way of the idiom
this branch already uses in TestFindAllCertificates: identify the objects
by an attribute instead of counting them. #79 snapshotted a baseline
count and asserted n + len(baseline), which still races against anything
created concurrently and, more importantly, passes even when the finder
returns the wrong keys. Matching on CKA_ID does not.

Add two generic helpers, requireKeysFound and requireKeysAbsent, and
convert the five sites. Keys whose CKA_ID cannot be read are skipped
rather than failing the test, since they are by definition not ours.
Searches by label keep their exact counts: the labels are randomBytes(),
so those searches are already isolated and the count is the stronger
assertion.

The CkaValueLen searches also gain an exclusion check. require.Len was
the only thing asserting that a 256-bit key does not answer a
CkaValueLen=16 query; containment alone would have dropped that, so
requireKeysAbsent now states it directly.

TestFindingPrivateKeysWithAttributes is not in #79 — it landed in 62bbca8
and reproduced the same pattern.

#79

Co-Authored-By: Rob Fitzpatrick <19215121+ProsaicSatsuma@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FindCertificate needs an id, label or serial, and FindAllPairedCertificates
only returns certificates that have a matching private key. A caller that
knows nothing about a token — the case PR #71 was opened
for in August 2020 — had no way to list what is on it. Add the plain
enumerator, symmetric with FindAllKeys and FindAllKeyPairs.

Take the API from #71, with three changes to the implementation:

- Page C_FindObjects instead of taking a single call's worth. The original
  patch stopped at maxHandlePerFind, silently returning 20 certificates on a
  token holding more. Enumeration now goes through findKeysWithAttributes,
  which already pages and is not key-specific, so the batching is right in
  one place rather than two.
- Match only CKC_X_509 objects. A WTLS or attribute certificate is not an
  X.509 certificate and would fail x509.ParseCertificate, so a single one
  would hide every real certificate on the token — the shape of #68. The
  token filters them out instead.
- Parse via parseCertificateValue, not x509.ParseCertificate directly, so
  the null-padded CKA_VALUE handling from d5490a7 applies here too. The
  original patch would have regressed on the tokens #106 reported.

A CKA_VALUE that claims to be X.509 but does not parse is still an error
rather than a skip: that is corruption, not a kind of certificate this
package cannot represent. The error now names the object by CKA_ID and
CKA_LABEL, since failing anonymously part-way through a token full of
certificates says nothing about which one is at fault. The label is quoted,
not interpolated raw — it is arbitrary bytes chosen by whoever wrote the
object.

Document that enumeration is not trust. This API's whole audience is the
caller who knows nothing about the token, which is exactly the caller most
likely to feed the result into a trust store; anyone able to write to the
token can add a certificate.

Tests import a few certificates and then one more than a batch, matching on
full DER so the finder is shown to return the right bytes and not merely the
right count. They delete only what they imported: #71 proposed a
removeAllCertificates helper that destroys every certificate object on the
token, which is not something a test should do to a shared or production
HSM.

#71

Co-Authored-By: mekpavit <21259908+mekpavit@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 4, 2026
Nicolas-Peiffer and others added 3 commits August 6, 2026 15:18
FindCertificate returns one certificate and FindAllCertificates returns
every certificate unordered, so a caller needing the issuers above a leaf
— TLS being the obvious case — had to reassemble the chain itself. Add
the walk, closing issue #91.

Take the API and the algorithm from PR #83, which proposed it in June 2021
and cannot be rebased onto the current tree, with four changes:

- Walk iteratively and skip certificates already placed. #83 recursed and
  guarded against repeats by scanning its own return value, which is
  always empty at the point it is read, so the guard never fired. Two CAs
  cross-signing each other recursed until the stack was exhausted, and
  anything able to write to the token decides whether that happens.
- Settle the issuer by signature, not by name. #83 took the first object
  whose CKA_SUBJECT matched, so a token holding a renewed or cross-signed
  CA — two certificates, one distinguished name — returned whichever the
  token listed first and a chain that need not verify.
- Return a short chain when the issuer is absent. #83's identifier
  fallback raised an error when nothing matched, so leaf plus intermediate
  on the token with the root in the system trust store — the ordinary
  arrangement — failed the call. Its test imported the root as well and
  never saw it.
- Reuse findKeysWithAttributes and parseCertificateValue rather than
  adding a second pager and calling x509.ParseCertificate directly, which
  would have reverted d5490a7 for the null-padded CKA_VALUE of #106.

Only run the identifier scan when the certificate names an authority key
identifier: it costs a read of every certificate on the token, and an
empty identifier matches every certificate that has no subject key
identifier.

FindAllCertificates and the new walk now share findX509Certificates, so
the CKC_X_509 filter and the DER parsing stay in one place.

The API is not extended to finding or deleting certificates by arbitrary
attributes, which #83 also carried: that is unrelated to the chain and is
its own decision.

Tests cover the full chain, a missing root, a same-subject decoy CA
imported ahead of the real one, a subject attribute that disagrees with
the DER so only the key identifier can link the chain, and a cross-signed
cycle. Removing the signature check, the identifier fallback or the
already-placed guard each fails its test, the last by hanging.

#91
#83

Co-Authored-By: Oleksandr Grytsov <oleksandr_grytsov@epam.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FindAllPairedCertificates built a tls.Certificate holding the leaf and
nothing else, which is not enough to hand to crypto/tls: a peer that does
not already hold the intermediate cannot verify a leaf on its own. Every
caller serving TLS from a token had to fetch the issuers separately and
splice them in, which is the work issue #91 asked the library to do.

Fill Certificate from findIssuerChain, the walk added for
FindCertificateChain. Certificate[0] is still the leaf, so a caller
reading only the first entry is unaffected; the issuers the token holds
follow it in the order crypto/tls sends them.

The chain is what the token holds and no more. It ends where the issuers
run out, so intermediates kept elsewhere are still the caller's to supply,
and a self-signed root is returned rather than dropped — trimming a
trailing entry that is its own issuer is one line for a caller who would
rather not send it, whereas a root the library discarded cannot be
recovered.

Tests pair a generated key with a leaf by CKA_ID and import the two
issuers above it under unrelated ids, so they can only be reached by the
walk. Returning the leaf alone fails the test.

#91

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies update dependencies enhancement github-actions security Security related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant