fleet/third_party/httpsig-go/keyutil/jwk.go
Victor Lyuboslavsky c25fed2492
Added a vendored version of httpsig-go. (#30820)
For #30473

This change adds a vendored `httpsig-go` library to our repo. We cannot
use the upstream library because it has not merged the change we need:
https://github.com/remitly-oss/httpsig-go/pull/25

Thus, we need our own copy at this point.

The instructions for keeping this library up to date (if needed) are in
`UPDATE_INSTRUCTIONS`.

None of the coderabbitai review comments are relevant to the
code/features we are going to use for HTTP message signatures.

We will use this library in subsequent PRs for the TPM-backed HTTP
message signature feature.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced a Go library for HTTP message signing and verification,
supporting multiple cryptographic algorithms (RSA, ECDSA, Ed25519,
HMAC).
* Added utilities for key management, including JWK and PEM key
handling.
* Provided HTTP client and server helpers for automatic request signing
and signature verification.
* Implemented structured error handling and metadata extraction for
signatures.

* **Documentation**
  * Added comprehensive README, usage examples, and update instructions.
* Included license and configuration files for third-party and testing
tools.

* **Tests**
* Added extensive unit, integration, and fuzz tests covering signing,
verification, and key handling.
* Included official RFC test vectors and various test data files for
robust validation.

* **Chores**
* Integrated continuous integration workflows and ignore files for code
quality and security analysis.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-14 20:26:50 +02:00

284 lines
6.7 KiB
Go

package keyutil
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"encoding/base64"
"encoding/json"
"fmt"
"math/big"
"os"
)
const (
KeyTypeEC = "EC"
KeyTypeOct = "oct"
)
func ReadJWKFile(jwkFile string) (JWK, error) {
keyBytes, err := os.ReadFile(jwkFile)
if err != nil {
return JWK{}, fmt.Errorf("Failed to read jwk key file '%s': %w", jwkFile, err)
}
return ReadJWK(keyBytes)
}
func ReadJWK(jwkBytes []byte) (JWK, error) {
base := jwk{}
err := json.Unmarshal(jwkBytes, &base)
if err != nil {
return JWK{}, fmt.Errorf("Failed to json parse JWK: %w", err)
}
jwk := JWK{
KeyType: base.KeyType,
Algorithm: base.Algo,
KeyID: base.KeyID,
}
switch base.KeyType {
case KeyTypeEC:
jec := jwkEC{}
err := json.Unmarshal(jwkBytes, &jec)
if err != nil {
return JWK{}, fmt.Errorf("Failed to json parse JWK: %w", err)
}
jwk.jwtImpl = jec
case KeyTypeOct:
jsym := jwkSymmetric{}
err := json.Unmarshal(jwkBytes, &jsym)
if err != nil {
return JWK{}, fmt.Errorf("Failed to json parse JWK: %w", err)
}
jwk.jwtImpl = jsym
default:
return JWK{}, fmt.Errorf("Unsupported key type/kty - '%s'", base.KeyType)
}
return jwk, nil
}
// ReadJWKFromPEM converts a PEM encoded private key to JWK. 'kty' is set based on the passed in PrivateKey type.
func ReadJWKFromPEM(pkeyBytes []byte) (JWK, error) {
pkey, err := ReadPrivateKey(pkeyBytes)
if err != nil {
return JWK{}, err
}
return FromPrivateKey(pkey)
}
// FromPrivateKey creates a JWK from a crypto.PrivateKey. 'kty' is set based on the passed in PrivateKey.
func FromPrivateKey(pkey crypto.PrivateKey) (JWK, error) {
switch key := pkey.(type) {
case *ecdsa.PrivateKey:
jec := jwkEC{
jwk: jwk{
KeyType: KeyTypeEC,
},
Curve: key.Curve.Params().Name,
X: &octet{*key.X},
Y: &octet{*key.Y},
D: &octet{*key.D},
}
return JWK{
KeyType: KeyTypeEC,
jwtImpl: jec,
}, nil
default:
return JWK{}, fmt.Errorf("Unsupported private key type '%T'", pkey)
}
}
// JWK provides basic data and usage for a JWK.
type JWK struct {
// Common fields are duplicated as struct members for better usability.
KeyType string // 'kty' - "EC", "RSA", "oct"
Algorithm string // 'alg'
KeyID string // 'kid'
jwtImpl any // the specific implementation of JWK based on KeyType.
}
func (ji *JWK) PublicKey() (crypto.PublicKey, error) {
switch ji.KeyType {
case ji.KeyType: // ECC
if jec, ok := ji.jwtImpl.(jwkEC); ok {
return jec.PublicKey()
}
}
return nil, fmt.Errorf("Unsupported key type for PublicKey - '%s'", ji.KeyType)
}
func (ji *JWK) PublicKeyJWK() (JWK, error) {
switch ji.KeyType {
case KeyTypeEC:
if jec, ok := ji.jwtImpl.(jwkEC); ok {
jec.jwk.Algo = ji.Algorithm
jec.jwk.KeyID = ji.KeyID
return jec.PublicKeyJWK()
}
}
return JWK{}, fmt.Errorf("Unsupported key type for PublicKey'%s'", ji.KeyType)
}
func (ji *JWK) PrivateKey() (crypto.PrivateKey, error) {
switch ji.KeyType {
case KeyTypeEC:
if jec, ok := ji.jwtImpl.(jwkEC); ok {
return jec.PrivateKey()
}
}
return nil, fmt.Errorf("Unsupported key type PrivateKey - '%s'", ji.KeyType)
}
func (ji *JWK) SecretKey() ([]byte, error) {
switch ji.KeyType {
case KeyTypeOct:
if jsym, ok := ji.jwtImpl.(jwkSymmetric); ok {
return jsym.Key(), nil
}
}
return nil, fmt.Errorf("Unsupported key type for Secret '%s'", ji.KeyType)
}
// octet represents the data for base64 URL encoded data as specified by JWKs.
type octet struct {
big.Int
}
func (ob octet) MarshalJSON() ([]byte, error) {
out := fmt.Sprintf("\"%s\"", base64.RawURLEncoding.EncodeToString(ob.Bytes()))
return []byte(out), nil
}
func (ob *octet) UnmarshalJSON(data []byte) error {
// data is the json value and must be unmarshaled into a go string first
encoded := ""
err := json.Unmarshal(data, &encoded)
if err != nil {
return err
}
rawBytes, err := base64.RawURLEncoding.DecodeString(encoded)
if err != nil {
return fmt.Errorf("Failed to base64 decode: %w", err)
}
x := new(big.Int)
x.SetBytes(rawBytes)
*ob = octet{*x}
return nil
}
type jwk struct {
KeyType string `json:"kty"` // kty algorithm family used with the key such as "RSA" or "EC".
Algo string `json:"alg,omitempty"` // alg identifies the algorithm intended for use with the key.
KeyID string `json:"kid,omitempty"` // Used to match a specific key
}
type jwkEC struct {
jwk
Curve string `json:"crv"` // The curve used with the key e.g. P-256
X *octet `json:"x"` // x coordinate of the curve.
Y *octet `json:"y"` // y coordinate of the curve.
D *octet `json:"d,omitempty"` // For private keys.
}
func (ec *jwkEC) params() (crv elliptic.Curve, byteLen int, e error) {
switch ec.Curve {
case "P-256":
crv = elliptic.P256()
case "P-384":
crv = elliptic.P384()
case "P-521":
crv = elliptic.P521()
default:
return nil, 0, fmt.Errorf("Unsupported ECC curve '%s'", ec.Curve)
}
return crv, crv.Params().BitSize / 8, nil
}
func (ec *jwkEC) PublicKey() (*ecdsa.PublicKey, error) {
crv, byteLen, err := ec.params()
if err != nil {
return nil, err
}
if len(ec.X.Bytes()) != byteLen {
return nil, fmt.Errorf("X coordinate must be %d byte length for curve '%s'. Got '%d'", byteLen, ec.Curve, len(ec.X.Bytes()))
}
if len(ec.Y.Bytes()) != byteLen {
return nil, fmt.Errorf("Y coordinate must be %d byte length for curve '%s'. Got '%d'", byteLen, ec.Curve, len(ec.Y.Bytes()))
}
return &ecdsa.PublicKey{
Curve: crv,
X: &ec.X.Int,
Y: &ec.Y.Int,
}, nil
}
func (ec *jwkEC) PublicKeyJWK() (JWK, error) {
return JWK{
KeyType: ec.KeyType,
Algorithm: ec.Algo,
KeyID: ec.KeyID,
jwtImpl: jwkEC{
jwk: ec.jwk,
Curve: ec.Curve,
X: ec.X,
Y: ec.Y,
},
}, nil
}
func (ec *jwkEC) PrivateKey() (*ecdsa.PrivateKey, error) {
if ec.D == nil {
return nil, fmt.Errorf("JWK does not contain a private key")
}
pubkey, err := ec.PublicKey()
if err != nil {
return nil, err
}
_, byteLen, err := ec.params()
if err != nil {
return nil, err
}
if len(ec.D.Bytes()) != byteLen {
return nil, fmt.Errorf("D coordinate must be %d byte length for curve '%s'. Got '%d'", byteLen, ec.Curve, len(ec.D.Bytes()))
}
return &ecdsa.PrivateKey{
PublicKey: *pubkey,
D: &ec.D.Int,
}, nil
}
type jwkSymmetric struct {
jwk
K *octet `json:"k" ` // Symmetric key
}
func (js *jwkSymmetric) Key() []byte {
return js.K.Bytes()
}
func (j JWK) MarshalJSON() ([]byte, error) {
// Set the Algo and KeyID in case the JWK fields have changed
switch jt := j.jwtImpl.(type) {
case jwkEC:
jt.jwk.Algo = j.Algorithm
jt.jwk.KeyID = j.KeyID
return json.Marshal(jt)
case jwkSymmetric:
jt.jwk.Algo = j.Algorithm
jt.jwk.KeyID = j.KeyID
return json.Marshal(jt)
}
return json.Marshal(j.jwtImpl)
}