Initial commit of pkg functions
This commit is contained in:
parent
c325fa17e5
commit
6b6866077c
17 changed files with 1689 additions and 0 deletions
72
pkg/ca/ca.go
Normal file
72
pkg/ca/ca.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
Package ca implements a disk file-backed certificate authority with a built-in
|
||||
CRL signer. The certificate authority can issue new certificates (including the
|
||||
creation of intermediate CAs) and revoke existing certificates, resulting in a
|
||||
fresh CRL.
|
||||
*/
|
||||
package ca
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"src.lwithers.me.uk/go/rsa/pkg/pemfile"
|
||||
)
|
||||
|
||||
type CA struct {
|
||||
dir string
|
||||
|
||||
key *rsa.PrivateKey
|
||||
root *x509.Certificate
|
||||
|
||||
crl *x509.RevocationList
|
||||
crlKey *rsa.PrivateKey
|
||||
crlCert *x509.Certificate
|
||||
}
|
||||
|
||||
const (
|
||||
rootKeyFilename = "root-key.pem"
|
||||
rootCertFilename = "root-cert.pem"
|
||||
)
|
||||
|
||||
// Open an existing certificate authority in the given directory.
|
||||
func Open(dir string) (*CA, error) {
|
||||
ca := &CA{
|
||||
dir: dir,
|
||||
}
|
||||
var err error
|
||||
ca.key, err = pemfile.ReadKey(filepath.Join(dir, rootKeyFilename))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("while opening CA (root key): %w", err)
|
||||
}
|
||||
ca.root, err = pemfile.ReadCert(filepath.Join(dir, rootCertFilename))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("while opening CA (root cert): %w", err)
|
||||
}
|
||||
if err = ca.loadCRLState(); err != nil {
|
||||
return nil, fmt.Errorf("while opening CA (CRL state): %w", err)
|
||||
}
|
||||
return ca, nil
|
||||
}
|
||||
|
||||
func (ca *CA) GetRoot() *x509.Certificate {
|
||||
return ca.root
|
||||
}
|
||||
|
||||
func (ca *CA) GetCRLSigner() *x509.Certificate {
|
||||
return ca.crlCert
|
||||
}
|
||||
|
||||
func (ca *CA) GetCRL() *x509.RevocationList {
|
||||
return ca.crl
|
||||
}
|
||||
|
||||
func ComputeSubjectKeyId(key *rsa.PublicKey) []byte {
|
||||
der, _ := asn1.Marshal(key)
|
||||
h := sha1.Sum(der)
|
||||
return h[:]
|
||||
}
|
||||
162
pkg/ca/create.go
Normal file
162
pkg/ca/create.go
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
Package ca implements a disk file-backed certificate authority with a built-in
|
||||
CRL signer. The certificate authority can issue new certificates (including the
|
||||
creation of intermediate CAs) and revoke existing certificates, resulting in a
|
||||
fresh CRL.
|
||||
*/
|
||||
package ca
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"src.lwithers.me.uk/go/rsa/pkg/pemfile"
|
||||
"src.lwithers.me.uk/go/rsa/pkg/serial"
|
||||
)
|
||||
|
||||
const (
|
||||
crlSignerBits = 3072
|
||||
)
|
||||
|
||||
func Create(dir string, template *x509.Certificate, key *rsa.PrivateKey) (*CA, error) {
|
||||
return createCA(dir, template, template, key, key)
|
||||
}
|
||||
|
||||
func (ca *CA) CreateIntermediate(dir string, template *x509.Certificate, key *rsa.PrivateKey) (*CA, error) {
|
||||
return createCA(dir, template, ca.root, key, ca.key)
|
||||
}
|
||||
|
||||
// createCA holds the shared code between Create and CreateIntermediate. All
|
||||
// that really differs between those operations is the signing key and whether
|
||||
// or not there is a parent certificate.
|
||||
func createCA(dir string, template, parent *x509.Certificate, newRootKey, signingKey *rsa.PrivateKey) (*CA, error) {
|
||||
// template must have at least the common name set
|
||||
if template.Subject.CommonName == "" {
|
||||
return nil, errors.New("template CA certificate must have common name set")
|
||||
}
|
||||
|
||||
// directory must not exist
|
||||
if _, err := os.Lstat(dir); !errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, &fs.PathError{
|
||||
Op: "mkdir",
|
||||
Path: dir,
|
||||
Err: fs.ErrExist,
|
||||
}
|
||||
}
|
||||
|
||||
// create the directory
|
||||
if err := os.Mkdir(dir, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// write out the private key
|
||||
der, err := x509.MarshalPKCS8PrivateKey(newRootKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal private key: %w", err)
|
||||
}
|
||||
if err = pemfile.Write(filepath.Join(dir, rootKeyFilename), pemfile.TypePKCS8PrivateKey, der); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ensure the template is valid for a root CA
|
||||
template.BasicConstraintsValid = true
|
||||
template.IsCA = true
|
||||
template.KeyUsage |= x509.KeyUsageCertSign
|
||||
if template.SerialNumber == nil {
|
||||
template.SerialNumber = serial.Rand()
|
||||
}
|
||||
if template.NotBefore.IsZero() {
|
||||
template.NotBefore = time.Now().Add(-2 * time.Hour)
|
||||
}
|
||||
if template.NotAfter.IsZero() {
|
||||
template.NotAfter = time.Now().Add(24 * 365 * 40 * time.Hour)
|
||||
}
|
||||
|
||||
// create the self-signed certificate; ensure we can parse it, and write
|
||||
// it to disk
|
||||
der, err = x509.CreateCertificate(rand.Reader, template, parent, &newRootKey.PublicKey, signingKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create/sign new root CA certificate: %w", err)
|
||||
}
|
||||
root, err := x509.ParseCertificate(der)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse just-signed root CA certificate: %w", err)
|
||||
}
|
||||
if err = pemfile.Write(filepath.Join(dir, rootCertFilename), pemfile.TypeX509Certificate, der); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// now create a CRL signer key and write it to disk
|
||||
crlKey, err := rsa.GenerateKey(rand.Reader, crlSignerBits)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate new CRL signer key: %w", err)
|
||||
}
|
||||
der, err = x509.MarshalPKCS8PrivateKey(crlKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal CRL signer private key: %w", err)
|
||||
}
|
||||
if err = pemfile.Write(filepath.Join(dir, crlKeyFilename), pemfile.TypePKCS8PrivateKey, der); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create a template for a CRL signing certificate
|
||||
crlTemplate := &x509.Certificate{
|
||||
SerialNumber: serial.Rand(),
|
||||
Subject: template.Subject,
|
||||
SubjectKeyId: ComputeSubjectKeyId(&crlKey.PublicKey),
|
||||
NotBefore: template.NotBefore,
|
||||
NotAfter: template.NotAfter,
|
||||
KeyUsage: x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
crlTemplate.Subject.CommonName += " CRL signer"
|
||||
|
||||
// create the CRL signing certificate, parse it, and write it to disk
|
||||
der, err = x509.CreateCertificate(rand.Reader, crlTemplate, root, &crlKey.PublicKey, newRootKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign CRL signer certificate: %w", err)
|
||||
}
|
||||
crlCert, err := x509.ParseCertificate(der)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse just-signed CRL signer certificate: %w", err)
|
||||
}
|
||||
if err = pemfile.Write(filepath.Join(dir, crlCertFilename), pemfile.TypeX509Certificate, der); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// finally, create an empty CRL
|
||||
crl := &x509.RevocationList{
|
||||
Number: big.NewInt(1),
|
||||
ThisUpdate: template.NotBefore,
|
||||
NextUpdate: template.NotAfter,
|
||||
}
|
||||
der, err = x509.CreateRevocationList(rand.Reader, crl, crlCert, crlKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create empty CRL: %w", err)
|
||||
}
|
||||
crl, err = x509.ParseRevocationList(der)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse just-created CRL: %w", err)
|
||||
}
|
||||
if err = pemfile.Write(filepath.Join(dir, crlFilename), pemfile.TypeX509CRL, der); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// all done
|
||||
return &CA{
|
||||
dir: dir,
|
||||
key: newRootKey,
|
||||
root: root,
|
||||
crl: crl,
|
||||
crlKey: crlKey,
|
||||
crlCert: crlCert,
|
||||
}, nil
|
||||
}
|
||||
87
pkg/ca/crl.go
Normal file
87
pkg/ca/crl.go
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
Package ca implements a disk file-backed certificate authority with a built-in
|
||||
CRL signer. The certificate authority can issue new certificates (including the
|
||||
creation of intermediate CAs) and revoke existing certificates, resulting in a
|
||||
fresh CRL.
|
||||
*/
|
||||
package ca
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"src.lwithers.me.uk/go/rsa/pkg/pemfile"
|
||||
)
|
||||
|
||||
const (
|
||||
crlFilename = "crl.pem"
|
||||
crlKeyFilename = "crl-signer-key.pem"
|
||||
crlCertFilename = "crl-signer-cert.pem"
|
||||
)
|
||||
|
||||
func (ca *CA) loadCRLState() error {
|
||||
var err error
|
||||
ca.crl, err = pemfile.ReadCRL(filepath.Join(ca.dir, crlFilename))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ca.crlKey, err = pemfile.ReadKey(filepath.Join(ca.dir, crlKeyFilename))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ca.crlCert, err = pemfile.ReadCert(filepath.Join(ca.dir, crlCertFilename))
|
||||
return err
|
||||
}
|
||||
|
||||
func (ca *CA) Revoke(serial *big.Int) (*x509.RevocationList, error) {
|
||||
// if the certificate is already revoked, update its revocation time
|
||||
var found bool
|
||||
for i := range ca.crl.RevokedCertificates {
|
||||
if ca.crl.RevokedCertificates[i].SerialNumber.Cmp(serial) == 0 {
|
||||
ca.crl.RevokedCertificates[i].RevocationTime = time.Now()
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise, add a new entry
|
||||
if !found {
|
||||
n := big.NewInt(0)
|
||||
n.SetBytes(serial.Bytes())
|
||||
ca.crl.RevokedCertificates = append(ca.crl.RevokedCertificates,
|
||||
pkix.RevokedCertificate{
|
||||
SerialNumber: n,
|
||||
RevocationTime: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// increment issue number
|
||||
ca.crl.Number.Add(ca.crl.Number, big.NewInt(1))
|
||||
ca.crl.ThisUpdate = time.Now()
|
||||
|
||||
// create new DER-form certificate
|
||||
der, err := x509.CreateRevocationList(rand.Reader, ca.crl, ca.crlCert, ca.crlKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign new CRL: %w", err)
|
||||
}
|
||||
|
||||
// ensure we can parse it
|
||||
newCRL, err := x509.ParseRevocationList(der)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse newly-signed CRL: %w", err)
|
||||
}
|
||||
|
||||
// save to disk
|
||||
if err := pemfile.Write(filepath.Join(ca.dir, crlFilename), pemfile.TypeX509CRL, der); err != nil {
|
||||
return nil, fmt.Errorf("failed to save CRL: %w", err)
|
||||
}
|
||||
|
||||
// update in-memory copy
|
||||
ca.crl = newCRL
|
||||
return ca.crl, nil
|
||||
}
|
||||
57
pkg/ca/sign.go
Normal file
57
pkg/ca/sign.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
Package ca implements a disk file-backed certificate authority with a built-in
|
||||
CRL signer. The certificate authority can issue new certificates (including the
|
||||
creation of intermediate CAs) and revoke existing certificates, resulting in a
|
||||
fresh CRL.
|
||||
*/
|
||||
package ca
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"src.lwithers.me.uk/go/rsa/pkg/pemfile"
|
||||
"src.lwithers.me.uk/go/rsa/pkg/serial"
|
||||
)
|
||||
|
||||
const (
|
||||
auditLogDir = "audit.log"
|
||||
)
|
||||
|
||||
func (ca *CA) Sign(template *x509.Certificate, key *rsa.PublicKey) (*x509.Certificate, string, error) {
|
||||
if template.SerialNumber == nil {
|
||||
template.SerialNumber = serial.Rand()
|
||||
}
|
||||
|
||||
// sign the certificate
|
||||
der, err := x509.CreateCertificate(rand.Reader, template, ca.root, key, ca.key)
|
||||
if err != nil {
|
||||
return nil, "",
|
||||
fmt.Errorf("failed to sign certificate: %w", err)
|
||||
}
|
||||
|
||||
// parse the just-signed certificate
|
||||
cert, err := x509.ParseCertificate(der)
|
||||
if err != nil {
|
||||
return nil, "",
|
||||
fmt.Errorf("failed to parse certificate after signing: %w", err)
|
||||
}
|
||||
|
||||
// save the newly-signed cert in the audit log
|
||||
auditDir := filepath.Join(ca.dir, auditLogDir, template.SerialNumber.Text(16))
|
||||
if err := os.MkdirAll(auditDir, 0700); err != nil {
|
||||
return nil, "",
|
||||
fmt.Errorf("failed to create audit log directory: %w", err)
|
||||
}
|
||||
|
||||
if err := pemfile.Write(filepath.Join(auditDir, "cert.pem"), pemfile.TypeX509Certificate, der); err != nil {
|
||||
return nil, "",
|
||||
fmt.Errorf("failed to save certificate in audit log: %w", err)
|
||||
}
|
||||
|
||||
return cert, auditDir, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue