2023-03-15 16:00:52 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2022-05-11 17:04:54 +00:00
|
|
|
package pki
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto"
|
|
|
|
"crypto/rand"
|
|
|
|
"crypto/rsa"
|
|
|
|
"crypto/x509"
|
|
|
|
"crypto/x509/pkix"
|
2023-01-30 21:38:38 +00:00
|
|
|
"encoding/asn1"
|
2022-05-11 17:04:54 +00:00
|
|
|
"encoding/pem"
|
|
|
|
"fmt"
|
2022-08-31 20:25:14 +00:00
|
|
|
"io"
|
2023-01-30 21:38:38 +00:00
|
|
|
"math"
|
|
|
|
"math/big"
|
2022-05-11 17:04:54 +00:00
|
|
|
"strings"
|
|
|
|
"testing"
|
2023-01-30 21:38:38 +00:00
|
|
|
"time"
|
2022-05-11 17:04:54 +00:00
|
|
|
|
|
|
|
"github.com/hashicorp/vault/api"
|
|
|
|
"github.com/hashicorp/vault/sdk/helper/certutil"
|
|
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Setup helpers
|
2022-11-14 23:26:26 +00:00
|
|
|
func CreateBackendWithStorage(t testing.TB) (*backend, logical.Storage) {
|
2023-01-27 17:29:11 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2022-05-11 17:04:54 +00:00
|
|
|
config := logical.TestBackendConfig()
|
|
|
|
config.StorageView = &logical.InmemStorage{}
|
|
|
|
|
|
|
|
var err error
|
|
|
|
b := Backend(config)
|
|
|
|
err = b.Setup(context.Background(), config)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
// Assume for our tests we have performed the migration already.
|
|
|
|
b.pkiStorageVersion.Store(1)
|
|
|
|
return b, config.StorageView
|
|
|
|
}
|
|
|
|
|
2022-05-11 17:29:57 +00:00
|
|
|
func mountPKIEndpoint(t testing.TB, client *api.Client, path string) {
|
2023-01-27 17:29:11 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2022-08-31 20:25:14 +00:00
|
|
|
err := client.Sys().Mount(path, &api.MountInput{
|
2022-05-11 17:04:54 +00:00
|
|
|
Type: "pki",
|
|
|
|
Config: api.MountConfigInput{
|
|
|
|
DefaultLeaseTTL: "16h",
|
|
|
|
MaxLeaseTTL: "32h",
|
|
|
|
},
|
|
|
|
})
|
|
|
|
require.NoError(t, err, "failed mounting pki endpoint")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Signing helpers
|
2022-10-03 16:39:54 +00:00
|
|
|
func requireSignedBy(t *testing.T, cert *x509.Certificate, signingCert *x509.Certificate) {
|
2023-01-27 17:29:11 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2022-10-03 16:39:54 +00:00
|
|
|
if err := cert.CheckSignatureFrom(signingCert); err != nil {
|
|
|
|
t.Fatalf("signature verification failed: %v", err)
|
2022-05-11 17:04:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Certificate helper
|
|
|
|
func parseCert(t *testing.T, pemCert string) *x509.Certificate {
|
2023-01-27 17:29:11 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2022-05-11 17:04:54 +00:00
|
|
|
block, _ := pem.Decode([]byte(pemCert))
|
|
|
|
require.NotNil(t, block, "failed to decode PEM block")
|
|
|
|
|
|
|
|
cert, err := x509.ParseCertificate(block.Bytes)
|
|
|
|
require.NoError(t, err)
|
|
|
|
return cert
|
|
|
|
}
|
|
|
|
|
|
|
|
func requireMatchingPublicKeys(t *testing.T, cert *x509.Certificate, key crypto.PublicKey) {
|
2023-01-27 17:29:11 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2022-05-11 17:04:54 +00:00
|
|
|
certPubKey := cert.PublicKey
|
|
|
|
areEqual, err := certutil.ComparePublicKeysAndType(certPubKey, key)
|
|
|
|
require.NoError(t, err, "failed comparing public keys: %#v", err)
|
|
|
|
require.True(t, areEqual, "public keys mismatched: got: %v, expected: %v", certPubKey, key)
|
|
|
|
}
|
|
|
|
|
|
|
|
func getSelfSigned(t *testing.T, subject, issuer *x509.Certificate, key *rsa.PrivateKey) (string, *x509.Certificate) {
|
|
|
|
t.Helper()
|
|
|
|
selfSigned, err := x509.CreateCertificate(rand.Reader, subject, issuer, key.Public(), key)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
cert, err := x509.ParseCertificate(selfSigned)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
pemSS := strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{
|
|
|
|
Type: "CERTIFICATE",
|
|
|
|
Bytes: selfSigned,
|
|
|
|
})))
|
|
|
|
return pemSS, cert
|
|
|
|
}
|
|
|
|
|
|
|
|
// CRL related helpers
|
|
|
|
func getCrlCertificateList(t *testing.T, client *api.Client, mountPoint string) pkix.TBSCertificateList {
|
2023-01-27 17:29:11 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2022-05-11 17:04:54 +00:00
|
|
|
path := fmt.Sprintf("/v1/%s/crl", mountPoint)
|
|
|
|
return getParsedCrlAtPath(t, client, path).TBSCertList
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseCrlPemBytes(t *testing.T, crlPem []byte) pkix.TBSCertificateList {
|
2023-01-27 17:29:11 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2022-05-11 17:04:54 +00:00
|
|
|
certList, err := x509.ParseCRL(crlPem)
|
|
|
|
require.NoError(t, err)
|
|
|
|
return certList.TBSCertList
|
|
|
|
}
|
|
|
|
|
2022-05-13 13:57:58 +00:00
|
|
|
func requireSerialNumberInCRL(t *testing.T, revokeList pkix.TBSCertificateList, serialNum string) bool {
|
2023-01-27 17:29:11 +00:00
|
|
|
if t != nil {
|
|
|
|
t.Helper()
|
|
|
|
}
|
|
|
|
|
2022-05-11 17:04:54 +00:00
|
|
|
serialsInList := make([]string, 0, len(revokeList.RevokedCertificates))
|
|
|
|
for _, revokeEntry := range revokeList.RevokedCertificates {
|
|
|
|
formattedSerial := certutil.GetHexFormatted(revokeEntry.SerialNumber.Bytes(), ":")
|
|
|
|
serialsInList = append(serialsInList, formattedSerial)
|
|
|
|
if formattedSerial == serialNum {
|
2022-05-13 13:57:58 +00:00
|
|
|
return true
|
2022-05-11 17:04:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-13 13:57:58 +00:00
|
|
|
if t != nil {
|
|
|
|
t.Fatalf("the serial number %s, was not found in the CRL list containing: %v", serialNum, serialsInList)
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
2022-05-11 17:04:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func getParsedCrl(t *testing.T, client *api.Client, mountPoint string) *pkix.CertificateList {
|
2023-01-27 17:29:11 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2022-05-11 17:04:54 +00:00
|
|
|
path := fmt.Sprintf("/v1/%s/crl", mountPoint)
|
|
|
|
return getParsedCrlAtPath(t, client, path)
|
|
|
|
}
|
|
|
|
|
|
|
|
func getParsedCrlAtPath(t *testing.T, client *api.Client, path string) *pkix.CertificateList {
|
2023-01-27 17:29:11 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2022-05-11 17:04:54 +00:00
|
|
|
req := client.NewRequest("GET", path)
|
|
|
|
resp, err := client.RawRequest(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
2022-08-31 20:25:14 +00:00
|
|
|
crlBytes, err := io.ReadAll(resp.Body)
|
2022-05-11 17:04:54 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %s", err)
|
|
|
|
}
|
|
|
|
if len(crlBytes) == 0 {
|
|
|
|
t.Fatalf("expected CRL in response body")
|
|
|
|
}
|
|
|
|
|
|
|
|
crl, err := x509.ParseDERCRL(crlBytes)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
return crl
|
|
|
|
}
|
2022-06-16 13:11:22 +00:00
|
|
|
|
|
|
|
func getParsedCrlFromBackend(t *testing.T, b *backend, s logical.Storage, path string) *pkix.CertificateList {
|
2023-01-27 17:29:11 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2022-06-16 13:11:22 +00:00
|
|
|
resp, err := CBRead(b, s, path)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
crl, err := x509.ParseDERCRL(resp.Data[logical.HTTPRawBody].([]byte))
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
return crl
|
|
|
|
}
|
|
|
|
|
|
|
|
// Direct storage backend helpers (b, s := createBackendWithStorage(t)) which
|
|
|
|
// are mostly compatible with client.Logical() operations. The main difference
|
|
|
|
// is that the JSON round-tripping hasn't occurred, so values are as the
|
|
|
|
// backend returns them (e.g., []string instead of []interface{}).
|
|
|
|
func CBReq(b *backend, s logical.Storage, operation logical.Operation, path string, data map[string]interface{}) (*logical.Response, error) {
|
|
|
|
resp, err := b.HandleRequest(context.Background(), &logical.Request{
|
|
|
|
Operation: operation,
|
|
|
|
Path: path,
|
|
|
|
Data: data,
|
|
|
|
Storage: s,
|
|
|
|
MountPoint: "pki/",
|
|
|
|
})
|
|
|
|
if err != nil || resp == nil {
|
|
|
|
return resp, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if msg, ok := resp.Data["error"]; ok && msg != nil && len(msg.(string)) > 0 {
|
|
|
|
return resp, fmt.Errorf("%s", msg)
|
|
|
|
}
|
|
|
|
|
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func CBRead(b *backend, s logical.Storage, path string) (*logical.Response, error) {
|
|
|
|
return CBReq(b, s, logical.ReadOperation, path, make(map[string]interface{}))
|
|
|
|
}
|
|
|
|
|
|
|
|
func CBWrite(b *backend, s logical.Storage, path string, data map[string]interface{}) (*logical.Response, error) {
|
|
|
|
return CBReq(b, s, logical.UpdateOperation, path, data)
|
|
|
|
}
|
|
|
|
|
Add PSS support to PKI Secrets Engine (#16519)
* Add PSS signature support to Vault PKI engine
Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
* Use issuer's RevocationSigAlg for CRL signing
We introduce a new parameter on issuers, revocation_signature_algorithm
to control the signature algorithm used during CRL signing. This is
because the SignatureAlgorithm value from the certificate itself is
incorrect for this purpose: a RSA root could sign an ECDSA intermediate
with say, SHA256WithRSA, but when the intermediate goes to sign a CRL,
it must use ECDSAWithSHA256 or equivalent instead of SHA256WithRSA. When
coupled with support for PSS-only keys, allowing the user to set the
signature algorithm value as desired seems like the best approach.
Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
* Add use_pss, revocation_signature_algorithm docs
Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
* Add PSS to signature role issuance test matrix
Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
* Add changelog
Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
* Allow roots to self-identify revocation alg
When using PSS support with a managed key, sometimes the underlying
device will not support PKCS#1v1.5 signatures. This results in CRL
building failing, unless we update the entry's signature algorithm
prior to building the CRL for the new root.
With a RSA-type key and use_pss=true, we use the signature bits value to
decide which hash function to use for PSS support.
Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
* Add clearer error message on failed import
When CRL building fails during cert/key import, due to PSS failures,
give a better indication to the user that import succeeded its just CRL
building that failed. This tells them the parameter to adjust on the
issuer and warns that CRL building will fail until this is fixed.
Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
* Add case insensitive SigAlgo matching
Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
* Convert UsePSS back to regular bool
Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
* Refactor PSS->certTemplate into helper function
Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
* Proper string output on rev_sig_alg display
Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
* Copy root's SignatureAlgorithm for CRL building
Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
2022-08-03 16:42:24 +00:00
|
|
|
func CBPatch(b *backend, s logical.Storage, path string, data map[string]interface{}) (*logical.Response, error) {
|
|
|
|
return CBReq(b, s, logical.PatchOperation, path, data)
|
|
|
|
}
|
|
|
|
|
2022-06-16 13:11:22 +00:00
|
|
|
func CBList(b *backend, s logical.Storage, path string) (*logical.Response, error) {
|
|
|
|
return CBReq(b, s, logical.ListOperation, path, make(map[string]interface{}))
|
|
|
|
}
|
|
|
|
|
|
|
|
func CBDelete(b *backend, s logical.Storage, path string) (*logical.Response, error) {
|
|
|
|
return CBReq(b, s, logical.DeleteOperation, path, make(map[string]interface{}))
|
|
|
|
}
|
2022-08-22 18:06:15 +00:00
|
|
|
|
|
|
|
func requireFieldsSetInResp(t *testing.T, resp *logical.Response, fields ...string) {
|
2023-01-27 17:29:11 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2022-08-22 18:06:15 +00:00
|
|
|
var missingFields []string
|
|
|
|
for _, field := range fields {
|
|
|
|
value, ok := resp.Data[field]
|
|
|
|
if !ok || value == nil {
|
|
|
|
missingFields = append(missingFields, field)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
require.Empty(t, missingFields, "The following fields were required but missing from response:\n%v", resp.Data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func requireSuccessNonNilResponse(t *testing.T, resp *logical.Response, err error, msgAndArgs ...interface{}) {
|
2023-01-27 17:29:11 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2022-08-22 18:06:15 +00:00
|
|
|
require.NoError(t, err, msgAndArgs...)
|
2022-11-17 21:53:05 +00:00
|
|
|
if resp.IsError() {
|
|
|
|
errContext := fmt.Sprintf("Expected successful response but got error: %v", resp.Error())
|
|
|
|
require.Falsef(t, resp.IsError(), errContext, msgAndArgs...)
|
|
|
|
}
|
2022-08-22 18:06:15 +00:00
|
|
|
require.NotNil(t, resp, msgAndArgs...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func requireSuccessNilResponse(t *testing.T, resp *logical.Response, err error, msgAndArgs ...interface{}) {
|
2023-01-27 17:29:11 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2022-08-22 18:06:15 +00:00
|
|
|
require.NoError(t, err, msgAndArgs...)
|
2022-11-17 21:53:05 +00:00
|
|
|
if resp.IsError() {
|
|
|
|
errContext := fmt.Sprintf("Expected successful response but got error: %v", resp.Error())
|
|
|
|
require.Falsef(t, resp.IsError(), errContext, msgAndArgs...)
|
|
|
|
}
|
|
|
|
if resp != nil {
|
|
|
|
msg := fmt.Sprintf("expected nil response but got: %v", resp)
|
|
|
|
require.Nilf(t, resp, msg, msgAndArgs...)
|
|
|
|
}
|
2022-08-22 18:06:15 +00:00
|
|
|
}
|
2023-01-30 21:38:38 +00:00
|
|
|
|
|
|
|
func getCRLNumber(t *testing.T, crl pkix.TBSCertificateList) int {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
for _, extension := range crl.Extensions {
|
|
|
|
if extension.Id.Equal(certutil.CRLNumberOID) {
|
|
|
|
bigInt := new(big.Int)
|
|
|
|
leftOver, err := asn1.Unmarshal(extension.Value, &bigInt)
|
|
|
|
require.NoError(t, err, "Failed unmarshalling crl number extension")
|
|
|
|
require.Empty(t, leftOver, "leftover bytes from unmarshalling crl number extension")
|
|
|
|
require.True(t, bigInt.IsInt64(), "parsed crl number integer is not an int64")
|
|
|
|
require.False(t, math.MaxInt <= bigInt.Int64(), "parsed crl number integer can not fit in an int")
|
|
|
|
return int(bigInt.Int64())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
t.Fatalf("failed to find crl number extension")
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
func getCrlReferenceFromDelta(t *testing.T, crl pkix.TBSCertificateList) int {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
for _, extension := range crl.Extensions {
|
|
|
|
if extension.Id.Equal(certutil.DeltaCRLIndicatorOID) {
|
|
|
|
bigInt := new(big.Int)
|
|
|
|
leftOver, err := asn1.Unmarshal(extension.Value, &bigInt)
|
|
|
|
require.NoError(t, err, "Failed unmarshalling delta crl indicator extension")
|
|
|
|
require.Empty(t, leftOver, "leftover bytes from unmarshalling delta crl indicator extension")
|
|
|
|
require.True(t, bigInt.IsInt64(), "parsed delta crl integer is not an int64")
|
|
|
|
require.False(t, math.MaxInt <= bigInt.Int64(), "parsed delta crl integer can not fit in an int")
|
|
|
|
return int(bigInt.Int64())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
t.Fatalf("failed to find delta crl indicator extension")
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
2023-02-01 13:47:26 +00:00
|
|
|
// waitForUpdatedCrl will wait until the CRL at the provided path has been reloaded
|
|
|
|
// up for a maxWait duration and gives up if the timeout has been reached. If a negative
|
|
|
|
// value for lastSeenCRLNumber is provided, the method will load the current CRL and wait
|
|
|
|
// for a newer CRL be generated.
|
|
|
|
func waitForUpdatedCrl(t *testing.T, client *api.Client, crlPath string, lastSeenCRLNumber int, maxWait time.Duration) pkix.TBSCertificateList {
|
2023-01-30 21:38:38 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2023-02-01 13:47:26 +00:00
|
|
|
newCrl, didTimeOut := waitForUpdatedCrlUntil(t, client, crlPath, lastSeenCRLNumber, maxWait)
|
|
|
|
if didTimeOut {
|
|
|
|
t.Fatalf("Timed out waiting for new CRL rebuild on path %s", crlPath)
|
|
|
|
}
|
|
|
|
return newCrl.TBSCertList
|
|
|
|
}
|
|
|
|
|
|
|
|
// waitForUpdatedCrlUntil is a helper method that will wait for a CRL to be updated up until maxWait duration
|
|
|
|
// or give up and return the last CRL it loaded. It will not fail, if it does not see a new CRL within the
|
|
|
|
// max duration unlike waitForUpdatedCrl. Returns the last loaded CRL at the provided path and a boolean
|
|
|
|
// indicating if we hit maxWait duration or not.
|
|
|
|
func waitForUpdatedCrlUntil(t *testing.T, client *api.Client, crlPath string, lastSeenCrlNumber int, maxWait time.Duration) (*pkix.CertificateList, bool) {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
crl := getParsedCrlAtPath(t, client, crlPath)
|
|
|
|
initialCrlRevision := getCRLNumber(t, crl.TBSCertList)
|
|
|
|
newCrlRevision := initialCrlRevision
|
|
|
|
|
|
|
|
// Short circuit the fetches if we have a version of the CRL we want
|
|
|
|
if lastSeenCrlNumber > 0 && getCRLNumber(t, crl.TBSCertList) > lastSeenCrlNumber {
|
|
|
|
return crl, false
|
|
|
|
}
|
|
|
|
|
|
|
|
start := time.Now()
|
|
|
|
iteration := 0
|
2023-01-30 21:38:38 +00:00
|
|
|
for {
|
2023-02-01 13:47:26 +00:00
|
|
|
iteration++
|
|
|
|
|
|
|
|
if time.Since(start) > maxWait {
|
|
|
|
t.Logf("Timed out waiting for new CRL on path %s after iteration %d, delay: %v",
|
|
|
|
crlPath, iteration, time.Now().Sub(start))
|
|
|
|
return crl, true
|
2023-01-30 21:38:38 +00:00
|
|
|
}
|
2023-02-01 13:47:26 +00:00
|
|
|
|
|
|
|
crl = getParsedCrlAtPath(t, client, crlPath)
|
|
|
|
newCrlRevision = getCRLNumber(t, crl.TBSCertList)
|
|
|
|
if newCrlRevision > initialCrlRevision {
|
|
|
|
t.Logf("Got new revision of CRL %s from %d to %d after iteration %d, delay %v",
|
|
|
|
crlPath, initialCrlRevision, newCrlRevision, iteration, time.Now().Sub(start))
|
|
|
|
return crl, false
|
|
|
|
}
|
|
|
|
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
2023-01-30 21:38:38 +00:00
|
|
|
}
|
|
|
|
}
|
2023-02-03 19:38:36 +00:00
|
|
|
|
|
|
|
// A quick CRL to string to provide better test error messages
|
|
|
|
func summarizeCrl(t *testing.T, crl pkix.TBSCertificateList) string {
|
|
|
|
version := getCRLNumber(t, crl)
|
|
|
|
serials := []string{}
|
|
|
|
for _, cert := range crl.RevokedCertificates {
|
|
|
|
serials = append(serials, normalizeSerialFromBigInt(cert.SerialNumber))
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("CRL Version: %d\n"+
|
|
|
|
"This Update: %s\n"+
|
|
|
|
"Next Update: %s\n"+
|
|
|
|
"Revoked Serial Count: %d\n"+
|
|
|
|
"Revoked Serials: %v", version, crl.ThisUpdate, crl.NextUpdate, len(serials), serials)
|
|
|
|
}
|