diff --git a/internal/deterministicecdsa/ecdsa.go b/internal/deterministicecdsa/ecdsa.go new file mode 100644 index 000000000..f18804c09 --- /dev/null +++ b/internal/deterministicecdsa/ecdsa.go @@ -0,0 +1,156 @@ +// Package deterministicecdsa contains the original ecdsa.GenerateKey before it was made non-deterministic. +package deterministicecdsa + +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package ecdsa implements the Elliptic Curve Digital Signature Algorithm, as +// defined in FIPS 186-4 and SEC 1, Version 2.0. +// +// Signatures generated by this package are not deterministic, but entropy is +// mixed with the private key and the message, achieving the same level of +// security in case of randomness source failure. + +// [FIPS 186-4] references ANSI X9.62-2005 for the bulk of the ECDSA algorithm. +// That standard is not freely available, which is a problem in an open source +// implementation, because not only the implementer, but also any maintainer, +// contributor, reviewer, auditor, and learner needs access to it. Instead, this +// package references and follows the equivalent [SEC 1, Version 2.0]. +// +// [FIPS 186-4]: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf +// [SEC 1, Version 2.0]: https://www.secg.org/sec1-v2.pdf + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "io" + "math/big" + + "golang.org/x/crypto/cryptobyte" + "golang.org/x/crypto/cryptobyte/asn1" +) + +var one = new(big.Int).SetInt64(1) + +// randFieldElement returns a random element of the order of the given +// curve using the procedure given in FIPS 186-4, Appendix B.5.1. +func randFieldElement(c elliptic.Curve, rand io.Reader) (k *big.Int, err error) { + params := c.Params() + // Note that for P-521 this will actually be 63 bits more than the order, as + // division rounds down, but the extra bit is inconsequential. + b := make([]byte, params.BitSize/8+8) // TODO: use params.N.BitLen() + _, err = io.ReadFull(rand, b) + if err != nil { + return + } + + k = new(big.Int).SetBytes(b) + n := new(big.Int).Sub(params.N, one) + k.Mod(k, n) + k.Add(k, one) + return +} + +// hashToInt converts a hash value to an integer. There is some disagreement +// about how this is done. [NSA] suggests that this is done in the obvious +// manner, but [SECG] truncates the hash to the bit-length of the curve order +// first. We follow [SECG] because that's what OpenSSL does. +func hashToInt(hash []byte, c elliptic.Curve) *big.Int { + orderBits := c.Params().N.BitLen() + orderBytes := (orderBits + 7) / 8 + if len(hash) > orderBytes { + hash = hash[:orderBytes] + } + + ret := new(big.Int).SetBytes(hash) + excess := orderBytes*8 - orderBits + if excess > 0 { + ret.Rsh(ret, uint(excess)) + } + return ret +} + +// GenerateKey generates a public and private key pair. +func GenerateKey(c elliptic.Curve, rand io.Reader) (*ecdsa.PrivateKey, error) { + k, err := randFieldElement(c, rand) + if err != nil { + return nil, err + } + + priv := new(ecdsa.PrivateKey) + priv.PublicKey.Curve = c + priv.D = k + priv.PublicKey.X, priv.PublicKey.Y = c.ScalarBaseMult(k.Bytes()) + return priv, nil +} + +type deterministicPrivateKey struct { + *ecdsa.PrivateKey +} + +// WrapPrivateKey wraps a private key so that the Sign method is deterministic +func WrapPrivateKey(privateKey *ecdsa.PrivateKey) crypto.PrivateKey { + return deterministicPrivateKey{PrivateKey: privateKey} +} + +// Sign signs digest with priv, reading randomness from rand. The opts argument +// is not currently used but, in keeping with the crypto.Signer interface, +// should be the hash function used to digest the message. +// +// This method implements crypto.Signer, which is an interface to support keys +// where the private part is kept in, for example, a hardware module. Common +// uses can use the SignASN1 function in this package directly. +func (priv deterministicPrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + r, s, err := Sign(rand, priv.PrivateKey, digest) + if err != nil { + return nil, err + } + + var b cryptobyte.Builder + b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) { + b.AddASN1BigInt(r) + b.AddASN1BigInt(s) + }) + return b.Bytes() +} + +// Sign signs an arbitrary length hash (which should be the result of hashing a +// larger message) using the private key, priv. It returns the signature as a +// pair of integers. The security of the private key depends on the entropy of +// rand. +func Sign(rand io.Reader, priv *ecdsa.PrivateKey, hash []byte) (r, s *big.Int, err error) { + // See [NSA] 3.4.1 + c := priv.PublicKey.Curve + N := c.Params().N + + var k, kInv *big.Int //nolint + for { + for { + k, err = randFieldElement(c, rand) + if err != nil { + r = nil + return + } + + kInv = new(big.Int).ModInverse(k, N) + r, _ = priv.Curve.ScalarBaseMult(k.Bytes()) + r.Mod(r, N) + if r.Sign() != 0 { + break + } + } + + e := hashToInt(hash, c) + s = new(big.Int).Mul(priv.D, r) + s.Add(s, e) + s.Mul(s, kInv) + s.Mod(s, N) + if s.Sign() != 0 { + break + } + } + + return //nolint +} diff --git a/pkg/derivecert/ca.go b/pkg/derivecert/ca.go index 3fef69953..9f1128a2e 100644 --- a/pkg/derivecert/ca.go +++ b/pkg/derivecert/ca.go @@ -2,21 +2,19 @@ package derivecert import ( "crypto/ecdsa" - "crypto/elliptic" "crypto/rand" - "crypto/sha256" "crypto/x509" "crypto/x509/pkix" "fmt" - "io" "math/big" "time" - "golang.org/x/crypto/hkdf" + "github.com/pomerium/pomerium/internal/deterministicecdsa" ) // CA is certificate authority type CA struct { + psk []byte // key is signing key key *ecdsa.PrivateKey // cert is a CA certificate @@ -45,9 +43,9 @@ var ( // and provides a better alternative to plaintext communication, // but is not a replacement for proper mTLS. func NewCA(psk []byte) (*CA, error) { - key, err := ecdsa.GenerateKey(elliptic.P256(), pskRandReader(psk)) + key, err := deriveKey(newReader(readerTypeCAPrivateKey, psk)) if err != nil { - return nil, fmt.Errorf("generating key: %w", err) + return nil, fmt.Errorf("derive key: %w", err) } cert, err := caCertTemplate(psk) @@ -55,7 +53,11 @@ func NewCA(psk []byte) (*CA, error) { return nil, err } - der, err := x509.CreateCertificate(pskRandReader(psk), cert, cert, &key.PublicKey, key) + der, err := x509.CreateCertificate( + newReader(readerTypeCACertificate, psk), + cert, cert, + key.Public(), deterministicecdsa.WrapPrivateKey(key), + ) if err != nil { return nil, fmt.Errorf("create cert: %w", err) } @@ -64,7 +66,7 @@ func NewCA(psk []byte) (*CA, error) { return nil, fmt.Errorf("parse cert: %w", err) } - ca := &CA{key, cert} + ca := &CA{psk, key, cert} return ca, nil } @@ -82,17 +84,21 @@ func CAFromPEM(p PEM) (*CA, string, error) { // NewServerCert generates certificate for the given domain name(s) func (ca *CA) NewServerCert(domains []string) (*PEM, error) { - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + key, err := deriveKey(newReader(readerTypeServerPrivateKey, ca.psk, domains...)) if err != nil { - return nil, fmt.Errorf("generate key: %w", err) + return nil, fmt.Errorf("derive key: %w", err) } - tmpl, err := serverCertTemplate(domains) + tmpl, err := serverCertTemplate(ca.psk, domains) if err != nil { return nil, fmt.Errorf("cert template: %w", err) } - cert, err := x509.CreateCertificate(rand.Reader, tmpl, ca.cert, key.Public(), ca.key) + cert, err := x509.CreateCertificate( + newReader(readerTypeServerCertificate, ca.psk, domains...), + tmpl, ca.cert, + key.Public(), deterministicecdsa.WrapPrivateKey(ca.key), + ) if err != nil { return nil, fmt.Errorf("create cert: %w", err) } @@ -105,12 +111,8 @@ func (ca *CA) PEM() (*PEM, error) { return ToPEM(ca.key, ca.cert.Raw) } -func pskRandReader(psk []byte) io.Reader { - return hkdf.New(sha256.New, psk, nil, nil) -} - func caCertTemplate(psk []byte) (*x509.Certificate, error) { - serial, err := newSerial() + serial, err := newSerial(psk) if err != nil { return nil, err } @@ -127,18 +129,18 @@ func caCertTemplate(psk []byte) (*x509.Certificate, error) { }, nil } -func serverCertTemplate(domains []string) (*x509.Certificate, error) { - serial, err := newSerial() +func serverCertTemplate(psk []byte, domains []string) (*x509.Certificate, error) { + serial, err := newSerial(psk, domains...) if err != nil { return nil, err } return &x509.Certificate{ SerialNumber: serial, - Subject: pkix.Name{Organization: []string{"Pomerium"}, CommonName: "Pomerium PSK domain cert"}, + Subject: pkix.Name{Organization: []string{"Pomerium"}}, NotBefore: notBefore, NotAfter: notAfter, - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, DNSNames: domains, }, nil @@ -149,9 +151,9 @@ func (ca *CA) Key() *ecdsa.PrivateKey { return ca.key } -func newSerial() (*big.Int, error) { +func newSerial(psk []byte, domains ...string) (*big.Int, error) { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + serialNumber, err := rand.Int(newReader(readerTypeSerialNumber, psk, domains...), serialNumberLimit) if err != nil { return nil, fmt.Errorf("failed to generate serial number: %w", err) } diff --git a/pkg/derivecert/ca_test.go b/pkg/derivecert/ca_test.go index 8f259c187..24d6ee88d 100644 --- a/pkg/derivecert/ca_test.go +++ b/pkg/derivecert/ca_test.go @@ -19,33 +19,35 @@ func TestCA(t *testing.T) { _, err := rand.Read(psk) require.NoError(t, err) - ca1, err := derivecert.NewCA(psk) - require.NoError(t, err) - ca2, err := derivecert.NewCA(psk) - require.NoError(t, err) + for i := 0; i < 100; i++ { + ca1, err := derivecert.NewCA(psk) + require.NoError(t, err) + ca2, err := derivecert.NewCA(psk) + require.NoError(t, err) - ca1PEM, err := ca2.PEM() - require.NoError(t, err) - ca2PEM, err := ca2.PEM() - require.NoError(t, err) + ca1PEM, err := ca2.PEM() + require.NoError(t, err) + ca2PEM, err := ca2.PEM() + require.NoError(t, err) - assert.Equal(t, ca1PEM.Key, ca2PEM.Key) + assert.Equal(t, ca1PEM.Key, ca2PEM.Key) - serverPEM, err := ca1.NewServerCert([]string{"myserver.com"}) - require.NoError(t, err) + serverPEM, err := ca1.NewServerCert([]string{"myserver.com"}) + require.NoError(t, err) - _, serverCert, err := serverPEM.KeyCert() - require.NoError(t, err) + _, serverCert, err := serverPEM.KeyCert() + require.NoError(t, err) - pool := x509.NewCertPool() - require.True(t, pool.AppendCertsFromPEM(ca2PEM.Cert)) + pool := x509.NewCertPool() + require.True(t, pool.AppendCertsFromPEM(ca2PEM.Cert)) - opts := x509.VerifyOptions{ - Roots: pool, - DNSName: "myserver.com", - Intermediates: x509.NewCertPool(), + opts := x509.VerifyOptions{ + Roots: pool, + DNSName: "myserver.com", + Intermediates: x509.NewCertPool(), + } + + _, err = serverCert.Verify(opts) + require.NoError(t, err) } - - _, err = serverCert.Verify(opts) - require.NoError(t, err) } diff --git a/pkg/derivecert/notrand.go b/pkg/derivecert/notrand.go new file mode 100644 index 000000000..da4df9305 --- /dev/null +++ b/pkg/derivecert/notrand.go @@ -0,0 +1,40 @@ +package derivecert + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/sha256" + "io" + + "golang.org/x/crypto/hkdf" + + "github.com/pomerium/pomerium/internal/deterministicecdsa" +) + +type readerType byte + +const ( + readerTypeCAPrivateKey readerType = iota + readerTypeCACertificate + readerTypeServerPrivateKey + readerTypeServerCertificate + readerTypeSerialNumber +) + +func newReader(readerType readerType, psk []byte, domains ...string) io.Reader { + var buf bytes.Buffer + buf.WriteByte(byte(readerType)) + buf.Write(psk) + buf.WriteByte(0) + for _, domain := range domains { + buf.WriteString(domain) + buf.WriteByte(0) + } + + return hkdf.New(sha256.New, buf.Bytes(), nil, nil) +} + +func deriveKey(r io.Reader) (*ecdsa.PrivateKey, error) { + return deterministicecdsa.GenerateKey(elliptic.P256(), r) +} diff --git a/pkg/derivecert/pem.go b/pkg/derivecert/pem.go index f0f17a51e..8a5654e9b 100644 --- a/pkg/derivecert/pem.go +++ b/pkg/derivecert/pem.go @@ -18,7 +18,7 @@ type PEM struct { func ToPEM(key *ecdsa.PrivateKey, certDer []byte) (*PEM, error) { b, err := x509.MarshalECPrivateKey(key) if err != nil { - return nil, fmt.Errorf("unable to marshal ECDSA private key: %w", err) + return nil, fmt.Errorf("unable to marshal ecdsa private key: %w", err) } return &PEM{ Key: pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}),