Start implementation of revoke action

This commit is contained in:
Jan Dittberner 2021-01-04 20:39:35 +01:00
parent 38566f35ef
commit 2de9771472
9 changed files with 739 additions and 57 deletions

View file

@ -1,8 +1,19 @@
package signer
import (
"bytes"
"crypto/rand"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"time"
"unsafe"
log "github.com/sirupsen/logrus"
@ -12,28 +23,45 @@ import (
"git.cacert.org/cacert-gosigner/shared"
)
const crlLifetime = time.Hour * 24 * 7
var emptyDataSha1 [20]byte
func init() {
emptyDataSha1 = sha1.Sum([]byte{})
}
type CommandProcessorSettings struct {
CABaseDir string
XDeltaPath string
OpenPGPUidEmail string
}
// The CommandProcessor takes parsed protocol data and executes the actual
// functionality.
type CommandProcessor struct {
Settings *CommandProcessorSettings
CryptoSystems map[shared.CryptoSystemId]*CryptoSystem
}
// Process the signer request
func (p *CommandProcessor) Process(command datastructures.SignerRequest) (
response *datastructures.SignerResponse,
err error,
func (p *CommandProcessor) Process(command *datastructures.SignerRequest) (
*datastructures.SignerResponse,
error,
) {
log.Infof("analyze %+v", command)
log.Infof("process %s", command)
if command.Version != shared.ProtocolVersion {
return nil, fmt.Errorf("unsupported protocol version %d", command.Version)
}
switch command.Action {
case shared.ActionNul:
response, err = p.handleNulAction(command)
return
return p.handleNulAction(command)
case shared.ActionSign:
response, err = p.handleSignAction(command)
return
return p.handleSignAction(command)
case shared.ActionRevoke:
response, err = p.handleRevokeAction(command)
return
return p.handleRevokeAction(command)
default:
return nil, errors.New(fmt.Sprintf(
"unsupported Action 0x%02x %s",
@ -43,7 +71,7 @@ func (p *CommandProcessor) Process(command datastructures.SignerRequest) (
}
}
func (*CommandProcessor) handleNulAction(command datastructures.SignerRequest) (
func (*CommandProcessor) handleNulAction(command *datastructures.SignerRequest) (
*datastructures.SignerResponse,
error,
) {
@ -64,13 +92,11 @@ func (*CommandProcessor) handleNulAction(command datastructures.SignerRequest) (
log.Errorf("could not set system time: %v", e1)
}
return &datastructures.SignerResponse{
Version: command.Version, Action: command.Action,
Content1: "", Content2: "", Content3: ""}, nil
return datastructures.NewNulResponse(command.Version), nil
}
func (p *CommandProcessor) handleSignAction(
command datastructures.SignerRequest,
command *datastructures.SignerRequest,
) (
*datastructures.SignerResponse,
error,
@ -80,11 +106,239 @@ func (p *CommandProcessor) handleSignAction(
}
func (p *CommandProcessor) handleRevokeAction(
command datastructures.SignerRequest,
command *datastructures.SignerRequest,
) (
*datastructures.SignerResponse,
error,
) {
log.Debugf("handle revoke call: %v", command)
idSystem, err := p.checkIdentitySystem(
command.System,
command.Root,
command.Profile,
command.MdAlgorithm,
)
if err != nil {
return nil, err
}
log.Debugf("identified id system: %+v", idSystem)
switch command.System {
case CsX509:
request := command.Content1
clientHash := command.Content3
return p.revokeX509(idSystem, request, clientHash)
default:
return nil, fmt.Errorf("revoke not implemented for crypto system %s", idSystem.System)
}
}
type IdSystemParameters struct {
System *CryptoSystem
Root *RootCredentials
RootId shared.CryptoSystemRootId
Profile string
MessageDigestAlgorithm x509.SignatureAlgorithm
}
func (p *CommandProcessor) checkIdentitySystem(
systemId shared.CryptoSystemId,
rootId shared.CryptoSystemRootId,
profileId shared.CertificateProfileId,
algorithmId shared.MessageDigestAlgorithmId,
) (*IdSystemParameters, error) {
cryptoSystem, ok := p.CryptoSystems[systemId]
if !ok {
return nil, fmt.Errorf(
"unsupported crypto system %d",
systemId,
)
}
root, ok := p.CryptoSystems[systemId].Roots[rootId]
if !ok {
return nil, fmt.Errorf(
"unsupported root certificate %d for crypto system %s",
rootId,
cryptoSystem,
)
}
profile, ok := p.CryptoSystems[systemId].Profiles[profileId]
if !ok {
return nil, fmt.Errorf(
"invalid CA profile %d for crypto system %s",
profileId,
cryptoSystem,
)
}
mdAlgorithm, ok := p.CryptoSystems[systemId].DigestAlgorithms[algorithmId]
if !ok {
return nil, fmt.Errorf(
"unsupported digest algorithm %d for crypto system %s",
algorithmId,
cryptoSystem,
)
}
return &IdSystemParameters{
System: cryptoSystem,
Root: root,
RootId: rootId,
Profile: profile,
MessageDigestAlgorithm: mdAlgorithm,
}, nil
}
func (p *CommandProcessor) revokeX509(
system *IdSystemParameters,
request string,
clientHash string,
) (*datastructures.SignerResponse, error) {
clientHashBytes, err := hex.DecodeString(clientHash)
if err != nil {
return nil, fmt.Errorf("could not parse '%s' as hex bytes", clientHash)
}
log.Debugf("revoke X.509 for root %s (%d)", system.Root, system.RootId)
log.Debugf("hash bytes from client %x", clientHashBytes)
crlFileName := fmt.Sprintf("revoke-root%d.crl", system.RootId)
stat, err := os.Stat(crlFileName)
if err != nil {
log.Debugf("CRL file %s does not exist", crlFileName)
} else if stat.IsDir() {
return nil, fmt.Errorf("CRL filename %s points to a directory", crlFileName)
}
currentHash := emptyDataSha1
if stat != nil {
crlData, err := ioutil.ReadFile(crlFileName)
if err != nil {
log.Warnf("could not read current CRL, assuming empty CRL")
}
currentHash = sha1.Sum(crlData)
}
log.Debugf("hash bytes on signer %x", currentHash)
crlIsCurrent := false
if bytes.Equal(clientHashBytes, currentHash[:]) {
log.Debug("client CRL hash and current CRL hash on signer match")
err := p.deleteOldCrl(system.RootId, clientHash)
if err != nil {
log.Warnf("could not delete old CRLs: %v", err)
}
if !bytes.Equal(clientHashBytes, emptyDataSha1[:]) {
crlIsCurrent = true
} else {
log.Debugf("client has an empty CRL, the signer too")
}
}
if len(request) > 0 {
_, err = revokeCertificate(request, system.System, system.RootId)
if err != nil {
return nil, fmt.Errorf("could not revoke certificate / create CRL: %v", err)
}
}
var crlBytes []byte
crlBytes, err = generateCrl(system.MessageDigestAlgorithm, system.System, system.RootId)
if err != nil {
return nil, fmt.Errorf("could not generate a new CRL for root %d: %v", system.RootId, err)
}
log.Debugf("crlIsCurrent: %v, crlBytes: %d", crlIsCurrent, len(crlBytes))
log.Tracef("New CRL\n%s", pem.EncodeToMemory(&pem.Block{
Type: "X509 CRL",
Bytes: crlBytes,
}))
err = ioutil.WriteFile(crlFileName, crlBytes, 0644)
if err != nil {
return nil, fmt.Errorf("could not write new CRL to %s: %v", crlFileName, err)
}
return nil, errors.New("not implemented yet")
}
func (p *CommandProcessor) deleteOldCrl(id shared.CryptoSystemRootId, hash string) error {
path.Join(p.Settings.CABaseDir, "currentcrls", fmt.Sprintf("%d", id))
// TODO: implement
return errors.New("not implemented yet")
}
func generateCrl(
algorithm x509.SignatureAlgorithm,
system *CryptoSystem,
rootId shared.CryptoSystemRootId,
) ([]byte, error) {
caCertificate, err := system.loadCertificate(rootId)
if err != nil {
return nil, err
}
caPrivateKey, err := system.getPrivateKey(rootId)
if err != nil {
return nil, err
}
certificatesToRevoke, err := system.loadRevokedCertificatesFromDatabase(rootId)
if err != nil {
return nil, err
}
nextCrlNumber, err := system.getNextCRLNumber(rootId)
if err != nil {
return 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 = system.bumpCRLNumber(rootId, nextCrlNumber); err != nil {
log.Errorf("could not bump CRL number: %v", err)
}
}()
return x509.CreateRevocationList(
rand.Reader,
crlTemplate,
caCertificate,
caPrivateKey,
)
}
func revokeCertificate(request string, system *CryptoSystem, rootId shared.CryptoSystemRootId) (*pkix.RevokedCertificate, error) {
pemBlock, _ := pem.Decode([]byte(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 system.recordRevocation(rootId, certificate)
}
func NewCommandProcessorSettings() *CommandProcessorSettings {
caBasedir, ok := os.LookupEnv("SIGNER_BASEDIR")
if !ok {
caBasedir = "."
}
gpgUidEmail, ok := os.LookupEnv("SIGNER_GPG_KEYRING_DIR")
if !ok {
gpgUidEmail = "gpg@cacert.org"
}
return &CommandProcessorSettings{CABaseDir: caBasedir, OpenPGPUidEmail: gpgUidEmail, XDeltaPath: "/usr/bin/xdelta"}
}

251
signer/crypto_system.go Normal file
View file

@ -0,0 +1,251 @@
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
}

View file

@ -43,7 +43,7 @@ func (p *PortHandler) MainLoop() {
return
}
log.Debugf("received command %v", command)
response, err := p.processor.Process(*command)
response, err := p.processor.Process(command)
if err != nil {
log.Errorf("error processing command: %v", err)
close(p.commandChan)
@ -207,6 +207,6 @@ func NewSignerProcess(port io.ReadWriteCloser) *PortHandler {
port: port,
errors: errorChan,
commandChan: make(chan *datastructures.SignerRequest, 1),
processor: &CommandProcessor{},
processor: NewCommandProcessor(),
}
}

131
signer/protocol_elements.go Normal file
View file

@ -0,0 +1,131 @@
package signer
import (
"crypto/x509"
"git.cacert.org/cacert-gosigner/shared"
)
const (
CsX509 shared.CryptoSystemId = 1
CsOpenPGP shared.CryptoSystemId = 2
)
const (
X509RootDefault shared.CryptoSystemRootId = 0
X509RootClass3 shared.CryptoSystemRootId = 1
X509RootClass3s shared.CryptoSystemRootId = 2
X509Root3 shared.CryptoSystemRootId = 3
X509Root4 shared.CryptoSystemRootId = 4
X509Root5 shared.CryptoSystemRootId = 5
)
const (
X509ProfileClient shared.CertificateProfileId = 0
X509ProfileClientOrg shared.CertificateProfileId = 1
X509ProfileClientCodesign shared.CertificateProfileId = 2
X509ProfileClientMachine shared.CertificateProfileId = 3
X509ProfileClientAds shared.CertificateProfileId = 4
X509ProfileServer shared.CertificateProfileId = 5
X509ProfileServerOrg shared.CertificateProfileId = 6
X509ProfileServerJabber shared.CertificateProfileId = 7
X509ProfileOCSP shared.CertificateProfileId = 8
X509ProfileTimestamp shared.CertificateProfileId = 9
X509ProfileProxy shared.CertificateProfileId = 10
X509ProfileSubCA shared.CertificateProfileId = 11
)
const (
X509MDDefault shared.MessageDigestAlgorithmId = 0
X509MDMd5 shared.MessageDigestAlgorithmId = 1
X509MDSha1 shared.MessageDigestAlgorithmId = 2
X509MDRipeMD160 shared.MessageDigestAlgorithmId = 3
X509MDSha256 shared.MessageDigestAlgorithmId = 8
X509MDSha384 shared.MessageDigestAlgorithmId = 9
X509MDSha512 shared.MessageDigestAlgorithmId = 10
)
const (
OpenPGPRoot0 shared.CryptoSystemRootId = 0
)
const (
OpenPGPDefaultProfile shared.CertificateProfileId = 0
)
const (
OpenPGPDefaultMD shared.MessageDigestAlgorithmId = 0
)
func NewCommandProcessor() *CommandProcessor {
cryptoSystems := make(map[shared.CryptoSystemId]*CryptoSystem)
cryptoSystems[CsX509] = &CryptoSystem{
Name: "X.509",
Roots: map[shared.CryptoSystemRootId]*RootCredentials{
X509RootDefault: {
Name: "openssl",
PrivateKeyFile: "/srv/ca/CA/private/ca.key.pem",
CertificateFile: "/srv/ca/CA/ca.crt.pem",
DatabaseFile: "/srv/ca/CA/index.txt",
CRLNumber: "/srv/ca/CA/crlnumber",
},
X509RootClass3: {
Name: "class3",
PrivateKeyFile: "/srv/ca/class3/private/ca.key.pem",
CertificateFile: "/srv/ca/class3/ca.crt.pem",
DatabaseFile: "/srv/ca/class3/index.txt",
CRLNumber: "/srv/ca/class3/crlnumber",
},
X509RootClass3s: {Name: "class3s"},
X509Root3: {Name: "root3"},
X509Root4: {Name: "root4"},
X509Root5: {Name: "root5"},
},
Profiles: map[shared.CertificateProfileId]string{
X509ProfileClient: "client",
X509ProfileClientOrg: "client-org",
X509ProfileClientCodesign: "client-codesign",
X509ProfileClientMachine: "client-machine",
X509ProfileClientAds: "client-ads",
X509ProfileServer: "server",
X509ProfileServerOrg: "server-org",
X509ProfileServerJabber: "server-jabber",
X509ProfileOCSP: "ocsp",
X509ProfileTimestamp: "timestamp",
X509ProfileProxy: "proxy",
X509ProfileSubCA: "subca",
},
// constants for openssl invocations. Should be replaced with
// something more useful
DigestAlgorithms: map[shared.MessageDigestAlgorithmId]x509.SignatureAlgorithm{
X509MDDefault: x509.SHA256WithRSA,
X509MDMd5: x509.MD5WithRSA,
X509MDSha1: x509.SHA1WithRSA,
X509MDRipeMD160: x509.UnknownSignatureAlgorithm,
X509MDSha256: x509.SHA256WithRSA,
X509MDSha384: x509.SHA384WithRSA,
X509MDSha512: x509.SHA512WithRSA,
},
}
cryptoSystems[CsOpenPGP] = &CryptoSystem{
Name: "OpenPGP",
Roots: map[shared.CryptoSystemRootId]*RootCredentials{
OpenPGPRoot0: {
Name: "OpenPGP Root",
PrivateKeyFile: "secring0.gpg",
PublicKeyFile: "pubring0.gpg",
},
},
Profiles: map[shared.CertificateProfileId]string{
OpenPGPDefaultProfile: "default",
},
// constants for gnupg cert-digest-algo parameter. Should be replaced with
// something more useful
DigestAlgorithms: map[shared.MessageDigestAlgorithmId]x509.SignatureAlgorithm{
OpenPGPDefaultMD: x509.SHA256WithRSA,
},
}
return &CommandProcessor{CryptoSystems: cryptoSystems, Settings: NewCommandProcessorSettings()}
}