From ca0e18b5af48bce738e41b5a5789427afec8747a Mon Sep 17 00:00:00 2001 From: Ken Takayama Date: Thu, 4 Dec 2025 10:26:18 +0000 Subject: [PATCH 1/3] add: COSE Key Thumbprint (RFC 9679) feature and its test Signed-off-by: Ken Takayama --- key.go | 106 +++++++++++++++++ key_test.go | 335 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 441 insertions(+) diff --git a/key.go b/key.go index b657bff..266db79 100644 --- a/key.go +++ b/key.go @@ -744,6 +744,112 @@ func (k *Key) PrivateKey() (crypto.PrivateKey, error) { } } +func (k *Key) Thumbprint(hash crypto.Hash) ([]byte, error) { + if !hash.Available() { + return nil, ErrUnavailableHashFunc + } + + type thumbprintHandler func() ([]byte, error) + handlers := map[KeyType]thumbprintHandler{ + KeyTypeOKP: k.calcOKPThumbprint, + KeyTypeEC2: k.calcEC2Thumbprint, + } + handler, ok := handlers[k.Type] + if !ok { + return nil, ErrOpNotSupported + } + + toBeHashedData, err := handler() + if err != nil { + return nil, err + } + + h := hash.New() + h.Write(toBeHashedData) + return h.Sum(nil), nil +} + +func (k *Key) calcOKPThumbprint() ([]byte, error) { + t, ok := k.Params[KeyLabelOKPX] + if !ok { + return nil, ErrOKPNoPub + } + x, ok := t.([]byte) + if !ok { + return nil, ErrInvalidPubKey + } + + switch k.Params[KeyLabelOKPCurve] { + case CurveEd25519: + if len(x) != 32 { + return nil, ErrInvalidPubKey + } + case CurveEd448: + if len(x) != 57 { + return nil, ErrInvalidPubKey + } + case CurveX25519: + if len(x) != 32 { + return nil, ErrInvalidPubKey + } + case CurveX448: + if len(x) != 56 { + return nil, ErrInvalidPubKey + } + default: + return nil, ErrOpNotSupported + } + + m := make(map[int]interface{}) + m[1] = k.Type + m[-1] = k.Params[KeyLabelOKPCurve] + m[-2] = k.Params[KeyLabelEC2X] + return encMode.Marshal(m) +} + +func (k *Key) calcEC2Thumbprint() ([]byte, error) { + t, ok := k.Params[KeyLabelEC2X] + if !ok { + return nil, ErrEC2NoPub + } + x, ok := t.([]byte) + if !ok { + return nil, ErrInvalidPubKey + } + t, ok = k.Params[KeyLabelEC2Y] + if !ok { + return nil, ErrEC2NoPub + } + y, ok := t.([]byte) + if !ok { + return nil, ErrInvalidPubKey + } + + switch k.Params[KeyLabelEC2Curve] { + case CurveP256: + if len(x) != 32 || len(y) != 32 { + return nil, ErrInvalidPubKey + } + case CurveP384: + if len(x) != 48 || len(y) != 48 { + return nil, ErrInvalidPubKey + } + case CurveP521: + if len(x) != 66 || len(y) != 66 { + return nil, ErrInvalidPubKey + } + default: + return nil, ErrOpNotSupported + } + + m := make(map[int]interface{}) + m[1] = k.Type + m[-1] = k.Params[KeyLabelEC2Curve] + m[-2] = x + m[-3] = y + return encMode.Marshal(m) +} + // AlgorithmOrDefault returns the Algorithm associated with Key. If // Key.Algorithm is set, that is what is returned. Otherwise, the algorithm is // inferred using Key.Curve. This method does NOT validate that Key.Algorithm, diff --git a/key_test.go b/key_test.go index 6e5ee0c..9ac100d 100644 --- a/key_test.go +++ b/key_test.go @@ -1892,3 +1892,338 @@ func newEC2(t *testing.T, crv elliptic.Curve) (x, y, d []byte) { } return priv.X.Bytes(), priv.Y.Bytes(), priv.D.Bytes() } + +func TestKey_Thumbprint(t *testing.T) { + // from section 6 of RFC 9679 + ec256x := []byte{ + 0x65, 0xed, 0xa5, 0xa1, 0x25, 0x77, 0xc2, 0xba, 0xe8, 0x29, 0x43, 0x7f, 0xe3, 0x38, 0x70, 0x1a, + 0x10, 0xaa, 0xa3, 0x75, 0xe1, 0xbb, 0x5b, 0x5d, 0xe1, 0x08, 0xde, 0x43, 0x9c, 0x08, 0x55, 0x1d, + } + ec256y := []byte{ + 0x1e, 0x52, 0xed, 0x75, 0x70, 0x11, 0x63, 0xf7, 0xf9, 0xe4, 0x0d, 0xdf, 0x9f, 0x34, 0x1b, 0x3d, + 0xc9, 0xba, 0x86, 0x0a, 0xf7, 0xe0, 0xca, 0x7c, 0xa7, 0xe9, 0xee, 0xcd, 0x00, 0x84, 0xd1, 0x9c, + } + ec256thumbprint := []byte{ + 0x49, 0x6b, 0xd8, 0xaf, 0xad, 0xf3, 0x07, 0xe5, 0xb0, 0x8c, 0x64, 0xb0, 0x42, 0x1b, 0xf9, 0xdc, + 0x01, 0x52, 0x8a, 0x34, 0x4a, 0x43, 0xbd, 0xa8, 0x8f, 0xad, 0xd1, 0x66, 0x9d, 0xa2, 0x53, 0xec, + } + + okpx := []byte{ + 0xd7, 0x5a, 0x98, 0x01, 0x82, 0xb1, 0x0a, 0xb7, 0xd5, 0x4b, 0xfe, 0xed, 0x3c, 0x96, 0x41, 0xce, + 0x83, 0xb8, 0x5c, 0xbc, 0xf6, 0xa6, 0x23, 0x25, 0xaf, 0x02, 0x1a, 0x68, 0xf7, 0x07, 0x51, 0x1a, + } + okpthumbprint := []byte{ + 0x28, 0xd9, 0xcc, 0x75, 0x8c, 0xce, 0x11, 0x80, 0x13, 0x61, 0x59, 0x1a, 0x6a, 0x38, 0x5b, 0xc8, + 0x43, 0x58, 0xeb, 0xbb, 0x4b, 0xf4, 0x24, 0x47, 0x9b, 0xb9, 0x5e, 0x9c, 0x8c, 0x3c, 0xdb, 0x74, + } + + tests := []struct { + name string + h crypto.Hash + k *Key + want []byte + wantErr string + }{ + { + "CurveP256", + crypto.SHA256, + &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: ec256x, + KeyLabelEC2Y: ec256y, + }, + }, + ec256thumbprint, + "", + }, { + "CurveEd25519", + crypto.SHA256, + &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: okpx, + }, + }, + okpthumbprint, + "", + }, { + "invalid hash", + 0, + nil, + nil, + ErrUnavailableHashFunc.Error(), + }, { + "missing type", + crypto.SHA256, + &Key{ + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: ec256x, + KeyLabelEC2Y: ec256y, + }, + }, + nil, + ErrOpNotSupported.Error(), + }, { + "unknown key type", + crypto.SHA256, + &Key{ + Type: KeyType(7), + }, + nil, + ErrOpNotSupported.Error(), + }, + { + "EC2 with missing x", + crypto.SHA256, + &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2Y: ec256y, + }, + }, + nil, + ErrEC2NoPub.Error(), + }, + { + "EC2 with invalid type x", + crypto.SHA256, + &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: 0, + KeyLabelEC2Y: ec256y, + }, + }, + nil, + ErrInvalidPubKey.Error(), + }, { + "EC2 P256 with short x", + crypto.SHA256, + &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: ec256x[:31], + KeyLabelEC2Y: ec256y, + }, + }, + nil, + ErrInvalidPubKey.Error(), + }, { + "EC2 P384 with short x", + crypto.SHA256, + &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP384, + KeyLabelEC2X: ec256x, + KeyLabelEC2Y: make([]byte, 48), + }, + }, + nil, + ErrInvalidPubKey.Error(), + }, { + "EC2 P521 with short x", + crypto.SHA256, + &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP521, + KeyLabelEC2X: ec256x, + KeyLabelEC2Y: make([]byte, 66), + }, + }, + nil, + ErrInvalidPubKey.Error(), + }, { + "EC2 with missing y", + crypto.SHA256, + &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: ec256x, + }, + }, + nil, + ErrEC2NoPub.Error(), + }, { + "EC2 with invalid type y", + crypto.SHA256, + &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: ec256x, + KeyLabelEC2Y: 0, + }, + }, + nil, + ErrInvalidPubKey.Error(), + }, { + "EC2 P256 with short y", + crypto.SHA256, + &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: ec256x, + KeyLabelEC2Y: ec256y[:31], + }, + }, + nil, + ErrInvalidPubKey.Error(), + }, { + "EC2 P384 with short y", + crypto.SHA256, + &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP384, + KeyLabelEC2X: make([]byte, 48), + KeyLabelEC2Y: ec256y, + }, + }, + nil, + ErrInvalidPubKey.Error(), + }, { + "EC2 P521 with short y", + crypto.SHA256, + &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveP256, + KeyLabelEC2X: make([]byte, 66), + KeyLabelEC2Y: ec256y, + }, + }, + nil, + ErrInvalidPubKey.Error(), + }, { + "EC2 with invalid crv", + crypto.SHA256, + &Key{ + Type: KeyTypeEC2, + Params: map[any]any{ + KeyLabelEC2Curve: CurveEd25519, + KeyLabelEC2X: ec256x, + KeyLabelEC2Y: ec256y, + }, + }, + nil, + ErrOpNotSupported.Error(), + }, { + "OKP with missing X", + crypto.SHA256, + &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + }, + }, + nil, + ErrOKPNoPub.Error(), + }, { + "OKP with invalid type x", + crypto.SHA256, + &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: 0, + }, + }, + nil, + ErrInvalidPubKey.Error(), + }, { + "OKP Ed25519 with short x", + crypto.SHA256, + &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd25519, + KeyLabelOKPX: okpx[:31], + }, + }, + nil, + ErrInvalidPubKey.Error(), + }, { + "OKP Ed448 with short x", + crypto.SHA256, + &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveEd448, + KeyLabelOKPX: okpx, + }, + }, + nil, + ErrInvalidPubKey.Error(), + }, { + "OKP X25519 with short x", + crypto.SHA256, + &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveX25519, + KeyLabelOKPX: okpx[:31], + }, + }, + nil, + ErrInvalidPubKey.Error(), + }, { + "OKP X448 with short x", + crypto.SHA256, + &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveX448, + KeyLabelOKPX: okpx, + }, + }, + nil, + ErrInvalidPubKey.Error(), + }, { + "OKP with missing crv", + crypto.SHA256, + &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPX: okpx, + }, + }, + nil, + ErrOpNotSupported.Error(), + }, { + "OKP with invalid crv", + crypto.SHA256, + &Key{ + Type: KeyTypeOKP, + Params: map[any]any{ + KeyLabelOKPCurve: CurveP256, + KeyLabelOKPX: okpx, + }, + }, + nil, + ErrOpNotSupported.Error(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.k.Thumbprint(tt.h) + if (err != nil && err.Error() != tt.wantErr) || (err == nil && tt.wantErr != "") { + t.Errorf("Key.Thumbprint(%v) error = %v, wantErr %v", tt.h, err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Key.Thumbprint(%v) = %v, want %v", tt.h, got, tt.want) + } + }) + } +} From 969f61743ca668918fd9b5d4bffb9f897fbd031d Mon Sep 17 00:00:00 2001 From: Ken Takayama Date: Thu, 4 Dec 2025 12:19:47 +0000 Subject: [PATCH 2/3] fix: Avoid magic numbers Signed-off-by: Ken Takayama --- key.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/key.go b/key.go index 266db79..b4e12c3 100644 --- a/key.go +++ b/key.go @@ -781,7 +781,7 @@ func (k *Key) calcOKPThumbprint() ([]byte, error) { switch k.Params[KeyLabelOKPCurve] { case CurveEd25519: - if len(x) != 32 { + if len(x) != ed25519.PublicKeySize { return nil, ErrInvalidPubKey } case CurveEd448: @@ -801,9 +801,9 @@ func (k *Key) calcOKPThumbprint() ([]byte, error) { } m := make(map[int]interface{}) - m[1] = k.Type - m[-1] = k.Params[KeyLabelOKPCurve] - m[-2] = k.Params[KeyLabelEC2X] + m[int(keyLabelKeyType)] = k.Type + m[int(KeyLabelOKPCurve)] = k.Params[KeyLabelOKPCurve] + m[int(KeyLabelOKPX)] = k.Params[KeyLabelOKPX] return encMode.Marshal(m) } @@ -827,15 +827,15 @@ func (k *Key) calcEC2Thumbprint() ([]byte, error) { switch k.Params[KeyLabelEC2Curve] { case CurveP256: - if len(x) != 32 || len(y) != 32 { + if len(x) != curveSize(CurveP256) || len(y) != curveSize(CurveP256) { return nil, ErrInvalidPubKey } case CurveP384: - if len(x) != 48 || len(y) != 48 { + if len(x) != curveSize(CurveP384) || len(y) != curveSize(CurveP384) { return nil, ErrInvalidPubKey } case CurveP521: - if len(x) != 66 || len(y) != 66 { + if len(x) != curveSize(CurveP521) || len(y) != curveSize(CurveP521) { return nil, ErrInvalidPubKey } default: @@ -843,10 +843,10 @@ func (k *Key) calcEC2Thumbprint() ([]byte, error) { } m := make(map[int]interface{}) - m[1] = k.Type - m[-1] = k.Params[KeyLabelEC2Curve] - m[-2] = x - m[-3] = y + m[int(keyLabelKeyType)] = k.Type + m[int(KeyLabelEC2Curve)] = k.Params[KeyLabelEC2Curve] + m[int(KeyLabelEC2X)] = k.Params[KeyLabelEC2X] + m[int(KeyLabelEC2Y)] = k.Params[KeyLabelEC2Y] return encMode.Marshal(m) } From 15e2e05f005a9b66fab48f97d028e46e3f64051c Mon Sep 17 00:00:00 2001 From: Ken Takayama Date: Thu, 4 Dec 2025 12:44:20 +0000 Subject: [PATCH 3/3] refactor: Simplified pre-Thumbprint checks by reusing the existing validate() function. Since validate() performs extensive checks, it may raise errors that are not critical for COSE Key Thumbprint (RFC 9679) calculation. Signed-off-by: Ken Takayama --- key.go | 66 ++++++---------------------------------------------------- 1 file changed, 6 insertions(+), 60 deletions(-) diff --git a/key.go b/key.go index b4e12c3..a4d7eb8 100644 --- a/key.go +++ b/key.go @@ -770,34 +770,9 @@ func (k *Key) Thumbprint(hash crypto.Hash) ([]byte, error) { } func (k *Key) calcOKPThumbprint() ([]byte, error) { - t, ok := k.Params[KeyLabelOKPX] - if !ok { - return nil, ErrOKPNoPub - } - x, ok := t.([]byte) - if !ok { - return nil, ErrInvalidPubKey - } - - switch k.Params[KeyLabelOKPCurve] { - case CurveEd25519: - if len(x) != ed25519.PublicKeySize { - return nil, ErrInvalidPubKey - } - case CurveEd448: - if len(x) != 57 { - return nil, ErrInvalidPubKey - } - case CurveX25519: - if len(x) != 32 { - return nil, ErrInvalidPubKey - } - case CurveX448: - if len(x) != 56 { - return nil, ErrInvalidPubKey - } - default: - return nil, ErrOpNotSupported + err := k.validate(KeyOpReserved) + if err != nil { + return nil, err } m := make(map[int]interface{}) @@ -808,38 +783,9 @@ func (k *Key) calcOKPThumbprint() ([]byte, error) { } func (k *Key) calcEC2Thumbprint() ([]byte, error) { - t, ok := k.Params[KeyLabelEC2X] - if !ok { - return nil, ErrEC2NoPub - } - x, ok := t.([]byte) - if !ok { - return nil, ErrInvalidPubKey - } - t, ok = k.Params[KeyLabelEC2Y] - if !ok { - return nil, ErrEC2NoPub - } - y, ok := t.([]byte) - if !ok { - return nil, ErrInvalidPubKey - } - - switch k.Params[KeyLabelEC2Curve] { - case CurveP256: - if len(x) != curveSize(CurveP256) || len(y) != curveSize(CurveP256) { - return nil, ErrInvalidPubKey - } - case CurveP384: - if len(x) != curveSize(CurveP384) || len(y) != curveSize(CurveP384) { - return nil, ErrInvalidPubKey - } - case CurveP521: - if len(x) != curveSize(CurveP521) || len(y) != curveSize(CurveP521) { - return nil, ErrInvalidPubKey - } - default: - return nil, ErrOpNotSupported + err := k.validate(KeyOpReserved) + if err != nil { + return nil, err } m := make(map[int]interface{})