163 lines
5.0 KiB
Go
163 lines
5.0 KiB
Go
|
/*
|
||
|
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
|
||
|
}
|