-
Notifications
You must be signed in to change notification settings - Fork 279
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
derive CA from pre-shared key (#3815)
- Loading branch information
Showing
4 changed files
with
267 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
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" | ||
) | ||
|
||
// CA is certificate authority | ||
type CA struct { | ||
// key is signing key | ||
key *ecdsa.PrivateKey | ||
// cert is a CA certificate | ||
cert *x509.Certificate | ||
} | ||
|
||
func mustParseDate(d string) time.Time { | ||
t, err := time.Parse("2006-Jan-02", d) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return t | ||
} | ||
|
||
var ( | ||
notBefore = mustParseDate("2022-Dec-01") | ||
notAfter = mustParseDate("2050-Dec-01") | ||
) | ||
|
||
// NewCA creates new certificate authority using a pre-shared key. | ||
// This certificate authority is generated on the fly | ||
// and would yield the same private key every time for the given PSK. | ||
// | ||
// That allows services that have a certain pre-shared key (i.e. shared_secret) | ||
// to have automatic TLS without need to share and distribute certs, | ||
// 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)) | ||
if err != nil { | ||
return nil, fmt.Errorf("generating key: %w", err) | ||
} | ||
|
||
cert, err := caCertTemplate(psk) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
der, err := x509.CreateCertificate(pskRandReader(psk), cert, cert, &key.PublicKey, key) | ||
if err != nil { | ||
return nil, fmt.Errorf("create cert: %w", err) | ||
} | ||
|
||
if cert, err = x509.ParseCertificate(der); err != nil { | ||
return nil, fmt.Errorf("parse cert: %w", err) | ||
} | ||
|
||
ca := &CA{key, cert} | ||
|
||
return ca, nil | ||
} | ||
|
||
// CAFromPEM loads CA from PEM encoded data | ||
func CAFromPEM(p PEM) (*CA, string, error) { | ||
key, cert, err := p.KeyCert() | ||
if err != nil { | ||
return nil, "", fmt.Errorf("decode key, cert: %w", err) | ||
} | ||
ca := CA{key: key, cert: cert} | ||
|
||
return &ca, ca.cert.Subject.CommonName, nil | ||
} | ||
|
||
// 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) | ||
if err != nil { | ||
return nil, fmt.Errorf("generate key: %w", err) | ||
} | ||
|
||
tmpl, err := serverCertTemplate(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) | ||
if err != nil { | ||
return nil, fmt.Errorf("create cert: %w", err) | ||
} | ||
|
||
return ToPEM(key, cert) | ||
} | ||
|
||
// PEM returns PEM-encoded cert and key | ||
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() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &x509.Certificate{ | ||
SerialNumber: serial, | ||
Subject: pkix.Name{Organization: []string{"Pomerium"}, CommonName: "Pomerium PSK CA"}, | ||
NotBefore: notBefore, | ||
NotAfter: notAfter, | ||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, | ||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, | ||
BasicConstraintsValid: true, | ||
IsCA: true, | ||
}, nil | ||
} | ||
|
||
func serverCertTemplate(domains []string) (*x509.Certificate, error) { | ||
serial, err := newSerial() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &x509.Certificate{ | ||
SerialNumber: serial, | ||
Subject: pkix.Name{Organization: []string{"Pomerium"}, CommonName: "Pomerium PSK domain cert"}, | ||
NotBefore: notBefore, | ||
NotAfter: notAfter, | ||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, | ||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, | ||
DNSNames: domains, | ||
}, nil | ||
} | ||
|
||
// Key returns CA private key | ||
func (ca *CA) Key() *ecdsa.PrivateKey { | ||
return ca.key | ||
} | ||
|
||
func newSerial() (*big.Int, error) { | ||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) | ||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to generate serial number: %w", err) | ||
} | ||
return serialNumber, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package derivecert_test | ||
|
||
import ( | ||
"crypto/rand" | ||
"crypto/x509" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/pomerium/pomerium/pkg/derivecert" | ||
) | ||
|
||
// TestCA creates two CA instances from same PSK | ||
// and asserts that they yield same private key, | ||
// and a certificate created by one CA is trusted by another | ||
func TestCA(t *testing.T) { | ||
psk := make([]byte, 32) | ||
_, 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) | ||
|
||
ca1PEM, err := ca2.PEM() | ||
require.NoError(t, err) | ||
ca2PEM, err := ca2.PEM() | ||
require.NoError(t, err) | ||
|
||
assert.Equal(t, ca1PEM.Key, ca2PEM.Key) | ||
|
||
serverPEM, err := ca1.NewServerCert([]string{"myserver.com"}) | ||
require.NoError(t, err) | ||
|
||
_, serverCert, err := serverPEM.KeyCert() | ||
require.NoError(t, err) | ||
|
||
pool := x509.NewCertPool() | ||
require.True(t, pool.AppendCertsFromPEM(ca2PEM.Cert)) | ||
|
||
opts := x509.VerifyOptions{ | ||
Roots: pool, | ||
DNSName: "myserver.com", | ||
Intermediates: x509.NewCertPool(), | ||
} | ||
|
||
_, err = serverCert.Verify(opts) | ||
require.NoError(t, err) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
// Package derivecert is used to deterministically generate TLS certificate authority and certificates out of pre-shared key | ||
package derivecert |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package derivecert | ||
|
||
import ( | ||
"crypto/ecdsa" | ||
"crypto/tls" | ||
"crypto/x509" | ||
"encoding/pem" | ||
"fmt" | ||
) | ||
|
||
// PEM representation of certificate authority data, serializable to JSON | ||
type PEM struct { | ||
Cert []byte | ||
Key []byte | ||
} | ||
|
||
// ToPEM converts private key and certificate into PEM representation | ||
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 &PEM{ | ||
Key: pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}), | ||
Cert: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDer}), | ||
}, nil | ||
} | ||
|
||
// TLS parses PEM and returns TLS certificate | ||
func (p *PEM) TLS() (tls.Certificate, error) { | ||
return tls.X509KeyPair(p.Cert, p.Key) | ||
} | ||
|
||
// KeyCert parses private key and cert from PEM encoded format | ||
func (p *PEM) KeyCert() (*ecdsa.PrivateKey, *x509.Certificate, error) { | ||
certDer, _ := pem.Decode(p.Cert) | ||
if certDer == nil { | ||
return nil, nil, fmt.Errorf("parse PEM cert") | ||
} | ||
keyDer, _ := pem.Decode(p.Key) | ||
if keyDer == nil { | ||
return nil, nil, fmt.Errorf("parse PEM key") | ||
} | ||
|
||
cert, err := x509.ParseCertificate(certDer.Bytes) | ||
if err != nil { | ||
return nil, nil, fmt.Errorf("parse cert: %w", err) | ||
} | ||
key, err := x509.ParseECPrivateKey(keyDer.Bytes) | ||
if err != nil { | ||
return nil, nil, fmt.Errorf("parse key: %w", err) | ||
} | ||
|
||
return key, cert, nil | ||
} |