406 lines
10 KiB
Go
406 lines
10 KiB
Go
package signer
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto"
|
|
"crypto/x509"
|
|
"encoding/hex"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"unsafe"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/sys/unix"
|
|
|
|
"git.cacert.org/cacert-gosigner/datastructures"
|
|
"git.cacert.org/cacert-gosigner/shared"
|
|
"git.cacert.org/cacert-gosigner/signer/openpgpops"
|
|
"git.cacert.org/cacert-gosigner/signer/x509ops"
|
|
)
|
|
|
|
type CommandProcessorSettings struct {
|
|
CABaseDir string
|
|
XDeltaPath string
|
|
OpenPGPKeyRingDir 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) (
|
|
*datastructures.SignerResponse,
|
|
error,
|
|
) {
|
|
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:
|
|
return p.handleNulAction(command)
|
|
case shared.ActionSign:
|
|
return p.handleSignAction(command)
|
|
case shared.ActionRevoke:
|
|
return p.handleRevokeAction(command)
|
|
default:
|
|
return nil, fmt.Errorf(
|
|
"unsupported Action 0x%02x %s",
|
|
int(command.Action),
|
|
command.Action,
|
|
)
|
|
}
|
|
}
|
|
|
|
func (*CommandProcessor) handleNulAction(command *datastructures.SignerRequest) (
|
|
*datastructures.SignerResponse,
|
|
error,
|
|
) {
|
|
var timeSpec unix.Timespec
|
|
|
|
err := unix.ClockGettime(unix.CLOCK_REALTIME, &timeSpec)
|
|
if err != nil {
|
|
log.Errorf("could not get system time: %v", err)
|
|
}
|
|
|
|
log.Debugf("current system time is %v", timeSpec)
|
|
// TODO: calculate the actual system time from the payload
|
|
_, _, e1 := unix.Syscall(
|
|
unix.SYS_CLOCK_SETTIME,
|
|
uintptr(unix.CLOCK_REALTIME),
|
|
uintptr(unsafe.Pointer(&timeSpec)),
|
|
0,
|
|
)
|
|
if e1 != 0 {
|
|
log.Errorf("could not set system time: %v", e1)
|
|
}
|
|
|
|
return datastructures.NewNulResponse(command.Version), nil
|
|
}
|
|
|
|
func (p *CommandProcessor) handleSignAction(
|
|
command *datastructures.SignerRequest,
|
|
) (
|
|
*datastructures.SignerResponse,
|
|
error,
|
|
) {
|
|
log.Debugf("handle sign call: %s", command)
|
|
|
|
idSystem, err := p.checkIdentitySystem(
|
|
command.System, command.Root, command.Profile, command.MdAlgorithm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Debugf("identified id system: %s", idSystem)
|
|
|
|
switch command.System {
|
|
case CsX509:
|
|
request := command.Content1
|
|
san := command.Content2
|
|
subject := command.Content3
|
|
|
|
content, err := p.signX509Certificate(idSystem, command.Days, command.Spkac, request, san, subject)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not sign X.509 certificate: %w", err)
|
|
}
|
|
|
|
return datastructures.NewSignResponse(command.Version, content), nil
|
|
case CsOpenPGP:
|
|
pubKey := command.Content1
|
|
|
|
content, err := p.signOpenpgpKey(idSystem, command.Days, pubKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not sign OpenPGP key: %w", err)
|
|
}
|
|
|
|
return datastructures.NewSignResponse(command.Version, content), nil
|
|
default:
|
|
return nil, fmt.Errorf("sign not implemented for crypto system %s", idSystem.System)
|
|
}
|
|
}
|
|
|
|
func (p *CommandProcessor) handleRevokeAction(
|
|
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
|
|
|
|
content, err := p.revokeX509(idSystem, request, clientHash)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not revoke X.509 certificate: %w", err)
|
|
}
|
|
|
|
return datastructures.NewRevokeResponse(command.Version, content), nil
|
|
default:
|
|
return nil, fmt.Errorf("revoke not implemented for crypto system %s", idSystem.System)
|
|
}
|
|
}
|
|
|
|
type IDSystemParams struct {
|
|
System *CryptoSystem
|
|
Root interface{}
|
|
Profile interface{}
|
|
MessageDigestAlgorithm interface{}
|
|
}
|
|
|
|
func (s *IDSystemParams) String() string {
|
|
return fmt.Sprintf("%s r:%s p:%s m:%s", s.System, s.Root, s.Profile, s.MessageDigestAlgorithm)
|
|
}
|
|
|
|
func (p *CommandProcessor) checkIdentitySystem(
|
|
systemID shared.CryptoSystemID,
|
|
rootID shared.CryptoSystemRootID,
|
|
profileID shared.CertificateProfileID,
|
|
algorithmID shared.SignatureAlgorithmID,
|
|
) (*IDSystemParams, 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 %d for crypto system %s",
|
|
rootID,
|
|
cryptoSystem,
|
|
)
|
|
}
|
|
|
|
profile, ok := p.CryptoSystems[systemID].Profiles[profileID]
|
|
if !ok {
|
|
return nil, fmt.Errorf(
|
|
"invalid 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 &IDSystemParams{
|
|
System: cryptoSystem,
|
|
Root: root,
|
|
Profile: profile,
|
|
MessageDigestAlgorithm: mdAlgorithm,
|
|
}, nil
|
|
}
|
|
|
|
func (p *CommandProcessor) revokeX509(system *IDSystemParams, request []byte, clientHash []byte) ([]byte, error) {
|
|
x509Root := system.Root.(*x509ops.Root)
|
|
signatureAlgorithm := system.MessageDigestAlgorithm.(x509.SignatureAlgorithm)
|
|
|
|
log.Debugf("revoke X.509 for root %s", x509Root)
|
|
log.Debugf("hash bytes from client %x", clientHash)
|
|
|
|
var (
|
|
err error
|
|
crlBytes []byte
|
|
newHash *[20]byte
|
|
)
|
|
|
|
if len(request) > 0 {
|
|
_, err = x509Root.RevokeCertificate(request)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not revoke certificate / create CRL: %w", err)
|
|
}
|
|
}
|
|
|
|
crlBytes, newHash, err = x509Root.GenerateCrl(signatureAlgorithm)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not generate a new CRL for root %s: %w", x509Root, err)
|
|
}
|
|
|
|
log.Debugf("crlBytes: %d", len(crlBytes))
|
|
|
|
var content []byte
|
|
|
|
oldCrlFile := x509Root.GetCrlFileName(string(clientHash))
|
|
newCrlFile := x509Root.GetCrlFileName(hex.EncodeToString(newHash[:]))
|
|
|
|
statOld, oldErr := os.Stat(oldCrlFile)
|
|
statNew, newErr := os.Stat(newCrlFile)
|
|
|
|
if oldErr == nil && newErr == nil && !statOld.IsDir() && !statNew.IsDir() {
|
|
content, err = p.buildXDelta(oldCrlFile, newCrlFile)
|
|
if err != nil {
|
|
log.Warnf("could not generate xdelta: %v", err)
|
|
}
|
|
|
|
log.Tracef("xdelta produced %d bytes", len(content))
|
|
}
|
|
|
|
if content == nil {
|
|
content = pem.EncodeToMemory(&pem.Block{
|
|
Type: "X509 CRL",
|
|
Bytes: crlBytes,
|
|
})
|
|
log.Tracef("sending %d PEM CRL bytes", len(content))
|
|
}
|
|
|
|
return content, nil
|
|
}
|
|
|
|
func (p *CommandProcessor) buildXDelta(oldFile string, newFile string) ([]byte, error) {
|
|
patchFile, err := ioutil.TempFile(os.TempDir(), "*.patch")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create temporary file for patch: %w", err)
|
|
}
|
|
|
|
patchName := patchFile.Name()
|
|
|
|
defer func() {
|
|
if err := os.Remove(patchName); err != nil {
|
|
log.Warnf("could not remove temporary file %s: %v", patchName, err)
|
|
}
|
|
}()
|
|
|
|
if err = patchFile.Close(); err != nil {
|
|
return nil, fmt.Errorf("could not close temporary file: %w", err)
|
|
}
|
|
|
|
buf := bytes.NewBuffer([]byte{})
|
|
|
|
// #nosec G204 no parameters are based on user input
|
|
cmd := exec.Command(p.Settings.XDeltaPath, "delta", oldFile, newFile, patchName)
|
|
cmd.Stdout = buf
|
|
cmd.Stderr = buf
|
|
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
var e *exec.ExitError
|
|
if !errors.As(err, &e) || e.ExitCode() != 1 {
|
|
// xdelta delta exits with status code 1 if a delta has been found
|
|
return nil, fmt.Errorf(
|
|
"xdelta command '%s' did not work correctly: %w\noutput was:\n%s",
|
|
strings.Join(cmd.Args, " "),
|
|
err,
|
|
buf.String(),
|
|
)
|
|
}
|
|
}
|
|
|
|
return ioutil.ReadFile(patchName)
|
|
}
|
|
|
|
func (p *CommandProcessor) signX509Certificate(
|
|
system *IDSystemParams,
|
|
days uint16,
|
|
spkac uint8,
|
|
request []byte,
|
|
san []byte,
|
|
subject []byte,
|
|
) ([]byte, error) {
|
|
x509Root := system.Root.(*x509ops.Root)
|
|
signatureAlgorithm := system.MessageDigestAlgorithm.(x509.SignatureAlgorithm)
|
|
profile := system.Profile.(*x509ops.Profile)
|
|
|
|
log.Debugf(
|
|
"sign X.509 certificate for root %s using profile %s and signature algorithm %s",
|
|
x509Root,
|
|
profile,
|
|
signatureAlgorithm,
|
|
)
|
|
log.Debugf(
|
|
"client wants %d days spkac is %v, requested subject '%s' and subjectAlternativeNames '%s'",
|
|
days, spkac == 1, subject, san,
|
|
)
|
|
log.Tracef("the following CSR should be signed\n%s", request)
|
|
|
|
content, err := x509Root.SignCertificate(
|
|
profile,
|
|
signatureAlgorithm,
|
|
&x509ops.SigningRequestParameters{
|
|
Request: request,
|
|
Subject: subject,
|
|
SubjectAlternativeNames: san,
|
|
Days: days,
|
|
IsSpkac: spkac == 1,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not sign X.509 CSR with root %s and profile %s: %w", x509Root, profile, err)
|
|
}
|
|
|
|
return content, nil
|
|
}
|
|
|
|
func (p *CommandProcessor) signOpenpgpKey(system *IDSystemParams, days uint16, pubKey []byte) ([]byte, error) {
|
|
openPgpRoot := system.Root.(*openpgpops.OpenPGPRoot)
|
|
signatureAlgorithm := system.MessageDigestAlgorithm.(crypto.Hash)
|
|
|
|
log.Debugf("sign openpgpops for root %s", openPgpRoot)
|
|
|
|
content, err := openPgpRoot.SignPublicKey(pubKey, signatureAlgorithm, days)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not sign openpgpops public key with root %s: %w", openPgpRoot, err)
|
|
}
|
|
|
|
return content, nil
|
|
}
|
|
|
|
func NewCommandProcessorSettings() *CommandProcessorSettings {
|
|
caBasedir, ok := os.LookupEnv("SIGNER_BASEDIR")
|
|
if !ok {
|
|
caBasedir = "."
|
|
}
|
|
|
|
gpgKeyringDir, ok := os.LookupEnv("SIGNER_GPG_KEYRING_DIR")
|
|
if !ok {
|
|
gpgKeyringDir = "."
|
|
}
|
|
|
|
gpgUIDEmail, ok := os.LookupEnv("SIGNER_GPG_ID")
|
|
if !ok {
|
|
gpgUIDEmail = "gpg@cacert.org"
|
|
}
|
|
|
|
return &CommandProcessorSettings{
|
|
CABaseDir: caBasedir,
|
|
OpenPGPKeyRingDir: gpgKeyringDir,
|
|
OpenPGPUidEmail: gpgUIDEmail,
|
|
XDeltaPath: "/usr/bin/xdelta",
|
|
}
|
|
}
|