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
22 changes: 22 additions & 0 deletions examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,28 @@ BznPJ5sSI1Jn+srosJB/GbEZ3Kg6PcEi+jODF9fdpNEaHGbbGdaVhJi1
signer.Sign(req)
}

func ExampleSigningKeyOpts() {
// Create your TPM instance. It must implement crypto.Signer
var tpm crypto.Signer

req := httptest.NewRequest("GET", "https://example.com/data", nil)

profile := httpsig.SigningProfile{
Algorithm: httpsig.Algo_ECDSA_P256_SHA256,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaKeyID},
}
skey := httpsig.SigningKey{
Opts: httpsig.SigningKeyOpts{
Signer: tpm,
},
MetaKeyID: "key123",
}

signer, _ := httpsig.NewSigner(profile, skey)
signer.Sign(req)
}

func ExampleVerify() {
pubkeyEncoded := `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIUctKvU5L/eEYxua5Zlz0HIQJRQq
Expand Down
17 changes: 17 additions & 0 deletions keyutil/keyutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package keyutil

import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
Expand Down Expand Up @@ -68,6 +70,21 @@ func MustReadPrivateKeyFile(pkFile string) crypto.PrivateKey {
return pk
}

func MustReadPrivateKeyFileECDSA(pkFile string) *ecdsa.PrivateKey {
pk := MustReadPrivateKeyFile(pkFile)
if ecpk, ok := pk.(*ecdsa.PrivateKey); ok {
return ecpk
}
panic(fmt.Sprintf("Not ECDSA private key file: %s", pkFile))
}
func MustReadPrivateKeyFileED25519(pkFile string) ed25519.PrivateKey {
pk := MustReadPrivateKeyFile(pkFile)
if edpk, ok := pk.(ed25519.PrivateKey); ok {
return edpk
}
panic("Not ed25519 private key file")
}

func MustReadPrivateKey(encodedPrivateKey []byte) crypto.PrivateKey {
pkey, err := ReadPrivateKey(encodedPrivateKey)
if err != nil {
Expand Down
150 changes: 149 additions & 1 deletion roundtrip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package httpsig_test

import (
"crypto"
"crypto/ecdsa"
"io"
"math/big"
"testing"

"github.com/remitly-oss/httpsig-go"
Expand All @@ -16,6 +19,7 @@ func TestRoundTrip(t *testing.T) {
testcases := []struct {
Name string
PrivateKey crypto.PrivateKey
SigningOpts httpsig.SigningKeyOpts
MetaKeyID string
Secret []byte
SignProfile httpsig.SigningProfile
Expand Down Expand Up @@ -90,7 +94,7 @@ func TestRoundTrip(t *testing.T) {
Profile: createVerifyProfile("sig1"),
},
{
Name: "ECDSA-p265",
Name: "ECDSA-p256",
PrivateKey: keyutil.MustReadPrivateKeyFile("testdata/test-key-ecc-p256.key"),
MetaKeyID: "test-key-ecdsa",
SignProfile: httpsig.SigningProfile{
Expand All @@ -109,6 +113,54 @@ func TestRoundTrip(t *testing.T) {
}),
Profile: createVerifyProfile("tst-ecdsa"),
},
{
Name: "ECDSA-p256-Signer-ASN1",
SigningOpts: httpsig.SigningKeyOpts{
Signer: keyutil.MustReadPrivateKeyFileECDSA("testdata/test-key-ecc-p256.key"),
ASN1ForECDSA: true,
},
MetaKeyID: "test-key-ecdsa",
SignProfile: httpsig.SigningProfile{
Algorithm: httpsig.Algo_ECDSA_P256_SHA256,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaCreated, httpsig.MetaKeyID},
Label: "tst-ecdsa",
},
RequestFile: "rfc-test-request.txt",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-ecdsa": {
KeyID: "test-key-ecds",
Algo: httpsig.Algo_ECDSA_P256_SHA256,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-ecc-p256.pub"),
},
}),
Profile: createVerifyProfile("tst-ecdsa"),
},
{
Name: "ECDSA-p256-Signer-NoASN1",
SigningOpts: httpsig.SigningKeyOpts{
Signer: &testSignerNoASN1{
PK: keyutil.MustReadPrivateKeyFileECDSA("testdata/test-key-ecc-p256.key"),
SigSize: 64,
},
},
MetaKeyID: "test-key-ecdsa",
SignProfile: httpsig.SigningProfile{
Algorithm: httpsig.Algo_ECDSA_P256_SHA256,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaCreated, httpsig.MetaKeyID},
Label: "tst-ecdsa",
},
RequestFile: "rfc-test-request.txt",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-ecdsa": {
KeyID: "test-key-ecds",
Algo: httpsig.Algo_ECDSA_P256_SHA256,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-ecc-p256.pub"),
},
}),
Profile: createVerifyProfile("tst-ecdsa"),
},
{
Name: "ECDSA-p384",
PrivateKey: keyutil.MustReadPrivateKeyFile("testdata/test-key-ecc-p384.key"),
Expand All @@ -129,6 +181,54 @@ func TestRoundTrip(t *testing.T) {
}),
Profile: createVerifyProfile("tst-ecdsa"),
},
{
Name: "ECDSA-p384-Signer-ASN1",
SigningOpts: httpsig.SigningKeyOpts{
Signer: keyutil.MustReadPrivateKeyFileECDSA("testdata/test-key-ecc-p384.key"),
ASN1ForECDSA: true,
},
MetaKeyID: "test-key-ecdsa",
SignProfile: httpsig.SigningProfile{
Algorithm: httpsig.Algo_ECDSA_P384_SHA384,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaCreated, httpsig.MetaKeyID},
Label: "tst-ecdsa",
},
RequestFile: "rfc-test-request.txt",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-ecdsa": {
KeyID: "test-key-ecdsa",
Algo: httpsig.Algo_ECDSA_P384_SHA384,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-ecc-p384.pub"),
},
}),
Profile: createVerifyProfile("tst-ecdsa"),
},
{
Name: "ECDSA-p384-Signer-NoASN1",
SigningOpts: httpsig.SigningKeyOpts{
Signer: &testSignerNoASN1{
PK: keyutil.MustReadPrivateKeyFileECDSA("testdata/test-key-ecc-p384.key"),
SigSize: 96,
},
},
MetaKeyID: "test-key-ecdsa",
SignProfile: httpsig.SigningProfile{
Algorithm: httpsig.Algo_ECDSA_P384_SHA384,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaCreated, httpsig.MetaKeyID},
Label: "tst-ecdsa",
},
RequestFile: "rfc-test-request.txt",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-ecdsa": {
KeyID: "test-key-ecdsa",
Algo: httpsig.Algo_ECDSA_P384_SHA384,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-ecc-p384.pub"),
},
}),
Profile: createVerifyProfile("tst-ecdsa"),
},
{
Name: "ED25519",
PrivateKey: keyutil.MustReadPrivateKeyFile("testdata/test-key-ed25519.key"),
Expand All @@ -149,6 +249,28 @@ func TestRoundTrip(t *testing.T) {
}),
Profile: createVerifyProfile("tst-ed"),
},
{
Name: "ED25519-Signer",
SigningOpts: httpsig.SigningKeyOpts{
Signer: keyutil.MustReadPrivateKeyFileED25519("testdata/test-key-ed25519.key"),
},
MetaKeyID: "test-key-ed",
SignProfile: httpsig.SigningProfile{
Algorithm: httpsig.Algo_ED25519,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaCreated, httpsig.MetaKeyID},
Label: "tst-ed",
},
RequestFile: "rfc-test-request.txt",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-ed": {
KeyID: "test-key-ed",
Algo: httpsig.Algo_ED25519,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-ed25519.pub"),
},
}),
Profile: createVerifyProfile("tst-ed"),
},
{
Name: "BadDigest",
PrivateKey: keyutil.MustReadPrivateKeyFile("testdata/test-key-ed25519.key"),
Expand Down Expand Up @@ -178,6 +300,7 @@ func TestRoundTrip(t *testing.T) {
var signer *httpsig.Signer
sk := httpsig.SigningKey{
Key: tc.PrivateKey,
Opts: tc.SigningOpts,
Secret: tc.Secret,
MetaKeyID: tc.MetaKeyID,
}
Expand Down Expand Up @@ -221,3 +344,28 @@ func createVerifyProfile(label string) httpsig.VerifyProfile {
vp.SignatureLabel = label
return vp
}

type testSignerNoASN1 struct {
PK *ecdsa.PrivateKey
SigSize int
}

func (ts *testSignerNoASN1) Sign(rand io.Reader, message []byte, opts crypto.SignerOpts) (signature []byte, err error) {
r, s, err := ecdsa.Sign(rand, ts.PK, message)
if err != nil {
return nil, err
}
return ecdsaConcatRS(r, s, ts.SigSize), nil
}

func (ts *testSignerNoASN1) Public() crypto.PublicKey {
return ts.PK.PublicKey
}

func ecdsaConcatRS(r, s *big.Int, signatureSize int) []byte {
half := signatureSize / 2
sigBytes := make([]byte, signatureSize)
r.FillBytes(sigBytes[0:half])
s.FillBytes(sigBytes[half:signatureSize])
return sigBytes
}
11 changes: 9 additions & 2 deletions sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,16 @@ func Fields(fields ...string) []SignedField {
type SigningKey struct {
Key crypto.PrivateKey // private key for asymmetric algorithms
Secret []byte // Secret to use for symmetric algorithms
Opts SigningKeyOpts // Options for advanced signing use cases like TPMs.
// Meta fields
MetaKeyID string // 'keyid' - Only used if 'keyid' is set in the SigningProfile. A value must be provided if the parameter is required in the SigningProfile. Metadata.
MetaTag string // 'tag'. Only used if 'tag' is set in the SigningProfile. A value must be provided if the parameter is required in the SigningProfile.
}

type SigningKeyOpts struct {
Signer crypto.Signer // crypto.Signer interface for TPMs and other custom use cases.
ASN1ForECDSA bool // Set to true to indicate the crypto.Signer returns ASN.1 formatted signatures for ECDSA algorithms. False (default) indicates ECDSA signatures are concatenated R and S parameters as per the HTTP Signatures spec.
}
type Signer struct {
profile SigningProfile
skey SigningKey
Expand Down Expand Up @@ -146,6 +151,7 @@ func (s *Signer) Sign(req *http.Request) error {
Algo: s.profile.Algorithm,
PrivateKey: s.skey.Key,
Secret: s.skey.Secret,
Opts: s.skey.Opts,
Label: s.profile.Label,
})
}
Expand All @@ -165,6 +171,7 @@ func (s *Signer) SignResponse(resp *http.Response) error {
Algo: s.profile.Algorithm,
PrivateKey: s.skey.Key,
Secret: s.skey.Secret,
Opts: s.skey.Opts,
Label: s.profile.Label,
})
}
Expand Down Expand Up @@ -214,8 +221,8 @@ func (so SigningProfile) validate(skey SigningKey) error {
if so.Algorithm.symmetric() && len(skey.Secret) == 0 {
return newError(ErrInvalidSignatureOptions, "Missing required 'Secret' value in SigningKey")
}
if !so.Algorithm.symmetric() && skey.Key == nil {
return newError(ErrInvalidSignatureOptions, "Missing required 'Key' value in SigningKey")
if !so.Algorithm.symmetric() && skey.Key == nil && skey.Opts.Signer == nil {
return newError(ErrInvalidSignatureOptions, "Missing required 'Key' or 'Opts.Signer' value in SigningKey")
}
if !isSafeString(so.Label) {
return fmt.Errorf("Invalid label name '%s'", so.Label)
Expand Down
Loading
Loading