Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions base.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ func (hrr httpMessage) Context() context.Context {
return hrr.Req.Context()
}

func (hrr httpMessage) isDebug() bool {
if dbgval, ok := hrr.Context().Value(ctxKeyAddDebug).(bool); ok {
return dbgval
}
return false
}

/*
calculateSignatureBase calculates the 'signature base' - the data used as the input to signing or verifying
The signature base is an ASCII string containing the canonicalized HTTP message components covered by the signature.
Expand Down Expand Up @@ -232,12 +239,10 @@ func deriveComponentValueRequest(req *http.Request, component componentID) (stri
// deriveTargetURI resolves to an absolute form as required by RFC 9110 and referenced by the http signatures spec.
// The target URI excludes the reference's fragment component, if any, since fragment identifiers are reserved for client-side processing
func deriveTargetURI(req *http.Request) string {
if req.URL.IsAbs() {
return req.RequestURI
}
scheme := "https"
if req.TLS == nil {
scheme = "http"
}

return fmt.Sprintf("%s://%s%s%s", scheme, req.Host, req.URL.RawPath, req.URL.RawQuery)
}
5 changes: 2 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ module github.com/remitly-oss/httpsig-go
go 1.21.4

require (
github.com/dunglas/httpsfv v1.0.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
github.com/dunglas/httpsfv v1.0.2
github.com/google/go-cmp v0.7.0
)
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0=
github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
161 changes: 106 additions & 55 deletions keyutil/jwk.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import (
"os"
)

const (
KeyTypeEC = "EC"
KeyTypeOct = "oct"
)

func ReadJWKFile(jwkFile string) (JWK, error) {
keyBytes, err := os.ReadFile(jwkFile)
if err != nil {
Expand All @@ -23,49 +28,61 @@ 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 public key: %w", err)
return JWK{}, fmt.Errorf("Failed to json parse JWK: %w", err)
}
return JWK{
jwk := JWK{
KeyType: base.KeyType,
Algorithm: base.Algo,
KeyID: base.KeyID,
raw: json.RawMessage(jwkBytes),
}, nil
}
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.jtype = 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.jtype = 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. Use 'fields' to set additional fields like kty, and kid. alg is set based on the passed in PrivateKey.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: update comment to not mention "fields".

func ReadJWKFromPEM(fields JWK, pkeyBytes []byte) (JWK, error) {
func ReadJWKFromPEM(pkeyBytes []byte) (JWK, error) {
pkey, err := ReadPrivateKey(pkeyBytes)
if err != nil {
return JWK{}, err
}
return FromPrivateKey(fields, pkey)
return FromPrivateKey(pkey)
}

// FromPrivateKey creates a JWK from a crypto.PrivateKey. Use 'fields' to set additional fields like kty, and kid. alg is set based on the passed in PrivateKey.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: same(update comment)

func FromPrivateKey(fields JWK, pkey crypto.PrivateKey) (JWK, error) {
func FromPrivateKey(pkey crypto.PrivateKey) (JWK, error) {
switch key := pkey.(type) {
case *ecdsa.PrivateKey:
jwk := jwkEC{
jec := jwkEC{
jwk: jwk{
KeyType: "EC",
Algo: fields.Algorithm,
KeyID: fields.KeyID,
KeyType: KeyTypeEC,
},
Curve: key.Curve.Params().Name,
X: octet{key.X},
Y: octet{key.Y},
D: octet{key.D},
}
out, err := json.Marshal(jwk)
if err != nil {
return JWK{}, fmt.Errorf("Error marshalling JWK: %w", err)
X: &octet{*key.X},
Y: &octet{*key.Y},
D: &octet{*key.D},
}

return JWK{
KeyType: "EC",
Algorithm: fields.Algorithm,
KeyID: fields.KeyID,
raw: out,
KeyType: KeyTypeEC,
jtype: jec,
}, nil
default:
return JWK{}, fmt.Errorf("Unsupported private key type '%T'", pkey)
Expand All @@ -74,55 +91,60 @@ func FromPrivateKey(fields JWK, pkey crypto.PrivateKey) (JWK, error) {

// JWK provides basic data and usage for a JWK.
type JWK struct {
KeyType string // 'kty'
KeyType string // 'kty' - "EC", "RSA", "oct"
Algorithm string // 'alg'
KeyID string // 'kid'
raw json.RawMessage
jtype any // the type of JWK based on KeyType.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the field jtype holds the concrete value. The name sounds like it's some type only, from direct look, but I think it's okay since it's not exposed 🤔

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good call out. I'll change it. jwtImpl or something similar might be more clear.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sgtm, thanks!

}

func (ji *JWK) PublicKey() (crypto.PublicKey, error) {
switch ji.KeyType {
case "EC": // ECC
jwk := jwkEC{}
err := json.Unmarshal(ji.raw, &jwk)
if err != nil {
return JWK{}, fmt.Errorf("Failed to json parse JWK into key type 'EC': %w", err)
case ji.KeyType: // ECC
if jec, ok := ji.jtype.(jwkEC); ok {
return jec.PublicKey()
}
return jwk.PublicKey()
}

return nil, fmt.Errorf("Unsupported key type for PublicKey'%s'", ji.KeyType)
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.jtype.(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 "EC":
jwk := jwkEC{}
err := json.Unmarshal(ji.raw, &jwk)
if err != nil {
return JWK{}, fmt.Errorf("Failed to json parse JWK into key type 'EC': %w", err)
case KeyTypeEC:
if jec, ok := ji.jtype.(jwkEC); ok {
return jec.PrivateKey()
}
return jwk.PrivateKey()
}
return nil, fmt.Errorf("Unsupported key type PrivateKey '%s'", ji.KeyType)
return nil, fmt.Errorf("Unsupported key type PrivateKey - '%s'", ji.KeyType)
}

func (ji *JWK) SecretKey() ([]byte, error) {
switch ji.KeyType {
case "oct":
jwk := jwkSymmetric{}
err := json.Unmarshal(ji.raw, &jwk)
if err != nil {
return nil, fmt.Errorf("Failed to json parse JWK into key type 'oct': %w", err)
case KeyTypeOct:
if jsym, ok := ji.jtype.(jwkSymmetric); ok {
return jsym.Key(), nil
}
return jwk.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
big.Int
}

func (ob octet) MarshalJSON() ([]byte, error) {
Expand All @@ -145,7 +167,7 @@ func (ob *octet) UnmarshalJSON(data []byte) error {

x := new(big.Int)
x.SetBytes(rawBytes)
*ob = octet{x}
*ob = octet{*x}

return nil
}
Expand All @@ -159,9 +181,9 @@ type jwk struct {
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.
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) {
Expand Down Expand Up @@ -193,12 +215,29 @@ func (ec *jwkEC) PublicKey() (*ecdsa.PublicKey, error) {

return &ecdsa.PublicKey{
Curve: crv,
X: ec.X.Int,
Y: ec.Y.Int,
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,
jtype: 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
Expand All @@ -214,19 +253,31 @@ func (ec *jwkEC) PrivateKey() (*ecdsa.PrivateKey, error) {

return &ecdsa.PrivateKey{
PublicKey: *pubkey,
D: ec.D.Int,
D: &ec.D.Int,
}, nil
}

type jwkSymmetric struct {
jwk
K octet `json:"k" ` // Symmetric key
K *octet `json:"k" ` // Symmetric key
}

func (js *jwkSymmetric) Key() []byte {
return js.K.Bytes()
}

func (jwk JWK) MarshalJSON() ([]byte, error) {
return jwk.raw, nil
func (j JWK) MarshalJSON() ([]byte, error) {
// Set the Algo and KeyID in case the JWK fields have changed
switch jt := j.jtype.(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.jtype)
}
11 changes: 7 additions & 4 deletions keyutil/jwk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"strings"
"testing"

Expand Down Expand Up @@ -78,11 +79,13 @@ func TestJWKMarshalRoundTrip(t *testing.T) {
inputType string
expectedErrContains string
keyid string
algorithm string
}{
{
name: "EC Key Round Trip",
inputType: "EC",
keyid: "mykey_123",
algorithm: "myalgo",
},
}

Expand All @@ -97,10 +100,10 @@ func TestJWKMarshalRoundTrip(t *testing.T) {
t.Fatal(err)
}
}
original, err := FromPrivateKey(JWK{
KeyID: tc.keyid,
}, pk)

original, err := FromPrivateKey(pk)
original.KeyID = tc.keyid
original.Algorithm = tc.algorithm
if err != nil {
if tc.expectedErrContains != "" {
if !strings.Contains(err.Error(), tc.expectedErrContains) {
Expand All @@ -112,7 +115,7 @@ func TestJWKMarshalRoundTrip(t *testing.T) {
}

// Marshal JWK to JSON
jsonBytes, err := original.MarshalJSON()
jsonBytes, err := json.Marshal(original)
if err != nil {
t.Fatalf("Failed to marshal JWK: %v", err)
}
Expand Down
4 changes: 2 additions & 2 deletions signatures.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func sign(hrr httpMessage, sp sigParameters) error {
r.FillBytes(sigBytes[0:32])
s.FillBytes(sigBytes[32:64])
} else {
return fmt.Errorf("Invalid private key. Requires ed25519.PrivateKey")
return fmt.Errorf("Invalid private key. Requires *ecdsa.PrivateKey")
}
case Algo_ECDSA_P384_SHA384:
if eccpk, ok := sp.PrivateKey.(*ecdsa.PrivateKey); ok {
Expand All @@ -108,7 +108,7 @@ func sign(hrr httpMessage, sp sigParameters) error {
r.FillBytes(sigBytes[0:48])
s.FillBytes(sigBytes[48:96])
} else {
return fmt.Errorf("Invalid private key. Requires ed25519.PrivateKey")
return fmt.Errorf("Invalid private key. Requires *ecdsa.PrivateKey")
}
case Algo_ED25519:
if edpk, ok := sp.PrivateKey.(ed25519.PrivateKey); ok {
Expand Down
2 changes: 1 addition & 1 deletion sigtest/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func ReadTestPrivateKey(t testing.TB, pkFile string, hint ...string) crypto.Priv
return pkey
}

func Diff(t *testing.T, expected, actual interface{}, msg string, opts ...cmp.Option) bool {
func Diff(t *testing.T, expected, actual any, msg string, opts ...cmp.Option) bool {
if diff := cmp.Diff(expected, actual, opts...); diff != "" {
t.Errorf("%s (-want +got):\n%s", msg, diff)
return true
Expand Down
Loading