boulder/cmd/ceremony/main.go

695 lines
20 KiB
Go

package main
import (
"bytes"
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/hex"
"encoding/pem"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"time"
"github.com/letsencrypt/boulder/pkcs11helpers"
"golang.org/x/crypto/ocsp"
"gopkg.in/yaml.v2"
)
const configDateLayout = "2006-01-02 15:04:05"
type keyGenConfig struct {
Type string `yaml:"type"`
RSAModLength uint `yaml:"rsa-mod-length"`
ECDSACurve string `yaml:"ecdsa-curve"`
}
var allowedCurves = map[string]bool{
"P-224": true,
"P-256": true,
"P-384": true,
"P-521": true,
}
func (kgc keyGenConfig) validate() error {
if kgc.Type == "" {
return errors.New("key.type is required")
}
if kgc.Type != "rsa" && kgc.Type != "ecdsa" {
return errors.New("key.type can only be 'rsa' or 'ecdsa'")
}
if kgc.Type == "rsa" && (kgc.RSAModLength != 2048 && kgc.RSAModLength != 4096) {
return errors.New("key.rsa-mod-length can only be 2048 or 4096")
}
if kgc.Type == "rsa" && kgc.ECDSACurve != "" {
return errors.New("if key.type = 'rsa' then key.ecdsa-curve is not used")
}
if kgc.Type == "ecdsa" && !allowedCurves[kgc.ECDSACurve] {
return errors.New("key.ecdsa-curve can only be 'P-224', 'P-256', 'P-384', or 'P-521'")
}
if kgc.Type == "ecdsa" && kgc.RSAModLength != 0 {
return errors.New("if key.type = 'ecdsa' then key.rsa-mod-length is not used")
}
return nil
}
type PKCS11KeyGenConfig struct {
Module string `yaml:"module"`
PIN string `yaml:"pin"`
StoreSlot uint `yaml:"store-key-in-slot"`
StoreLabel string `yaml:"store-key-with-label"`
}
func (pkgc PKCS11KeyGenConfig) validate() error {
if pkgc.Module == "" {
return errors.New("pkcs11.module is required")
}
if pkgc.StoreLabel == "" {
return errors.New("pkcs11.store-key-with-label is required")
}
// key-slot is allowed to be 0 (which is a valid slot).
// PIN is allowed to be "", which will commonly happen when
// PIN entry is done via PED.
return nil
}
type rootConfig struct {
CeremonyType string `yaml:"ceremony-type"`
PKCS11 PKCS11KeyGenConfig `yaml:"pkcs11"`
Key keyGenConfig `yaml:"key"`
Outputs struct {
PublicKeyPath string `yaml:"public-key-path"`
CertificatePath string `yaml:"certificate-path"`
} `yaml:"outputs"`
CertProfile certProfile `yaml:"certificate-profile"`
}
func (rc rootConfig) validate() error {
if err := rc.PKCS11.validate(); err != nil {
return err
}
// Key gen fields
if err := rc.Key.validate(); err != nil {
return err
}
// Output fields
if rc.Outputs.PublicKeyPath == "" {
return errors.New("outputs.public-key-path is required")
}
if rc.Outputs.CertificatePath == "" {
return errors.New("outputs.certificate-path is required")
}
// Certificate profile
if err := rc.CertProfile.verifyProfile(rootCert); err != nil {
return err
}
return nil
}
type PKCS11SigningConfig struct {
Module string `yaml:"module"`
PIN string `yaml:"pin"`
SigningSlot uint `yaml:"signing-key-slot"`
SigningLabel string `yaml:"signing-key-label"`
SigningKeyID string `yaml:"signing-key-id"`
}
func (psc PKCS11SigningConfig) validate() error {
if psc.Module == "" {
return errors.New("pkcs11.module is required")
}
if psc.SigningLabel == "" {
return errors.New("pkcs11.signing-key-label is required")
}
if psc.SigningKeyID == "" {
return errors.New("pkcs11.signing-key-id is required")
}
// key-slot is allowed to be 0 (which is a valid slot).
return nil
}
type intermediateConfig struct {
CeremonyType string `yaml:"ceremony-type"`
PKCS11 PKCS11SigningConfig `yaml:"pkcs11"`
Inputs struct {
PublicKeyPath string `yaml:"public-key-path"`
IssuerCertificatePath string `yaml:"issuer-certificate-path"`
} `yaml:"inputs"`
Outputs struct {
CertificatePath string `yaml:"certificate-path"`
} `yaml:"outputs"`
CertProfile certProfile `yaml:"certificate-profile"`
}
func (ic intermediateConfig) validate(ct certType) error {
if err := ic.PKCS11.validate(); err != nil {
return err
}
// Input fields
if ic.Inputs.PublicKeyPath == "" {
return errors.New("inputs.public-key-path is required")
}
if ic.Inputs.IssuerCertificatePath == "" {
return errors.New("inputs.issuer-certificate is required")
}
// Output fields
if ic.Outputs.CertificatePath == "" {
return errors.New("outputs.certificate-path is required")
}
// Certificate profile
if err := ic.CertProfile.verifyProfile(ct); err != nil {
return err
}
return nil
}
type keyConfig struct {
CeremonyType string `yaml:"ceremony-type"`
PKCS11 PKCS11KeyGenConfig `yaml:"pkcs11"`
Key keyGenConfig `yaml:"key"`
Outputs struct {
PublicKeyPath string `yaml:"public-key-path"`
} `yaml:"outputs"`
}
func (kc keyConfig) validate() error {
if err := kc.PKCS11.validate(); err != nil {
return err
}
// Key gen fields
if err := kc.Key.validate(); err != nil {
return err
}
// Output fields
if kc.Outputs.PublicKeyPath == "" {
return errors.New("outputs.public-key-path is required")
}
return nil
}
type ocspRespConfig struct {
CeremonyType string `yaml:"ceremony-type"`
PKCS11 PKCS11SigningConfig `yaml:"pkcs11"`
Inputs struct {
CertificatePath string `yaml:"certificate-path"`
IssuerCertificatePath string `yaml:"issuer-certificate-path"`
DelegatedIssuerCertificatePath string `yaml:"delegated-issuer-certificate-path"`
} `yaml:"inputs"`
Outputs struct {
ResponsePath string `yaml:"response-path"`
} `yaml:"outputs"`
OCSPProfile struct {
ThisUpdate string `yaml:"this-update"`
NextUpdate string `yaml:"next-update"`
Status string `yaml:"status"`
} `yaml:"ocsp-profile"`
}
func (orc ocspRespConfig) validate() error {
if err := orc.PKCS11.validate(); err != nil {
return err
}
// Input fields
if orc.Inputs.CertificatePath == "" {
return errors.New("inputs.certificate-path is required")
}
if orc.Inputs.IssuerCertificatePath == "" {
return errors.New("inputs.issuer-certificate-path is required")
}
// DelegatedIssuerCertificatePath may be omitted
// Output fields
if orc.Outputs.ResponsePath == "" {
return errors.New("outputs.response-path is required")
}
// OCSP fields
if orc.OCSPProfile.ThisUpdate == "" {
return errors.New("ocsp-profile.this-update is required")
}
if orc.OCSPProfile.NextUpdate == "" {
return errors.New("ocsp-profile.next-update is required")
}
if orc.OCSPProfile.Status != "good" && orc.OCSPProfile.Status != "revoked" {
return errors.New("ocsp-profile.status must be either \"good\" or \"revoked\"")
}
return nil
}
type crlConfig struct {
CeremonyType string `yaml:"ceremony-type"`
PKCS11 PKCS11SigningConfig `yaml:"pkcs11"`
Inputs struct {
IssuerCertificatePath string `yaml:"issuer-certificate-path"`
} `yaml:"inputs"`
Outputs struct {
CRLPath string `yaml:"crl-path"`
} `yaml:"outputs"`
CRLProfile struct {
ThisUpdate string `yaml:"this-update"`
NextUpdate string `yaml:"next-update"`
Number int64 `yaml:"number"`
RevokedCertificates []struct {
CertificatePath string `yaml:"certificate-path"`
RevocationDate string `yaml:"revocation-date"`
RevocationReason int `yaml:"revocation-reason"`
} `yaml:"revoked-certificates"`
} `yaml:"crl-profile"`
}
func (cc crlConfig) validate() error {
if err := cc.PKCS11.validate(); err != nil {
return err
}
// Input fields
if cc.Inputs.IssuerCertificatePath == "" {
return errors.New("inputs.issuer-certificate-path is required")
}
// Output fields
if cc.Outputs.CRLPath == "" {
return errors.New("outputs.crl-path is required")
}
// CRL profile fields
if cc.CRLProfile.ThisUpdate == "" {
return errors.New("crl-profile.this-update is required")
}
if cc.CRLProfile.NextUpdate == "" {
return errors.New("crl-profile.next-update is required")
}
if cc.CRLProfile.Number == 0 {
return errors.New("crl-profile.number must be non-zero")
}
for _, rc := range cc.CRLProfile.RevokedCertificates {
if rc.CertificatePath == "" {
return errors.New("crl-profile.revoked-certificates.certificate-path is required")
}
if rc.RevocationDate == "" {
return errors.New("crl-profile.revoked-certificates.revocation-date is required")
}
if rc.RevocationReason == 0 {
return errors.New("crl-profile.revoked-certificates.revocation-reason is required")
}
}
return nil
}
// loadCert loads a PEM certificate specified by filename or returns an error
func loadCert(filename string) (cert *x509.Certificate, err error) {
certPEM, err := ioutil.ReadFile(filename)
if err != nil {
return
}
block, _ := pem.Decode(certPEM)
if block == nil {
return nil, fmt.Errorf("No data in cert PEM file %s", filename)
}
cert, err = x509.ParseCertificate(block.Bytes)
return
}
func equalPubKeys(a, b interface{}) bool {
aBytes, err := x509.MarshalPKIXPublicKey(a)
if err != nil {
return false
}
bBytes, err := x509.MarshalPKIXPublicKey(b)
if err != nil {
return false
}
return bytes.Equal(aBytes, bBytes)
}
func openSigner(cfg PKCS11SigningConfig, issuer *x509.Certificate) (crypto.Signer, *hsmRandReader, error) {
session, err := pkcs11helpers.Initialize(cfg.Module, cfg.SigningSlot, cfg.PIN)
if err != nil {
return nil, nil, fmt.Errorf("failed to setup session and PKCS#11 context for slot %d: %s",
cfg.SigningSlot, err)
}
log.Printf("Opened PKCS#11 session for slot %d\n", cfg.SigningSlot)
keyID, err := hex.DecodeString(cfg.SigningKeyID)
if err != nil {
return nil, nil, fmt.Errorf("failed to decode key-id: %s", err)
}
signer, err := session.NewSigner(cfg.SigningLabel, keyID)
if err != nil {
return nil, nil, fmt.Errorf("failed to retrieve private key handle: %s", err)
}
if !equalPubKeys(signer.Public(), issuer.PublicKey) {
return nil, nil, fmt.Errorf("signer pubkey did not match issuer pubkey")
}
log.Println("Retrieved private key handle")
return signer, newRandReader(session), nil
}
func signAndWriteCert(tbs, issuer *x509.Certificate, subjectPubKey crypto.PublicKey, signer crypto.Signer, certPath string) error {
// x509.CreateCertificate uses a io.Reader here for signing methods that require
// a source of randomness. Since PKCS#11 based signing generates needed randomness
// at the HSM we don't need to pass a real reader. Instead of passing a nil reader
// we use one that always returns errors in case the internal usage of this reader
// changes.
certBytes, err := x509.CreateCertificate(&failReader{}, tbs, issuer, subjectPubKey, signer)
if err != nil {
return fmt.Errorf("failed to create certificate: %s", err)
}
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
log.Printf("Signed certificate PEM:\n%s", pemBytes)
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
return fmt.Errorf("failed to parse signed certificate: %s", err)
}
if tbs == issuer {
// If cert is self-signed we need to populate the issuer subject key to
// verify the signature
issuer.PublicKey = cert.PublicKey
issuer.PublicKeyAlgorithm = cert.PublicKeyAlgorithm
}
if err := cert.CheckSignatureFrom(issuer); err != nil {
return fmt.Errorf("failed to verify certificate signature: %s", err)
}
if err := ioutil.WriteFile(certPath, pemBytes, 0644); err != nil {
return fmt.Errorf("failed to write certificate to %q: %s", certPath, err)
}
log.Printf("Certificate written to %q\n", certPath)
return nil
}
func rootCeremony(configBytes []byte) error {
var config rootConfig
err := yaml.UnmarshalStrict(configBytes, &config)
if err != nil {
return fmt.Errorf("failed to parse config: %s", err)
}
if err := config.validate(); err != nil {
return fmt.Errorf("failed to validate config: %s", err)
}
session, err := pkcs11helpers.Initialize(config.PKCS11.Module, config.PKCS11.StoreSlot, config.PKCS11.PIN)
if err != nil {
return fmt.Errorf("failed to setup session and PKCS#11 context for slot %d: %s", config.PKCS11.StoreSlot, err)
}
log.Printf("Opened PKCS#11 session for slot %d\n", config.PKCS11.StoreSlot)
keyInfo, err := generateKey(session, config.PKCS11.StoreLabel, config.Outputs.PublicKeyPath, config.Key)
if err != nil {
return err
}
signer, err := session.NewSigner(config.PKCS11.StoreLabel, keyInfo.id)
if err != nil {
return fmt.Errorf("failed to retrieve signer: %s", err)
}
template, err := makeTemplate(newRandReader(session), &config.CertProfile, keyInfo.der, rootCert)
if err != nil {
return fmt.Errorf("failed to create certificate profile: %s", err)
}
err = signAndWriteCert(template, template, keyInfo.key, signer, config.Outputs.CertificatePath)
if err != nil {
return err
}
return nil
}
func intermediateCeremony(configBytes []byte, ct certType) error {
var config intermediateConfig
err := yaml.UnmarshalStrict(configBytes, &config)
if err != nil {
return fmt.Errorf("failed to parse config: %s", err)
}
if err := config.validate(ct); err != nil {
return fmt.Errorf("failed to validate config: %s", err)
}
pubPEMBytes, err := ioutil.ReadFile(config.Inputs.PublicKeyPath)
if err != nil {
return fmt.Errorf("failed to read public key %q: %s", config.Inputs.PublicKeyPath, err)
}
pubPEM, _ := pem.Decode(pubPEMBytes)
if pubPEM == nil {
return fmt.Errorf("failed to parse public key")
}
pub, err := x509.ParsePKIXPublicKey(pubPEM.Bytes)
if err != nil {
return fmt.Errorf("failed to parse public key: %s", err)
}
issuer, err := loadCert(config.Inputs.IssuerCertificatePath)
if err != nil {
return fmt.Errorf("failed to load issuer certificate %q: %s", config.Inputs.IssuerCertificatePath, err)
}
signer, randReader, err := openSigner(config.PKCS11, issuer)
if err != nil {
return err
}
template, err := makeTemplate(randReader, &config.CertProfile, pubPEM.Bytes, ct)
if err != nil {
return fmt.Errorf("failed to create certificate profile: %s", err)
}
template.AuthorityKeyId = issuer.SubjectKeyId
err = signAndWriteCert(template, issuer, pub, signer, config.Outputs.CertificatePath)
if err != nil {
return err
}
return nil
}
func keyCeremony(configBytes []byte) error {
var config keyConfig
err := yaml.UnmarshalStrict(configBytes, &config)
if err != nil {
return fmt.Errorf("failed to parse config: %s", err)
}
if err := config.validate(); err != nil {
return fmt.Errorf("failed to validate config: %s", err)
}
session, err := pkcs11helpers.Initialize(config.PKCS11.Module, config.PKCS11.StoreSlot, config.PKCS11.PIN)
if err != nil {
return fmt.Errorf("failed to setup session and PKCS#11 context for slot %d: %s", config.PKCS11.StoreSlot, err)
}
log.Printf("Opened PKCS#11 session for slot %d\n", config.PKCS11.StoreSlot)
if _, err = generateKey(session, config.PKCS11.StoreLabel, config.Outputs.PublicKeyPath, config.Key); err != nil {
return err
}
return nil
}
func ocspRespCeremony(configBytes []byte) error {
var config ocspRespConfig
err := yaml.UnmarshalStrict(configBytes, &config)
if err != nil {
return fmt.Errorf("failed to parse config: %s", err)
}
if err := config.validate(); err != nil {
return fmt.Errorf("failed to validate config: %s", err)
}
cert, err := loadCert(config.Inputs.CertificatePath)
if err != nil {
return fmt.Errorf("failed to load certificate %q: %s", config.Inputs.CertificatePath, err)
}
issuer, err := loadCert(config.Inputs.IssuerCertificatePath)
if err != nil {
return fmt.Errorf("failed to load issuer certificate %q: %s", config.Inputs.IssuerCertificatePath, err)
}
var signer crypto.Signer
var delegatedIssuer *x509.Certificate
if config.Inputs.DelegatedIssuerCertificatePath != "" {
delegatedIssuer, err = loadCert(config.Inputs.DelegatedIssuerCertificatePath)
if err != nil {
return fmt.Errorf("failed to load delegated issuer certificate %q: %s", config.Inputs.DelegatedIssuerCertificatePath, err)
}
signer, _, err = openSigner(config.PKCS11, delegatedIssuer)
if err != nil {
return err
}
} else {
signer, _, err = openSigner(config.PKCS11, issuer)
if err != nil {
return err
}
}
thisUpdate, err := time.Parse(configDateLayout, config.OCSPProfile.ThisUpdate)
if err != nil {
return fmt.Errorf("unable to parse ocsp-profile.this-update: %s", err)
}
nextUpdate, err := time.Parse(configDateLayout, config.OCSPProfile.NextUpdate)
if err != nil {
return fmt.Errorf("unable to parse ocsp-profile.next-update: %s", err)
}
var status int
switch config.OCSPProfile.Status {
case "good":
status = int(ocsp.Good)
case "revoked":
status = int(ocsp.Revoked)
default:
// this shouldn't happen if the config is validated
return fmt.Errorf("unexpected ocsp-profile.stats: %s", config.OCSPProfile.Status)
}
resp, err := generateOCSPResponse(signer, issuer, delegatedIssuer, cert, thisUpdate, nextUpdate, status)
if err != nil {
return err
}
if err := ioutil.WriteFile(config.Outputs.ResponsePath, resp, 0644); err != nil {
return fmt.Errorf("failed to write OCSP response to %q: %s", config.Outputs.ResponsePath, err)
}
return nil
}
func crlCeremony(configBytes []byte) error {
var config crlConfig
err := yaml.UnmarshalStrict(configBytes, &config)
if err != nil {
return fmt.Errorf("failed to parse config: %s", err)
}
if err := config.validate(); err != nil {
return fmt.Errorf("failed to validate config: %s", err)
}
issuer, err := loadCert(config.Inputs.IssuerCertificatePath)
if err != nil {
return fmt.Errorf("failed to load issuer certificate %q: %s", config.Inputs.IssuerCertificatePath, err)
}
signer, _, err := openSigner(config.PKCS11, issuer)
if err != nil {
return err
}
thisUpdate, err := time.Parse(configDateLayout, config.CRLProfile.ThisUpdate)
if err != nil {
return fmt.Errorf("unable to parse crl-profile.this-update: %s", err)
}
nextUpdate, err := time.Parse(configDateLayout, config.CRLProfile.NextUpdate)
if err != nil {
return fmt.Errorf("unable to parse crl-profile.next-update: %s", err)
}
var revokedCertificates []pkix.RevokedCertificate
for _, rc := range config.CRLProfile.RevokedCertificates {
cert, err := loadCert(rc.CertificatePath)
if err != nil {
return fmt.Errorf("failed to load revoked certificate %q: %s", rc.CertificatePath, err)
}
revokedAt, err := time.Parse(configDateLayout, rc.RevocationDate)
if err != nil {
return fmt.Errorf("unable to parse crl-profile.revoked-certificates.revocation-date")
}
revokedCert := pkix.RevokedCertificate{
SerialNumber: cert.SerialNumber,
RevocationTime: revokedAt,
}
encReason, err := asn1.Marshal(rc.RevocationReason)
if err != nil {
return fmt.Errorf("failed to marshal revocation reason %q: %s", rc.RevocationReason, err)
}
revokedCert.Extensions = []pkix.Extension{{
Id: asn1.ObjectIdentifier{2, 5, 29, 21}, // id-ce-reasonCode
Value: encReason,
}}
revokedCertificates = append(revokedCertificates, revokedCert)
}
crlBytes, err := generateCRL(signer, issuer, thisUpdate, nextUpdate, config.CRLProfile.Number, revokedCertificates)
if err != nil {
return err
}
log.Printf("Signed CRL PEM:\n%s", crlBytes)
if err := ioutil.WriteFile(config.Outputs.CRLPath, crlBytes, 0644); err != nil {
return fmt.Errorf("failed to write CRL to %q: %s", config.Outputs.CRLPath, err)
}
return nil
}
func main() {
configPath := flag.String("config", "", "Path to ceremony configuration file")
flag.Parse()
if *configPath == "" {
log.Fatal("--config is required")
}
configBytes, err := ioutil.ReadFile(*configPath)
if err != nil {
log.Fatalf("Failed to read config file: %s", err)
}
var ct struct {
CeremonyType string `yaml:"ceremony-type"`
}
err = yaml.Unmarshal(configBytes, &ct)
if err != nil {
log.Fatalf("Failed to parse config: %s", err)
}
switch ct.CeremonyType {
case "root":
err = rootCeremony(configBytes)
if err != nil {
log.Fatalf("root ceremony failed: %s", err)
}
case "intermediate":
err = intermediateCeremony(configBytes, intermediateCert)
if err != nil {
log.Fatalf("intermediate ceremony failed: %s", err)
}
case "ocsp-signer":
err = intermediateCeremony(configBytes, ocspCert)
if err != nil {
log.Fatalf("ocsp signer ceremony failed: %s", err)
}
case "key":
err = keyCeremony(configBytes)
if err != nil {
log.Fatalf("key ceremony failed: %s", err)
}
case "ocsp-response":
err = ocspRespCeremony(configBytes)
if err != nil {
log.Fatalf("ocsp response ceremony failed: %s", err)
}
case "crl":
err = crlCeremony(configBytes)
if err != nil {
log.Fatalf("crl ceremony failed: %s", err)
}
case "crl-signer":
err = intermediateCeremony(configBytes, crlCert)
if err != nil {
log.Fatalf("crl signer ceremony failed: %s", err)
}
default:
log.Fatalf("unknown ceremony-type, must be one of: root, intermediate, ocsp-signer, crl-signer, key, ocsp-response")
}
}