···
-
indigoCrypto "github.com/bluesky-social/indigo/atproto/atcrypto"
-
"github.com/golang-jwt/jwt/v5"
-
"github.com/google/uuid"
-
// === Test Helpers ===
-
// testECKey holds a test ES256 key pair
-
type testECKey struct {
-
privateKey *ecdsa.PrivateKey
-
publicKey *ecdsa.PublicKey
-
jwk map[string]interface{}
-
// generateTestES256Key generates a test ES256 key pair and JWK
-
func generateTestES256Key(t *testing.T) *testECKey {
-
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
-
t.Fatalf("Failed to generate test key: %v", err)
-
// Encode public key coordinates as base64url
-
xBytes := privateKey.PublicKey.X.Bytes()
-
yBytes := privateKey.PublicKey.Y.Bytes()
-
// P-256 coordinates must be 32 bytes (pad if needed)
-
xBytes = padTo32Bytes(xBytes)
-
yBytes = padTo32Bytes(yBytes)
-
x := base64.RawURLEncoding.EncodeToString(xBytes)
-
y := base64.RawURLEncoding.EncodeToString(yBytes)
-
jwk := map[string]interface{}{
-
// Calculate thumbprint
-
thumbprint, err := CalculateJWKThumbprint(jwk)
-
t.Fatalf("Failed to calculate thumbprint: %v", err)
-
privateKey: privateKey,
-
publicKey: &privateKey.PublicKey,
-
thumbprint: thumbprint,
-
// padTo32Bytes pads a byte slice to 32 bytes (required for P-256 coordinates)
-
func padTo32Bytes(b []byte) []byte {
-
padded := make([]byte, 32)
-
copy(padded[32-len(b):], b)
-
// createDPoPProof creates a DPoP proof JWT for testing
-
func createDPoPProof(t *testing.T, key *testECKey, method, uri string, iat time.Time, jti string) string {
-
RegisteredClaims: jwt.RegisteredClaims{
-
IssuedAt: jwt.NewNumericDate(iat),
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
-
token.Header["typ"] = "dpop+jwt"
-
token.Header["jwk"] = key.jwk
-
tokenString, err := token.SignedString(key.privateKey)
-
t.Fatalf("Failed to create DPoP proof: %v", err)
-
// === JWK Thumbprint Tests (RFC 7638) ===
-
func TestCalculateJWKThumbprint_EC_P256(t *testing.T) {
-
// Test with known values from RFC 7638 Appendix A (adapted for P-256)
-
jwk := map[string]interface{}{
-
"x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis",
-
"y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE",
-
thumbprint, err := CalculateJWKThumbprint(jwk)
-
t.Fatalf("CalculateJWKThumbprint failed: %v", err)
-
t.Error("Expected non-empty thumbprint")
-
// Verify it's valid base64url
-
_, err = base64.RawURLEncoding.DecodeString(thumbprint)
-
t.Errorf("Thumbprint is not valid base64url: %v", err)
-
// Verify length (SHA-256 produces 32 bytes = 43 base64url chars)
-
if len(thumbprint) != 43 {
-
t.Errorf("Expected thumbprint length 43, got %d", len(thumbprint))
-
func TestCalculateJWKThumbprint_Deterministic(t *testing.T) {
-
// Same key should produce same thumbprint
-
jwk := map[string]interface{}{
-
"x": "test-x-coordinate",
-
"y": "test-y-coordinate",
-
thumbprint1, err := CalculateJWKThumbprint(jwk)
-
t.Fatalf("First CalculateJWKThumbprint failed: %v", err)
-
thumbprint2, err := CalculateJWKThumbprint(jwk)
-
t.Fatalf("Second CalculateJWKThumbprint failed: %v", err)
-
if thumbprint1 != thumbprint2 {
-
t.Errorf("Thumbprints are not deterministic: %s != %s", thumbprint1, thumbprint2)
-
func TestCalculateJWKThumbprint_DifferentKeys(t *testing.T) {
-
// Different keys should produce different thumbprints
-
jwk1 := map[string]interface{}{
-
jwk2 := map[string]interface{}{
-
thumbprint1, err := CalculateJWKThumbprint(jwk1)
-
t.Fatalf("First CalculateJWKThumbprint failed: %v", err)
-
thumbprint2, err := CalculateJWKThumbprint(jwk2)
-
t.Fatalf("Second CalculateJWKThumbprint failed: %v", err)
-
if thumbprint1 == thumbprint2 {
-
t.Error("Different keys produced same thumbprint (collision)")
-
func TestCalculateJWKThumbprint_MissingKty(t *testing.T) {
-
jwk := map[string]interface{}{
-
_, err := CalculateJWKThumbprint(jwk)
-
t.Error("Expected error for missing kty, got nil")
-
if err != nil && !contains(err.Error(), "missing kty") {
-
t.Errorf("Expected error about missing kty, got: %v", err)
-
func TestCalculateJWKThumbprint_EC_MissingCrv(t *testing.T) {
-
jwk := map[string]interface{}{
-
_, err := CalculateJWKThumbprint(jwk)
-
t.Error("Expected error for missing crv, got nil")
-
if err != nil && !contains(err.Error(), "missing crv") {
-
t.Errorf("Expected error about missing crv, got: %v", err)
-
func TestCalculateJWKThumbprint_EC_MissingX(t *testing.T) {
-
jwk := map[string]interface{}{
-
_, err := CalculateJWKThumbprint(jwk)
-
t.Error("Expected error for missing x, got nil")
-
if err != nil && !contains(err.Error(), "missing x") {
-
t.Errorf("Expected error about missing x, got: %v", err)
-
func TestCalculateJWKThumbprint_EC_MissingY(t *testing.T) {
-
jwk := map[string]interface{}{
-
_, err := CalculateJWKThumbprint(jwk)
-
t.Error("Expected error for missing y, got nil")
-
if err != nil && !contains(err.Error(), "missing y") {
-
t.Errorf("Expected error about missing y, got: %v", err)
-
func TestCalculateJWKThumbprint_RSA(t *testing.T) {
-
// Test RSA key thumbprint calculation
-
jwk := map[string]interface{}{
-
thumbprint, err := CalculateJWKThumbprint(jwk)
-
t.Fatalf("CalculateJWKThumbprint failed for RSA: %v", err)
-
t.Error("Expected non-empty thumbprint for RSA key")
-
func TestCalculateJWKThumbprint_OKP(t *testing.T) {
-
// Test OKP (Octet Key Pair) thumbprint calculation
-
jwk := map[string]interface{}{
-
"x": "test-x-coordinate",
-
thumbprint, err := CalculateJWKThumbprint(jwk)
-
t.Fatalf("CalculateJWKThumbprint failed for OKP: %v", err)
-
t.Error("Expected non-empty thumbprint for OKP key")
-
func TestCalculateJWKThumbprint_UnsupportedKeyType(t *testing.T) {
-
jwk := map[string]interface{}{
-
_, err := CalculateJWKThumbprint(jwk)
-
t.Error("Expected error for unsupported key type, got nil")
-
if err != nil && !contains(err.Error(), "unsupported JWK key type") {
-
t.Errorf("Expected error about unsupported key type, got: %v", err)
-
func TestCalculateJWKThumbprint_CanonicalJSON(t *testing.T) {
-
// RFC 7638 requires lexicographic ordering of keys in canonical JSON
-
// This test verifies that the canonical JSON is correctly ordered
-
jwk := map[string]interface{}{
-
// The canonical JSON should be: {"crv":"P-256","kty":"EC","x":"x-coord","y":"y-coord"}
-
// (lexicographically ordered: crv, kty, x, y)
-
canonical := map[string]string{
-
canonicalJSON, err := json.Marshal(canonical)
-
t.Fatalf("Failed to marshal canonical JSON: %v", err)
-
expectedHash := sha256.Sum256(canonicalJSON)
-
expectedThumbprint := base64.RawURLEncoding.EncodeToString(expectedHash[:])
-
actualThumbprint, err := CalculateJWKThumbprint(jwk)
-
t.Fatalf("CalculateJWKThumbprint failed: %v", err)
-
if actualThumbprint != expectedThumbprint {
-
t.Errorf("Thumbprint doesn't match expected canonical JSON hash\nExpected: %s\nGot: %s",
-
expectedThumbprint, actualThumbprint)
-
// === DPoP Proof Verification Tests ===
-
func TestVerifyDPoPProof_Valid(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
jti := uuid.New().String()
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
-
result, err := verifier.VerifyDPoPProof(proof, method, uri)
-
t.Fatalf("VerifyDPoPProof failed for valid proof: %v", err)
-
t.Fatal("Expected non-nil proof result")
-
if result.Claims.HTTPMethod != method {
-
t.Errorf("Expected method %s, got %s", method, result.Claims.HTTPMethod)
-
if result.Claims.HTTPURI != uri {
-
t.Errorf("Expected URI %s, got %s", uri, result.Claims.HTTPURI)
-
if result.Claims.ID != jti {
-
t.Errorf("Expected jti %s, got %s", jti, result.Claims.ID)
-
if result.Thumbprint != key.thumbprint {
-
t.Errorf("Expected thumbprint %s, got %s", key.thumbprint, result.Thumbprint)
-
func TestVerifyDPoPProof_InvalidSignature(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
wrongKey := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
jti := uuid.New().String()
-
// Create proof with one key
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
-
// Parse and modify to use wrong key's JWK in header (signature won't match)
-
parts := splitJWT(proof)
-
header := parseJWTHeader(t, parts[0])
-
header["jwk"] = wrongKey.jwk
-
modifiedHeader := encodeJSON(t, header)
-
tamperedProof := modifiedHeader + "." + parts[1] + "." + parts[2]
-
_, err := verifier.VerifyDPoPProof(tamperedProof, method, uri)
-
t.Error("Expected error for invalid signature, got nil")
-
if err != nil && !contains(err.Error(), "signature verification failed") {
-
t.Errorf("Expected signature verification error, got: %v", err)
-
func TestVerifyDPoPProof_WrongHTTPMethod(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
jti := uuid.New().String()
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
-
_, err := verifier.VerifyDPoPProof(proof, wrongMethod, uri)
-
t.Error("Expected error for HTTP method mismatch, got nil")
-
if err != nil && !contains(err.Error(), "htm mismatch") {
-
t.Errorf("Expected htm mismatch error, got: %v", err)
-
func TestVerifyDPoPProof_WrongURI(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
wrongURI := "https://api.example.com/different"
-
jti := uuid.New().String()
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
-
_, err := verifier.VerifyDPoPProof(proof, method, wrongURI)
-
t.Error("Expected error for URI mismatch, got nil")
-
if err != nil && !contains(err.Error(), "htu mismatch") {
-
t.Errorf("Expected htu mismatch error, got: %v", err)
-
func TestVerifyDPoPProof_URIWithQuery(t *testing.T) {
-
// URI comparison should strip query and fragment
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
baseURI := "https://api.example.com/resource"
-
uriWithQuery := baseURI + "?param=value"
-
jti := uuid.New().String()
-
proof := createDPoPProof(t, key, method, baseURI, iat, jti)
-
// Should succeed because query is stripped
-
_, err := verifier.VerifyDPoPProof(proof, method, uriWithQuery)
-
t.Fatalf("VerifyDPoPProof failed for URI with query: %v", err)
-
func TestVerifyDPoPProof_URIWithFragment(t *testing.T) {
-
// URI comparison should strip query and fragment
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
baseURI := "https://api.example.com/resource"
-
uriWithFragment := baseURI + "#section"
-
jti := uuid.New().String()
-
proof := createDPoPProof(t, key, method, baseURI, iat, jti)
-
// Should succeed because fragment is stripped
-
_, err := verifier.VerifyDPoPProof(proof, method, uriWithFragment)
-
t.Fatalf("VerifyDPoPProof failed for URI with fragment: %v", err)
-
func TestVerifyDPoPProof_ExpiredProof(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
// Proof issued 10 minutes ago (exceeds default MaxProofAge of 5 minutes)
-
iat := time.Now().Add(-10 * time.Minute)
-
jti := uuid.New().String()
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
-
_, err := verifier.VerifyDPoPProof(proof, method, uri)
-
t.Error("Expected error for expired proof, got nil")
-
if err != nil && !contains(err.Error(), "too old") {
-
t.Errorf("Expected 'too old' error, got: %v", err)
-
func TestVerifyDPoPProof_FutureProof(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
// Proof issued 1 minute in the future (exceeds MaxClockSkew)
-
iat := time.Now().Add(1 * time.Minute)
-
jti := uuid.New().String()
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
-
_, err := verifier.VerifyDPoPProof(proof, method, uri)
-
t.Error("Expected error for future proof, got nil")
-
if err != nil && !contains(err.Error(), "in the future") {
-
t.Errorf("Expected 'in the future' error, got: %v", err)
-
func TestVerifyDPoPProof_WithinClockSkew(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
// Proof issued 15 seconds in the future (within MaxClockSkew of 30s)
-
iat := time.Now().Add(15 * time.Second)
-
jti := uuid.New().String()
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
-
_, err := verifier.VerifyDPoPProof(proof, method, uri)
-
t.Fatalf("VerifyDPoPProof failed for proof within clock skew: %v", err)
-
func TestVerifyDPoPProof_MissingJti(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
RegisteredClaims: jwt.RegisteredClaims{
-
IssuedAt: jwt.NewNumericDate(iat),
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
-
token.Header["typ"] = "dpop+jwt"
-
token.Header["jwk"] = key.jwk
-
proof, err := token.SignedString(key.privateKey)
-
t.Fatalf("Failed to create test proof: %v", err)
-
_, err = verifier.VerifyDPoPProof(proof, method, uri)
-
t.Error("Expected error for missing jti, got nil")
-
if err != nil && !contains(err.Error(), "missing jti") {
-
t.Errorf("Expected missing jti error, got: %v", err)
-
func TestVerifyDPoPProof_MissingTypHeader(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
jti := uuid.New().String()
-
RegisteredClaims: jwt.RegisteredClaims{
-
IssuedAt: jwt.NewNumericDate(iat),
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
-
// Don't set typ header
-
token.Header["jwk"] = key.jwk
-
proof, err := token.SignedString(key.privateKey)
-
t.Fatalf("Failed to create test proof: %v", err)
-
_, err = verifier.VerifyDPoPProof(proof, method, uri)
-
t.Error("Expected error for missing typ header, got nil")
-
if err != nil && !contains(err.Error(), "typ must be 'dpop+jwt'") {
-
t.Errorf("Expected typ header error, got: %v", err)
-
func TestVerifyDPoPProof_WrongTypHeader(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
jti := uuid.New().String()
-
RegisteredClaims: jwt.RegisteredClaims{
-
IssuedAt: jwt.NewNumericDate(iat),
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
-
token.Header["typ"] = "JWT" // Wrong typ
-
token.Header["jwk"] = key.jwk
-
proof, err := token.SignedString(key.privateKey)
-
t.Fatalf("Failed to create test proof: %v", err)
-
_, err = verifier.VerifyDPoPProof(proof, method, uri)
-
t.Error("Expected error for wrong typ header, got nil")
-
if err != nil && !contains(err.Error(), "typ must be 'dpop+jwt'") {
-
t.Errorf("Expected typ header error, got: %v", err)
-
func TestVerifyDPoPProof_MissingJWK(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
jti := uuid.New().String()
-
RegisteredClaims: jwt.RegisteredClaims{
-
IssuedAt: jwt.NewNumericDate(iat),
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
-
token.Header["typ"] = "dpop+jwt"
-
proof, err := token.SignedString(key.privateKey)
-
t.Fatalf("Failed to create test proof: %v", err)
-
_, err = verifier.VerifyDPoPProof(proof, method, uri)
-
t.Error("Expected error for missing jwk header, got nil")
-
if err != nil && !contains(err.Error(), "missing jwk") {
-
t.Errorf("Expected missing jwk error, got: %v", err)
-
func TestVerifyDPoPProof_CustomTimeSettings(t *testing.T) {
-
verifier := &DPoPVerifier{
-
MaxClockSkew: 1 * time.Minute,
-
MaxProofAge: 10 * time.Minute,
-
key := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
// Proof issued 50 seconds in the future (within custom MaxClockSkew)
-
iat := time.Now().Add(50 * time.Second)
-
jti := uuid.New().String()
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
-
_, err := verifier.VerifyDPoPProof(proof, method, uri)
-
t.Fatalf("VerifyDPoPProof failed with custom time settings: %v", err)
-
func TestVerifyDPoPProof_HTTPMethodCaseInsensitive(t *testing.T) {
-
// HTTP method comparison should be case-insensitive per spec
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
jti := uuid.New().String()
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
-
// Verify with uppercase method
-
_, err := verifier.VerifyDPoPProof(proof, "POST", uri)
-
t.Fatalf("VerifyDPoPProof failed for case-insensitive method: %v", err)
-
// === Token Binding Verification Tests ===
-
func TestVerifyTokenBinding_Matching(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
jti := uuid.New().String()
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
-
result, err := verifier.VerifyDPoPProof(proof, method, uri)
-
t.Fatalf("VerifyDPoPProof failed: %v", err)
-
// Verify token binding with matching thumbprint
-
err = verifier.VerifyTokenBinding(result, key.thumbprint)
-
t.Fatalf("VerifyTokenBinding failed for matching thumbprint: %v", err)
-
func TestVerifyTokenBinding_Mismatch(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
wrongKey := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
jti := uuid.New().String()
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
-
result, err := verifier.VerifyDPoPProof(proof, method, uri)
-
t.Fatalf("VerifyDPoPProof failed: %v", err)
-
// Verify token binding with wrong thumbprint
-
err = verifier.VerifyTokenBinding(result, wrongKey.thumbprint)
-
t.Error("Expected error for thumbprint mismatch, got nil")
-
if err != nil && !contains(err.Error(), "thumbprint mismatch") {
-
t.Errorf("Expected thumbprint mismatch error, got: %v", err)
-
// === ExtractCnfJkt Tests ===
-
func TestExtractCnfJkt_Valid(t *testing.T) {
-
expectedJkt := "test-thumbprint-123"
-
Confirmation: map[string]interface{}{
-
jkt, err := ExtractCnfJkt(claims)
-
t.Fatalf("ExtractCnfJkt failed for valid claims: %v", err)
-
if jkt != expectedJkt {
-
t.Errorf("Expected jkt %s, got %s", expectedJkt, jkt)
-
func TestExtractCnfJkt_MissingCnf(t *testing.T) {
-
_, err := ExtractCnfJkt(claims)
-
t.Error("Expected error for missing cnf, got nil")
-
if err != nil && !contains(err.Error(), "missing cnf claim") {
-
t.Errorf("Expected missing cnf error, got: %v", err)
-
func TestExtractCnfJkt_NilCnf(t *testing.T) {
-
_, err := ExtractCnfJkt(claims)
-
t.Error("Expected error for nil cnf, got nil")
-
if err != nil && !contains(err.Error(), "missing cnf claim") {
-
t.Errorf("Expected missing cnf error, got: %v", err)
-
func TestExtractCnfJkt_MissingJkt(t *testing.T) {
-
Confirmation: map[string]interface{}{
-
_, err := ExtractCnfJkt(claims)
-
t.Error("Expected error for missing jkt, got nil")
-
if err != nil && !contains(err.Error(), "missing jkt") {
-
t.Errorf("Expected missing jkt error, got: %v", err)
-
func TestExtractCnfJkt_EmptyJkt(t *testing.T) {
-
Confirmation: map[string]interface{}{
-
_, err := ExtractCnfJkt(claims)
-
t.Error("Expected error for empty jkt, got nil")
-
if err != nil && !contains(err.Error(), "missing jkt") {
-
t.Errorf("Expected missing jkt error, got: %v", err)
-
func TestExtractCnfJkt_WrongType(t *testing.T) {
-
Confirmation: map[string]interface{}{
-
"jkt": 123, // Not a string
-
_, err := ExtractCnfJkt(claims)
-
t.Error("Expected error for wrong type jkt, got nil")
-
if err != nil && !contains(err.Error(), "missing jkt") {
-
t.Errorf("Expected missing jkt error, got: %v", err)
-
// === Helper Functions for Tests ===
-
// splitJWT splits a JWT into its three parts
-
func splitJWT(token string) []string {
-
token[:strings.IndexByte(token, '.')],
-
token[strings.IndexByte(token, '.')+1 : strings.LastIndexByte(token, '.')],
-
token[strings.LastIndexByte(token, '.')+1:],
-
// parseJWTHeader parses a base64url-encoded JWT header
-
func parseJWTHeader(t *testing.T, encoded string) map[string]interface{} {
-
decoded, err := base64.RawURLEncoding.DecodeString(encoded)
-
t.Fatalf("Failed to decode header: %v", err)
-
var header map[string]interface{}
-
if err := json.Unmarshal(decoded, &header); err != nil {
-
t.Fatalf("Failed to unmarshal header: %v", err)
-
// encodeJSON encodes a value to base64url-encoded JSON
-
func encodeJSON(t *testing.T, v interface{}) string {
-
data, err := json.Marshal(v)
-
t.Fatalf("Failed to marshal JSON: %v", err)
-
return base64.RawURLEncoding.EncodeToString(data)
-
// === ES256K (secp256k1) Test Helpers ===
-
// testES256KKey holds a test ES256K key pair using indigo
-
type testES256KKey struct {
-
privateKey indigoCrypto.PrivateKey
-
publicKey indigoCrypto.PublicKey
-
jwk map[string]interface{}
-
// generateTestES256KKey generates a test ES256K (secp256k1) key pair and JWK
-
func generateTestES256KKey(t *testing.T) *testES256KKey {
-
privateKey, err := indigoCrypto.GeneratePrivateKeyK256()
-
t.Fatalf("Failed to generate ES256K test key: %v", err)
-
publicKey, err := privateKey.PublicKey()
-
t.Fatalf("Failed to get public key from ES256K private key: %v", err)
-
// Get the JWK representation
-
jwkStruct, err := publicKey.JWK()
-
t.Fatalf("Failed to get JWK from ES256K public key: %v", err)
-
jwk := map[string]interface{}{
-
"kty": jwkStruct.KeyType,
-
"crv": jwkStruct.Curve,
-
// Calculate thumbprint
-
thumbprint, err := CalculateJWKThumbprint(jwk)
-
t.Fatalf("Failed to calculate ES256K thumbprint: %v", err)
-
privateKey: privateKey,
-
thumbprint: thumbprint,
-
// createES256KDPoPProof creates a DPoP proof JWT using ES256K for testing
-
func createES256KDPoPProof(t *testing.T, key *testES256KKey, method, uri string, iat time.Time, jti string) string {
-
claims := map[string]interface{}{
-
header := map[string]interface{}{
-
// Encode header and claims
-
headerJSON, err := json.Marshal(header)
-
t.Fatalf("Failed to marshal header: %v", err)
-
claimsJSON, err := json.Marshal(claims)
-
t.Fatalf("Failed to marshal claims: %v", err)
-
headerB64 := base64.RawURLEncoding.EncodeToString(headerJSON)
-
claimsB64 := base64.RawURLEncoding.EncodeToString(claimsJSON)
-
signingInput := headerB64 + "." + claimsB64
-
signature, err := key.privateKey.HashAndSign([]byte(signingInput))
-
t.Fatalf("Failed to sign ES256K proof: %v", err)
-
signatureB64 := base64.RawURLEncoding.EncodeToString(signature)
-
return signingInput + "." + signatureB64
-
// === ES256K Tests ===
-
func TestVerifyDPoPProof_ES256K_Valid(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256KKey(t)
-
uri := "https://api.example.com/resource"
-
jti := uuid.New().String()
-
proof := createES256KDPoPProof(t, key, method, uri, iat, jti)
-
result, err := verifier.VerifyDPoPProof(proof, method, uri)
-
t.Fatalf("VerifyDPoPProof failed for valid ES256K proof: %v", err)
-
t.Fatal("Expected non-nil proof result")
-
if result.Claims.HTTPMethod != method {
-
t.Errorf("Expected method %s, got %s", method, result.Claims.HTTPMethod)
-
if result.Claims.HTTPURI != uri {
-
t.Errorf("Expected URI %s, got %s", uri, result.Claims.HTTPURI)
-
if result.Thumbprint != key.thumbprint {
-
t.Errorf("Expected thumbprint %s, got %s", key.thumbprint, result.Thumbprint)
-
func TestVerifyDPoPProof_ES256K_InvalidSignature(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256KKey(t)
-
wrongKey := generateTestES256KKey(t)
-
uri := "https://api.example.com/resource"
-
jti := uuid.New().String()
-
// Create proof with one key
-
proof := createES256KDPoPProof(t, key, method, uri, iat, jti)
-
// Tamper by replacing JWK with wrong key
-
parts := splitJWT(proof)
-
header := parseJWTHeader(t, parts[0])
-
header["jwk"] = wrongKey.jwk
-
modifiedHeader := encodeJSON(t, header)
-
tamperedProof := modifiedHeader + "." + parts[1] + "." + parts[2]
-
_, err := verifier.VerifyDPoPProof(tamperedProof, method, uri)
-
t.Error("Expected error for invalid ES256K signature, got nil")
-
if err != nil && !contains(err.Error(), "signature verification failed") {
-
t.Errorf("Expected signature verification error, got: %v", err)
-
func TestCalculateJWKThumbprint_ES256K(t *testing.T) {
-
// Test thumbprint calculation for secp256k1 keys
-
key := generateTestES256KKey(t)
-
thumbprint, err := CalculateJWKThumbprint(key.jwk)
-
t.Fatalf("CalculateJWKThumbprint failed for ES256K: %v", err)
-
t.Error("Expected non-empty thumbprint for ES256K key")
-
// Verify it's valid base64url
-
_, err = base64.RawURLEncoding.DecodeString(thumbprint)
-
t.Errorf("ES256K thumbprint is not valid base64url: %v", err)
-
// Verify length (SHA-256 produces 32 bytes = 43 base64url chars)
-
if len(thumbprint) != 43 {
-
t.Errorf("Expected ES256K thumbprint length 43, got %d", len(thumbprint))
-
// === Algorithm-Curve Binding Tests ===
-
func TestVerifyDPoPProof_AlgorithmCurveMismatch_ES256KWithP256Key(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t) // P-256 key
-
uri := "https://api.example.com/resource"
-
jti := uuid.New().String()
-
// Create a proof claiming ES256K but using P-256 key
-
RegisteredClaims: jwt.RegisteredClaims{
-
IssuedAt: jwt.NewNumericDate(iat),
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
-
token.Header["typ"] = "dpop+jwt"
-
token.Header["alg"] = "ES256K" // Claim ES256K
-
token.Header["jwk"] = key.jwk // But use P-256 key
-
proof, err := token.SignedString(key.privateKey)
-
t.Fatalf("Failed to create test proof: %v", err)
-
_, err = verifier.VerifyDPoPProof(proof, method, uri)
-
t.Error("Expected error for ES256K algorithm with P-256 curve, got nil")
-
if err != nil && !contains(err.Error(), "requires curve secp256k1") {
-
t.Errorf("Expected curve mismatch error, got: %v", err)
-
func TestVerifyDPoPProof_AlgorithmCurveMismatch_ES256WithSecp256k1Key(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256KKey(t) // secp256k1 key
-
uri := "https://api.example.com/resource"
-
jti := uuid.New().String()
-
claims := map[string]interface{}{
-
// Build header claiming ES256 but using secp256k1 key
-
header := map[string]interface{}{
-
"alg": "ES256", // Claim ES256
-
"jwk": key.jwk, // But use secp256k1 key
-
headerJSON, _ := json.Marshal(header)
-
claimsJSON, _ := json.Marshal(claims)
-
headerB64 := base64.RawURLEncoding.EncodeToString(headerJSON)
-
claimsB64 := base64.RawURLEncoding.EncodeToString(claimsJSON)
-
signingInput := headerB64 + "." + claimsB64
-
signature, err := key.privateKey.HashAndSign([]byte(signingInput))
-
t.Fatalf("Failed to sign: %v", err)
-
proof := signingInput + "." + base64.RawURLEncoding.EncodeToString(signature)
-
_, err = verifier.VerifyDPoPProof(proof, method, uri)
-
t.Error("Expected error for ES256 algorithm with secp256k1 curve, got nil")
-
if err != nil && !contains(err.Error(), "requires curve P-256") {
-
t.Errorf("Expected curve mismatch error, got: %v", err)
-
// === exp/nbf Validation Tests ===
-
func TestVerifyDPoPProof_ExpiredWithExpClaim(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
iat := time.Now().Add(-2 * time.Minute)
-
exp := time.Now().Add(-1 * time.Minute) // Expired 1 minute ago
-
jti := uuid.New().String()
-
RegisteredClaims: jwt.RegisteredClaims{
-
IssuedAt: jwt.NewNumericDate(iat),
-
ExpiresAt: jwt.NewNumericDate(exp),
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
-
token.Header["typ"] = "dpop+jwt"
-
token.Header["jwk"] = key.jwk
-
proof, err := token.SignedString(key.privateKey)
-
t.Fatalf("Failed to create test proof: %v", err)
-
_, err = verifier.VerifyDPoPProof(proof, method, uri)
-
t.Error("Expected error for expired proof with exp claim, got nil")
-
if err != nil && !contains(err.Error(), "expired") {
-
t.Errorf("Expected expiration error, got: %v", err)
-
func TestVerifyDPoPProof_NotYetValidWithNbfClaim(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
nbf := time.Now().Add(5 * time.Minute) // Not valid for another 5 minutes
-
jti := uuid.New().String()
-
RegisteredClaims: jwt.RegisteredClaims{
-
IssuedAt: jwt.NewNumericDate(iat),
-
NotBefore: jwt.NewNumericDate(nbf),
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
-
token.Header["typ"] = "dpop+jwt"
-
token.Header["jwk"] = key.jwk
-
proof, err := token.SignedString(key.privateKey)
-
t.Fatalf("Failed to create test proof: %v", err)
-
_, err = verifier.VerifyDPoPProof(proof, method, uri)
-
t.Error("Expected error for not-yet-valid proof with nbf claim, got nil")
-
if err != nil && !contains(err.Error(), "not valid before") {
-
t.Errorf("Expected not-before error, got: %v", err)
-
func TestVerifyDPoPProof_ValidWithExpClaimInFuture(t *testing.T) {
-
verifier := NewDPoPVerifier()
-
key := generateTestES256Key(t)
-
uri := "https://api.example.com/resource"
-
exp := time.Now().Add(5 * time.Minute) // Valid for 5 more minutes
-
jti := uuid.New().String()
-
RegisteredClaims: jwt.RegisteredClaims{
-
IssuedAt: jwt.NewNumericDate(iat),
-
ExpiresAt: jwt.NewNumericDate(exp),
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
-
token.Header["typ"] = "dpop+jwt"
-
token.Header["jwk"] = key.jwk
-
proof, err := token.SignedString(key.privateKey)
-
t.Fatalf("Failed to create test proof: %v", err)
-
result, err := verifier.VerifyDPoPProof(proof, method, uri)
-
t.Fatalf("VerifyDPoPProof failed for valid proof with exp in future: %v", err)
-
t.Error("Expected non-nil result for valid proof")