cacert-gosigner/signer/crypto_system.go

252 lines
7.4 KiB
Go

package signer
import (
"bufio"
"crypto"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io/ioutil"
"math/big"
"os"
"path"
"strconv"
"strings"
"time"
log "github.com/sirupsen/logrus"
"git.cacert.org/cacert-gosigner/shared"
)
type RootCredentials struct {
Name string
PrivateKeyFile string
PublicKeyFile string
CertificateFile string
DatabaseFile string
CRLNumber string
}
type CryptoSystem struct {
Name string
Roots map[shared.CryptoSystemRootId]*RootCredentials
Profiles map[shared.CertificateProfileId]string
DigestAlgorithms map[shared.MessageDigestAlgorithmId]x509.SignatureAlgorithm
}
func (system CryptoSystem) String() string {
return system.Name
}
func (system CryptoSystem) loadCertificate(rootId shared.CryptoSystemRootId) (*x509.Certificate, error) {
certificateFile := system.Roots[rootId].CertificateFile
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 (system CryptoSystem) getPrivateKey(rootId shared.CryptoSystemRootId) (crypto.Signer, error) {
privateKeyFile := system.Roots[rootId].PrivateKeyFile
pemBytes, err := ioutil.ReadFile(privateKeyFile)
if err != nil {
return nil, fmt.Errorf(
"could not load private key %s: %v",
privateKeyFile,
err,
)
}
pemBlock, _ := pem.Decode(pemBytes)
if pemBlock.Type != "PRIVATE KEY" {
log.Warnf(
"PEM in %s is probably not a private key. PEM block has type %s",
privateKeyFile,
pemBlock.Type,
)
}
privateKey, err := x509.ParsePKCS8PrivateKey(pemBlock.Bytes)
if err != nil {
return nil, fmt.Errorf(
"could no parse private key from %s: %v",
privateKeyFile,
err,
)
}
return privateKey.(*rsa.PrivateKey), nil
}
func (system *CryptoSystem) getNextCRLNumber(rootId shared.CryptoSystemRootId) (*big.Int, error) {
crlNumberFile := system.Roots[rootId].CRLNumber
_, err := os.Stat(crlNumberFile)
if err != nil {
log.Warnf("CRL number file %s does not exist: %v", crlNumberFile, err)
return big.NewInt(1), nil
}
data, err := ioutil.ReadFile(crlNumberFile)
if err != nil {
return nil, fmt.Errorf("could not read CRL number file %s", crlNumberFile)
}
result, err := stringAsBigInt(data)
if err != nil {
return nil, fmt.Errorf("could not parse content of %s as CRL number: %v", crlNumberFile, err)
}
return result, nil
}
func (system *CryptoSystem) bumpCRLNumber(rootId shared.CryptoSystemRootId, current *big.Int) error {
serial := current.Int64() + 1
crlNumberFile := system.Roots[rootId].CRLNumber
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 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: %v", dataString, err)
}
return big.NewInt(parseInt), nil
}
func (system *CryptoSystem) loadRevokedCertificatesFromDatabase(rootId shared.CryptoSystemRootId) ([]pkix.RevokedCertificate, error) {
databaseFile := system.Roots[rootId].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 := 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 (system *CryptoSystem) recordRevocation(rootId shared.CryptoSystemRootId, certificate *x509.Certificate) (*pkix.RevokedCertificate, error) {
databaseFile := system.Roots[rootId].DatabaseFile
_, err := os.Stat(databaseFile)
if err != nil {
return nil, fmt.Errorf("openssl certificate database file %s does not exist: %v", databaseFile, err)
}
inFile, err := os.Open(databaseFile)
if err != nil {
return nil, fmt.Errorf("could not open openssl certificate database file %s: %v", databaseFile, err)
}
defer func() { _ = inFile.Close() }()
outFile, err := ioutil.TempFile(path.Dir(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 := 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", databaseFile, err)
}
if err = os.Rename(databaseFile, fmt.Sprintf("%s.old", databaseFile)); err != nil {
return nil, fmt.Errorf("could not rename %s to %s.old: %v", databaseFile, databaseFile, err)
}
if err = os.Rename(outFile.Name(), databaseFile); err != nil {
return nil, fmt.Errorf("could not rename temporary file %s to %s: %v", outFile.Name(), databaseFile, err)
}
if !found {
log.Warnf("entry not found in database")
}
return &pkix.RevokedCertificate{
SerialNumber: certificate.SerialNumber,
RevocationTime: revocationTime,
}, nil
}