Add util/pki for creating and signing certificates (#1555)
This commit is contained in:
parent
211fd9b9a3
commit
05d2b34e10
85
util/pki/certoptions.go
Normal file
85
util/pki/certoptions.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
164
util/pki/pki.go
Normal file
164
util/pki/pki.go
Normal file
@ -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
|
||||||
|
}
|
90
util/pki/pki_test.go
Normal file
90
util/pki/pki_test.go
Normal file
@ -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())
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user