cacert-gosigner/internal/hsm/hsm.go

654 lines
14 KiB
Go

/*
Copyright 2022 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package hsm handles hardware security modules.
package hsm
import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"errors"
"fmt"
"math/big"
"os"
"path"
"syscall"
"time"
"github.com/ThalesIgnite/crypto11"
"github.com/sirupsen/logrus"
"git.cacert.org/cacert-gosigner/pkg/messages"
"git.cacert.org/cacert-gosigner/internal/config"
"git.cacert.org/cacert-gosigner/internal/health"
)
var (
// 1.3.6.1.4.1.18506.2.3.1 Class3 Policy Version 1
oidCAcertClass3PolicyV1 = []int{1, 3, 6, 1, 4, 1, 18506, 2, 3, 1}
errKeyGenerationRefused = errors.New("not in setup mode, refusing key generation")
errCertificateGenerationRefused = errors.New("not in setup mode, refusing certificate generation")
)
type caFile struct {
sc *config.SignerConfig
label string
}
type Access struct {
logger *logrus.Logger
caDirectory string
signerConfig *config.SignerConfig
p11Contexts map[string]*crypto11.Context
setupMode bool
verbose bool
}
func (a *Access) Healthy() (*health.Info, error) {
healthy := true
moreInfo := make(map[string]string)
var checkFailed = messages.CertificateInfo{Status: "failed", Signing: false}.String()
for _, ca := range a.signerConfig.RootCAs() {
infoKey := fmt.Sprintf("root-%s", ca)
cert, err := a.GetRootCACertificate(ca)
if err != nil {
healthy = false
moreInfo[infoKey] = checkFailed
continue
}
moreInfo[infoKey] = messages.CertificateInfo{
Status: "ok",
Signing: false,
Profiles: []messages.CAProfile{},
ValidUntil: cert.NotAfter.UTC(),
}.String()
}
for _, ca := range a.signerConfig.SubordinateCAs() {
infoKey := fmt.Sprintf("sub-%s", ca)
cert, err := a.GetSubordinateCACertificate(ca)
if err != nil {
healthy = false
moreInfo[infoKey] = checkFailed
continue
}
def, err := a.signerConfig.GetCADefinition(ca)
if err != nil {
healthy = false
moreInfo[infoKey] = checkFailed
continue
}
_, err = a.getKeyPair(ca, def.KeyInfo)
if err != nil {
healthy = false
moreInfo[infoKey] = checkFailed
continue
}
moreInfo[infoKey] = messages.CertificateInfo{
Status: "ok",
Signing: true,
Profiles: def.Profiles,
ValidUntil: cert.NotAfter.UTC(),
}.String()
}
return &health.Info{
Healthy: healthy,
Source: "HSM",
MoreInfo: moreInfo,
}, nil
}
func NewAccess(logger *logrus.Logger, options ...ConfigOption) (*Access, error) {
access := &Access{logger: logger}
access.setupContext(options...)
return access, nil
}
func (c *caFile) buildCertificatePath(caDirectory string) string {
fileName := c.sc.CertificateFileName(c.label)
if caDirectory == "" {
return fileName
}
return path.Join(caDirectory, fileName)
}
func (c *caFile) loadCertificate(caDirectory string) (*x509.Certificate, error) {
certFile := c.buildCertificatePath(caDirectory)
certFileInfo, err := os.Stat(certFile)
if err != nil {
if errors.Is(err, syscall.ENOENT) {
return nil, nil
}
return nil, fmt.Errorf("could not get info for %s: %w", certFile, err)
}
if !certFileInfo.Mode().IsRegular() {
return nil, fmt.Errorf("certificate file %s is not a regular file", certFile)
}
certData, err := os.ReadFile(certFile)
if err != nil {
return nil, fmt.Errorf("could not read %s: %w", certFile, err)
}
pemData, _ := pem.Decode(certData)
if pemData == nil {
return nil, fmt.Errorf("no PEM data in %s", certFile)
}
if pemData.Type != "CERTIFICATE" {
return nil, fmt.Errorf("no certificate found in %s", certFile)
}
certificate, err := x509.ParseCertificate(pemData.Bytes)
if err != nil {
return nil, fmt.Errorf("could not parse certificate from %s: %w", certFile, err)
}
return certificate, nil
}
func (c *caFile) storeCertificate(caDirectory string, certificate []byte) error {
certFile := c.buildCertificatePath(caDirectory)
err := os.WriteFile(certFile, certificate, 0o600)
if err != nil {
return fmt.Errorf("could not write certificate file %s: %w", certFile, err)
}
return nil
}
func (a *Access) GetRootCACertificate(label string) (*x509.Certificate, error) {
var (
certificate *x509.Certificate
keyPair crypto.Signer
)
sc := a.GetSignerConfig()
caCert, err := sc.GetCADefinition(label)
if err != nil {
return nil, fmt.Errorf("could not get CA definition for label %s: %w", label, err)
}
if !caCert.IsRoot() {
return nil, fmt.Errorf("CA definition %s is not a root CA definition", label)
}
caFile := &caFile{sc: sc, label: label}
certificate, err = caFile.loadCertificate(a.caDirectory)
if err != nil {
return nil, err
}
if certificate != nil && !a.IsSetupMode() {
caCert.Certificate = certificate
return certificate, nil
}
keyPair, err = a.getKeyPair(label, caCert.KeyInfo)
if err != nil {
return nil, err
}
if certificate != nil {
err := certificateMatches(certificate, keyPair)
if err != nil {
return nil, err
}
return certificate, nil
}
if !a.IsSetupMode() {
return nil, errCertificateGenerationRefused
}
notBefore, notAfter := sc.CalculateValidity(caCert, time.Now())
subject := sc.CalculateSubject(caCert)
certificate, err = a.generateRootCACertificate(
caFile,
keyPair,
&x509.Certificate{
Subject: subject,
NotBefore: notBefore,
NotAfter: notAfter,
MaxPathLen: 0,
MaxPathLenZero: false,
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageCRLSign | x509.KeyUsageCertSign,
IsCA: true,
},
)
if err != nil {
return nil, err
}
p11Context, err := a.GetP11Context(caCert)
if err != nil {
return nil, err
}
err = addCertificate(p11Context, label, certificate)
if err != nil {
return nil, err
}
caCert.Certificate, caCert.KeyPair = certificate, keyPair
return certificate, nil
}
func (a *Access) GetSubordinateCACertificate(certLabel string) (*x509.Certificate, error) {
var (
certificate *x509.Certificate
keyPair crypto.Signer
)
sc := a.GetSignerConfig()
caCert, err := sc.GetCADefinition(certLabel)
if err != nil {
return nil, fmt.Errorf("could not get CA definition for label %s: %w", certLabel, err)
}
if caCert.IsRoot() {
return nil, fmt.Errorf(
"CA definition %s is a root CA definition, subordinate expected",
certLabel,
)
}
keyPair, err = a.getKeyPair(certLabel, caCert.KeyInfo)
if err != nil {
return nil, err
}
certFile := &caFile{sc: sc, label: certLabel}
certificate, err = certFile.loadCertificate(a.caDirectory)
if err != nil {
return nil, err
}
if certificate != nil {
err := certificateMatches(certificate, keyPair)
if err != nil {
return nil, err
}
caCert.Certificate, caCert.KeyPair = certificate, keyPair
return certificate, nil
}
if !a.IsSetupMode() {
return nil, errCertificateGenerationRefused
}
notBefore, notAfter := sc.CalculateValidity(caCert, time.Now())
subject := sc.CalculateSubject(caCert)
certificate, err = a.generateSubordinateCACertificate(
certFile,
sc,
certLabel,
keyPair.Public(),
&x509.Certificate{
Subject: subject,
NotBefore: notBefore,
NotAfter: notAfter,
MaxPathLen: caCert.MaxPathLen,
MaxPathLenZero: true,
BasicConstraintsValid: true,
IsCA: true,
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
ExtKeyUsage: caCert.ExtKeyUsage,
IssuingCertificateURL: []string{sc.BuildIssuerURL(caCert)},
OCSPServer: []string{sc.BuildOCSPURL(caCert)},
CRLDistributionPoints: []string{sc.BuildCRLUrl(caCert)},
PolicyIdentifiers: []asn1.ObjectIdentifier{
// use policy identifiers from http://wiki.cacert.org/OidAllocation
oidCAcertClass3PolicyV1,
},
},
)
if err != nil {
return nil, err
}
p11Context, err := a.GetP11Context(caCert)
if err != nil {
return nil, err
}
err = addCertificate(p11Context, certLabel, certificate)
if err != nil {
return nil, err
}
caCert.Certificate, caCert.KeyPair = certificate, keyPair
return certificate, nil
}
func (a *Access) generateSubordinateCACertificate(
certFile *caFile,
config *config.SignerConfig,
certLabel string,
publicKey crypto.PublicKey,
template *x509.Certificate,
) (*x509.Certificate, error) {
parent, err := config.GetParentCA(certLabel)
if err != nil {
return nil, fmt.Errorf("could not get parent CA for label %s: %w", certLabel, err)
}
serial, err := randomSerialNumber()
if err != nil {
return nil, err
}
template.SerialNumber = serial
template.SignatureAlgorithm, err = determineSignatureAlgorithm(parent.KeyPair)
if err != nil {
return nil, err
}
certBytes, err := x509.CreateCertificate(
rand.Reader,
template,
parent.Certificate,
publicKey,
parent.KeyPair,
)
if err != nil {
return nil, fmt.Errorf("could not create subordinate CA certificate: %w", err)
}
certBlock := &pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
}
err = certFile.storeCertificate(a.caDirectory, pem.EncodeToMemory(certBlock))
if err != nil {
return nil, err
}
certificate, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, fmt.Errorf("could not parse generated certificate: %w", err)
}
return certificate, nil
}
func addCertificate(p11Context *crypto11.Context, label string, certificate *x509.Certificate) error {
objectID, err := randomObjectID()
if err != nil {
return err
}
err = p11Context.ImportCertificateWithLabel(objectID, []byte(label), certificate)
if err != nil {
return fmt.Errorf("could not import certificate into token: %w", err)
}
return nil
}
func (a *Access) getKeyPair(label string, keyInfo *config.PrivateKeyInfo) (crypto.Signer, error) {
sc := a.GetSignerConfig()
cert, err := sc.GetCADefinition(label)
if err != nil {
return nil, fmt.Errorf("could not get CA definition for label %s: %w", label, err)
}
if cert.KeyPair != nil {
return cert.KeyPair, nil
}
p11Context, err := a.GetP11Context(cert)
if err != nil {
return nil, err
}
keyPair, err := p11Context.FindKeyPair(nil, []byte(label))
if err != nil {
return nil, fmt.Errorf("could not find requested key pair: %w", err)
}
if keyPair != nil {
return keyPair, nil
}
if !a.IsSetupMode() {
return nil, errKeyGenerationRefused
}
switch keyInfo.Algorithm {
case x509.RSA:
keyPair, err = generateRSAKeyPair(p11Context, label, keyInfo)
if err != nil {
return nil, err
}
case x509.ECDSA:
keyPair, err = generateECDSAKeyPair(p11Context, label, keyInfo)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf(
"could not generate private key with label %s with unsupported key algorithm %s",
label,
keyInfo.Algorithm,
)
}
return keyPair, nil
}
func generateECDSAKeyPair(
p11Context *crypto11.Context,
label string,
keyInfo *config.PrivateKeyInfo,
) (crypto11.Signer, error) {
var (
err error
newObjectID []byte
signer crypto11.Signer
)
newObjectID, err = randomObjectID()
if err != nil {
return nil, err
}
signer, err = p11Context.GenerateECDSAKeyPairWithLabel(newObjectID, []byte(label), keyInfo.EccCurve)
if err != nil {
return nil, fmt.Errorf("could not generate ECDSA key pair: %w", err)
}
return signer, nil
}
func generateRSAKeyPair(
p11Context *crypto11.Context,
label string,
keyInfo *config.PrivateKeyInfo,
) (crypto11.Signer, error) {
var (
err error
newObjectID []byte
signer crypto11.Signer
)
newObjectID, err = randomObjectID()
if err != nil {
return nil, err
}
signer, err = p11Context.GenerateRSAKeyPairWithLabel(newObjectID, []byte(label), keyInfo.RSABits)
if err != nil {
return nil, fmt.Errorf("could not generate RSA key pair: %w", err)
}
return signer, nil
}
func randomObjectID() ([]byte, error) {
const objectIDSize = 20
result := make([]byte, objectIDSize)
_, err := rand.Read(result)
if err != nil {
return nil, fmt.Errorf("could not create new random object id: %w", err)
}
return result, nil
}
func (a *Access) generateRootCACertificate(
certFile *caFile,
keyPair crypto.Signer,
template *x509.Certificate,
) (*x509.Certificate, error) {
serial, err := randomSerialNumber()
if err != nil {
return nil, err
}
template.SerialNumber = serial
template.SignatureAlgorithm, err = determineSignatureAlgorithm(keyPair)
if err != nil {
return nil, err
}
certBytes, err := x509.CreateCertificate(
rand.Reader,
template,
template,
keyPair.Public(),
keyPair,
)
if err != nil {
return nil, fmt.Errorf("could not create root certificate: %w", err)
}
certBlock := &pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
}
if err = certFile.storeCertificate(a.caDirectory, pem.EncodeToMemory(certBlock)); err != nil {
return nil, err
}
certificate, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, fmt.Errorf("could not parse generated certificate: %w", err)
}
return certificate, nil
}
func determineSignatureAlgorithm(keyPair crypto.Signer) (x509.SignatureAlgorithm, error) {
switch keyPair.Public().(type) {
case *ecdsa.PublicKey:
return x509.ECDSAWithSHA256, nil
case *rsa.PublicKey:
return x509.SHA256WithRSA, nil
default:
return x509.UnknownSignatureAlgorithm,
fmt.Errorf("could not determine signature algorithm for key of type %T", keyPair)
}
}
func certificateMatches(certificate *x509.Certificate, key crypto.Signer) error {
switch v := certificate.PublicKey.(type) {
case *ecdsa.PublicKey:
if pub, ok := key.Public().(*ecdsa.PublicKey); ok {
if v.Equal(pub) {
return nil
}
}
case *rsa.PublicKey:
if pub, ok := key.Public().(*rsa.PublicKey); ok {
if v.Equal(pub) {
return nil
}
}
default:
return fmt.Errorf("unsupported public key %v", v)
}
return fmt.Errorf(
"public key from certificate does not match private key: %s != %s",
certificate.PublicKey,
key.Public(),
)
}
func randomSerialNumber() (*big.Int, error) {
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return nil, fmt.Errorf("could not generate serial number: %w", err)
}
return serialNumber, nil
}