/* 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 }