cacert-gosigner/internal/hsm/hsm.go
Jan Dittberner afe7d23c9b Implement CA information command
This commit defines command codes for planned commands and response codes for
their corresponding responses.

The health response from the HSM access component has been reduced to avoid
unnecessary data transmissions.

A new CA information command has been implemented. This command can be used
to retrieve the CA certificate and profile information for a given CA name.

The client simulator has been updated to retrieve CA information for all
CAs when the list of CAs changes.
2022-12-02 18:31:59 +01:00

643 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"
"errors"
"fmt"
"math/big"
"os"
"path"
"syscall"
"time"
"github.com/ThalesIgnite/crypto11"
"github.com/sirupsen/logrus"
"git.cacert.org/cacert-gosigner/internal/cainfo"
"git.cacert.org/cacert-gosigner/internal/config"
"git.cacert.org/cacert-gosigner/internal/health"
"git.cacert.org/cacert-gosigner/pkg/messages"
)
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
}
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)
}
certificate, err := x509.ParseCertificate(certData)
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
}
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)
for _, ca := range a.signerConfig.RootCAs() {
_, err := a.GetRootCACertificate(ca)
if err != nil {
healthy = false
moreInfo[ca] = string(messages.CertStatusFailed)
continue
}
moreInfo[ca] = string(messages.CertStatusOk)
}
for _, ca := range a.signerConfig.SubordinateCAs() {
_, err := a.GetSubordinateCACertificate(ca)
if err != nil {
healthy = false
moreInfo[ca] = string(messages.CertStatusFailed)
continue
}
def, err := a.signerConfig.GetCADefinition(ca)
if err != nil {
healthy = false
moreInfo[ca] = string(messages.CertStatusFailed)
continue
}
_, err = a.getKeyPair(ca, def.KeyInfo)
if err != nil {
healthy = false
moreInfo[ca] = string(messages.CertStatusFailed)
continue
}
moreInfo[ca] = string(messages.CertStatusOk)
}
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 (a *Access) GetCAInfo(name string) (*cainfo.Result, error) {
sc := a.GetSignerConfig()
caFile := &caFile{sc: sc, label: name}
def, err := sc.GetCADefinition(name)
if err != nil {
return nil, fmt.Errorf("certificate %s unknown: %w", name, err)
}
certificate, err := caFile.loadCertificate(a.caDirectory)
if err != nil {
return nil, fmt.Errorf("certificate %s not available: %w", name, err)
}
return &cainfo.Result{
Certificate: certificate.Raw,
Profiles: def.Profiles,
}, 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
}
if a.IsSetupMode() {
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
}
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.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(certLabel)},
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 %s: %w", certLabel, err)
}
err = certFile.storeCertificate(a.caDirectory, certBytes)
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)
}
if err = certFile.storeCertificate(a.caDirectory, certBytes); 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
}