Configure golangci-lint and fix warnings
This commit is contained in:
parent
ecd1846975
commit
2e467b3d2e
20 changed files with 915 additions and 559 deletions
987
signer/x509ops/x509ops.go
Normal file
987
signer/x509ops/x509ops.go
Normal file
|
@ -0,0 +1,987 @@
|
|||
package x509ops
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1" // #nosec G505 needed for protocol version 1
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/longsleep/pkac"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"git.cacert.org/cacert-gosigner/shared"
|
||||
)
|
||||
|
||||
const crlLifetime = time.Hour * 24 * 7
|
||||
|
||||
const (
|
||||
pemTypeCertificate = "CERTIFICATE"
|
||||
pemTypeCertificateRequest = "CERTIFICATE REQUEST"
|
||||
pemTypePrivateKey = "PRIVATE KEY"
|
||||
)
|
||||
|
||||
var (
|
||||
oidPkcs9EmailAddress = []int{1, 2, 840, 113549, 1, 9, 1}
|
||||
)
|
||||
|
||||
type Root struct {
|
||||
Name string
|
||||
privateKey crypto.Signer
|
||||
certificate *x509.Certificate
|
||||
databaseFile string
|
||||
serialNumberFile string
|
||||
crlNumberFile string
|
||||
crlFileName string
|
||||
crlHashDir string
|
||||
crlDistributionPoints []string
|
||||
ocspServers []string
|
||||
}
|
||||
|
||||
func (x *Root) String() string {
|
||||
return x.Name
|
||||
}
|
||||
|
||||
func loadCertificate(certificateFile string) (*x509.Certificate, error) {
|
||||
pemBytes, err := ioutil.ReadFile(certificateFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"could not load certificate %s: %w",
|
||||
certificateFile,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
pemBlock, _ := pem.Decode(pemBytes)
|
||||
if pemBlock.Type != pemTypeCertificate {
|
||||
log.Warnf(
|
||||
"PEM in %s is probably not a certificate. PEM block has type %s",
|
||||
certificateFile,
|
||||
pemBlock.Type,
|
||||
)
|
||||
}
|
||||
|
||||
certificate, err := x509.ParseCertificate(pemBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"could no parse certificate from %s: %w",
|
||||
certificateFile,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return certificate, nil
|
||||
}
|
||||
|
||||
func loadPrivateKey(filename string) (crypto.Signer, error) {
|
||||
pemBytes, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"could not load private key %s: %w",
|
||||
filename,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
pemBlock, _ := pem.Decode(pemBytes)
|
||||
if pemBlock == nil {
|
||||
return nil, fmt.Errorf("no PEM data found in %s", filename)
|
||||
}
|
||||
|
||||
if pemBlock.Type != pemTypePrivateKey {
|
||||
log.Warnf(
|
||||
"PEM in %s is probably not a private key. PEM block has type %s",
|
||||
filename,
|
||||
pemBlock.Type,
|
||||
)
|
||||
}
|
||||
|
||||
privateKey, err := x509.ParsePKCS8PrivateKey(pemBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"could no parse private key from %s: %w",
|
||||
filename,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return privateKey.(*rsa.PrivateKey), nil
|
||||
}
|
||||
|
||||
func (x *Root) getNextSerialNumber() (*big.Int, error) {
|
||||
// TODO: decide whether we should use 64 bit random serial numbers as
|
||||
// recommended by CAB forum baseline requirements
|
||||
serialNumberFile := x.serialNumberFile
|
||||
|
||||
_, err := os.Stat(serialNumberFile)
|
||||
if err != nil {
|
||||
log.Warnf("serial number file %s does not exist: %v", x.serialNumberFile, err)
|
||||
|
||||
return big.NewInt(1), nil
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(x.serialNumberFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read serial number file %s: %w", x.serialNumberFile, err)
|
||||
}
|
||||
|
||||
result, err := stringAsBigInt(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse content of %s as serial number: %w", x.serialNumberFile, err)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (x *Root) getNextCRLNumber() (*big.Int, error) {
|
||||
_, err := os.Stat(x.crlNumberFile)
|
||||
if err != nil {
|
||||
log.Warnf("CRL number file %s does not exist: %v", x.crlNumberFile, err)
|
||||
|
||||
return big.NewInt(1), nil
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(x.crlNumberFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read CRL number file %s: %w", x.crlNumberFile, err)
|
||||
}
|
||||
|
||||
result, err := stringAsBigInt(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse content of %s as CRL number: %w", x.crlNumberFile, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (x *Root) bumpCRLNumber(current *big.Int) error {
|
||||
serial := current.Int64() + 1
|
||||
crlNumberFile := x.crlNumberFile
|
||||
|
||||
outFile, err := ioutil.TempFile(path.Dir(crlNumberFile), "*.txt")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create temporary crl number file: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = outFile.Close() }()
|
||||
|
||||
_, err = outFile.WriteString(fmt.Sprintf(
|
||||
"%s\n",
|
||||
strings.ToUpper(strconv.FormatInt(serial, 16)),
|
||||
))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not write new CRL number %d to %s: %w", serial, outFile.Name(), err)
|
||||
}
|
||||
|
||||
if err = outFile.Close(); err != nil {
|
||||
return fmt.Errorf("could not close temporary file %s: %w", outFile.Name(), err)
|
||||
}
|
||||
|
||||
if err = os.Rename(crlNumberFile, fmt.Sprintf("%s.old", crlNumberFile)); err != nil {
|
||||
return fmt.Errorf("could not rename %s to %s.old: %w", crlNumberFile, crlNumberFile, err)
|
||||
}
|
||||
|
||||
if err = os.Rename(outFile.Name(), crlNumberFile); err != nil {
|
||||
return fmt.Errorf("could not rename %s to %s: %w", outFile.Name(), crlNumberFile, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Root) bumpSerialNumber(current *big.Int) error {
|
||||
serial := current.Int64() + 1
|
||||
|
||||
outFile, err := ioutil.TempFile(path.Dir(x.serialNumberFile), "*.txt")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open temporary serial number file: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = outFile.Close() }()
|
||||
|
||||
_, err = outFile.WriteString(fmt.Sprintf(
|
||||
"%s\n",
|
||||
strings.ToUpper(strconv.FormatInt(serial, 16)),
|
||||
))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not write new serial number %d to %s: %w", serial, outFile.Name(), err)
|
||||
}
|
||||
|
||||
if err = outFile.Close(); err != nil {
|
||||
return fmt.Errorf("could not close temporary file %s: %w", outFile.Name(), err)
|
||||
}
|
||||
|
||||
if _, err = os.Stat(x.serialNumberFile); err == nil {
|
||||
if err = os.Rename(x.serialNumberFile, fmt.Sprintf("%s.old", x.serialNumberFile)); err != nil {
|
||||
return fmt.Errorf("could not rename %s to %s.old: %w", x.serialNumberFile, x.serialNumberFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = os.Rename(outFile.Name(), x.serialNumberFile); err != nil {
|
||||
return fmt.Errorf("could not rename %s to %s: %w", outFile.Name(), x.serialNumberFile, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Root) loadRevokedCertificatesFromDatabase() ([]pkix.RevokedCertificate, error) {
|
||||
databaseFile := x.databaseFile
|
||||
|
||||
_, err := os.Stat(databaseFile)
|
||||
if err != nil {
|
||||
log.Warnf("openssl certificate database file %s does not exist: %v", databaseFile, err)
|
||||
|
||||
return []pkix.RevokedCertificate{}, nil
|
||||
}
|
||||
|
||||
file, err := os.Open(databaseFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open openssl certificate database file %s: %w", databaseFile, err)
|
||||
}
|
||||
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
result := make([]pkix.RevokedCertificate, 0)
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.Split(scanner.Text(), "\t")
|
||||
if line[0] == "R" {
|
||||
serialNumber, err := stringAsBigInt([]byte(line[3]))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse serial number %s as big int: %w", line[3], err)
|
||||
}
|
||||
|
||||
revokeTs, err := strconv.ParseInt(line[2][:len(line[2])-1], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse serial number: %w", err)
|
||||
}
|
||||
|
||||
result = append(result, pkix.RevokedCertificate{
|
||||
SerialNumber: serialNumber,
|
||||
RevocationTime: time.Unix(revokeTs, 0),
|
||||
Extensions: nil,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (x *Root) recordRevocation(certificate *x509.Certificate) (*pkix.RevokedCertificate, error) {
|
||||
_, err := os.Stat(x.databaseFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("openssl certificate database file %s does not exist: %w", x.databaseFile, err)
|
||||
}
|
||||
|
||||
inFile, err := os.Open(x.databaseFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open openssl certificate database file %s: %w", x.databaseFile, err)
|
||||
}
|
||||
|
||||
defer func() { _ = inFile.Close() }()
|
||||
|
||||
outFile, err := ioutil.TempFile(path.Dir(x.databaseFile), "*.txt")
|
||||
|
||||
defer func() { _ = outFile.Close() }()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open temporary database file: %w", err)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(inFile)
|
||||
writer := bufio.NewWriter(outFile)
|
||||
|
||||
found := false
|
||||
revocationTime := time.Now()
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
parts := strings.Split(line, "\t")
|
||||
|
||||
serialNumber, err := stringAsBigInt([]byte(parts[3]))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse serial number %s as big int: %w", parts[3], err)
|
||||
}
|
||||
|
||||
if serialNumber == certificate.SerialNumber {
|
||||
line = strings.Join(
|
||||
[]string{"R", parts[1], strconv.FormatInt(revocationTime.Unix(), 10) + "Z", parts[3], parts[4]},
|
||||
"\t",
|
||||
)
|
||||
found = true
|
||||
}
|
||||
|
||||
if _, err = writer.WriteString(fmt.Sprintf("%s\n", line)); err != nil {
|
||||
return nil, fmt.Errorf("could not write '%s' to %s: %w", line, outFile.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = outFile.Close(); err != nil {
|
||||
return nil, fmt.Errorf("could not close temporary file %s: %w", outFile.Name(), err)
|
||||
}
|
||||
|
||||
if err = inFile.Close(); err != nil {
|
||||
return nil, fmt.Errorf("could not close %s: %w", x.databaseFile, err)
|
||||
}
|
||||
|
||||
if err = os.Rename(x.databaseFile, fmt.Sprintf("%s.old", x.databaseFile)); err != nil {
|
||||
return nil, fmt.Errorf("could not rename %s to %s.old: %w", x.databaseFile, x.databaseFile, err)
|
||||
}
|
||||
|
||||
if err = os.Rename(outFile.Name(), x.databaseFile); err != nil {
|
||||
return nil, fmt.Errorf("could not rename temporary file %s to %s: %w", outFile.Name(), x.databaseFile, err)
|
||||
}
|
||||
|
||||
if !found {
|
||||
log.Warnf("entry not found in database")
|
||||
}
|
||||
|
||||
return &pkix.RevokedCertificate{
|
||||
SerialNumber: certificate.SerialNumber,
|
||||
RevocationTime: revocationTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (x *Root) RevokeCertificate(request []byte) (*pkix.RevokedCertificate, error) {
|
||||
pemBlock, _ := pem.Decode(request)
|
||||
if pemBlock.Type != pemTypeCertificate {
|
||||
log.Warnf(
|
||||
"PEM structure is probably not a certificate. PEM block has type %s",
|
||||
pemBlock.Type,
|
||||
)
|
||||
log.Trace(request)
|
||||
}
|
||||
|
||||
certificate, err := x509.ParseCertificate(pemBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"could no parse certificate: %w",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return x.recordRevocation(certificate)
|
||||
}
|
||||
|
||||
func (x *Root) GenerateCrl(algorithm x509.SignatureAlgorithm) ([]byte, *[20]byte, error) {
|
||||
certificatesToRevoke, err := x.loadRevokedCertificatesFromDatabase()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
nextCrlNumber, err := x.getNextCRLNumber()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
crlTemplate := &x509.RevocationList{
|
||||
SignatureAlgorithm: algorithm,
|
||||
RevokedCertificates: certificatesToRevoke,
|
||||
Number: nextCrlNumber,
|
||||
ThisUpdate: time.Now(),
|
||||
NextUpdate: time.Now().Add(crlLifetime),
|
||||
ExtraExtensions: nil,
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err = x.bumpCRLNumber(nextCrlNumber); err != nil {
|
||||
log.Errorf("could not bump CRL number: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
crlBytes, err := x509.CreateRevocationList(
|
||||
rand.Reader,
|
||||
crlTemplate,
|
||||
x.certificate,
|
||||
x.privateKey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not create new CRL: %w", err)
|
||||
}
|
||||
|
||||
if err = ioutil.WriteFile(x.crlFileName, crlBytes, 0600); err != nil {
|
||||
return nil, nil, fmt.Errorf("could not write new CRL to %s: %w", x.crlFileName, err)
|
||||
}
|
||||
|
||||
// sha1 is implied by protocol version 1
|
||||
// #nosec G401
|
||||
newCrlHash := sha1.Sum(crlBytes)
|
||||
hashedCrlFileName := path.Join(
|
||||
x.crlHashDir,
|
||||
fmt.Sprintf("%s.crl", hex.EncodeToString(newCrlHash[:])),
|
||||
)
|
||||
|
||||
if err = ioutil.WriteFile(hashedCrlFileName, crlBytes, 0600); err != nil {
|
||||
return nil, nil, fmt.Errorf("could not write new CRL to %s: %w", hashedCrlFileName, err)
|
||||
}
|
||||
|
||||
return crlBytes, &newCrlHash, nil
|
||||
}
|
||||
|
||||
func (x *Root) DeleteOldCRLs(keepHashes ...string) error {
|
||||
log.Debugf("will look for CRLs in %s", x.crlHashDir)
|
||||
|
||||
found, err := filepath.Glob(path.Join(x.crlHashDir, "*.crl"))
|
||||
if err != nil {
|
||||
log.Warnf("could not match files: %v", err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
nextFound:
|
||||
for _, filename := range found {
|
||||
for _, keepHash := range keepHashes {
|
||||
if x.GetCrlFileName(keepHash) == filename {
|
||||
continue nextFound
|
||||
}
|
||||
}
|
||||
if err := os.Remove(filename); err != nil {
|
||||
return fmt.Errorf("could not delete %s: %w", filename, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Root) GetCrlFileName(hash string) string {
|
||||
return path.Join(x.crlHashDir, fmt.Sprintf("%s.crl", hash))
|
||||
}
|
||||
|
||||
func (x *Root) checkPreconditions() {
|
||||
results := []bool{
|
||||
x.checkFile(x.databaseFile, "database"),
|
||||
x.checkFile(x.serialNumberFile, "serial number"),
|
||||
x.checkFile(x.crlNumberFile, "CRL serial number"),
|
||||
x.checkFile(x.crlFileName, "CRL"),
|
||||
x.checkDir(x.crlHashDir, "directory for hash indexed CRLs"),
|
||||
}
|
||||
for _, success := range results {
|
||||
if !success {
|
||||
log.Warnf("preconditions for %s failed, operations may fail too", x)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Root) checkFile(path, prefix string) bool {
|
||||
ok := true
|
||||
|
||||
if s, e := os.Stat(path); e != nil {
|
||||
log.Warnf("%s file %s of %s has issues: %v", prefix, path, x, e)
|
||||
|
||||
ok = false
|
||||
} else if s.IsDir() {
|
||||
log.Warnf("%s file %s of %s is a directory", prefix, path, x)
|
||||
|
||||
ok = false
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
func (x *Root) checkDir(path, prefix string) bool {
|
||||
ok := true
|
||||
|
||||
if s, e := os.Stat(path); e != nil {
|
||||
log.Warnf("%s %s of %s has issues: %v", prefix, path, x, e)
|
||||
|
||||
if err := os.MkdirAll(path, 0755); err != nil {
|
||||
log.Warnf("could not create %s %s of %s: %v", prefix, path, x, err)
|
||||
}
|
||||
|
||||
ok = false
|
||||
} else if !s.IsDir() {
|
||||
log.Warnf("%s %s of %s is not a directory", prefix, path, x)
|
||||
|
||||
ok = false
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
type SigningRequestParameters struct {
|
||||
Request []byte
|
||||
Subject []byte
|
||||
SubjectAlternativeNames []byte
|
||||
Days uint16
|
||||
IsSpkac bool
|
||||
}
|
||||
|
||||
func (x *Root) SignCertificate(
|
||||
profile *Profile,
|
||||
algorithm x509.SignatureAlgorithm,
|
||||
params *SigningRequestParameters,
|
||||
) ([]byte, error) {
|
||||
var publicKey interface{}
|
||||
|
||||
// nolint:nestif
|
||||
if params.IsSpkac {
|
||||
var err error
|
||||
|
||||
const spkacPrefix = "SPKAC="
|
||||
|
||||
if !bytes.Equal([]byte(spkacPrefix), params.Request[:len(spkacPrefix)]) {
|
||||
return nil, fmt.Errorf("request does not contain a valid SPKAC string")
|
||||
}
|
||||
|
||||
derBytes, err := base64.StdEncoding.DecodeString(string(params.Request[len(spkacPrefix):]))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not decode SPKAC bytes: %w", err)
|
||||
}
|
||||
|
||||
publicKey, err = pkac.ParseSPKAC(derBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse SPKAC: %w", err)
|
||||
}
|
||||
} else {
|
||||
csrBlock, _ := pem.Decode(params.Request)
|
||||
if csrBlock.Type != pemTypeCertificateRequest {
|
||||
return nil, fmt.Errorf(
|
||||
"unexpected PEM block '%s' instead of '%s'",
|
||||
csrBlock.Type,
|
||||
pemTypeCertificateRequest,
|
||||
)
|
||||
}
|
||||
csr, err := x509.ParseCertificateRequest(csrBlock.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse CSR: %w", err)
|
||||
}
|
||||
publicKey = csr.PublicKey
|
||||
}
|
||||
|
||||
nextSerialNumber, err := x.getNextSerialNumber()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get next serial number: %w", err)
|
||||
}
|
||||
|
||||
// copy profile
|
||||
notBefore := time.Now()
|
||||
notAfter := notBefore.Add(time.Hour * 24 * time.Duration(params.Days))
|
||||
certificate := &x509.Certificate{
|
||||
KeyUsage: profile.prototype.KeyUsage,
|
||||
ExtKeyUsage: profile.prototype.ExtKeyUsage,
|
||||
SignatureAlgorithm: algorithm,
|
||||
CRLDistributionPoints: x.crlDistributionPoints,
|
||||
OCSPServer: x.ocspServers,
|
||||
SerialNumber: nextSerialNumber,
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
}
|
||||
|
||||
// check subject
|
||||
subject, err := profile.parseSubject(params.Subject)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse subject: %w", err)
|
||||
}
|
||||
|
||||
certificate.Subject = *subject
|
||||
|
||||
// check altNames
|
||||
err = profile.parseAltNames(certificate, params.SubjectAlternativeNames)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse subject alternative names: %w", err)
|
||||
}
|
||||
|
||||
moveEmailsFromSubjectToAlternativeNames(certificate)
|
||||
|
||||
log.Tracef("prepared for signing: %+v", certificate)
|
||||
|
||||
certBytes, err := x509.CreateCertificate(rand.Reader, certificate, x.certificate, publicKey, x.privateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not sign certificate: %w", err)
|
||||
}
|
||||
|
||||
parsedCertificate, err := x509.ParseCertificate(certBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse signed certificate: %w", err)
|
||||
}
|
||||
|
||||
if err = x.bumpSerialNumber(nextSerialNumber); err != nil {
|
||||
return nil, fmt.Errorf("could not bump serial number: %w", err)
|
||||
}
|
||||
|
||||
err = x.recordIssuedCertificate(parsedCertificate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not record signed certificate in database: %w", err)
|
||||
}
|
||||
|
||||
pemBytes := pem.EncodeToMemory(&pem.Block{Type: pemTypeCertificate, Bytes: certBytes})
|
||||
log.Tracef("signed new certificate\n%s", pemBytes)
|
||||
|
||||
return pemBytes, nil
|
||||
}
|
||||
|
||||
// we move the email addresses to alt names because they are encoded to UTF-8
|
||||
// otherwise which is not compliant to RFC-5280
|
||||
func moveEmailsFromSubjectToAlternativeNames(certificate *x509.Certificate) {
|
||||
extraNames := make([]pkix.AttributeTypeAndValue, 0)
|
||||
|
||||
for _, p := range certificate.Subject.ExtraNames {
|
||||
if p.Type.Equal(oidPkcs9EmailAddress) {
|
||||
email := p.Value.(string)
|
||||
|
||||
if certificate.EmailAddresses == nil {
|
||||
certificate.EmailAddresses = []string{email}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for _, e := range certificate.EmailAddresses {
|
||||
if e == p.Value {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
certificate.EmailAddresses = append(certificate.EmailAddresses, email)
|
||||
} else {
|
||||
extraNames = append(extraNames, p)
|
||||
}
|
||||
}
|
||||
|
||||
certificate.Subject.ExtraNames = extraNames
|
||||
}
|
||||
|
||||
func (x *Root) recordIssuedCertificate(certificate *x509.Certificate) error {
|
||||
log.Tracef("recording %+v", certificate)
|
||||
|
||||
tempFile, err := ioutil.TempFile(path.Dir(x.databaseFile), "*.txt")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create temporary file: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = tempFile.Close() }()
|
||||
|
||||
tempName := tempFile.Name()
|
||||
|
||||
dbExists := false
|
||||
|
||||
_, err = os.Stat(x.databaseFile)
|
||||
if err != nil {
|
||||
log.Warnf("openssl certificate database file %s does not exist: %v", x.databaseFile, err)
|
||||
} else {
|
||||
dbExists = true
|
||||
inFile, err := os.Open(x.databaseFile)
|
||||
defer func() { _ = inFile.Close() }()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open openssl certificate database file %s: %w", x.databaseFile, err)
|
||||
}
|
||||
_, err = io.Copy(tempFile, inFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not copy %s to temporary file %s: %w", x.databaseFile, tempName, err)
|
||||
}
|
||||
if err = inFile.Close(); err != nil {
|
||||
return fmt.Errorf("could not close %s: %w", x.databaseFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = tempFile.Close(); err != nil {
|
||||
return fmt.Errorf("could not close temporary file %s: %w", tempName, err)
|
||||
}
|
||||
|
||||
outFile, err := os.OpenFile(tempName, os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open temporary file for writing %s: %w", tempName, err)
|
||||
}
|
||||
|
||||
defer func() { _ = outFile.Close() }()
|
||||
|
||||
line := strings.Join(
|
||||
// nolint:gomnd
|
||||
[]string{
|
||||
"V",
|
||||
strconv.FormatInt(certificate.NotBefore.Unix(), 10) + "Z",
|
||||
"",
|
||||
strings.ToUpper(certificate.SerialNumber.Text(16)),
|
||||
"unknown",
|
||||
opensslFormatDN(certificate.Subject),
|
||||
},
|
||||
"\t",
|
||||
)
|
||||
|
||||
_, err = fmt.Fprintln(outFile, line)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not write '%s' to %s: %w", line, tempName, err)
|
||||
}
|
||||
|
||||
if err = outFile.Close(); err != nil {
|
||||
return fmt.Errorf("could not close temporary file %s: %w", tempName, err)
|
||||
}
|
||||
|
||||
if dbExists {
|
||||
if err = os.Rename(x.databaseFile, fmt.Sprintf("%s.old", x.databaseFile)); err != nil {
|
||||
return fmt.Errorf("could not rename %s to %s.old: %w", x.databaseFile, x.databaseFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = os.Rename(tempName, x.databaseFile); err != nil {
|
||||
return fmt.Errorf("could not rename temporary file %s to %s: %w", tempName, x.databaseFile, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func opensslFormatDN(subject pkix.Name) string {
|
||||
const (
|
||||
oidSuffixCommonName = 3
|
||||
oidSuffixCountryName = 6
|
||||
oidSuffixLocalityName = 7
|
||||
oidSuffixProvinceName = 8
|
||||
oidSuffixOrganization = 10
|
||||
oidSuffixOrganizationalUnit = 11
|
||||
)
|
||||
|
||||
var buf strings.Builder
|
||||
|
||||
for _, rdn := range subject.ToRDNSequence() {
|
||||
if len(rdn) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, atv := range rdn {
|
||||
value, ok := atv.Value.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
t := atv.Type
|
||||
if len(t) == 4 && t[:3].Equal([]int{2, 5, 4}) {
|
||||
switch t[3] {
|
||||
case oidSuffixCommonName:
|
||||
buf.WriteString("/CN=")
|
||||
buf.WriteString(value)
|
||||
case oidSuffixCountryName:
|
||||
buf.WriteString("/C=")
|
||||
buf.WriteString(value)
|
||||
case oidSuffixLocalityName:
|
||||
buf.WriteString("/L=")
|
||||
buf.WriteString(value)
|
||||
case oidSuffixProvinceName:
|
||||
buf.WriteString("/ST=")
|
||||
buf.WriteString(value)
|
||||
case oidSuffixOrganization:
|
||||
buf.WriteString("/O=")
|
||||
buf.WriteString(value)
|
||||
case oidSuffixOrganizationalUnit:
|
||||
buf.WriteString("/OU=")
|
||||
buf.WriteString(value)
|
||||
}
|
||||
} else if t.Equal(oidPkcs9EmailAddress) {
|
||||
buf.WriteString("/emailAddress=")
|
||||
buf.WriteString(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func NewRoot(
|
||||
basedir, name, subdir string,
|
||||
id shared.CryptoSystemRootID,
|
||||
crlDistributionPoints, ocspServers []string,
|
||||
) *Root {
|
||||
key, err := loadPrivateKey(path.Join(basedir, subdir, "private", "ca.key.pem"))
|
||||
if err != nil {
|
||||
log.Fatalf("could not load private key: %v", err)
|
||||
}
|
||||
|
||||
cert, err := loadCertificate(path.Join(basedir, subdir, "ca.crt.pem"))
|
||||
if err != nil {
|
||||
log.Fatalf("could not load CA certificate: %v", err)
|
||||
}
|
||||
|
||||
root := &Root{
|
||||
Name: name,
|
||||
privateKey: key,
|
||||
certificate: cert,
|
||||
databaseFile: path.Join(basedir, subdir, "index.txt"),
|
||||
serialNumberFile: path.Join(basedir, subdir, "serial"),
|
||||
crlNumberFile: path.Join(basedir, subdir, "crlnumber"),
|
||||
crlFileName: fmt.Sprintf("revoke-root%d.crl", id),
|
||||
crlHashDir: path.Join(
|
||||
basedir,
|
||||
"currentcrls",
|
||||
fmt.Sprintf("%d", id),
|
||||
),
|
||||
crlDistributionPoints: crlDistributionPoints,
|
||||
ocspServers: ocspServers,
|
||||
}
|
||||
root.checkPreconditions()
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
type AltNameType string
|
||||
|
||||
const (
|
||||
NameTypeDNS AltNameType = "DNS"
|
||||
NameTypeXMPPJid AltNameType = "otherName:1.3.6.1.5.5.7.8.5;UTF8" // from RFC 3920, 6120
|
||||
)
|
||||
|
||||
type SubjectDnField string
|
||||
|
||||
const (
|
||||
SubjectDnFieldCountryName SubjectDnField = "C"
|
||||
SubjectDnFieldStateOrProvinceName SubjectDnField = "ST"
|
||||
SubjectDnFieldLocalityName SubjectDnField = "L"
|
||||
SubjectDnFieldOrganizationName SubjectDnField = "O"
|
||||
SubjectDnFieldOrganizationalUnitName SubjectDnField = "OU"
|
||||
SubjectDnFieldCommonName SubjectDnField = "CN"
|
||||
SubjectDnFieldEmailAddress SubjectDnField = "emailAddress"
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
name string
|
||||
prototype *x509.Certificate
|
||||
subjectDNFields []SubjectDnField
|
||||
altNameTypes []AltNameType
|
||||
copyEmail bool // whether to copy emailAddress values from subjectDN to email address subject alternative name
|
||||
}
|
||||
|
||||
func (p *Profile) String() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p *Profile) parseSubject(subject []byte) (*pkix.Name, error) {
|
||||
parts := strings.Split(string(subject), "/")
|
||||
subjectDN := &pkix.Name{}
|
||||
|
||||
for _, part := range parts {
|
||||
if len(strings.TrimSpace(part)) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
handled := false
|
||||
item := strings.SplitN(part, "=", 2)
|
||||
|
||||
for _, f := range p.subjectDNFields {
|
||||
if !strings.EqualFold(item[0], string(f)) {
|
||||
continue
|
||||
}
|
||||
|
||||
value := item[1]
|
||||
handled = true
|
||||
|
||||
switch f {
|
||||
case SubjectDnFieldCountryName:
|
||||
subjectDN.Country = append(subjectDN.Country, value)
|
||||
case SubjectDnFieldStateOrProvinceName:
|
||||
subjectDN.Province = append(subjectDN.Province, value)
|
||||
case SubjectDnFieldLocalityName:
|
||||
subjectDN.Locality = append(subjectDN.Locality, value)
|
||||
case SubjectDnFieldOrganizationName:
|
||||
subjectDN.Organization = append(subjectDN.Organization, value)
|
||||
case SubjectDnFieldOrganizationalUnitName:
|
||||
subjectDN.OrganizationalUnit = append(subjectDN.OrganizationalUnit, value)
|
||||
case SubjectDnFieldCommonName:
|
||||
subjectDN.CommonName = value
|
||||
case SubjectDnFieldEmailAddress:
|
||||
subjectDN.ExtraNames = append(subjectDN.ExtraNames, pkix.AttributeTypeAndValue{
|
||||
Type: oidPkcs9EmailAddress,
|
||||
Value: value,
|
||||
})
|
||||
default:
|
||||
log.Warnf("unhandled subject DN type %s", f)
|
||||
}
|
||||
}
|
||||
|
||||
if !handled {
|
||||
return nil, fmt.Errorf("skipped part %s because it is not supported by profile %s", part, p)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("created subject DN %s", subjectDN)
|
||||
|
||||
return subjectDN, nil
|
||||
}
|
||||
|
||||
func (p *Profile) parseAltNames(template *x509.Certificate, altNames []byte) error {
|
||||
parts := strings.Split(string(altNames), ",")
|
||||
for _, part := range parts {
|
||||
if len(strings.TrimSpace(part)) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
handled := false
|
||||
item := strings.SplitN(part, ":", 3)
|
||||
|
||||
if item[0] == "otherName" {
|
||||
item = []string{strings.Join(item[:2], ":"), item[2]}
|
||||
} else {
|
||||
item = []string{item[0], strings.Join(item[1:], ":")}
|
||||
}
|
||||
|
||||
for _, f := range p.altNameTypes {
|
||||
if item[0] != string(f) {
|
||||
continue
|
||||
}
|
||||
|
||||
value := item[1]
|
||||
handled = true
|
||||
|
||||
switch f {
|
||||
case NameTypeDNS:
|
||||
if template.DNSNames == nil {
|
||||
template.DNSNames = []string{value}
|
||||
} else {
|
||||
template.DNSNames = append(template.DNSNames, value)
|
||||
}
|
||||
case NameTypeXMPPJid:
|
||||
// x509ops.Certificate has no support for otherName alternative names
|
||||
log.Warnf("skipping %s because it cannot be supported", part)
|
||||
default:
|
||||
log.Warnf("unhandled alternative name type %s", f)
|
||||
}
|
||||
}
|
||||
|
||||
if !handled {
|
||||
return fmt.Errorf("skipped alternative name %s because it is not supported by profile %s", part, p)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewProfile(
|
||||
name string,
|
||||
prototype *x509.Certificate,
|
||||
subjectDnFields []SubjectDnField,
|
||||
altNameTypes []AltNameType,
|
||||
copyEmail bool,
|
||||
) *Profile {
|
||||
return &Profile{
|
||||
name: name,
|
||||
prototype: prototype,
|
||||
subjectDNFields: subjectDnFields,
|
||||
altNameTypes: altNameTypes,
|
||||
copyEmail: copyEmail,
|
||||
}
|
||||
}
|
||||
|
||||
func stringAsBigInt(data []byte) (*big.Int, error) {
|
||||
dataString := strings.TrimSpace(string(data))
|
||||
|
||||
parseInt, err := strconv.ParseInt(dataString, 16, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse %s as big int: %w", dataString, err)
|
||||
}
|
||||
|
||||
return big.NewInt(parseInt), nil
|
||||
}
|
227
signer/x509ops/x509ops_test.go
Normal file
227
signer/x509ops/x509ops_test.go
Normal file
|
@ -0,0 +1,227 @@
|
|||
package x509ops
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRoot_SignClientCertificateWithCSR(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
root := &Root{
|
||||
Name: "test",
|
||||
privateKey: loadTestKey(t),
|
||||
certificate: loadTestCACertificate(t),
|
||||
databaseFile: path.Join(tempDir, "index.txt"),
|
||||
serialNumberFile: path.Join(tempDir, "serial"),
|
||||
crlDistributionPoints: []string{"http://crl.example.org/revoke.crl"},
|
||||
ocspServers: []string{"http://ocsp.example.org/"},
|
||||
}
|
||||
|
||||
clientProfile := &Profile{
|
||||
name: "client",
|
||||
prototype: &x509.Certificate{
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageEmailProtection,
|
||||
x509.ExtKeyUsageClientAuth,
|
||||
},
|
||||
},
|
||||
subjectDNFields: []SubjectDnField{SubjectDnFieldCommonName, SubjectDnFieldEmailAddress},
|
||||
copyEmail: true,
|
||||
}
|
||||
|
||||
certificate, err := root.SignCertificate(clientProfile, x509.SHA256WithRSA, &SigningRequestParameters{
|
||||
Request: decodeToBytes(t, testRequest),
|
||||
Subject: []byte(testClientSubject),
|
||||
SubjectAlternativeNames: []byte(testClientSan),
|
||||
Days: 30,
|
||||
IsSpkac: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("error signing certificate: %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
certificateDer, _ := pem.Decode(certificate)
|
||||
if certificateDer.Type != pemTypeCertificate {
|
||||
t.Errorf("invalid PEM type '%s' instead of '%s'", certificateDer.Type, pemTypeCertificate)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_, err = x509.ParseCertificate(certificateDer.Bytes)
|
||||
if err != nil {
|
||||
t.Errorf("could not parse generated certificate: %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoot_SignClientCertificateWithSPKAC(t *testing.T) {
|
||||
root := &Root{
|
||||
Name: "test",
|
||||
privateKey: loadTestKey(t),
|
||||
certificate: loadTestCACertificate(t),
|
||||
databaseFile: path.Join(t.TempDir(), "index.txt"),
|
||||
serialNumberFile: path.Join(t.TempDir(), "serial"),
|
||||
crlDistributionPoints: []string{"http://crl.example.org/revoke.crl"},
|
||||
ocspServers: []string{"http://ocsp.example.org/"},
|
||||
}
|
||||
|
||||
clientProfile := &Profile{
|
||||
name: "client",
|
||||
prototype: &x509.Certificate{
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageEmailProtection,
|
||||
x509.ExtKeyUsageClientAuth,
|
||||
},
|
||||
},
|
||||
subjectDNFields: []SubjectDnField{SubjectDnFieldCommonName, SubjectDnFieldEmailAddress},
|
||||
copyEmail: true,
|
||||
}
|
||||
certificate, err := root.SignCertificate(clientProfile, x509.SHA256WithRSA, &SigningRequestParameters{
|
||||
Request: decodeToBytes(t, testSpkac),
|
||||
Subject: []byte(testClientSubject),
|
||||
SubjectAlternativeNames: []byte(testClientSan),
|
||||
Days: 30,
|
||||
IsSpkac: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("error signing certificate: %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
certificateDer, _ := pem.Decode(certificate)
|
||||
if certificateDer.Type != pemTypeCertificate {
|
||||
t.Errorf("invalid PEM type '%s' instead of '%s'", certificateDer.Type, pemTypeCertificate)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_, err = x509.ParseCertificate(certificateDer.Bytes)
|
||||
if err != nil {
|
||||
t.Errorf("could not parse generated certificate: %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoot_SignServerCertificateWithCSR(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
root := &Root{
|
||||
Name: "test",
|
||||
privateKey: loadTestKey(t),
|
||||
certificate: loadTestCACertificate(t),
|
||||
databaseFile: path.Join(tempDir, "index.txt"),
|
||||
serialNumberFile: path.Join(tempDir, "serial"),
|
||||
crlDistributionPoints: []string{"http://crl.example.org/revoke.crl"},
|
||||
ocspServers: []string{"http://ocsp.example.org/"},
|
||||
}
|
||||
|
||||
clientProfile := &Profile{
|
||||
name: "server",
|
||||
prototype: &x509.Certificate{
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageEmailProtection,
|
||||
x509.ExtKeyUsageClientAuth,
|
||||
},
|
||||
},
|
||||
subjectDNFields: []SubjectDnField{
|
||||
SubjectDnFieldCountryName,
|
||||
SubjectDnFieldStateOrProvinceName,
|
||||
SubjectDnFieldLocalityName,
|
||||
SubjectDnFieldOrganizationName,
|
||||
SubjectDnFieldOrganizationalUnitName,
|
||||
SubjectDnFieldCommonName,
|
||||
},
|
||||
altNameTypes: []AltNameType{NameTypeDNS, NameTypeXMPPJid},
|
||||
copyEmail: false,
|
||||
}
|
||||
|
||||
certificate, err := root.SignCertificate(clientProfile, x509.SHA256WithRSA, &SigningRequestParameters{
|
||||
Request: decodeToBytes(t, testRequest),
|
||||
Subject: []byte(testServerSubject),
|
||||
SubjectAlternativeNames: []byte(testServerSan),
|
||||
Days: 30,
|
||||
IsSpkac: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("error signing certificate: %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
certificateDer, _ := pem.Decode(certificate)
|
||||
if certificateDer.Type != "CERTIFICATE" {
|
||||
t.Errorf("invalid PEM type '%s' instead of 'CERTIFICATE'", certificateDer.Type)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_, err = x509.ParseCertificate(certificateDer.Bytes)
|
||||
if err != nil {
|
||||
t.Errorf("could not parse generated certificate: %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func loadTestKey(t *testing.T) crypto.Signer {
|
||||
testKeyBytes := decodeToBytes(t, testCAKey)
|
||||
|
||||
key, err := x509.ParsePKCS1PrivateKey(testKeyBytes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func loadTestCACertificate(t *testing.T) *x509.Certificate {
|
||||
testCertBytes := decodeToBytes(t, testCACertificate)
|
||||
|
||||
cert, err := x509.ParseCertificate(testCertBytes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return cert
|
||||
}
|
||||
|
||||
func decodeToBytes(t *testing.T, request string) []byte {
|
||||
decodeString, err := base64.StdEncoding.DecodeString(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return decodeString
|
||||
}
|
||||
|
||||
// nolint:lll
|
||||
const testRequest = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ1ZEQ0NBVHdDQVFBd0R6RU5NQXNHQTFVRUF3d0VWR1Z6ZERDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRApnZ0VQQURDQ0FRb0NnZ0VCQU9wQ1JmMVZWQjVwK3RscVFjM25qekYyZVAydG40bGZ2NVlJU3RFMktMSmxJRDRECi9YZTRVUGdodlNuMUN3R1VzeCtQcFBDaTdZVFdQNXRKSWYwbmNLMm02YVZIWUtqcDN2K3NvMnRENkJ4V0lMSFAKS0tQSm5qbnBjU0l1Q3hKUytRU2xyMHh0aEJYYzZ2UzVWRE5Ib285VXJWRUYzSVlTd3VDTklqTWpMR25kYmpCagprNm5TUk5JZWVmeVBaVGI4MHFsVTJFZ3hJMFdFYTA1dm5sQTY5L2tQZGhmTjVRRHBHQ1NxU25GdXo1cGFmRXVIClZMOE1aQXVtVDJySkVkYnorcHRPRjBqMWZSWEQ4b1RKZ0ppQmszbGR6YlBqeFpndW5LSTl3NVcrSWdEWmxsNm8KRzM2TUN6WHNYREdkb25NbCt0K1JIbEdocjN0VDQrKzBobXVYL2pzQ0F3RUFBYUFBTUEwR0NTcUdTSWIzRFFFQgpDd1VBQTRJQkFRRFBlYnhmR3pRaWRud1pxUGdQYUliRmxNM0xsWXpROEJFbFUrYXV5bC90VjhJRC85a0dkekNxClN4N0QyRWVNVC83QmZ6L29XUjRMczVUeFpzdjR0N3RiSEUrSStFdEcwS3FnUDhNTTJPWStGckJBMXVnY3JJa2YKNmVpbXFEVkFtUFBNMHhCNUg3aFdNY1BMVUhzbW1GNlV4ajNsVXphOVQ5OXpxTWppMXlyYlpIc1pkMEM0RFd6RQo1YWtZU1hTTGNuK1F3R25LY1pvV1QwczNWZU5pMHNUK3BTNEVkdk1SbzV6Q3JUMW1SbFlYQkNqU0tpQzZEVjNpCnhyaDI2WWJqMjRKSys5dlNUR3N4RFlpMXUzOG04a1AxRVR2L0lCVnRDSVpKVmJ2eXhWbUpuemV2QnJONHpxdncKV1QvQi9jOGdrK0FQR1BKM3ZaZDUxNVhvM2QzVld4NkwKLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0tCg=="
|
||||
|
||||
// nolint:lll
|
||||
const testSpkac = "U1BLQUM9TUlJQ1FEQ0NBU2d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRHFRa1g5VlZRZWFmclpha0hONTQ4eGRuajlyWitKWDcrV0NFclJOaWl5WlNBK0EvMTN1RkQ0SWIwcDlRc0JsTE1majZUd291MkUxaitiU1NIOUozQ3RwdW1sUjJDbzZkNy9yS05yUStnY1ZpQ3h6eWlqeVo0NTZYRWlMZ3NTVXZrRXBhOU1iWVFWM09yMHVWUXpSNktQVksxUkJkeUdFc0xnalNJekl5eHAzVzR3WTVPcDBrVFNIbm44ajJVMi9OS3BWTmhJTVNORmhHdE9iNTVRT3ZmNUQzWVh6ZVVBNlJna3FrcHhicythV254TGgxUy9ER1FMcGs5cXlSSFc4L3FiVGhkSTlYMFZ3L0tFeVlDWWdaTjVYYzJ6NDhXWUxweWlQY09WdmlJQTJaWmVxQnQrakFzMTdGd3huYUp6SmZyZmtSNVJvYTk3VStQdnRJWnJsLzQ3QWdNQkFBRVdBREFOQmdrcWhraUc5dzBCQVFRRkFBT0NBUUVBaDdqWmxaYXpPOXdHRkl3Mll1SVN5WjVYb2JjU0pSS2dMbG52UDh5cUkyVkxTdVBmdkNXOGJxKzNMWnNWcFZ6U0dhbDgwUTk5empOVy9lRm0xTElMRXZNZ0FCeEFtdk9UNlNKZURyajc4WkQyL0haazdmdHVLWU1VbkZTOWhzUWlkWmoveUttbDd4Nm9hQnlpalg5UVU0eTZYcCs3NzlhREFSenZOWTR5RGlRZmNuaFBVN1dablJyTUJZOHBSTVJVVjNKc2MvZXBQclZjOVhoaVE0KzBRNHFVQW1VNlVHNWNQeXFROVJJK2ZIL3VMbm8vZkNHUGZFVjM1NDV3WG5NeXNubC9HYlNLa0FHdVFmcm5ZT2dReFV4dUx1aHBoazVIaGtlMzNUUy9FYVlGc2JTZEhzRktoaCt4Uks3NW9wWExJTkpzNTFpYVdySUFTVmZvRTI0a3FRPT0K"
|
||||
|
||||
const testClientSubject = "/CN=Test/emailAddress=test@example.org"
|
||||
|
||||
const testClientSan = ""
|
||||
|
||||
const testServerSubject = "/CN=www.example.org"
|
||||
|
||||
const testServerSan = "DNS:www.example.org,otherName:1.3.6.1.5.5.7.8.5;UTF8:www.example.org"
|
||||
|
||||
// nolint:lll
|
||||
const testCAKey = "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"
|
||||
|
||||
// nolint:lll
|
||||
const testCACertificate = "MIIFmDCCBACgAwIBAgIUOlITUGXFrKZesL4LlawzVxLTXFYwDQYJKoZIhvcNAQELBQAwNzELMAkGA1UEBhMCQVUxFDASBgNVBAoMC0NBY2VydCBJbmMuMRIwEAYDVQQDDAlUZXN0IFJvb3QwHhcNMjEwMTA4MTMwNDM5WhcNMjYwMTA3MTMwNDM5WjA9MQswCQYDVQQGEwJBVTEUMBIGA1UECgwLQ0FjZXJ0IEluYy4xGDAWBgNVBAMMD0NsYXNzIDMgVGVzdCBDQTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAKjFj9hHPw8SeZ+Wwr9HuqEwaBr7KZatah1bcDwxC/28JYVG04oBj0BqFCvmN3ktk+RWP6qXX6o1AYvxZz4ijcBJ5pGLkmpEkkuhsq4LD/O+s9cV7YedAiJFJwD0dWCdG2JoVKRIrUSYKFmuZst20vi7jbdYFRw/s8gjpW3zSWUyC50BxS7c4v8D4r5U1qVAycx1r76PiouHtvalkTGgGrYly1Kv8FtOn7EUmvaZZITb+45YPYavGNkhIntJkb7shYCCr5AZnE91+cj4QlmT6JrKkJxCVyp8Z6NEpKzPl6FlgXrrGD7GZH15JxsMv8L6DG1U+TH6sjGGNQk8PP82kg6Av7I/F5gC+OLfhLGfiRFGIElVumNy9ionrqgom4OAfhvKiOAEJtnI12p419ZFk1AgrRjuspcUSjatMwloX5RNotrP/uL/kbh0NZWmJ35d7yEGR89P4LUCJUdgKl3Njg0aEzvHfNHV2Y8CJp64x0P8h8eCxKxzKazY0G9Aaa7QgwIDAQABo4IBlDCCAZAwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRI3ALoQ7dcRqu0gHtxaLVAv2DcOzAfBgNVHSMEGDAWgBTrwahlUMEoV1OavhMbkLcSar8PIzB3BggrBgEFBQcBAQRrMGkwNwYIKwYBBQUHMAKGK2h0dHA6Ly90ZXN0LmNhY2VydC5sb2NhbGhvc3QvY2Evcm9vdC9jYS5jcnQwLgYIKwYBBQUHMAGGImh0dHA6Ly9vY3NwLnRlc3QuY2FjZXJ0LmxvY2FsaG9zdC8wPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybC50ZXN0LmNhY2VydC5sb2NhbGhvc3QvY2xhc3MzLmNybDBUBgNVHSAETTBLMEkGCCsGAQUFBwIBMD0wOwYIKwYBBQUHAgEWL2h0dHA6Ly90ZXN0LmNhY2VydC5sb2NhbGhvc3QvY2EvY2xhc3MzL2Nwcy5odG1sMA0GCSqGSIb3DQEBCwUAA4IBgQAcqK68GOxTfM9zSRbHWHchsbiyKcbxPo42se9dm/nLHT/N2XEW9Ycj5dZD8+XgoW8dVPS3uVZGj57Pr8ix3OXhMKGqcdO2QRAQaoyjw7t9dCkaJ8b7h39sY/5pFSSIdYAyyb9uPgJ1FPLueOqm3bZHVFcbiiA8/miiwGWPVEfK7zdEmFKMAkY2wYtWBeovKNVnCbuQ1Pd8CxvkCs5R9KnMfbU7bgJK8zkhlHwdtalmg2IS4yMuvYeL9S3QwL7fYcCjjTLCKwkj3frsnkRC5pGPHQ6/iVVbdsqAI70A1Uqcl15Jcpzg0Nc2EABjhbWO7gLpHpzMI5Alt+Tr+oWhe2M7wnBhuojgwASA10CnXT27GYXziIzr8d3P+T0PVLD2WcvQeEUJoQySw6W8CIkaZEZG6YBWjrAkGcO6JB+YJ5UiJOCHA6W4pmwNkGR2oh6JMQCUikaFVywb1HMIGOINOBHymj4KkuywC2w6SXMD4OqJcsCmHSNcqjFvcT/22kYCtDE="
|
Loading…
Add table
Add a link
Reference in a new issue