diff options
author | Martin Polden <mpolden@mpolden.no> | 2021-08-31 11:13:21 +0200 |
---|---|---|
committer | Martin Polden <mpolden@mpolden.no> | 2021-08-31 11:16:28 +0200 |
commit | a4a9b847b86281c1bf057fee935ae9a897065c8c (patch) | |
tree | 9b555f77e0f2824cd5d9a0327ef842ec2f15efd2 /client | |
parent | aea6001d997421766bf4eec326be7ef9058dc1b8 (diff) |
Implement request signing
Diffstat (limited to 'client')
-rw-r--r-- | client/go/vespa/crypto.go | 92 | ||||
-rw-r--r-- | client/go/vespa/crypto_test.go | 59 |
2 files changed, 139 insertions, 12 deletions
diff --git a/client/go/vespa/crypto.go b/client/go/vespa/crypto.go index a410f65c620..fd28a95b3c4 100644 --- a/client/go/vespa/crypto.go +++ b/client/go/vespa/crypto.go @@ -1,15 +1,20 @@ package vespa import ( + "bytes" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/sha256" "crypto/x509" "crypto/x509/pkix" + "encoding/base64" "encoding/pem" "errors" "fmt" + "io" "math/big" + "net/http" "os" "time" ) @@ -83,21 +88,86 @@ func CreateKeyPair() (PemKeyPair, error) { return PemKeyPair{Certificate: pemCertificate, PrivateKey: pemPrivateKey}, nil } -// LoadKeyPair reads a key pair located in privateKeyFile and certificateFile. -func LoadKeyPair(privateKeyFile, certificateFile string) (PemKeyPair, error) { - var ( - kp PemKeyPair - err error - ) - kp.PrivateKey, err = os.ReadFile(privateKeyFile) +type RequestSigner struct { + now func() time.Time + rnd io.Reader + KeyID string + PemPrivateKey []byte +} + +// NewRequestSigner creates a new signer using the EC pemPrivateKey. keyID names the key used to sign requests. +func NewRequestSigner(keyID string, pemPrivateKey []byte) *RequestSigner { + return &RequestSigner{ + now: time.Now, + rnd: rand.Reader, + KeyID: keyID, + PemPrivateKey: pemPrivateKey, + } +} + +// SignRequest signs the given HTTP request using the private key in rs +func (rs *RequestSigner) SignRequest(request *http.Request) error { + timestamp := rs.now().UTC().Format(time.RFC3339) + contentHash, body, err := contentHash(request.Body) if err != nil { - return PemKeyPair{}, err + return err } - kp.Certificate, err = os.ReadFile(certificateFile) + privateKey, err := ecPrivateKeyFrom(rs.PemPrivateKey) if err != nil { - return PemKeyPair{}, err + return err + } + pemPublicKey, err := pemPublicKeyFrom(privateKey) + if err != nil { + return err + } + base64PemPublicKey := base64.StdEncoding.EncodeToString(pemPublicKey) + signature, err := rs.hashAndSign(privateKey, request, timestamp, contentHash) + if err != nil { + return err + } + base64Signature := base64.StdEncoding.EncodeToString(signature) + request.Body = io.NopCloser(body) + request.Header.Set("X-Timestamp", timestamp) + request.Header.Set("X-Content-Hash", contentHash) + request.Header.Set("X-Key-Id", rs.KeyID) + request.Header.Set("X-Key", base64PemPublicKey) + request.Header.Set("X-Authorization", base64Signature) + return nil +} + +func (rs *RequestSigner) hashAndSign(privateKey *ecdsa.PrivateKey, request *http.Request, timestamp, contentHash string) ([]byte, error) { + msg := []byte(request.Method + "\n" + request.URL.String() + "\n" + timestamp + "\n" + contentHash) + hasher := sha256.New() + hasher.Write(msg) + hash := hasher.Sum(nil) + return ecdsa.SignASN1(rs.rnd, privateKey, hash) +} + +func ecPrivateKeyFrom(pemPrivateKey []byte) (*ecdsa.PrivateKey, error) { + privateKeyBlock, _ := pem.Decode(pemPrivateKey) + if privateKeyBlock == nil { + return nil, fmt.Errorf("invalid pem private key") + } + return x509.ParseECPrivateKey(privateKeyBlock.Bytes) +} + +func pemPublicKeyFrom(privateKey *ecdsa.PrivateKey) ([]byte, error) { + publicKeyDER, err := x509.MarshalPKIXPublicKey(privateKey.Public()) + if err != nil { + return nil, err + } + return pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: publicKeyDER}), nil +} + +func contentHash(r io.Reader) (string, io.Reader, error) { + var copy bytes.Buffer + teeReader := io.TeeReader(r, ©) // Copy reader contents while we hash it + hasher := sha256.New() + if _, err := io.Copy(hasher, teeReader); err != nil { + return "", nil, err } - return kp, err + hashSum := hasher.Sum(nil) + return base64.StdEncoding.EncodeToString(hashSum), ©, nil } func randomSerialNumber() (*big.Int, error) { diff --git a/client/go/vespa/crypto_test.go b/client/go/vespa/crypto_test.go index 6a6d1d6a10a..cb78f06fae9 100644 --- a/client/go/vespa/crypto_test.go +++ b/client/go/vespa/crypto_test.go @@ -1,8 +1,20 @@ package vespa import ( - "github.com/stretchr/testify/assert" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "io" + "math/rand" + "net/http" + "strings" "testing" + "time" + + "github.com/stretchr/testify/assert" ) func TestCreateKeyPair(t *testing.T) { @@ -11,3 +23,48 @@ func TestCreateKeyPair(t *testing.T) { assert.NotEmpty(t, kp.Certificate) assert.NotEmpty(t, kp.PrivateKey) } + +func TestSignRequest(t *testing.T) { + fixedTime := time.Unix(0, 0) + rnd := rand.New(rand.NewSource(0)) // Fixed seed for testing purposes + privateKey := pemECPrivateKey(t, rnd) + rs := RequestSigner{ + now: func() time.Time { return fixedTime }, + rnd: rnd, + KeyID: "my-key", + PemPrivateKey: []byte(privateKey), + } + req, err := http.NewRequest("POST", "https://example.com", strings.NewReader("body")) + if err != nil { + assert.Nil(t, err) + } + + if err := rs.SignRequest(req); err != nil { + assert.Nil(t, err) + } + + assert.Equal(t, "1970-01-01T00:00:00Z", req.Header.Get("X-Timestamp")) + assert.Equal(t, "Iw2DWNyOiJC0xY3utikS7i8gNXrpKlzIYbmOaP4xrLU=", req.Header.Get("X-Content-Hash")) + assert.Equal(t, "my-key", req.Header.Get("X-Key-Id")) + key := req.Header.Get("X-Key") + assert.NotEmpty(t, key) + _, err = base64.StdEncoding.DecodeString(key) + fmt.Println(err) + assert.Nil(t, err) + auth := req.Header.Get("X-Authorization") + assert.NotEmpty(t, auth) + _, err = base64.StdEncoding.DecodeString(auth) + assert.Nil(t, err) +} + +func pemECPrivateKey(t *testing.T, rnd io.Reader) []byte { + key, err := ecdsa.GenerateKey(elliptic.P256(), rnd) + if err != nil { + t.Fatal(err) + } + der, err := x509.MarshalECPrivateKey(key) + if err != nil { + t.Fatal(err) + } + return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der}) +} |