Jan Dittberner
60be959c24
This commit adds health check capabilities to the hsm.Access and health response data to the messages returned by the health command.
633 lines
14 KiB
Go
633 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"
|
|
"log"
|
|
"math/big"
|
|
"os"
|
|
"path"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/ThalesIgnite/crypto11"
|
|
|
|
"git.cacert.org/cacert-gosigner/pkg/health"
|
|
|
|
"git.cacert.org/cacert-gosigner/pkg/config"
|
|
)
|
|
|
|
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 {
|
|
infoLog *log.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)
|
|
|
|
const checkFailed = "failed"
|
|
|
|
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
|
|
}
|
|
|
|
moreInfo[infoKey] = fmt.Sprintf("ok, valid until %s", cert.NotAfter.UTC().Format(time.RFC3339))
|
|
}
|
|
|
|
for _, ca := range a.signerConfig.IntermediaryCAs() {
|
|
infoKey := fmt.Sprintf("sub-%s", ca)
|
|
|
|
cert, err := a.GetIntermediaryCACertificate(ca)
|
|
if err != nil {
|
|
healthy = false
|
|
|
|
moreInfo[infoKey] = checkFailed
|
|
}
|
|
|
|
def, err := a.signerConfig.GetCADefinition(ca)
|
|
if err != nil {
|
|
healthy = false
|
|
|
|
moreInfo[infoKey] = checkFailed
|
|
}
|
|
|
|
_, err = a.getKeyPair(ca, def.KeyInfo)
|
|
if err != nil {
|
|
healthy = false
|
|
|
|
moreInfo[infoKey] = checkFailed
|
|
}
|
|
|
|
moreInfo[infoKey] = fmt.Sprintf("ok, valid until %s", cert.NotAfter.UTC().Format(time.RFC3339))
|
|
}
|
|
|
|
return &health.Info{
|
|
Healthy: healthy,
|
|
Source: "HSM",
|
|
MoreInfo: moreInfo,
|
|
}, nil
|
|
}
|
|
|
|
func NewAccess(infoLog *log.Logger, options ...ConfigOption) (*Access, error) {
|
|
access := &Access{infoLog: infoLog}
|
|
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) GetIntermediaryCACertificate(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, intermediary 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
|
|
}
|
|
|
|
return certificate, nil
|
|
}
|
|
|
|
if !a.IsSetupMode() {
|
|
return nil, errCertificateGenerationRefused
|
|
}
|
|
|
|
notBefore, notAfter := sc.CalculateValidity(caCert, time.Now())
|
|
subject := sc.CalculateSubject(caCert)
|
|
|
|
certificate, err = a.generateIntermediaryCACertificate(
|
|
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) generateIntermediaryCACertificate(
|
|
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 intermediary 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
|
|
}
|