Jan Dittberner
0d69a9013d
- create new type hsm.Access to encapsulate HSM operations - make setup options operate on hsm.Access instances - adapt tests and cmd/signer to work with hsm.Access
373 lines
9.8 KiB
Go
373 lines
9.8 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_test
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"git.cacert.org/cacert-gosigner/pkg/x509/revoking"
|
|
)
|
|
|
|
type testRepo struct {
|
|
crlNumber *big.Int
|
|
revoked []pkix.RevokedCertificate
|
|
}
|
|
|
|
func (t *testRepo) NextCRLNumber() (*big.Int, error) {
|
|
newNumber := new(big.Int).Add(t.crlNumber, big.NewInt(1))
|
|
|
|
t.crlNumber = newNumber
|
|
|
|
return t.crlNumber, nil
|
|
}
|
|
|
|
func (t *testRepo) RevokedCertificates() ([]pkix.RevokedCertificate, error) {
|
|
result := make([]pkix.RevokedCertificate, len(t.revoked))
|
|
|
|
copy(result, t.revoked)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (t *testRepo) StoreRevocation(revoked *pkix.RevokedCertificate) error {
|
|
t.revoked = append(t.revoked, *revoked)
|
|
|
|
return nil
|
|
}
|
|
|
|
type brokenRepo struct{}
|
|
|
|
func (r *brokenRepo) NextCRLNumber() (*big.Int, error) {
|
|
return nil, errors.New("don't know")
|
|
}
|
|
|
|
func (r *brokenRepo) RevokedCertificates() ([]pkix.RevokedCertificate, error) {
|
|
return nil, errors.New("no revocations for you")
|
|
}
|
|
|
|
func (*brokenRepo) StoreRevocation(_ *pkix.RevokedCertificate) error {
|
|
return errors.New("cannot store")
|
|
}
|
|
|
|
type brokenRepoNoCrlNumber struct {
|
|
}
|
|
|
|
func (b brokenRepoNoCrlNumber) StoreRevocation(_ *pkix.RevokedCertificate) error {
|
|
// do nothing
|
|
return nil
|
|
}
|
|
|
|
func (b brokenRepoNoCrlNumber) RevokedCertificates() ([]pkix.RevokedCertificate, error) {
|
|
return make([]pkix.RevokedCertificate, 0), nil
|
|
}
|
|
|
|
func (b brokenRepoNoCrlNumber) NextCRLNumber() (*big.Int, error) {
|
|
return nil, errors.New("don't know")
|
|
}
|
|
|
|
type brokenRepoNoRevocations struct {
|
|
}
|
|
|
|
func (b brokenRepoNoRevocations) StoreRevocation(_ *pkix.RevokedCertificate) error {
|
|
// do nothing
|
|
return nil
|
|
}
|
|
|
|
func (b brokenRepoNoRevocations) RevokedCertificates() ([]pkix.RevokedCertificate, error) {
|
|
return nil, errors.New("no revocations known")
|
|
}
|
|
|
|
func (b brokenRepoNoRevocations) NextCRLNumber() (*big.Int, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func randomSerial(t *testing.T) *big.Int {
|
|
t.Helper()
|
|
|
|
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
if err != nil {
|
|
t.Fatalf("could not generate random serial number: %v", err)
|
|
}
|
|
|
|
return serial
|
|
}
|
|
|
|
func TestX509Revoking_Revoke(t *testing.T) {
|
|
testRepository := testRepo{revoked: make([]pkix.RevokedCertificate, 0), crlNumber: big.NewInt(0)}
|
|
|
|
caKey, caCertificate := prepareTestCA(t)
|
|
|
|
r := revoking.NewX509Revoking(&testRepository, x509.ECDSAWithSHA256, caCertificate, caKey)
|
|
|
|
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
if err != nil {
|
|
t.Errorf("could not create random serial: %v", err)
|
|
}
|
|
|
|
revoke, err := r.Revoke(revoking.NewRevokeCertificate(serial, revoking.CRLReasonKeyCompromise))
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, revoking.CRLReasonKeyCompromise.BuildExtension(), revoke.Extensions[0])
|
|
assert.Equal(t, serial, revoke.SerialNumber)
|
|
|
|
var found bool
|
|
|
|
for _, r := range testRepository.revoked {
|
|
if r.SerialNumber.Cmp(serial) == 0 {
|
|
found = true
|
|
}
|
|
}
|
|
|
|
assert.True(t, found)
|
|
}
|
|
|
|
func TestX509Revoking_Revoke_BrokenRepo(t *testing.T) {
|
|
caKey, caCertificate := prepareTestCA(t)
|
|
|
|
r := revoking.NewX509Revoking(&brokenRepo{}, x509.SHA256WithRSA, caCertificate, caKey)
|
|
|
|
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
if err != nil {
|
|
t.Errorf("could not create random serial: %v", err)
|
|
}
|
|
|
|
revoke, err := r.Revoke(revoking.NewRevokeCertificate(serial, revoking.CRLReasonKeyCompromise))
|
|
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, "could not store revocation")
|
|
assert.Nil(t, revoke)
|
|
}
|
|
|
|
func TestX509Revoking_CreateCRL(t *testing.T) {
|
|
key, certificate := prepareTestCA(t)
|
|
|
|
r := revoking.NewX509Revoking(
|
|
&testRepo{revoked: make([]pkix.RevokedCertificate, 0), crlNumber: big.NewInt(0)},
|
|
x509.SHA256WithRSA,
|
|
certificate,
|
|
key,
|
|
)
|
|
|
|
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
if err != nil {
|
|
t.Errorf("could not create random serial: %v", err)
|
|
}
|
|
|
|
_, err = r.Revoke(revoking.NewRevokeCertificate(serial, revoking.CRLReasonKeyCompromise))
|
|
require.NoError(t, err)
|
|
|
|
crl, err := r.CreateCRL()
|
|
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, crl)
|
|
assert.NotEmpty(t, crl.CRL)
|
|
|
|
parsedCRL, err := x509.ParseCRL(crl.CRL)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.ElementsMatch(t, certificate.Subject.ToRDNSequence(), parsedCRL.TBSCertList.Issuer)
|
|
|
|
var found bool
|
|
|
|
for _, item := range parsedCRL.TBSCertList.RevokedCertificates {
|
|
if item.SerialNumber.Cmp(serial) == 0 {
|
|
found = true
|
|
|
|
assert.Contains(t, item.Extensions, revoking.CRLReasonKeyCompromise.BuildExtension())
|
|
}
|
|
}
|
|
|
|
assert.True(t, found)
|
|
}
|
|
|
|
func TestX509Revoking_CreateCRL_BrokenRepoNoRevocations(t *testing.T) {
|
|
caKey, caCertificate := prepareTestCA(t)
|
|
|
|
r := revoking.NewX509Revoking(&brokenRepoNoRevocations{}, x509.SHA256WithRSA, caCertificate, caKey)
|
|
|
|
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
if err != nil {
|
|
t.Errorf("could not create random serial: %v", err)
|
|
}
|
|
|
|
_, err = r.Revoke(revoking.NewRevokeCertificate(serial, revoking.CRLReasonKeyCompromise))
|
|
require.NoError(t, err)
|
|
|
|
crl, err := r.CreateCRL()
|
|
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, "could not get revocation information")
|
|
assert.Nil(t, crl)
|
|
}
|
|
|
|
func TestX509Revoking_CreateCRL_BrokenRepoNoCRLNumber(t *testing.T) {
|
|
caKey, caCertificate := prepareTestCA(t)
|
|
|
|
r := revoking.NewX509Revoking(&brokenRepoNoCrlNumber{}, x509.SHA256WithRSA, caCertificate, caKey)
|
|
|
|
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
if err != nil {
|
|
t.Errorf("could not create random serial: %v", err)
|
|
}
|
|
|
|
_, err = r.Revoke(revoking.NewRevokeCertificate(serial, revoking.CRLReasonKeyCompromise))
|
|
require.NoError(t, err)
|
|
|
|
crl, err := r.CreateCRL()
|
|
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, "could not get next CRL number")
|
|
assert.Nil(t, crl)
|
|
}
|
|
|
|
func TestX509Revoking_CreateCRL_WrongAlgorithm(t *testing.T) {
|
|
key, certificate := prepareTestCA(t)
|
|
|
|
r := revoking.NewX509Revoking(
|
|
&testRepo{revoked: make([]pkix.RevokedCertificate, 0), crlNumber: big.NewInt(0)},
|
|
x509.ECDSAWithSHA256,
|
|
certificate,
|
|
key,
|
|
)
|
|
|
|
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
if err != nil {
|
|
t.Errorf("could not create random serial: %v", err)
|
|
}
|
|
|
|
_, err = r.Revoke(revoking.NewRevokeCertificate(serial, revoking.CRLReasonKeyCompromise))
|
|
require.NoError(t, err)
|
|
|
|
crl, err := r.CreateCRL()
|
|
|
|
assert.Error(t, err)
|
|
assert.ErrorContains(t, err, "could not sign revocation list")
|
|
assert.Nil(t, crl)
|
|
}
|
|
|
|
func prepareTestCA(t *testing.T) (*rsa.PrivateKey, *x509.Certificate) {
|
|
t.Helper()
|
|
|
|
caKey, err := rsa.GenerateKey(rand.Reader, 3072)
|
|
if err != nil {
|
|
t.Fatalf("could not generate key pair: %v", err)
|
|
}
|
|
|
|
caTemplate := &x509.Certificate{
|
|
Subject: pkix.Name{CommonName: "Test CA"},
|
|
SerialNumber: randomSerial(t),
|
|
IsCA: true,
|
|
MaxPathLenZero: true,
|
|
BasicConstraintsValid: true,
|
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
|
}
|
|
|
|
certificateBytes, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, caKey.Public(), caKey)
|
|
if err != nil {
|
|
t.Fatalf("could not self-sign CA certificate: %v", err)
|
|
}
|
|
|
|
caCertificate, err := x509.ParseCertificate(certificateBytes)
|
|
if err != nil {
|
|
t.Fatalf("could not create test CA certificate: %v", err)
|
|
}
|
|
|
|
return caKey, caCertificate
|
|
}
|
|
|
|
func TestCRLReason_BuildExtension(t *testing.T) {
|
|
reasons := []revoking.CRLReason{
|
|
revoking.CRLReasonUnspecified,
|
|
revoking.CRLReasonKeyCompromise,
|
|
revoking.CRLReasonCACompromise,
|
|
revoking.CRLReasonAffiliationChanged,
|
|
revoking.CRLReasonSuperseded,
|
|
revoking.CRLReasonCessationOfOperation,
|
|
revoking.CRLReasonCertificateHold,
|
|
revoking.CRLReasonRemoveFromCRL,
|
|
revoking.CRLReasonPrivilegeWithdrawn,
|
|
revoking.CRLReasonAACompromise,
|
|
}
|
|
|
|
for _, reason := range reasons {
|
|
t.Run(fmt.Sprintf("reason %d", int(reason)), func(t *testing.T) {
|
|
ext := reason.BuildExtension()
|
|
|
|
assert.Equal(t, revoking.OidCRLReason, ext.Id)
|
|
|
|
var value int
|
|
|
|
rest, err := asn1.Unmarshal(ext.Value, &value)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, rest, 0)
|
|
assert.Equal(t, value, int(reason))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCRLReason_String(t *testing.T) {
|
|
reasons := []revoking.CRLReason{
|
|
revoking.CRLReasonUnspecified,
|
|
revoking.CRLReasonKeyCompromise,
|
|
revoking.CRLReasonCACompromise,
|
|
revoking.CRLReasonAffiliationChanged,
|
|
revoking.CRLReasonSuperseded,
|
|
revoking.CRLReasonCessationOfOperation,
|
|
revoking.CRLReasonCertificateHold,
|
|
revoking.CRLReasonRemoveFromCRL,
|
|
revoking.CRLReasonPrivilegeWithdrawn,
|
|
revoking.CRLReasonAACompromise,
|
|
}
|
|
|
|
for _, reason := range reasons {
|
|
t.Run(fmt.Sprintf("reason %d", int(reason)), func(t *testing.T) {
|
|
assert.NotEmpty(t, reason.String())
|
|
})
|
|
}
|
|
|
|
var customReason revoking.CRLReason = 40
|
|
|
|
assert.Equal(t, customReason.String(), revoking.CRLReasonUnspecified.String())
|
|
}
|
|
|
|
func TestParseReason(t *testing.T) {
|
|
expected := map[string]revoking.CRLReason{
|
|
"keyCompromise": revoking.CRLReasonKeyCompromise,
|
|
"kEyCoMpRoMiSe": revoking.CRLReasonKeyCompromise,
|
|
"unknown": revoking.CRLReasonUnspecified,
|
|
"": revoking.CRLReasonUnspecified,
|
|
}
|
|
|
|
for key, reason := range expected {
|
|
t.Run(fmt.Sprintf("parse '%s'", key), func(t *testing.T) {
|
|
assert.Equal(t, revoking.ParseReason(key), reason)
|
|
})
|
|
}
|
|
}
|