860 lines
25 KiB
Go
860 lines
25 KiB
Go
package x509_ops
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha1"
|
|
"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"
|
|
"git.cacert.org/cacert-gosigner/signer/common"
|
|
)
|
|
|
|
const crlLifetime = time.Hour * 24 * 7
|
|
|
|
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: %v",
|
|
certificateFile,
|
|
err,
|
|
)
|
|
}
|
|
pemBlock, _ := pem.Decode(pemBytes)
|
|
if pemBlock.Type != "CERTIFICATE" {
|
|
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: %v",
|
|
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: %v",
|
|
filename,
|
|
err,
|
|
)
|
|
}
|
|
pemBlock, _ := pem.Decode(pemBytes)
|
|
if pemBlock == nil {
|
|
return nil, fmt.Errorf("no PEM data found in %s", filename)
|
|
}
|
|
if pemBlock.Type != "PRIVATE KEY" {
|
|
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: %v",
|
|
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: %v", x.serialNumberFile, err)
|
|
}
|
|
result, err := common.StringAsBigInt(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse content of %s as serial number: %v", 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: %v", x.crlNumberFile, err)
|
|
}
|
|
result, err := common.StringAsBigInt(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse content of %s as CRL number: %v", 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")
|
|
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: %v", serial, outFile.Name(), err)
|
|
}
|
|
|
|
if err = outFile.Close(); err != nil {
|
|
return fmt.Errorf("could not close temporary file %s: %v", 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: %v", crlNumberFile, crlNumberFile, err)
|
|
}
|
|
if err = os.Rename(outFile.Name(), crlNumberFile); err != nil {
|
|
return fmt.Errorf("could not rename %s to %s: %v", 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")
|
|
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: %v", serial, outFile.Name(), err)
|
|
}
|
|
|
|
if err = outFile.Close(); err != nil {
|
|
return fmt.Errorf("could not close temporary file %s: %v", 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: %v", x.serialNumberFile, x.serialNumberFile, err)
|
|
}
|
|
}
|
|
if err = os.Rename(outFile.Name(), x.serialNumberFile); err != nil {
|
|
return fmt.Errorf("could not rename %s to %s: %v", 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: %v", 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 := common.StringAsBigInt([]byte(line[3]))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse serial number %s as big int: %v", line[3], err)
|
|
}
|
|
revokeTs, err := strconv.ParseInt(line[2][:len(line[2])-1], 10, 64)
|
|
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: %v", x.databaseFile, err)
|
|
}
|
|
inFile, err := os.Open(x.databaseFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not open openssl certificate database file %s: %v", x.databaseFile, err)
|
|
}
|
|
defer func() { _ = inFile.Close() }()
|
|
|
|
outFile, err := ioutil.TempFile(path.Dir(x.databaseFile), "*.txt")
|
|
defer func() { _ = outFile.Close() }()
|
|
|
|
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 := common.StringAsBigInt([]byte(parts[3]))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse serial number %s as big int: %v", 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: %v", line, outFile.Name(), err)
|
|
}
|
|
}
|
|
|
|
if err = outFile.Close(); err != nil {
|
|
return nil, fmt.Errorf("could not close temporary file %s: %v", outFile.Name(), err)
|
|
}
|
|
if err = inFile.Close(); err != nil {
|
|
return nil, fmt.Errorf("could not close %s: %v", 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: %v", 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: %v", 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 != "CERTIFICATE" {
|
|
if pemBlock.Type != "CERTIFICATE" {
|
|
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: %v",
|
|
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: %v", err)
|
|
}
|
|
|
|
if err = ioutil.WriteFile(x.crlFileName, crlBytes, 0644); err != nil {
|
|
return nil, nil, fmt.Errorf("could not write new CRL to %s: %v", x.crlFileName, err)
|
|
}
|
|
|
|
newCrlHash := sha1.Sum(crlBytes)
|
|
hashedCrlFileName := path.Join(
|
|
x.crlHashDir,
|
|
fmt.Sprintf("%s.crl", hex.EncodeToString(newCrlHash[:])),
|
|
)
|
|
if err = ioutil.WriteFile(hashedCrlFileName, crlBytes, 0644); err != nil {
|
|
return nil, nil, fmt.Errorf("could not write new CRL to %s: %v", 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: %v", 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
|
|
}
|
|
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{}
|
|
if params.IsSpkac {
|
|
var err error
|
|
const spkacPrefix = "SPKAC="
|
|
if bytes.Compare([]byte(spkacPrefix), params.Request[:len(spkacPrefix)]) != 0 {
|
|
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: %v", err)
|
|
}
|
|
publicKey, err = pkac.ParseSPKAC(derBytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse SPKAC: %v", err)
|
|
}
|
|
} else {
|
|
csrBlock, _ := pem.Decode(params.Request)
|
|
if csrBlock.Type != "CERTIFICATE REQUEST" {
|
|
return nil, fmt.Errorf("unexpected PEM block '%s' instead of 'CERTIFICATE REQUEST'", csrBlock.Type)
|
|
}
|
|
csr, err := x509.ParseCertificateRequest(csrBlock.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse CSR: %v", err)
|
|
}
|
|
publicKey = csr.PublicKey
|
|
}
|
|
|
|
nextSerialNumber, err := x.getNextSerialNumber()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not get next serial number: %v", 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: %v", 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: %v", 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: %v", err)
|
|
}
|
|
|
|
parsedCertificate, err := x509.ParseCertificate(certBytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse signed certificate: %v", err)
|
|
}
|
|
|
|
if err = x.bumpSerialNumber(nextSerialNumber); err != nil {
|
|
log.Errorf("could not bump serial number: %v", err)
|
|
}
|
|
|
|
err = x.recordIssuedCertificate(parsedCertificate)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not record signed certificate in database: %v", err)
|
|
}
|
|
|
|
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", 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: %v", 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: %v", x.databaseFile, err)
|
|
}
|
|
_, err = io.Copy(tempFile, inFile)
|
|
if err != nil {
|
|
return fmt.Errorf("could not copy %s to temporary file %s: %v", x.databaseFile, tempName, err)
|
|
}
|
|
if err = inFile.Close(); err != nil {
|
|
return fmt.Errorf("could not close %s: %v", x.databaseFile, err)
|
|
}
|
|
}
|
|
if err = tempFile.Close(); err != nil {
|
|
return fmt.Errorf("could not close temporary file %s: %v", 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: %v", tempName, err)
|
|
}
|
|
defer func() { _ = outFile.Close() }()
|
|
line := strings.Join([]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: %v", line, tempName, err)
|
|
}
|
|
if err = outFile.Close(); err != nil {
|
|
return fmt.Errorf("could not close temporary file %s: %v", 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: %v", 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: %v", tempName, x.databaseFile, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func opensslFormatDN(subject pkix.Name) string {
|
|
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 3:
|
|
buf.WriteString("/CN=")
|
|
buf.WriteString(value)
|
|
case 6:
|
|
buf.WriteString("/C=")
|
|
buf.WriteString(value)
|
|
case 7:
|
|
buf.WriteString("/L=")
|
|
buf.WriteString(value)
|
|
case 8:
|
|
buf.WriteString("/ST=")
|
|
buf.WriteString(value)
|
|
case 10:
|
|
buf.WriteString("/O=")
|
|
buf.WriteString(value)
|
|
case 11:
|
|
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 = "otherName:1.3.6.1.5.5.7.8.5;UTF8" // from RFC 3920, 6120
|
|
)
|
|
|
|
type SubjectDnField string
|
|
|
|
const (
|
|
SubjectDnFieldCountryName SubjectDnField = "C"
|
|
SubjectDnFieldStateOrProvinceName = "ST"
|
|
SubjectDnFieldLocalityName = "L"
|
|
SubjectDnFieldOrganizationName = "O"
|
|
SubjectDnFieldOrganizationalUnitName = "OU"
|
|
SubjectDnFieldCommonName = "CN"
|
|
SubjectDnFieldEmailAddress = "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.ToUpper(item[0]) != strings.ToUpper(string(f)) {
|
|
continue
|
|
}
|
|
value := item[1]
|
|
handled = true
|
|
switch f {
|
|
case SubjectDnFieldCountryName:
|
|
if subjectDN.Country == nil {
|
|
subjectDN.Country = []string{value}
|
|
} else {
|
|
subjectDN.Country = append(subjectDN.Country, value)
|
|
}
|
|
case SubjectDnFieldStateOrProvinceName:
|
|
if subjectDN.Province == nil {
|
|
subjectDN.Province = []string{value}
|
|
} else {
|
|
subjectDN.Province = append(subjectDN.Province, value)
|
|
}
|
|
case SubjectDnFieldLocalityName:
|
|
if subjectDN.Locality == nil {
|
|
subjectDN.Locality = []string{value}
|
|
} else {
|
|
subjectDN.Locality = append(subjectDN.Locality, value)
|
|
}
|
|
case SubjectDnFieldOrganizationName:
|
|
if subjectDN.Organization == nil {
|
|
subjectDN.Organization = []string{value}
|
|
} else {
|
|
subjectDN.Organization = append(subjectDN.Organization, value)
|
|
}
|
|
case SubjectDnFieldOrganizationalUnitName:
|
|
if subjectDN.OrganizationalUnit == nil {
|
|
subjectDN.OrganizationalUnit = []string{value}
|
|
} else {
|
|
subjectDN.OrganizationalUnit = append(subjectDN.OrganizationalUnit, value)
|
|
}
|
|
case SubjectDnFieldCommonName:
|
|
subjectDN.CommonName = value
|
|
case SubjectDnFieldEmailAddress:
|
|
emailIA5 := pkix.AttributeTypeAndValue{
|
|
Type: oidPkcs9EmailAddress,
|
|
Value: value,
|
|
}
|
|
if subjectDN.ExtraNames == nil {
|
|
subjectDN.ExtraNames = []pkix.AttributeTypeAndValue{emailIA5}
|
|
} else {
|
|
subjectDN.ExtraNames = append(subjectDN.ExtraNames, emailIA5)
|
|
}
|
|
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:
|
|
// x509.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,
|
|
}
|
|
}
|