Files
estserver/service.go
Volodymyr Khoroz 1cf5a72794 Feature: support multiple root CA certificates
This is needed to support the root CA renewal feature.
During the initial phase of that renewal, a server needs to send clients:
- A new root CA,
- A cross-signed copy of that new CA, so that clients can validate the chain of trust using a previous root CA,
- And a previous root CA, so that clients can still trust the existing server TLS certificates during interregnum.

Signed-off-by: Volodymyr Khoroz <volodymyr.khoroz@foundries.io>
2024-06-17 19:26:56 +03:00

296 lines
8.4 KiB
Go

package est
import (
"bytes"
"context"
"crypto"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"math/big"
"time"
"go.mozilla.org/pkcs7"
)
var (
ErrEst = errors.New("base EstError")
)
type EstErrorType int
const (
ErrInvalidSignatureAlgorithm EstErrorType = iota
ErrSubjectMismatch
ErrSubjectAltNameMismatch
ErrInvalidBase64
ErrInvalidCsr
ErrInvalidCsrSignature
)
func (e EstErrorType) Unwrap() error {
return ErrEst
}
func (e EstErrorType) Error() string {
switch e {
case ErrInvalidSignatureAlgorithm:
return "Signature algorithm of the CSR does not match that of the CA"
case ErrSubjectMismatch:
return "Subject field of CSR must match the current client certificate"
case ErrSubjectAltNameMismatch:
return "SubjectAltName field of CSR must match the current client certificate"
case ErrInvalidBase64:
return "The CSR payload is not base64 encoded"
case ErrInvalidCsr:
return "The CSR could not be decoded"
case ErrInvalidCsrSignature:
return "The CSR signature is invalid"
}
panic("Unsupported error type")
}
var (
oidKeyUsage = asn1.ObjectIdentifier([]int{2, 5, 29, 15})
oidSubjectAltName = asn1.ObjectIdentifier([]int{2, 5, 29, 17})
oidExtendedKeyUsage = asn1.ObjectIdentifier([]int{2, 5, 29, 37})
asn1DigitalSignature = []byte{3, 2, 7, 128}
asn1TlsWebClientAuth = []byte{48, 10, 6, 8, 43, 6, 1, 5, 5, 7, 3, 2}
)
type ServiceHandler interface {
GetService(ctx context.Context, serverName string) (Service, error)
}
type staticSvcHandler struct {
Service
}
func (s staticSvcHandler) GetService(ctx context.Context, serverName string) (Service, error) {
return s.Service, nil
}
func NewStaticServiceHandler(svc Service) ServiceHandler {
return &staticSvcHandler{svc}
}
// Service represents a thin API to handle required operations of EST7030.
// This service implements the required parts of EST. Specifically:
//
// "cas" - Section 4.1
// "enroll" and "reenroll" - Section 4.2
//
// Optional APIs are not implemented including:
//
// 4.3 - cmc
// 4.4 - server side key generation
// 4.5 - CSR attributes
type Service struct {
// Root CAs for a Factory
rootCa []*x509.Certificate
// ca and key are the EST7030 keypair used for signing EST7030 requests
ca *x509.Certificate
key crypto.Signer
certDuration time.Duration
}
// NewService creates an EST7030 API for a Factory
func NewService(rootCa []*x509.Certificate, ca *x509.Certificate, key crypto.Signer, certDuration time.Duration) Service {
return Service{
rootCa: rootCa,
ca: ca,
key: key,
certDuration: certDuration,
}
}
// CaCerts return the root CA certificates as per:
// https://www.rfc-editor.org/rfc/rfc7030.html#section-4.1.2
func (s Service) CaCerts(ctx context.Context) ([]byte, error) {
if envelope, err := pkcs7.NewSignedData(nil); err != nil {
return nil, err
} else {
for _, cert := range s.rootCa {
envelope.AddCertificate(cert)
}
if bytes, err := envelope.Finish(); err != nil {
return nil, err
} else {
return []byte(base64.StdEncoding.EncodeToString(bytes)), nil
}
}
}
// Enroll perform EST7030 enrollment operation as per
// https://www.rfc-editor.org/rfc/rfc7030.html#section-4.2.1
// Errors can be generic errors or of the type EstError
func (s Service) Enroll(ctx context.Context, csrBytes []byte) ([]byte, error) {
csr, err := s.loadCsr(ctx, csrBytes)
if err != nil {
return nil, err
}
return s.signCsr(ctx, csr)
}
// ReEnroll perform EST7030 enrollment operation as per
// https://www.rfc-editor.org/rfc/rfc7030.html#section-4.2.2
// Errors can be generic errors or of the type EstError
func (s Service) ReEnroll(ctx context.Context, csrBytes []byte, curCert *x509.Certificate) ([]byte, error) {
log := CtxGetLog(ctx)
csr, err := s.loadCsr(ctx, csrBytes)
if err != nil {
return nil, err
}
if !bytes.Equal(csr.RawSubject, curCert.RawSubject) {
log.Warn().
Str("current-subject", curCert.Subject.String()).
Str("requests-subject", csr.Subject.String()).
Msg("Subject name mismatch")
return nil, ErrSubjectMismatch
}
var csrSAN pkix.Extension
var certSAN pkix.Extension
for _, ext := range csr.Extensions {
if ext.Id.Equal(oidSubjectAltName) {
csrSAN = ext
break
}
}
for _, ext := range curCert.Extensions {
if ext.Id.Equal(oidSubjectAltName) {
certSAN = ext
break
}
}
if !bytes.Equal(csrSAN.Value, certSAN.Value) {
return nil, ErrSubjectAltNameMismatch
}
// TODO: Should we allow this:
// "The ChangeSubjectName attribute, as defined in [RFC6402], MAY be included
// in the CSR to request that these fields be changed in the new certificate."
// Parts of the subject like dn,ou, and businessCategory=production *can't* be altered
return s.signCsr(ctx, csr)
}
// loadCsr parses the certifcate signing request based on rules of
// https://www.rfc-editor.org/rfc/rfc7030.html#section-4.2.1
// - content is a base64 encoded certificate signing request
func (s Service) loadCsr(ctx context.Context, bytes []byte) (*x509.CertificateRequest, error) {
bytes, err := base64.StdEncoding.DecodeString(string(bytes))
log := CtxGetLog(ctx)
if err != nil {
log.Error().Err(err).Msg("Unable to decode base64 data")
return nil, fmt.Errorf("%w: %s", ErrInvalidBase64, err)
}
csr, err := x509.ParseCertificateRequest(bytes)
if err != nil {
log.Error().Err(err).Msg("Unable to parse CSR")
return nil, fmt.Errorf("%w: %s", ErrInvalidCsr, err)
}
if err = csr.CheckSignature(); err != nil {
log.Error().Err(err).Msg("Invalid CSR Signature")
return nil, fmt.Errorf("%w: %s", ErrInvalidCsrSignature, err)
}
return csr, nil
}
// signCsr returns a base64 PCKS7 encoded certificate as per
// https://www.rfc-editor.org/rfc/rfc7030.html#section-4.1.3
func (s Service) signCsr(ctx context.Context, csr *x509.CertificateRequest) ([]byte, error) {
log := CtxGetLog(ctx)
if s.ca.SignatureAlgorithm != csr.SignatureAlgorithm {
return nil, ErrInvalidSignatureAlgorithm
}
sn, err := rand.Int(rand.Reader, big.NewInt(1).Exp(big.NewInt(2), big.NewInt(128), nil))
if err != nil {
return nil, err
}
now := time.Now()
notAfter := now.Add(s.certDuration)
if notAfter.After(s.ca.NotAfter) {
log.Warn().Msg("Adjusting default cert expiry")
notAfter = s.ca.NotAfter
}
// This deviates from 4.2.1, but we limit the extensions and not allow
// clients to create CAs
var ku x509.KeyUsage
var eku []x509.ExtKeyUsage
for _, e := range csr.Extensions {
if e.Id.Equal(oidKeyUsage) {
if !bytes.Equal(e.Value, asn1DigitalSignature) {
log.Error().Bytes("Value", e.Value).Msg("Unsupported CSR KeyUsage options")
return nil, fmt.Errorf("%w: Unsupported CSR KeyUsage value", ErrInvalidCsr)
}
ku |= x509.KeyUsageDigitalSignature
} else if e.Id.Equal(oidExtendedKeyUsage) {
if !bytes.Equal(e.Value, asn1TlsWebClientAuth) {
log.Error().Bytes("Value", e.Value).Msg("Unsupported CSR ExtendedKeyUsage options")
return nil, fmt.Errorf("%w: Unsupported CSR ExtendedKeyUsage value", ErrInvalidCsr)
}
eku = append(eku, x509.ExtKeyUsageClientAuth)
} else {
log.Error().Str("OID", e.Id.String()).Msg("Unsupported CSR Extension")
}
}
var tmpl = &x509.Certificate{
SerialNumber: sn,
NotBefore: now,
NotAfter: notAfter,
RawSubject: csr.RawSubject,
Signature: csr.Signature,
SignatureAlgorithm: csr.SignatureAlgorithm,
PublicKeyAlgorithm: csr.PublicKeyAlgorithm,
Issuer: s.ca.Subject,
PublicKey: csr.PublicKey,
BasicConstraintsValid: true,
IsCA: false,
KeyUsage: ku,
ExtKeyUsage: eku,
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, s.ca, csr.PublicKey, s.key)
if err != nil {
log.Error().Err(err).Msg("Unable to create new certificate")
return nil, err
}
cert, err := x509.ParseCertificate(der)
if err != nil {
log.Error().Err(err).Msg("Unable to parse created certificate")
return nil, err
}
bytes, err := pkcs7.DegenerateCertificate(cert.Raw)
if err != nil {
log.Error().Err(err).Msg("Unable to PKCS7 encode certificate")
return nil, err
}
pub, _ := pubkey(cert)
log.Info().Str("value", pub).Msg("New certificate for key")
return []byte(base64.StdEncoding.EncodeToString(bytes)), nil
}
func pubkey(cert *x509.Certificate) (string, error) {
derBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
if err != nil {
return "", err
}
block := &pem.Block{
Type: "PUBLIC KEY",
Bytes: derBytes,
}
return string(pem.EncodeToMemory(block)), nil
}