Refactor code structure
Move X.509 and Openpgp operations into custom packages. Implement more robust input reading. Do not convert []byte to string unnecessarily. Finish implementation of X.509 CRL creation.
This commit is contained in:
parent
2de9771472
commit
9f0916b14a
9 changed files with 715 additions and 493 deletions
418
signer/x509_ops/operations.go
Normal file
418
signer/x509_ops/operations.go
Normal file
|
@ -0,0 +1,418 @@
|
|||
package x509_ops
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"git.cacert.org/cacert-gosigner/shared"
|
||||
"git.cacert.org/cacert-gosigner/signer/common"
|
||||
)
|
||||
|
||||
const crlLifetime = time.Hour * 24 * 7
|
||||
|
||||
type Root struct {
|
||||
Name string
|
||||
privateKeyFile string
|
||||
certificateFile string
|
||||
databaseFile string
|
||||
crlNumberFile string
|
||||
crlFileName string
|
||||
crlHashDir string
|
||||
}
|
||||
|
||||
func (x *Root) String() string {
|
||||
return x.Name
|
||||
}
|
||||
|
||||
type Profile struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (x *Root) loadCertificate() (*x509.Certificate, error) {
|
||||
certificateFile := x.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 (x *Root) getPrivateKey() (crypto.Signer, error) {
|
||||
privateKeyFile := x.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 (x *Root) getNextCRLNumber() (*big.Int, error) {
|
||||
crlNumberFile := x.crlNumberFile
|
||||
_, 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 := common.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 (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) 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) {
|
||||
databaseFile := x.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 := 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", 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
|
||||
}
|
||||
|
||||
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) {
|
||||
caCertificate, err := x.loadCertificate()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
caPrivateKey, err := x.getPrivateKey()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
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,
|
||||
caCertificate,
|
||||
caPrivateKey,
|
||||
)
|
||||
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.privateKeyFile, "private key"),
|
||||
x.checkFile(x.certificateFile, "certificate"),
|
||||
x.checkFile(x.databaseFile, "database"),
|
||||
x.checkFile(x.crlNumberFile, "CRL serial number"),
|
||||
x.checkFile(x.crlFileName, "CRL file"),
|
||||
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
|
||||
}
|
||||
|
||||
func NewRoot(basedir, name, subdir string, id shared.CryptoSystemRootId) *Root {
|
||||
root := &Root{
|
||||
Name: name,
|
||||
privateKeyFile: path.Join(basedir, subdir, "private", "ca.key.pem"),
|
||||
certificateFile: path.Join(basedir, subdir, "ca.crt.pem"),
|
||||
databaseFile: path.Join(basedir, subdir, "index.txt"),
|
||||
crlNumberFile: path.Join(basedir, subdir, "crlnumber"),
|
||||
crlFileName: fmt.Sprintf("revoke-root%d.crl", id),
|
||||
crlHashDir: path.Join(
|
||||
basedir,
|
||||
"currentcrls",
|
||||
fmt.Sprintf("%d", id),
|
||||
),
|
||||
}
|
||||
root.checkPreconditions()
|
||||
return root
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue