diff --git a/util/pki/certoptions.go b/util/pki/certoptions.go new file mode 100644 index 00000000..61026967 --- /dev/null +++ b/util/pki/certoptions.go @@ -0,0 +1,85 @@ +package pki + +import ( + "crypto/ed25519" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "net" + "time" +) + +// CertOptions are passed to cert options +type CertOptions struct { + IsCA bool + Subject pkix.Name + DNSNames []string + IPAddresses []net.IP + SerialNumber *big.Int + NotBefore time.Time + NotAfter time.Time + + Parent *x509.Certificate + Pub ed25519.PublicKey + Priv ed25519.PrivateKey +} + +// CertOption sets CertOptions +type CertOption func(c *CertOptions) + +// Subject sets the Subject field +func Subject(subject pkix.Name) CertOption { + return func(c *CertOptions) { + c.Subject = subject + } +} + +// IsCA states the cert is a CA +func IsCA() CertOption { + return func(c *CertOptions) { + c.IsCA = true + } +} + +// DNSNames is a list of hosts to sign in to the certificate +func DNSNames(names ...string) CertOption { + return func(c *CertOptions) { + c.DNSNames = names + } +} + +// IPAddresses is a list of IPs to sign in to the certificate +func IPAddresses(ips ...net.IP) CertOption { + return func(c *CertOptions) { + c.IPAddresses = ips + } +} + +// KeyPair is the key pair to sign the certificate with +func KeyPair(pub ed25519.PublicKey, priv ed25519.PrivateKey) CertOption { + return func(c *CertOptions) { + c.Pub = pub + c.Priv = priv + } +} + +// SerialNumber is the Certificate Serial number +func SerialNumber(serial *big.Int) CertOption { + return func(c *CertOptions) { + c.SerialNumber = serial + } +} + +// NotBefore is the time the certificate is not valid before +func NotBefore(time time.Time) CertOption { + return func(c *CertOptions) { + c.NotBefore = time + } +} + +// NotAfter is the time the certificate is not valid after +func NotAfter(time time.Time) CertOption { + return func(c *CertOptions) { + c.NotAfter = time + } +} diff --git a/util/pki/pki.go b/util/pki/pki.go new file mode 100644 index 00000000..c4ac6f96 --- /dev/null +++ b/util/pki/pki.go @@ -0,0 +1,164 @@ +// Package pki provides PKI all the PKI functions necessary to run micro over an untrusted network +// including a CA +package pki + +import ( + "bytes" + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "encoding/pem" + + "github.com/pkg/errors" +) + +// GenerateKey returns an ed25519 key +func GenerateKey() (ed25519.PublicKey, ed25519.PrivateKey, error) { + return ed25519.GenerateKey(rand.Reader) +} + +// CA generates a self signed CA and returns cert, key in PEM format +func CA(opts ...CertOption) ([]byte, []byte, error) { + opts = append(opts, IsCA()) + options := CertOptions{} + for _, o := range opts { + o(&options) + } + template := &x509.Certificate{ + SignatureAlgorithm: x509.PureEd25519, + Subject: options.Subject, + DNSNames: options.DNSNames, + IPAddresses: options.IPAddresses, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + NotBefore: options.NotBefore, + NotAfter: options.NotAfter, + SerialNumber: options.SerialNumber, + BasicConstraintsValid: true, + } + if options.IsCA { + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + } + x509Cert, err := x509.CreateCertificate(rand.Reader, template, template, options.Pub, options.Priv) + if err != nil { + return nil, nil, err + } + cert, key := &bytes.Buffer{}, &bytes.Buffer{} + if err := pem.Encode(cert, &pem.Block{Type: "CERTIFICATE", Bytes: x509Cert}); err != nil { + return nil, nil, err + } + x509Key, err := x509.MarshalPKCS8PrivateKey(options.Priv) + if err != nil { + return nil, nil, err + } + if err := pem.Encode(key, &pem.Block{Type: "PRIVATE KEY", Bytes: x509Key}); err != nil { + return nil, nil, err + } + + return cert.Bytes(), key.Bytes(), nil +} + +// CSR generates a certificate request in PEM format +func CSR(opts ...CertOption) ([]byte, error) { + options := CertOptions{} + for _, o := range opts { + o(&options) + } + csrTemplate := &x509.CertificateRequest{ + Subject: options.Subject, + SignatureAlgorithm: x509.PureEd25519, + DNSNames: options.DNSNames, + IPAddresses: options.IPAddresses, + } + out := &bytes.Buffer{} + csr, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, options.Priv) + if err != nil { + return nil, err + } + if err := pem.Encode(out, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr}); err != nil { + return nil, err + } + + return out.Bytes(), nil +} + +// Sign decodes a CSR and signs it with the CA +func Sign(CACrt, CAKey, CSR []byte, opts ...CertOption) ([]byte, error) { + options := CertOptions{} + for _, o := range opts { + o(&options) + } + asn1CACrt, err := decodePEM(CACrt) + if err != nil { + return nil, errors.Wrap(err, "failed to decode CA Crt PEM") + } + if len(asn1CACrt) != 1 { + return nil, errors.Errorf("expected 1 CA Crt, got %d", len(asn1CACrt)) + } + caCrt, err := x509.ParseCertificate(asn1CACrt[0].Bytes) + if err != nil { + return nil, errors.Wrap(err, "ca is not a valid certificate") + } + asn1CAKey, err := decodePEM(CAKey) + if err != nil { + return nil, errors.Wrap(err, "failed to decode CA Key PEM") + } + if len(asn1CAKey) != 1 { + return nil, errors.Errorf("expected 1 CA Key, got %d", len(asn1CACrt)) + } + caKey, err := x509.ParsePKCS8PrivateKey(asn1CAKey[0].Bytes) + if err != nil { + return nil, errors.Wrap(err, "ca key is not a valid private key") + } + asn1CSR, err := decodePEM(CSR) + if err != nil { + return nil, errors.Wrap(err, "failed to decode CSR PEM") + } + if len(asn1CSR) != 1 { + return nil, errors.Errorf("expected 1 CSR, got %d", len(asn1CSR)) + } + csr, err := x509.ParseCertificateRequest(asn1CSR[0].Bytes) + if err != nil { + return nil, errors.Wrap(err, "csr is invalid") + } + template := &x509.Certificate{ + SignatureAlgorithm: x509.PureEd25519, + Subject: csr.Subject, + DNSNames: csr.DNSNames, + IPAddresses: csr.IPAddresses, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + NotBefore: options.NotBefore, + NotAfter: options.NotAfter, + SerialNumber: options.SerialNumber, + BasicConstraintsValid: true, + } + + x509Cert, err := x509.CreateCertificate(rand.Reader, template, caCrt, caCrt.PublicKey, caKey) + if err != nil { + return nil, errors.Wrap(err, "Couldn't sign certificate") + } + out := &bytes.Buffer{} + if err := pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: x509Cert}); err != nil { + return nil, errors.Wrap(err, "couldn't encode cert") + } + return out.Bytes(), nil +} + +func decodePEM(PEM []byte) ([]*pem.Block, error) { + var blocks []*pem.Block + var asn1 *pem.Block + var rest []byte + for { + asn1, rest = pem.Decode(PEM) + if asn1 == nil { + return nil, errors.New("PEM is not valid") + } + blocks = append(blocks, asn1) + if len(rest) == 0 { + break + } + } + return blocks, nil +} diff --git a/util/pki/pki_test.go b/util/pki/pki_test.go new file mode 100644 index 00000000..67e81d13 --- /dev/null +++ b/util/pki/pki_test.go @@ -0,0 +1,90 @@ +package pki + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestPrivateKey(t *testing.T) { + _, _, err := GenerateKey() + assert.NoError(t, err) +} + +func TestCA(t *testing.T) { + pub, priv, err := GenerateKey() + assert.NoError(t, err) + + serialNumberMax := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberMax) + assert.NoError(t, err, "Couldn't generate serial") + + cert, key, err := CA( + KeyPair(pub, priv), + Subject(pkix.Name{ + Organization: []string{"test"}, + }), + DNSNames("localhost"), + IPAddresses(net.ParseIP("127.0.0.1")), + SerialNumber(serialNumber), + NotBefore(time.Now().Add(time.Minute*-1)), + NotAfter(time.Now().Add(time.Minute)), + ) + assert.NoError(t, err, "Couldn't sign CA") + asn1Key, _ := pem.Decode(key) + assert.NotNil(t, asn1Key, "Couldn't decode key") + assert.Equal(t, "PRIVATE KEY", asn1Key.Type) + decodedKey, err := x509.ParsePKCS8PrivateKey(asn1Key.Bytes) + assert.NoError(t, err, "Couldn't decode ASN1 Key") + assert.Equal(t, priv, decodedKey.(ed25519.PrivateKey)) + + pool := x509.NewCertPool() + assert.True(t, pool.AppendCertsFromPEM(cert), "Coudn't parse cert") + + asn1Cert, _ := pem.Decode(cert) + assert.NotNil(t, asn1Cert, "Couldn't parse pem cert") + x509cert, err := x509.ParseCertificate(asn1Cert.Bytes) + assert.NoError(t, err, "Couldn't parse asn1 cert") + chains, err := x509cert.Verify(x509.VerifyOptions{ + Roots: pool, + }) + assert.NoError(t, err, "Cert didn't verify") + assert.Len(t, chains, 1, "CA should have 1 cert in chain") +} + +func TestCSR(t *testing.T) { + pub, priv, err := GenerateKey() + assert.NoError(t, err) + csr, err := CSR( + Subject( + pkix.Name{ + CommonName: "testnode", + Organization: []string{"microtest"}, + OrganizationalUnit: []string{"super-testers"}, + }, + ), + DNSNames("localhost"), + IPAddresses(net.ParseIP("127.0.0.1")), + KeyPair(pub, priv), + ) + assert.NoError(t, err, "CSR couldn't be encoded") + + asn1csr, _ := pem.Decode(csr) + assert.NotNil(t, asn1csr) + decodedcsr, err := x509.ParseCertificateRequest(asn1csr.Bytes) + assert.NoError(t, err) + expected := pkix.Name{ + CommonName: "testnode", + Organization: []string{"microtest"}, + OrganizationalUnit: []string{"super-testers"}, + } + assert.Equal(t, decodedcsr.Subject.String(), expected.String()) +}