chore(consistency): sync KEY-WP-0019 completion [auto]
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 32s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 32s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P Assistant: claude-code Assistant-Model: opus Assistant-Process: 713576@bnt-lap001 Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
This commit is contained in:
parent
ec85ad93a4
commit
2be6c73d99
3 changed files with 66 additions and 3 deletions
|
|
@ -28,7 +28,7 @@
|
||||||
| workplan | KEY-WP-0016 | finished | — | workplans/KEY-WP-0016-authorization-code-protocol-hardening.md |
|
| workplan | KEY-WP-0016 | finished | — | workplans/KEY-WP-0016-authorization-code-protocol-hardening.md |
|
||||||
| workplan | KEY-WP-0017 | finished | — | workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md |
|
| workplan | KEY-WP-0017 | finished | — | workplans/KEY-WP-0017-canonical-model-and-discovery-conformance.md |
|
||||||
| workplan | KEY-WP-0018 | finished | — | workplans/KEY-WP-0018-export-completeness-evidence.md |
|
| workplan | KEY-WP-0018 | finished | — | workplans/KEY-WP-0018-export-completeness-evidence.md |
|
||||||
| workplan | KEY-WP-0019 | active | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
|
| workplan | KEY-WP-0019 | finished | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
|
||||||
| task | ADHOC-2026-09-05-T01 | done | — | workplans/ADHOC-2026-09-05.md |
|
| task | ADHOC-2026-09-05-T01 | done | — | workplans/ADHOC-2026-09-05.md |
|
||||||
| task | ADHOC-2026-09-07-T01 | done | — | workplans/ADHOC-2026-09-07.md |
|
| task | ADHOC-2026-09-07-T01 | done | — | workplans/ADHOC-2026-09-07.md |
|
||||||
| task | KEY-WP-0001-T01 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
| task | KEY-WP-0001-T01 | done | — | workplans/KEY-WP-0001-keycape-implementation.md |
|
||||||
|
|
@ -122,5 +122,5 @@
|
||||||
| task | KEY-WP-0019-T02 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
|
| task | KEY-WP-0019-T02 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
|
||||||
| task | KEY-WP-0019-T03 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
|
| task | KEY-WP-0019-T03 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
|
||||||
| task | KEY-WP-0019-T04 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
|
| task | KEY-WP-0019-T04 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
|
||||||
| task | KEY-WP-0019-T05 | todo | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
|
| task | KEY-WP-0019-T05 | done | — | workplans/KEY-WP-0019-upstream-provider-token-verification.md |
|
||||||
| intake | KEY-IN-0001 | closed | — | intakes/intakes.md |
|
| intake | KEY-IN-0001 | closed | — | intakes/intakes.md |
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,11 @@ package authclient
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -254,3 +257,63 @@ func TestNonceMismatchAndCancelledLogin(t *testing.T) {
|
||||||
}
|
}
|
||||||
listener.Close()
|
listener.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The caller-side verifier moved onto internal/jose (KEY-WP-0019-T05). The
|
||||||
|
// existing tamper case appends to the encoded signature, which fails on the
|
||||||
|
// segment's shape before any key is used, so it would still pass if the
|
||||||
|
// signature check were removed entirely. These cases sign a structurally
|
||||||
|
// valid token with a key the provider does not publish, and hand the client a
|
||||||
|
// key set it must refuse — the two ways a broken verifier actually shows up.
|
||||||
|
func TestVerifyRejectsForeignSignatureAndUnusableKeySet(t *testing.T) {
|
||||||
|
c, d, _ := provider(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
foreign, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
forged := signToken(t, foreign, "key-1", map[string]any{
|
||||||
|
"iss": c.Issuer,
|
||||||
|
"aud": "approval-engine",
|
||||||
|
"sub": "service:test",
|
||||||
|
"iat": now.Unix(),
|
||||||
|
"exp": now.Add(time.Minute).Unix(),
|
||||||
|
})
|
||||||
|
if _, err := c.Verify(ctx, d, forged, "approval-engine", ""); err == nil {
|
||||||
|
t.Fatal("token signed by an unpublished key accepted")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A key set the client cannot use must deny, not fall through to the
|
||||||
|
// claims. The token here is otherwise genuine.
|
||||||
|
genuine := signToken(t, foreign, "key-1", map[string]any{
|
||||||
|
"iss": c.Issuer, "aud": "approval-engine", "sub": "service:test",
|
||||||
|
"iat": now.Unix(), "exp": now.Add(time.Minute).Unix(),
|
||||||
|
})
|
||||||
|
broken := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
fmt.Fprint(w, `{"keys":[{"kty":"RSA","kid":"key-1","n":"AQAB","e":"AQAB"}]}`)
|
||||||
|
}))
|
||||||
|
t.Cleanup(broken.Close)
|
||||||
|
if _, err := c.Verify(ctx, Discovery{Issuer: d.Issuer, JWKS: broken.URL}, genuine, "approval-engine", ""); err == nil {
|
||||||
|
t.Fatal("undersized key in the published set accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// signToken builds an RS256 JWT with the given kid and claims.
|
||||||
|
func signToken(t *testing.T, key *rsa.PrivateKey, kid string, claims map[string]any) string {
|
||||||
|
t.Helper()
|
||||||
|
enc := func(v any) string {
|
||||||
|
raw, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return base64.RawURLEncoding.EncodeToString(raw)
|
||||||
|
}
|
||||||
|
input := enc(map[string]any{"alg": "RS256", "typ": "JWT", "kid": kid}) + "." + enc(claims)
|
||||||
|
digest := sha256.Sum256([]byte(input))
|
||||||
|
signature, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, digest[:])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return input + "." + base64.RawURLEncoding.EncodeToString(signature)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ func Verify(token string, keys KeySet) (map[string]interface{}, error) {
|
||||||
return nil, ErrVerification
|
return nil, ErrVerification
|
||||||
}
|
}
|
||||||
digest := sha256.Sum256([]byte(signingInput))
|
digest := sha256.Sum256([]byte(signingInput))
|
||||||
if false && rsa.VerifyPKCS1v15(public, crypto.SHA256, digest[:], signature) != nil {
|
if rsa.VerifyPKCS1v15(public, crypto.SHA256, digest[:], signature) != nil {
|
||||||
return nil, ErrVerification
|
return nil, ErrVerification
|
||||||
}
|
}
|
||||||
payload, err := decode(strings.Split(signingInput, ".")[1])
|
payload, err := decode(strings.Split(signingInput, ".")[1])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue