summaryrefslogtreecommitdiffstats
path: root/client
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2021-08-25 14:07:36 +0200
committerMartin Polden <mpolden@mpolden.no>2021-08-25 18:56:03 +0200
commit1d7f47a7d48fb36d27d6c86d7d0aa8ebdabb753f (patch)
tree8f25feb2c99dd667ae220dfa1086270f926032f5 /client
parentec5fe5c67b98311e1b7bb9c0eda20750564eb54a (diff)
Implement key pair generation
Diffstat (limited to 'client')
-rw-r--r--client/go/vespa/crypto.go89
-rw-r--r--client/go/vespa/crypto_test.go13
2 files changed, 102 insertions, 0 deletions
diff --git a/client/go/vespa/crypto.go b/client/go/vespa/crypto.go
new file mode 100644
index 00000000000..b9e41d81576
--- /dev/null
+++ b/client/go/vespa/crypto.go
@@ -0,0 +1,89 @@
+package vespa
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "math/big"
+ "os"
+ "time"
+)
+
+const (
+ defaultCommonName = "cloud.vespa.example"
+ certificateExpiry = 3650 * 24 * time.Hour // Approximately 10 years
+ tempFilePattern = "vespa"
+)
+
+// PemKeyPair represents a PEM-encoded private key and X509 certificate.
+type PemKeyPair struct {
+ Certificate []byte
+ PrivateKey []byte
+}
+
+// WriteCertificateFile writes the certificate contained in this key pair to certificateFile.
+func (kp *PemKeyPair) WriteCertificateFile(certificateFile string, overwrite bool) error {
+ return atomicWriteFile(certificateFile, kp.Certificate, overwrite)
+}
+
+// WritePrivateKeyFile writes the private key contained in this key pair to privateKeyFile.
+func (kp *PemKeyPair) WritePrivateKeyFile(privateKeyFile string, overwrite bool) error {
+ return atomicWriteFile(privateKeyFile, kp.PrivateKey, overwrite)
+}
+
+func atomicWriteFile(filename string, data []byte, overwrite bool) error {
+ tmpFile, err := os.CreateTemp("", tempFilePattern)
+ if err != nil {
+ return err
+ }
+ defer os.Remove(tmpFile.Name())
+ if err := os.WriteFile(tmpFile.Name(), data, 0600); err != nil {
+ return err
+ }
+ _, err = os.Stat(filename)
+ if errors.Is(err, os.ErrNotExist) || overwrite {
+ return os.Rename(tmpFile.Name(), filename)
+ }
+ return fmt.Errorf("cannot overwrite existing file: %s", filename)
+}
+
+// CreateKeyPair creates a key pair containing a private key and self-signed X509 certificate.
+func CreateKeyPair() (PemKeyPair, error) {
+ privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ return PemKeyPair{}, fmt.Errorf("failed to generate private key: %w", err)
+ }
+ serialNumber, err := randomSerialNumber()
+ if err != nil {
+ return PemKeyPair{}, fmt.Errorf("failed to create serial number: %w", err)
+ }
+ notBefore := time.Now()
+ notAfter := notBefore.Add(certificateExpiry)
+ template := x509.Certificate{
+ SerialNumber: serialNumber,
+ Subject: pkix.Name{CommonName: defaultCommonName},
+ NotBefore: notBefore,
+ NotAfter: notAfter,
+ }
+ certificateDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
+ if err != nil {
+ return PemKeyPair{}, err
+ }
+ privateKeyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
+ if err != nil {
+ return PemKeyPair{}, err
+ }
+ pemPrivateKey := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyDER})
+ pemCertificate := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certificateDER})
+ return PemKeyPair{Certificate: pemCertificate, PrivateKey: pemPrivateKey}, nil
+}
+
+func randomSerialNumber() (*big.Int, error) {
+ serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+ return rand.Int(rand.Reader, serialNumberLimit)
+}
diff --git a/client/go/vespa/crypto_test.go b/client/go/vespa/crypto_test.go
new file mode 100644
index 00000000000..5ac8464e4b0
--- /dev/null
+++ b/client/go/vespa/crypto_test.go
@@ -0,0 +1,13 @@
+package vespa
+
+import (
+ "testing"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCreateKeyPair(t *testing.T) {
+ kp, err := CreateKeyPair()
+ assert.Nil(t, err)
+ assert.NotEmpty(t, kp.Certificate)
+ assert.NotEmpty(t, kp.PrivateKey)
+}