Repository navigation
sec(oidc): check Azure Key Vault key type and curve before using it for ES256 - #1167
Conversation
…or ES256 resolveOnce read only X and Y from the Key Vault JWK and parsed them as a P-256 point, never looking at kty or crv. A P-384 or P-521 key failed with an opaque coordinate-length error, and a P-256K (secp256k1) key whose coordinates happened to parse on P-256 was accepted and would be published in the JWKS labeled as P-256. Require kty EC or EC-HSM and crv P-256 before building the public key, and name the key and the reported values in the error so the operator sees the misconfigured key instead of a downstream parse or sign failure. Closes LeanerCloud#46 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing: 2
✨ Finishing Touches
Comment |
|
Independent review 2 (claude-fleet, cold): APPROVE 79f5604 Findings (none blocking; a related low-severity note was shared privately with the maintainers per SECURITY.md):
Against the issue: the missing Crv check is fixed, together with the Kty check requested in the issue's review comment. It is not a duplicate of Attacked:
Verified locally, on a
Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged. |
|
VERDICT: APPROVE 79f5604 Scope: The change fixes #46 as the issue and its 2026-07-28 comment ask: Findings:
Attacked:
|
|
Local verification at
|
|
Marked ready and merged by the owner's agent after 2 independent reviews and local verification (see the review and verification comments above, all at |
Resolve the conflict in internal/oidc/azure_signer.go: main (LeanerCloud#1167) added a kty/crv check (validateES256KeyType) inside resolveOnce, and this branch replaced resolveOnce with fetchPublicKey behind publicKeyCache. The check now runs in fetchPublicKey at the same point, after the missing X/Y check and before the coordinate parse, and returns its error instead of memoizing it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Root cause
AzureKeyVaultSigner.resolveOnce(internal/oidc/azure_signer.go) read onlyXandYfrom the Key Vault JWK and parsed them withecdsa.ParseUncompressedPublicKey(elliptic.P256(), ...). It never looked atresp.Key.Ktyorresp.Key.Crv, even thoughSignalways requests ES256. So:EC key coordinate exceeds 32 bytes, which does not say what is actually wrong;"crv":"P-256";ktywas not rejected either.PublicJWKinjwks.goalready rejects non-P-256 keys; the Azure fetch side did not.Fix
Before building the point,
resolveOncenow calls a newvalidateES256KeyTypehelper. It requiresktyto beECorEC-HSMandcrvto beP-256, and treats a missing field as a failure. The error names the key and the values Key Vault reported, for example:This covers both parts of the issue: the original
Crvcheck and theKtycheck added in the 2026-07-28 review comment. The Terraform module already provisionskey_type = "EC",curve = "P-256"(terraform/modules/secrets/azure/signing-key.tf), so correctly provisioned deployments are not affected.The test fake
fakeAzureKeyVaultClientnow reportskty/crvthe way Key Vault does. By default it sendsECand the curve of the backing key, and it can override or omit either field. The other Azure fake, inbackend_jws_test.go, already set both fields.Closes #46
Verification
All of this is unit/fake evidence. No real Key Vault was called, so whether a live Key Vault always fills
kty/crvfor EC keys rests on the azkeys SDK model and the Key Vault REST docs, not on a live test. Tests were run withAWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/nullbecause the local~/.aws/configdefault profile is broken. That setting does not affect this package's logic.Fail before / pass after with the new
TestAzureSigner_RejectsNonP256Key(8 cases: EC P-256 accepted, EC-HSM P-256 accepted, P-384, P-521, P-256K with P-256-valid coordinates, missing crv, RSA kty, missing kty). Each rejected case also checks thatPublicKeyreturns nil, thatKeyIDreturns the same error, and that the error names the key.An error is expected but got nil. P-384 gets"...coordinate exceeds 32 bytes" does not contain "crv=\"P-384\"".Mutation probes, each applied separately and reverted. Every one makes
go test ./internal/oidc/fail:EC-HSMacceptancektyOK && crvOKchanged to||Other checks:
go build ./...: OKgo vet ./internal/oidc/: OKgo test ./internal/oidc/ -count=1andgo test -race ./internal/oidc/ -count=1: OKgo test ./internal/mocks/ -count=1: OKgolangci-lintv2.10.1run --allow-parallel-runners ./internal/oidc/...: 0 issuesgocyclo -over 10 -ignore "_test\.go" internal/oidc/: silent.resolveOnceis now at exactly 10; the new checks live in a helper to keep it at the limit.git-secrets --scanon both changed files, in a freshgit initrepo with an isolated HOME usingscripts/setup-git-secrets.shand.gitallowed: cleanscripts/test-git-secrets-allowlist.sh: 42 PASS, 0 FAILgofmt -landgit diff --check: cleanThere are no DB, migration or concurrency changes. The check runs inside the existing
sync.Once, so concurrent callers behave as before.Out of scope / notes
sync.Once, just like every otherresolveOncefailure today. That is the right result for a misconfigured key, which needs a redeploy anyway. Retrying after transient errors is handled separately by test(auth): cover remaining auth failure paths; fix invite re-send and sticky KMS signer errors #1152.resolveOncewithfetchPublicKey. Whichever of the two merges second will hit a small textual conflict: the four-linevalidateES256KeyTypecall moves intofetchPublicKeyasreturn nil, fmt.Errorf(...). I'll resolve it with a merge commit on whichever branch needs it.KeySpecand GCP reports the key version's algorithm, and whether those backends need the same check is a separate question.Maintainers: please mirror the issue's triage labels onto this PR:
bug,triaged,type/security,priority/p3,severity/low,urgency/eventually,impact/few,effort/s.🤖 Generated with Claude Code
Summary by CodeRabbit