Repository navigation
sec(oidc): switch JWT signing from PKCS1v15 to ES256 - #882
Conversation
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 58 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (12)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai full review Generated by Claude Code |
|
🧠 Learnings used✅ Action performedFull review finished. |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
|
@coderabbitai review |
Rate Limit Exceeded
|
|
@coderabbitai review |
Rate Limit Exceeded
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai full review |
|
Merge-prep: rebased onto origin/main (27fdb06). Conflicts in 4 OIDC signer files (commit 1: struct fields RSA->ECDSA) + 2 test files (commit 3: ECDSA test helpers). All resolved by taking PR's ECDSA version (security intent preserved). go build/vet/lint/gocyclo exit 0; gosec exit 0; all 14 OIDC tests pass. |
✅ Action performedFull review finished. You're currently rate limited under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. Your next review will be available in 2 minutes. |
|
Rebased onto main; fixed staticcheck SA1019 lint failure (the root cause of the Lint Code CI failure). Replaced deprecated |
Replace RSA-PKCS1v15 (RS256) with ECDSA P-256 (ES256) across the entire oidc package: - LocalSigner: ecdsa.GenerateKey(P256) + ecdsa.SignASN1 instead of rsa.GenerateKey + rsa.SignPKCS1v15 - Signer interface: PublicKey() returns crypto.PublicKey (was *rsa.PublicKey) - Algorithm constant: "ES256" (was "RS256") - ComputeKeyID: accepts crypto.PublicKey, hashes uncompressed EC point - JWK: EC fields (kty=EC, crv=P-256, x, y); RSA fields (n, e) removed - AWSKMSSigner: SigningAlgorithmSpecEcdsaSha256 + *ecdsa.PublicKey assertion - AzureKeyVaultSigner: SignatureAlgorithmES256 + EC key (X/Y) extraction - GCPKMSSigner: *ecdsa.PublicKey assertion (digest call is key-type-agnostic) - All tests: positive assertions on ES256 algorithm and EC JWK fields
…er backends (refs #422) Mint base64url-encoded the signer's raw output directly into the JWS signature segment, but the Local/AWS/GCP signers return DER/ASN.1 ECDSA signatures (ecdsa.SignASN1, AWS ECDSA_SHA_256, GCP EC sign all yield ASN.1 DER). RFC 7518 section 3.4 requires an ES256 JWS signature to be the raw fixed-length R || S concatenation (32 bytes each = 64 bytes for P-256), NOT DER. As a result AWS/GCP/Local minted tokens that real OIDC consumers (the stated Azure AD target) reject. Azure Key Vault already returns raw R||S, so only Azure was correct, leaving the four backends mutually inconsistent. The Azure Sign comment also wrongly claimed Key Vault returns DER. Contract: Signer.Sign now returns the RFC 7518 raw R||S form. DER backends (Local/AWS/GCP) convert via a single shared derToRawECDSASignature helper; Azure passes through unchanged (no double-conversion). Mint encodes the result directly and defensively rejects any signature that is not 64 bytes. - signer.go: add derToRawECDSASignature; LocalSigner.Sign converts; Mint enforces the 64-byte contract; document the contract on the Signer interface. - aws_signer.go, gcp_signer.go: convert KMS DER output to raw R||S. - azure_signer.go: fix the wrong "DER-encoded" comment; keep passthrough. - tests: add assertRawES256JWS asserting the JWS sig is exactly 64 bytes, splitting R||S for ecdsa.Verify, and parsing with a real golang-jwt ES256 parser. Cover Local, AWS (DER fake), GCP (DER fake) and Azure (raw fake). The DER-path tests fail on the pre-fix code (71/69-byte signatures) and pass after; the Azure raw path stays correct. - go.mod: promote golang-jwt/jwt/v5 to a direct dependency (test use).
…ation - Replace TestAzureSigner_ExponentRange (RSA exponent tests) with TestAzureSigner_ECKeyCompleteness: the azure_factory_test.go added on main (#1044) tested RSA exponent validation removed by this PR; new test covers EC key nil-field rejection to keep coverage parity. - Apply fieldalignment ordering (govet) to aws_signer.go, azure_signer.go, gcp_signer.go, and the updated azure_factory_test.go struct literals. - Suppress staticcheck SA1019 on elliptic.Marshal in ComputeKeyID with an inline nolint; elliptic.Marshal is the only stdlib path from *ecdsa.PublicKey to the uncompressed point without converting through crypto/ecdh. - Fix staticcheck QF1008 (remove unnecessary .PublicKey embed selector) in aws_signer_test.go and backend_jws_test.go. - Fix shadow declarations in signer_test.go (json.Unmarshal err vars). - Replace deprecated ecdsa.Sign with ecdsa.SignASN1 + derToRawECDSASignature in the fakeAzureKVClient test stub (backend_jws_test.go). - Fix gofmt import ordering in backend_jws_test.go (cloud.google.com before github.com).
… ComputeKeyID Use (*ecdsa.PublicKey).ECDH() and (*ecdh.PublicKey).Bytes() to obtain the uncompressed EC point, replacing the SA1019-deprecated elliptic.Marshal. The wire format is identical (0x04 || X || Y), so kid stability is preserved. Removes the staticcheck nolint directive.
…cKey The oidc.Signer interface PublicKey method was changed from *rsa.PublicKey to crypto.PublicKey as part of the ES256 migration. Update the test stub in internal/credentials to match, fixing the go vet type-assertion failure.
Rebasing onto main (Go 1.26.5) surfaces staticcheck SA1019: the raw ecdsa.PublicKey.X/Y fields are deprecated since Go 1.26. Replace the two uses introduced by the ES256 test code: - aws_signer_test.go: compare public keys via (*ecdsa.PublicKey).Equal instead of X.Cmp/Y.Cmp. - azure_factory_test.go: derive the fixed-width JWK coordinates from the uncompressed SEC 1 point via crypto/ecdh (ECDH().Bytes()), matching ComputeKeyID and PublicJWK. No behaviour change; the Azure fake now emits left-padded coordinates, which big.Int.SetBytes reconstructs identically.
…compressedPublicKey Staticcheck SA1019 flagged direct struct-literal initialization of ecdsa.PublicKey.X and ecdsa.PublicKey.Y (deprecated since Go 1.23). azure_signer.go: build the 65-byte SEC 1 uncompressed point from the JWK X/Y bytes (right-aligned to 32 bytes each, per JWK spec), then call ecdsa.ParseUncompressedPublicKey(elliptic.P256(), ...) which validates point membership and produces a PublicKey without touching the deprecated fields. Drop the now-unused math/big import. Add a guard for coordinate lengths > 32 bytes. backend_jws_test.go: replace f.key.X.Bytes()/f.key.Y.Bytes() with key.PublicKey.ECDH().Bytes() (uncompressed point), then slice X from [1:33] and Y from [33:65]. Add "fmt" import for the new error path.
PR #882 switched the OIDC issuer (JWKS + client-assertion signing) to require ECDSA P-256 (ES256) keys and reject any non-ECDSA key at resolve time. The Terraform modules that provision the actual signing key still specified RSA, so a fresh deploy would provision a key the Go code immediately rejects, taking down the OIDC issuer. Update all three provisioning modules to match: - AWS: aws_kms_key customer_master_key_spec -> ECC_NIST_P256 - GCP: google_kms_crypto_key version_template.algorithm -> EC_SIGN_P256_SHA256 - Azure: azurerm_key_vault_key key_type -> EC with curve = P-256 Also update the design spec (specs/azure-wif-redesign.md) to describe the ES256/EC design instead of the superseded RSA/RS256 plan. Changing the key spec forces key replacement on the next terraform apply (new kid), which is expected.
…6 code (follow-up to #882) (#1480) * fix(iac): provision EC P-256 signing keys to match ES256 OIDC code PR #882 switched the OIDC issuer (JWKS + client-assertion signing) to require ECDSA P-256 (ES256) keys and reject any non-ECDSA key at resolve time. The Terraform modules that provision the actual signing key still specified RSA, so a fresh deploy would provision a key the Go code immediately rejects, taking down the OIDC issuer. Update all three provisioning modules to match: - AWS: aws_kms_key customer_master_key_spec -> ECC_NIST_P256 - GCP: google_kms_crypto_key version_template.algorithm -> EC_SIGN_P256_SHA256 - Azure: azurerm_key_vault_key key_type -> EC with curve = P-256 Also update the design spec (specs/azure-wif-redesign.md) to describe the ES256/EC design instead of the superseded RSA/RS256 plan. Changing the key spec forces key replacement on the next terraform apply (new kid), which is expected. * fix(oidc): reject non-P-256 ECDSA keys instead of publishing a corrupt JWK PublicJWK's doc comment claimed only P-256 keys are accepted, but the code accepted any *ecdsa.PublicKey and unconditionally hardcoded Crv: "P-256" / Alg: "ES256" in the output. A misconfigured wrong-curve key (e.g. P-384) would be published through /.well-known/jwks.json as a mislabeled P-256 JWK with actually-longer coordinates, and the real failure would only surface later at the KMS signing call, far from the misconfiguration's cause. Check ecPub.Curve against elliptic.P256() and return an explicit error immediately in PublicJWK, and in the AWS KMS / GCP KMS signers' resolveOnce so a wrong-curve key fails fast with a clear message at key resolution instead of producing a corrupt public JWKS. (Azure Key Vault's resolveOnce already forces P-256 when parsing the raw point, so it needed no change.) Adds regression tests generating a P-384 key and asserting each path returns an error; verified failing pre-fix and passing post-fix.
* sec(oidc): switch JWT signing from PKCS1v15 to ES256 (closes #422) Replace RSA-PKCS1v15 (RS256) with ECDSA P-256 (ES256) across the entire oidc package: - LocalSigner: ecdsa.GenerateKey(P256) + ecdsa.SignASN1 instead of rsa.GenerateKey + rsa.SignPKCS1v15 - Signer interface: PublicKey() returns crypto.PublicKey (was *rsa.PublicKey) - Algorithm constant: "ES256" (was "RS256") - ComputeKeyID: accepts crypto.PublicKey, hashes uncompressed EC point - JWK: EC fields (kty=EC, crv=P-256, x, y); RSA fields (n, e) removed - AWSKMSSigner: SigningAlgorithmSpecEcdsaSha256 + *ecdsa.PublicKey assertion - AzureKeyVaultSigner: SignatureAlgorithmES256 + EC key (X/Y) extraction - GCPKMSSigner: *ecdsa.PublicKey assertion (digest call is key-type-agnostic) - All tests: positive assertions on ES256 algorithm and EC JWK fields * fix(oidc): emit RFC7518 raw R||S ES256 JWS signatures across all signer backends (refs #422) Mint base64url-encoded the signer's raw output directly into the JWS signature segment, but the Local/AWS/GCP signers return DER/ASN.1 ECDSA signatures (ecdsa.SignASN1, AWS ECDSA_SHA_256, GCP EC sign all yield ASN.1 DER). RFC 7518 section 3.4 requires an ES256 JWS signature to be the raw fixed-length R || S concatenation (32 bytes each = 64 bytes for P-256), NOT DER. As a result AWS/GCP/Local minted tokens that real OIDC consumers (the stated Azure AD target) reject. Azure Key Vault already returns raw R||S, so only Azure was correct, leaving the four backends mutually inconsistent. The Azure Sign comment also wrongly claimed Key Vault returns DER. Contract: Signer.Sign now returns the RFC 7518 raw R||S form. DER backends (Local/AWS/GCP) convert via a single shared derToRawECDSASignature helper; Azure passes through unchanged (no double-conversion). Mint encodes the result directly and defensively rejects any signature that is not 64 bytes. - signer.go: add derToRawECDSASignature; LocalSigner.Sign converts; Mint enforces the 64-byte contract; document the contract on the Signer interface. - aws_signer.go, gcp_signer.go: convert KMS DER output to raw R||S. - azure_signer.go: fix the wrong "DER-encoded" comment; keep passthrough. - tests: add assertRawES256JWS asserting the JWS sig is exactly 64 bytes, splitting R||S for ecdsa.Verify, and parsing with a real golang-jwt ES256 parser. Cover Local, AWS (DER fake), GCP (DER fake) and Azure (raw fake). The DER-path tests fail on the pre-fix code (71/69-byte signatures) and pass after; the Azure raw path stays correct. - go.mod: promote golang-jwt/jwt/v5 to a direct dependency (test use). * fix(oidc): rebase on main, fix test/lint regressions after ES256 migration - Replace TestAzureSigner_ExponentRange (RSA exponent tests) with TestAzureSigner_ECKeyCompleteness: the azure_factory_test.go added on main (#1044) tested RSA exponent validation removed by this PR; new test covers EC key nil-field rejection to keep coverage parity. - Apply fieldalignment ordering (govet) to aws_signer.go, azure_signer.go, gcp_signer.go, and the updated azure_factory_test.go struct literals. - Suppress staticcheck SA1019 on elliptic.Marshal in ComputeKeyID with an inline nolint; elliptic.Marshal is the only stdlib path from *ecdsa.PublicKey to the uncompressed point without converting through crypto/ecdh. - Fix staticcheck QF1008 (remove unnecessary .PublicKey embed selector) in aws_signer_test.go and backend_jws_test.go. - Fix shadow declarations in signer_test.go (json.Unmarshal err vars). - Replace deprecated ecdsa.Sign with ecdsa.SignASN1 + derToRawECDSASignature in the fakeAzureKVClient test stub (backend_jws_test.go). - Fix gofmt import ordering in backend_jws_test.go (cloud.google.com before github.com). * fix(oidc): replace deprecated elliptic.Marshal with ECDH().Bytes() in ComputeKeyID Use (*ecdsa.PublicKey).ECDH() and (*ecdh.PublicKey).Bytes() to obtain the uncompressed EC point, replacing the SA1019-deprecated elliptic.Marshal. The wire format is identical (0x04 || X || Y), so kid stability is preserved. Removes the staticcheck nolint directive. * fix(oidc): update stubOIDCSigner in credentials tests to crypto.PublicKey The oidc.Signer interface PublicKey method was changed from *rsa.PublicKey to crypto.PublicKey as part of the ES256 migration. Update the test stub in internal/credentials to match, fixing the go vet type-assertion failure. * fix(oidc): avoid deprecated ecdsa.PublicKey.X/Y in ES256 tests Rebasing onto main (Go 1.26.5) surfaces staticcheck SA1019: the raw ecdsa.PublicKey.X/Y fields are deprecated since Go 1.26. Replace the two uses introduced by the ES256 test code: - aws_signer_test.go: compare public keys via (*ecdsa.PublicKey).Equal instead of X.Cmp/Y.Cmp. - azure_factory_test.go: derive the fixed-width JWK coordinates from the uncompressed SEC 1 point via crypto/ecdh (ECDH().Bytes()), matching ComputeKeyID and PublicJWK. No behaviour change; the Azure fake now emits left-padded coordinates, which big.Int.SetBytes reconstructs identically. * fix(oidc): replace deprecated ecdsa.PublicKey X/Y fields with ParseUncompressedPublicKey Staticcheck SA1019 flagged direct struct-literal initialization of ecdsa.PublicKey.X and ecdsa.PublicKey.Y (deprecated since Go 1.23). azure_signer.go: build the 65-byte SEC 1 uncompressed point from the JWK X/Y bytes (right-aligned to 32 bytes each, per JWK spec), then call ecdsa.ParseUncompressedPublicKey(elliptic.P256(), ...) which validates point membership and produces a PublicKey without touching the deprecated fields. Drop the now-unused math/big import. Add a guard for coordinate lengths > 32 bytes. backend_jws_test.go: replace f.key.X.Bytes()/f.key.Y.Bytes() with key.PublicKey.ECDH().Bytes() (uncompressed point), then slice X from [1:33] and Y from [33:65]. Add "fmt" import for the new error path.
Summary
internal/oidcpackageAlgorithmconstant,Signerinterface,LocalSigner, all three KMS signer stubs, the JWKS builder, and all testskty=EC,crv=P-256,x,y) -- regression to RS256 will fail testsCloses #422
Test plan
go test ./internal/oidc/...-- 9 tests passgo test ./internal/api/...-- handler_oidc_test.go asserts EC JWK fieldsgo vet ./...-- cleanTestLocalSignerMintAndVerifyassertsalg == "ES256"and verifies withecdsa.VerifyASN1TestBuildJWKSassertskty=EC,crv=P-256, presence ofx/y, absence of RSAn/eTestBuildDiscoveryassertsid_token_signing_alg_values_supported == ["ES256"]