Skip to content

v1 major improvements and fixes and migration to Eclipse - #28

Draft
Nicolas-Peiffer wants to merge 60 commits into
masterfrom
remove-ml-kem-after-draf6-jose-cose
Draft

v1 major improvements and fixes and migration to Eclipse#28
Nicolas-Peiffer wants to merge 60 commits into
masterfrom
remove-ml-kem-after-draf6-jose-cose

Conversation

@Nicolas-Peiffer

Copy link
Copy Markdown
Contributor

No description provided.

but for now crypto11's tag is rc1

Signed-off-by: Nicolas-Peiffer <102670102+Nicolas-Peiffer@users.noreply.github.com>
Signed-off-by: Nicolas-Peiffer <102670102+Nicolas-Peiffer@users.noreply.github.com>
Signed-off-by: Nicolas-Peiffer <102670102+Nicolas-Peiffer@users.noreply.github.com>
Signed-off-by: Nicolas-Peiffer <102670102+Nicolas-Peiffer@users.noreply.github.com>
Signed-off-by: Nicolas-Peiffer <102670102+Nicolas-Peiffer@users.noreply.github.com>
…weProtectedHeader and JweRfc7516Compact respectively. Jwe.Unmarshal inline DEPRECATED comment converted to standard GoDoc format
interfaces.go — Get(issuer, kid string) → Get(ctx context.Context, issuer, kid string)
keystore.go — TrustKeyStoreImpl.Get updated; ctx is _ since the store is in-memory
jwks_truststore.go — httpClient interface: Get(url) → Do(req *http.Request); JwksTrustStore.Get accepts and threads ctx via http.NewRequestWithContext; TODO comment removed
jwt_verifier.go — passes context.Background() (keeps JwtVerifier interface stable)
jwks_truststore_test.go — mock updated to Do, all ioutil.NopCloser → io.NopCloser, all store.Get calls pass context.Background()
keystore_test.go — same
jwt_verifier_test.go — mock Get signature updated

Signed-off-by: Nicolas-Peiffer <102670102+Nicolas-Peiffer@users.noreply.github.com>
…me.Now in the constructor. Verify now calls verifier.now() instead of time.Now()
… verifier.now = atUnix(...) — which works cleanly since the test is in the same package
Replaced the single overloaded rsaBitsToAlg (with its TODO) with two clear functions: rsaBitsToAlg for signing (PS family by NIST recommendation) and rsaAlgFromOps which picks AlgRSAOAEP when the operations include encrypt/decrypt, falling back to rsaBitsToAlg otherwise.

Both JwkFromPrivateKey and JwkFromPublicKey now use rsaAlgFromOps, which also fixes the HSM path (hsm/asymmetric_decryption_key.go) that was silently getting a wrong PS algorithm for an encryption key
Replaced the // TODO: add symmetric verification. stub with an explanatory comment describing the actual design constraint: the VerificationKey interface includes Certificates() and MarshalPem() which have no meaningful implementation for symmetric keys
added identical doc comments to algToOptsMap (signer.go), gcmAlgToEncMap (jwe_direct_encryptor_aead.go), and cbcAlgToEncMap (jwe_direct_encryptor_block.go) making it explicit they are initialized once and must not be mutated at runtime.
CrvMLKEM512/768/1024 — parameter set identifiers

Six AlgMLKEM* constants — three direct key-agreement (MLKEM768-KMAC256, etc.) and three with AES key-wrapping
EncapsPubMlKemKey interface — Encapsulate() → (kemCiphertext, sharedSecret, err)
DecapsPrivMlKemKey interface — Decapsulate(kemCiphertext) → (sharedSecret, err) + Encapsulator()
DecapsPrivMlKemKeyStore interface — Get(kid) → DecapsPrivMlKemKey
EncapsPubMlKemKey struct — JWK for public key (kty=LWE, crv, x)
DecapsPrivMlKemKey struct — JWK for private key (adds d seed)
UnmarshalJwk updated to dispatch on KtyLWE, distinguishing pub vs priv by whether d is populated
Encapsulate() calls ek.Encapsulate() (method, not package function; returns sharedKey, ciphertext in that order — reordered to match the interface's kemCiphertext, sharedSecret)
Jwk() reconstructs a jose.EncapsPubMlKemKey from the live key bytes
NewEncapsPubMlKemKeyImpl validates key_ops, switches on curve, returns a clear error for ML-KEM-512
mlkem_private.go — DecapsPrivMlKemKeyImpl + GenerateMlKemKeyPair

mlkem_private.go — DecapsPrivMlKemKeyImpl + GenerateMlKemKeyPair

Decapsulate() delegates to dk.Decapsulate(ciphertext)
Encapsulator() derives the public key from the live decapsulation key (no need to store separately)
GenerateMlKemKeyPair(crv, kid, alg) generates a fresh pair and returns both impls, ready for use
hsm/mlkem_key.go

EncapsPubMlKemHsmKey — wraps crypto11.MLKEMEncapsulator; calls Encapsulate() on the HSM, extracts shared secret via ss.Bytes() (requires CKA_EXTRACTABLE=true in template), returns (kemCt, ssBytes)
DecapsPrivMlKemHsmKey — wraps crypto11.MLKEMKeyPair; private key stays on HSM, only 32-byte shared secret crosses the boundary for the software KMAC KDF
sharedSecretTemplate() — transient CKO_SECRET_KEY / CKK_AES / 32 bytes / CKA_EXTRACTABLE=true (same as k8s-kms-plugin)
paramSetToAlg — maps MLKEM512/768/1024 → AlgMLKEM512KMAC128/AlgMLKEM768KMAC256/AlgMLKEM1024KMAC256
Both types have compile-time interface assertions

hsm/mlkem_key_store.go

DecapsPrivMlKemHsmKeyStore — Get(kid) calls ctx.FindMLKEMKeyPair([]byte(kid), nil), wraps result in DecapsPrivMlKemHsmKey
…yPair that returns an unrecognised paramSet value, confirming the constructor rejects it with the right message. No HSM needed since the failure happens before any crypto11 call.

TestDecapsPrivMlKemHsmKeyStore_Get_InvalidHexKid — passes a non-hex string to Get with a nil context; the function returns before ever touching ctx, so no HSM needed there either.
Nicolas-Peiffer and others added 27 commits June 17, 2026 18:21
Add the shared CI standard used across pkcs11-go / crypto11 / gose:
CI (build/vet/test), CodeQL, govulncheck, Gitleaks, OpenSSF Scorecard,
dependency review, golangci-lint and grouped Dependabot, plus
.golangci.yml. All go tooling runs in vendor mode so the local
`replace` directives resolve from vendor/. Remove the obsolete
.travis.yml (Go 1.13).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
draft-ietf-jose-pqc-kem-06 dropped JOSE from its scope and now covers
COSE only (retitled "PQ KEMs for COSE"); there is no standards-track
way left to express ML-KEM in JWE. Rather than ship a bespoke,
non-interoperable JWE envelope, pull ML-KEM support until a JOSE-side
draft exists again.

Removed:
- mlkem_private.go, mlkem_public.go
- jwe_mlkem_encryptor.go, jwe_mlkem_decryptor.go, jwe_mlkem_test.go
- hsm/mlkem_key.go, hsm/mlkem_key_store.go, hsm/mlkem_test.go

Trimmed (ML-KEM-specific pieces only, rest of each file untouched):
- interfaces.go: EncapsPubMlKemKey, DecapsPrivMlKemKey,
  DecapsPrivMlKemKeyStore
- jose/types.go: AlgMLKEM*, CrvMLKEM*, KtyLWE
- jose/jwk.go: EncapsPubMlKemKey/DecapsPrivMlKemKey JWK types and the
  KtyLWE case in UnmarshalJwk
- jose/jwe.go: the ek header field
Update go.mod, all internal imports, and doc/link references (README,
NOTICES, golangci-lint, Makefile) from github.com/ThalesGroup/gose to
github.com/eclipse-keypont/gose.
Pin GitHub Actions to the same commit SHAs used in pkcs11-go for
supply-chain consistency, replace the deprecated golint Makefile target
with golangci-lint (adding lint-fix), and add the missing Go Reference
badge to the README.
crypto11 moved to a real /v2 module path (breaking API: new PKCS#11
binding, ML-KEM support). Update gose's dependency accordingly and
use it as the integration test ahead of tagging crypto11 v2.0.0
stable.

- go.mod/go.sum: require github.com/eclipse-keypont/crypto11/v2
  v2.0.0-rc3 (fetched for real, no local replace)
- hsm/*.go: update the two crypto11 imports to the /v2 path
- Makefile: fix the notices ignore-list entry to the new import path
- Update go toolchain to go1.26.5

Dropping the local pkcs11-go replace as a side effect removed the
last reason CI needed vendor mode. vendor/ was never actually
committed (gitignored), so ci.yml/codeql.yml/lint.yml/govulncheck.yml
setting GOFLAGS=-mod=vendor meant a fresh checkout had no vendor tree
to build against - this is fixed now, matching how crypto11 already
resolved the same issue (a9d4f90).

Verified clean: build, vet, full test suite, golangci-lint (0
issues), govulncheck (0 vulnerabilities) all pass without vendor
mode or local replaces.
Each concrete JWK's UnmarshalJSON (PublicRsaKey, PrivateRsaKey,
PublicEcKey, PrivateEcKey, OctSecretKey) set err to
ErrUnexpectedKeyType when the JSON kty field didn't match the target
type, but then unconditionally overwrote err with the result of
CheckConsistency(). A genuine kty mismatch (e.g. unmarshalling an EC
key into a PublicRsaKey) was silently discarded whenever
CheckConsistency() happened to pass, so callers never saw the error.
Found while fixing ineffassign findings from golangci-lint.
Resolves all 78 findings from `make lint` (bodyclose, errcheck,
errorlint, gofmt, goimports, gosec, ineffassign, misspell, prealloc,
revive, staticcheck, unconvert, unused):

- close the JWKS HTTP response body and check/wrap Close errors
- justify the two intentional file-path-from-caller reads with
  #nosec G304
- add missing doc comments on exported types/methods (revive)
- drop dead code (unused base64EncodeUInt32 helper, unused cekAlg
  field)
- fix several tests that dropped or duplicated error checks
  (ineffassign)
- rename a local variable shadowing the len builtin
- drop unnecessary type conversions, preallocate a slice
Rewrites the module path to github.com/eclipse-keypont/gose/v2, as
required by Go's semantic import versioning for major version >=2,
and updates all internal imports, golangci-lint local-prefixes, the
go-licenses ignore list, and the pkg.go.dev README badge to match.

Reflects the breaking API surface introduced by the crypto11/v2 and
pkcs11-go migration (replacing miekg/pkcs11).
Documents the v2.0.0 breaking changes (pkcs11-go/crypto11-v2 migration,
/v2 module path, ML-KEM removal), fixes, and additions in one place,
and links it from a changelog badge and an "upgrading from v1?" note
so README readers know it exists.
gose has been v0.x with no stability contract since its start. The
/v2 module path convention exists to let a real v1 and v2 coexist for
consumers migrating between them; there's no v1 of gose to coexist
with, so it buys nothing here and just adds import-path churn.

Depending on crypto11/v2 doesn't require gose itself to be v2 — a
library's major version reflects its own API stability, not its
dependencies'. This is gose's first stable release, so it's v1.0.0.

Reverts the module path, internal imports, golangci-lint
local-prefix, go-licenses ignore list, and README badges back to the
non-suffixed path, and reframes CHANGELOG.md as v0.x -> v1.0.0.
…S.md

Two separate hazards in the same file.

The `command -v` guards on lint and govulncheck asked whether a name resolved,
not whether it ran. Under goenv that is always true: a shim stays on PATH for
every tool it has ever seen, so the guard passed and the recipe then died with
"goenv: '<tool>' command not found" instead of printing the install hint. The
shared $(call require,...) macro probes by running the tool and treats exit
127 — what both a missing binary and a dead shim return — as missing.
lint-fix and notices had no guard at all and are now covered.

More seriously, notices redirected go-licenses straight into NOTICES.md, so any
failure truncated the committed file to zero bytes before anyone saw the error.
That is not hypothetical: go-licenses v1.6.0 currently fails against Go 1.26 on
stdlib packages (google/go-licenses#128), so running the target destroys
NOTICES.md and reports nothing useful. It now writes a temp file and moves it
into place only on success, matching crypto11.

The underlying go-licenses breakage is untouched here — `make notices` still
cannot regenerate the file, it just no longer destroys it in the attempt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RFC 7518 §4.3 defines "RSA-OAEP" as OAEP with SHA-1 and "RSA-OAEP-256" as
the SHA-256 variant. gose emitted "RSA-OAEP" for both, because the three
constants AlgRSAOAEP, AlgRSAOAEPSHA1 and AlgRSAOAEPSHA2 all held the same
string — the SHA-256 variant had no wire representation at all, so it could
not have been labelled correctly even in principle. The output round-tripped
against itself, but a conformant recipient derives SHA-1 from the header and
fails to unwrap the CEK, so these JWEs were not portable.

AlgRSAOAEPSHA2 becomes "RSA-OAEP-256". New rsa_oaep_alg.go holds the
alg<->digest mapping in one place, with OaepHashFromAlg / OaepAlgFromHash
exported for callers that need to inspect or label a JWE.

The encryptor now derives the header alg from the digest passed to Encrypt
rather than copying the recipient JWK's alg, and rejects digests RFC 7518
registers no OAEP algorithm for. Previously any digest was accepted and
labelled "RSA-OAEP", so SHA-512 produced a JWE that no alg value described.
The constructor also rejects RSA keys whose alg is not OAEP, checked after
the key-type parse so a non-RSA key still yields ErrInvalidKeyType.

The decryptor derives the digest from the header when passed crypto.Hash(0),
which is the conformant path. Its alg check is now family-based, since the
header names a variant while key.Algorithm() names the family.

Generated JWKs and hsm.AsymmetricDecryptionKey keep "RSA-OAEP" as their alg,
naming the OAEP family: the per-message digest comes from the JWE header, so
one key still serves both variants. SoftHSMv2 implements only SHA-1.

Existing ciphertexts stay readable. Passing a non-zero crypto.Hash to Decrypt
still overrides the header, which is how JWEs written by earlier versions
("RSA-OAEP" in the header, SHA-256 on the wire) are read; no data at rest
needs re-encrypting. Old headers cannot be rewritten in place instead, since
the protected header is the AEAD's additional authenticated data.

Tests: the RFC 7516 A.1 KAT now also passes with the digest derived from the
header; both variants round-trip with their emitted alg asserted; and
TestJweRsaKeyEncryptionDecryptorImpl_Decrypt_LegacyMislabelledJwe builds a
genuine pre-fix JWE and asserts header-derived decryption fails while the
override succeeds.

The CBOM in assets/cbom gains an RSA-OAEP-256 entry. It previously listed one
RSA-OAEP asset named RSA/ECB/OAEPWithSHA-1AndMGF1Padding, so SHA-256 wrapping
— always reachable, since the digest was a runtime parameter — was missing
from the inventory. RSA-OAEP-256 shares id-RSAES-OAEP (1.2.840.113549.1.1.7),
which carries the digest in RSAES-OAEP-params rather than a separate arc, and
uses the XML Encryption 1.1 URI, the 1.0 one being fixed to SHA-1.
cbom.cyclonedx.json was hand-edited to match algo-list.csv, xbom-manager not
being released yet.

BREAKING CHANGE: jose.AlgRSAOAEPSHA2 is now "RSA-OAEP-256" rather than
"RSA-OAEP". JWEs produced with crypto.SHA256 carry the new value and are
readable by other RFC 7518 implementations; peers pinned to the previous
mislabelled output will not interoperate until updated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Nicolas-Peiffer Nicolas-Peiffer added this to the v1.0.0 milestone Aug 6, 2026
@Nicolas-Peiffer Nicolas-Peiffer self-assigned this Aug 6, 2026
@Nicolas-Peiffer Nicolas-Peiffer added the enhancement New feature or request label Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant