Compare commits

...

2 Commits

Author SHA1 Message Date
André Roth a7862627a9 fix build 2025-02-15 23:48:12 +01:00
Charles Duffy 43a4d06dbe Implement support for TPM-backed signing keys (#953) 2025-02-15 23:48:12 +01:00
3 changed files with 93 additions and 29 deletions
+1
View File
@@ -68,3 +68,4 @@ List of contributors, in chronological order:
* Blake Kostner (https://github.com/btkostner)
* Leigh London (https://github.com/leighlondon)
* Gordian Schoenherr (https://github.com/schoenherrg)
* Charles Duffy (https://github.com/charles-dyfis-net)
+1 -1
View File
@@ -1622,7 +1622,7 @@ GPG passphrase\-file for the key (warning: could be insecure)
.
.TP
\-\fBsecret\-keyring\fR=
GPG secret keyring to use (instead of default)
GPG secret keyring to use (instead of default); may be of the form \fBtpm://HANDLE?dev=DEVICE\fR to use a TPM-backed key if the selected \fBgpgProvider\fR is \fBinternal\fR, where \fBHANDLE\fR is of the form \fB0x81000003\fR, and \fBdev\fR is a (URL-escaped) value similar to \fB/dev/tpmrm0\fR (which happens to be the default if not given).
.
.TP
\-\fBskip\-bz2\fR
+85 -22
View File
@@ -4,17 +4,22 @@ import (
"bytes"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"syscall"
"time"
"github.com/folbricht/tpmk"
"github.com/google/go-tpm/tpmutil"
"github.com/pkg/errors"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
"github.com/ProtonMail/go-crypto/openpgp/armor"
openpgp_errors "github.com/ProtonMail/go-crypto/openpgp/errors"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"golang.org/x/term"
@@ -38,12 +43,33 @@ type GoSigner struct {
passphrase, passphraseFile string
batch bool
tpmPrivateKey *tpmk.RSAPrivateKey
publicKeyring openpgp.EntityList
secretKeyring openpgp.EntityList
signer *openpgp.Entity
signerConfig *packet.Config
}
func findKey(keyRef string, keyring openpgp.EntityList) *openpgp.Entity {
for _, signer := range keyring {
key := KeyFromUint64(signer.PrimaryKey.KeyId)
if key.Matches(Key(keyRef)) {
return signer
}
if !validEntity(signer) {
continue
}
for name := range signer.Identities {
if strings.Contains(name, keyRef) {
return signer
}
}
}
return nil
}
// SetBatch controls whether we allowed to interact with user, for example
// for getting the passphrase from stdin.
func (g *GoSigner) SetBatch(batch bool) {
@@ -104,12 +130,56 @@ func (g *GoSigner) Init() error {
return errors.Wrap(err, "error loading public keyring")
}
if strings.HasPrefix(g.secretKeyringFile, "tpm://") {
// Expected form of tpm://0x81000002 -- optionally with query parameters holding extra values
// f/e, ?dev=%2Fdev%2Ftpmrm1 to specify the device as /dev/tpmrm1; or ?dev=sim for simulator
tpmSecretURL, err := url.Parse(g.secretKeyringFile)
if err != nil {
return errors.Wrap(err, "parsing TPM URI")
}
tpmQueryArgs := tpmSecretURL.Query()
devStrings, hasDev := tpmQueryArgs["dev"]
tpmDevFilename := "/dev/tpmrm0"
if hasDev && len(devStrings) != 0 {
if len(devStrings) > 1 {
return errors.Errorf("Parsing TPM address, more than one device name found")
}
tpmDevFilename = devStrings[0]
}
tpmDev, err := tpmk.OpenDevice(tpmDevFilename)
if err != nil {
return errors.Wrap(err, "opening TPM device")
}
tpmHandleInt, err := strconv.ParseUint(tpmSecretURL.Host, 0, 32)
if err != nil {
return errors.Wrap(err, "parsing TPM URI host as integer handle")
}
tpmHandle := tpmutil.Handle(tpmHandleInt)
privKey, err := tpmk.NewRSAPrivateKey(tpmDev, tpmHandle, g.passphrase)
if err != nil {
return errors.Wrap(err, "opening TPM key handle")
}
g.tpmPrivateKey = &privKey
} else {
g.secretKeyring, err = loadKeyRing(g.secretKeyringFile, false)
if err != nil {
return errors.Wrap(err, "error load secret keyring")
}
}
if g.keyRef == "" {
if g.secretKeyring == nil {
// Happens if our private key is TPM-backed; means we only have a public key
if g.keyRef == "" && len(g.publicKeyring) == 1 {
g.signer = g.publicKeyring[0]
} else if g.keyRef != "" {
g.signer = findKey(g.keyRef, g.publicKeyring)
if g.signer == nil {
return errors.Errorf("couldn't find key for key reference %+v in public keyring", g.keyRef)
}
} else {
return errors.Errorf("must either only have our signing key in the public keyring, or provide the identity of the signing key when in tpm mode")
}
} else if g.keyRef == "" {
// no key reference, pick the first key
for _, signer := range g.secretKeyring {
if !validEntity(signer) {
@@ -124,28 +194,9 @@ func (g *GoSigner) Init() error {
return fmt.Errorf("looks like there are no keys in gpg, please create one (official manual: http://www.gnupg.org/gph/en/manual.html)")
}
} else {
pickKeyLoop:
for _, signer := range g.secretKeyring {
key := KeyFromUint64(signer.PrimaryKey.KeyId)
if key.Matches(Key(g.keyRef)) {
g.signer = signer
break
}
if !validEntity(signer) {
continue
}
for name := range signer.Identities {
if strings.Contains(name, g.keyRef) {
g.signer = signer
break pickKeyLoop
}
}
}
g.signer = findKey(g.keyRef, g.secretKeyring)
if g.signer == nil {
return errors.Errorf("couldn't find key for key reference %v", g.keyRef)
return errors.Errorf("couldn't find key for key reference %v in private keyring", g.keyRef)
}
}
@@ -232,10 +283,22 @@ func (g *GoSigner) DetachedSign(source string, destination string) error {
}
defer signature.Close()
if g.tpmPrivateKey != nil {
encoder, err := armor.Encode(signature, openpgp.SignatureType, nil)
if err != nil {
return errors.Wrap(err, "error creating armoring encoder")
}
defer encoder.Close()
err = tpmk.OpenPGPDetachSign(encoder, g.signer, message, nil, g.tpmPrivateKey)
if err != nil {
return errors.Wrap(err, "error creating detached signature with TPM-backed key")
}
} else {
err = openpgp.ArmoredDetachSign(signature, g.signer, message, g.signerConfig)
if err != nil {
return errors.Wrap(err, "error creating detached signature")
}
}
return nil
}