summaryrefslogtreecommitdiffstats
path: root/client
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2021-08-31 11:13:21 +0200
committerMartin Polden <mpolden@mpolden.no>2021-08-31 11:16:28 +0200
commita4a9b847b86281c1bf057fee935ae9a897065c8c (patch)
tree9b555f77e0f2824cd5d9a0327ef842ec2f15efd2 /client
parentaea6001d997421766bf4eec326be7ef9058dc1b8 (diff)
Implement request signing
Diffstat (limited to 'client')
-rw-r--r--client/go/vespa/crypto.go92
-rw-r--r--client/go/vespa/crypto_test.go59
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) // 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), &copy, 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})
+}