Jan Dittberner
65c5bac2d9
This commit provides real CRL support with persistent storage of CRLs as well as support for delta CRLs using github.com/balacode/go-delta
275 lines
7.1 KiB
Go
275 lines
7.1 KiB
Go
/*
|
|
Copyright 2021-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 revoking takes care of handling certificate revocation requests.
|
|
package revoking
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/balacode/go-delta"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var OidCRLReason = asn1.ObjectIdentifier{2, 5, 29, 21}
|
|
|
|
const defaultCRLValidity = 24 * time.Hour
|
|
|
|
type CRLReason int
|
|
|
|
// CRL reason codes as defined in RFC 5280 section 5.3.1
|
|
const (
|
|
CRLReasonUnspecified CRLReason = 0
|
|
CRLReasonKeyCompromise CRLReason = 1
|
|
CRLReasonCACompromise CRLReason = 2
|
|
CRLReasonAffiliationChanged CRLReason = 3
|
|
CRLReasonSuperseded CRLReason = 4
|
|
CRLReasonCessationOfOperation CRLReason = 5
|
|
CRLReasonCertificateHold CRLReason = 6
|
|
CRLReasonRemoveFromCRL CRLReason = 8
|
|
CRLReasonPrivilegeWithdrawn CRLReason = 9
|
|
CRLReasonAACompromise CRLReason = 10
|
|
)
|
|
|
|
var crlReasonNames = map[CRLReason]string{
|
|
CRLReasonUnspecified: "unspecified",
|
|
CRLReasonKeyCompromise: "keyCompromise",
|
|
CRLReasonCACompromise: "CACompromise",
|
|
CRLReasonAffiliationChanged: "affiliationChanged",
|
|
CRLReasonSuperseded: "superseded",
|
|
CRLReasonCessationOfOperation: "cessationOfOperation",
|
|
CRLReasonCertificateHold: "certificateHold",
|
|
CRLReasonRemoveFromCRL: "removeFromCRL",
|
|
CRLReasonPrivilegeWithdrawn: "privilegeWithdrawn",
|
|
CRLReasonAACompromise: "AACompromise",
|
|
}
|
|
|
|
func (r CRLReason) String() string {
|
|
if reason, ok := crlReasonNames[r]; ok {
|
|
return reason
|
|
}
|
|
|
|
return crlReasonNames[CRLReasonUnspecified]
|
|
}
|
|
|
|
func (r CRLReason) BuildExtension() pkix.Extension {
|
|
extBytes, _ := asn1.Marshal(r)
|
|
|
|
return pkix.Extension{Id: OidCRLReason, Value: extBytes}
|
|
}
|
|
|
|
// ParseReason takes a reason string and performs a case-insensitive match to a reason code
|
|
func ParseReason(rs string) CRLReason {
|
|
for key, name := range crlReasonNames {
|
|
if strings.EqualFold(name, rs) {
|
|
return key
|
|
}
|
|
}
|
|
|
|
return CRLReasonUnspecified
|
|
}
|
|
|
|
type X509Revoking struct {
|
|
repository Repository
|
|
crlAlgorithm x509.SignatureAlgorithm
|
|
crlIssuer *x509.Certificate
|
|
signer crypto.Signer
|
|
logger *logrus.Logger
|
|
}
|
|
|
|
type RevokeCertificate struct {
|
|
serialNumber *big.Int
|
|
reason CRLReason
|
|
}
|
|
|
|
func NewRevokeCertificate(serialNumber *big.Int, reason CRLReason) *RevokeCertificate {
|
|
return &RevokeCertificate{
|
|
serialNumber: serialNumber,
|
|
reason: reason,
|
|
}
|
|
}
|
|
|
|
type CRLInformation struct {
|
|
CRL []byte // DER encoded CRL
|
|
Number *big.Int
|
|
NextUpdate time.Time
|
|
}
|
|
|
|
func (r *X509Revoking) Revoke(revokeCertificate *RevokeCertificate) (*pkix.RevokedCertificate, error) {
|
|
revoked := &pkix.RevokedCertificate{
|
|
SerialNumber: revokeCertificate.serialNumber,
|
|
RevocationTime: time.Now(),
|
|
Extensions: []pkix.Extension{revokeCertificate.reason.BuildExtension()},
|
|
}
|
|
|
|
if err := r.repository.StoreRevocation(revoked); err != nil {
|
|
return nil, fmt.Errorf("could not store revocation %w", err)
|
|
}
|
|
|
|
return revoked, nil
|
|
}
|
|
|
|
func (r *X509Revoking) createCRL() (*CRLInformation, error) {
|
|
revoked, err := r.repository.RevokedCertificates()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not get revocation information: %w", err)
|
|
}
|
|
|
|
nextNumber, err := r.repository.NextCRLNumber()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not get next CRL number: %w", err)
|
|
}
|
|
|
|
nextUpdate := time.Now().UTC().Add(defaultCRLValidity)
|
|
|
|
list, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
|
|
SignatureAlgorithm: r.crlAlgorithm,
|
|
RevokedCertificates: revoked,
|
|
Number: nextNumber,
|
|
ThisUpdate: time.Now().UTC(),
|
|
NextUpdate: nextUpdate,
|
|
}, r.crlIssuer, r.signer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not sign revocation list: %w", err)
|
|
}
|
|
|
|
err = r.repository.StoreCRL(nextNumber, list)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not store new CRL: %w", err)
|
|
}
|
|
|
|
return &CRLInformation{CRL: list, Number: nextNumber, NextUpdate: nextUpdate}, nil
|
|
}
|
|
|
|
func (r *X509Revoking) GetCurrentCRL() (*CRLInformation, error) {
|
|
return r.GetCRL(nil)
|
|
}
|
|
|
|
const maximumCRLLifetime = 6 * time.Hour
|
|
|
|
func (r *X509Revoking) GetCRL(number *big.Int) (*CRLInformation, error) {
|
|
var (
|
|
crl []byte
|
|
err error
|
|
list *x509.RevocationList
|
|
)
|
|
|
|
crl, err = r.repository.LoadCRL(number)
|
|
if err != nil {
|
|
r.logger.WithError(err).Warn("could not load CRL")
|
|
|
|
if number != nil {
|
|
return nil, fmt.Errorf("could not load crl number 0x%0x", number)
|
|
}
|
|
}
|
|
|
|
if crl != nil {
|
|
list, err = x509.ParseRevocationList(crl)
|
|
if err != nil {
|
|
r.logger.WithError(err).Warn("could not parse CRL")
|
|
}
|
|
|
|
stillValid := list.ThisUpdate.Add(maximumCRLLifetime).After(time.Now().UTC())
|
|
|
|
if number != nil || stillValid {
|
|
return &CRLInformation{CRL: crl, Number: list.Number, NextUpdate: list.NextUpdate}, nil
|
|
}
|
|
}
|
|
|
|
return r.createCRL()
|
|
}
|
|
|
|
func NewX509Revoking(
|
|
repo Repository,
|
|
crlAlgorithm x509.SignatureAlgorithm,
|
|
issuer *x509.Certificate,
|
|
signer crypto.Signer,
|
|
logger *logrus.Logger,
|
|
) *X509Revoking {
|
|
return &X509Revoking{
|
|
repository: repo,
|
|
crlAlgorithm: crlAlgorithm,
|
|
crlIssuer: issuer,
|
|
signer: signer,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
type Result struct {
|
|
CRLData []byte
|
|
IsDelta bool
|
|
Number *big.Int
|
|
}
|
|
|
|
type FetchCRLHandler struct {
|
|
repositories map[string]*X509Revoking
|
|
}
|
|
|
|
func NewFetchCRLHandler(repositories map[string]*X509Revoking) *FetchCRLHandler {
|
|
return &FetchCRLHandler{repositories: repositories}
|
|
}
|
|
|
|
func (h *FetchCRLHandler) FetchCRL(issuerID string, crlNumber *big.Int) (*Result, error) {
|
|
var (
|
|
currentCRL, oldCRL *CRLInformation
|
|
err error
|
|
)
|
|
|
|
repo, ok := h.repositories[issuerID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown issuer ID %s", issuerID)
|
|
}
|
|
|
|
if repo.signer == nil {
|
|
return nil, fmt.Errorf("key for issuer ID %s is not available", issuerID)
|
|
}
|
|
|
|
currentCRL, err = repo.GetCurrentCRL()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not get CRL for issuer ID %s: %w", issuerID, err)
|
|
}
|
|
|
|
if crlNumber == nil {
|
|
return &Result{CRLData: currentCRL.CRL, Number: currentCRL.Number, IsDelta: false}, nil
|
|
}
|
|
|
|
if crlNumber.Cmp(currentCRL.Number) == 0 {
|
|
return &Result{CRLData: nil, Number: currentCRL.Number, IsDelta: false}, nil
|
|
}
|
|
|
|
oldCRL, err = repo.GetCRL(crlNumber)
|
|
if err != nil {
|
|
return &Result{CRLData: currentCRL.CRL, Number: currentCRL.Number, IsDelta: false}, nil
|
|
}
|
|
|
|
diff := delta.Make(oldCRL.CRL, currentCRL.CRL)
|
|
patchBytes := diff.Bytes()
|
|
|
|
if len(patchBytes) > len(currentCRL.CRL) {
|
|
return &Result{CRLData: currentCRL.CRL, Number: currentCRL.Number, IsDelta: false}, nil
|
|
}
|
|
|
|
return &Result{CRLData: patchBytes, Number: currentCRL.Number, IsDelta: true}, nil
|
|
}
|