diff --git a/README.md b/README.md index 3f1a134..ec8adfc 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,14 @@ [![GoDoc](https://godoc.org/github.com/folbricht/tpmk?status.svg)](https://godoc.org/github.com/folbricht/tpmk) -This toolkit strives to simplify common tasks around key and certificates involving TPM2. It also provides the tools necessary to make use of keys in the module for TLS connections in Go. It does not attempt to provide a feature-rich interface to support all possible use-cases and features. tpmk consists of a Go library and a tool with a simple interface. It currently provides: +This toolkit strives to simplify common tasks around key and certificates involving TPM2. It also provides the tools necessary to make use of keys in the module for TLS connections and OpenPGP in Go. It does not attempt to provide a feature-rich interface to support all possible use-cases and features. tpmk consists of a Go library and a tool with a simple interface. It currently provides: - Generating RSA/SHA256 primary keys in the TPM and exporting the public key - Generating x509 certificates and signing with a (file) CA -- Writing of arbirary data to NV storage - intended to be used to store certificates +- Writing of arbitrary data to NV storage - intended to be used to store certificates - Creating SSH certificates for keys in the TPM +- Generating OpenPGP public keys based on RSA keys in the TPM +- OpenPGP Signing and Decryption A range of features and options are **not** available at this point, but may be implemented in the future. Suggestions and contributions are welcome. @@ -52,6 +54,12 @@ go get -u github.com/folbricht/tpmk/cmd/tpmk - `pub` Convert a PKCS1 key to OpenSSH format - `client` Start SSH client and execute remote command +- `openpgp` Commands to use keys in OpenPGP format + + - `generate` Create an OpenPGP identity based on a key in the TPM + - `sign` Sign data with a TPM key + - `decrypt` Decrypt data using the private TPM key + ## Use-cases / Examples ### RSA key generation and certificate storage in NV @@ -242,6 +250,22 @@ if err != nil { fmt.Println(string(b)) ``` +### Create an OpenPGP identity and sign data with it + +This example shows how to produce an OpenPGP public key (identity) and use it to sign data. + +In order to sign with a TPM key, an OpenPGP identity needs to be created with name and email address. This identity contains the public key and should be stored (either separately or in a TPM NV index). For this command, the key (handle 0x81000000 in this example) has to be present in the TPM already and must have the `sign` attribute to allow signing. + +```sh +tpmk openpgp generate -n Testing -e test@example.com 0x81000000 identity.pgp +``` + +The same key and identity should be used when signing data. In this case a detached and armored signature is created and written to STDOUT. + +```sh +tpmk openpgp sign -a 0x81000000 identity.pgp data.txt - +``` + ## Links - TPM2 specification - [https://trustedcomputinggroup.org/resource/tpm-library-specification/](https://trustedcomputinggroup.org/resource/tpm-library-specification/) diff --git a/cmd/tpmk/main.go b/cmd/tpmk/main.go index b85190c..47d32a1 100644 --- a/cmd/tpmk/main.go +++ b/cmd/tpmk/main.go @@ -14,6 +14,7 @@ func main() { newKeyCommand(), newx509Command(), newSSHCommand(), + newOpenPGPCommand(), ) rootCmd.SetOutput(os.Stderr) diff --git a/cmd/tpmk/openpgp.go b/cmd/tpmk/openpgp.go new file mode 100644 index 0000000..75bdd38 --- /dev/null +++ b/cmd/tpmk/openpgp.go @@ -0,0 +1,19 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +func newOpenPGPCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "openpgp", + Short: "Manage OpenPGP identities and sign and decrypt data", + SilenceUsage: true, + } + cmd.AddCommand( + newOpenPGPGenCommand(), + newOpenPGPSignCommand(), + newOpenPGPDecryptCommand(), + ) + return cmd +} diff --git a/cmd/tpmk/openpgpdecrypt.go b/cmd/tpmk/openpgpdecrypt.go new file mode 100644 index 0000000..245af73 --- /dev/null +++ b/cmd/tpmk/openpgpdecrypt.go @@ -0,0 +1,149 @@ +package main + +import ( + "io" + "os" + + "github.com/folbricht/tpmk" + "github.com/pkg/errors" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + "golang.org/x/crypto/openpgp/packet" + + "github.com/spf13/cobra" +) + +type openpgpDecryptOptions struct { + armor bool + pubArmor bool + device string + password string +} + +func newOpenPGPDecryptCommand() *cobra.Command { + var opt openpgpDecryptOptions + + cmd := &cobra.Command{ + Use: "decrypt ", + Short: "Decrypt data with a TPM key", + Long: `Decrypt data using an existing private key in the TPM. +The key must already be present and be an RSA key. Input can either +be a file or '-' to read the data from STDIN. Use '-' to write the +output to STDOUT. + +This command does not validate any signatures, but it will exit with +a non-zero exit code if an invalid MAC is encountered. Note that the +decrypted data is written to the output even though validation may +only happen after it is fully decrypted. The exit code has to be used +to determine if decryption was successful. + +While the public key can also be read from STDIN, it is not possible +to read the input data from there at the same time.`, + Example: ` tpmk openpgp decrypt 0x81000000 pub.pgp encrypted.pgp decrypted.txt + tpmk openpgp decrypt -a 0x81000000 pup.pgp - - + tpmk openpgp decrypt -a -m 0x81000000 - secret.txt.pgp -`, + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + return runOpenPGPDecrypt(opt, args) + }, + SilenceUsage: true, + } + flags := cmd.Flags() + flags.BoolVarP(&opt.armor, "armor", "a", false, "Input data is armored") + flags.BoolVarP(&opt.pubArmor, "public-armor", "m", false, "Public key is armored") + flags.StringVarP(&opt.device, "device", "d", "/dev/tpmrm0", "TPM device, 'sim' for simulator") + flags.StringVarP(&opt.password, "password", "p", "", "Password for the TPM key") + return cmd +} + +func runOpenPGPDecrypt(opt openpgpDecryptOptions, args []string) error { + handle, err := parseHandle(args[0]) + if err != nil { + return err + } + pubkey := args[1] + input := args[2] + output := args[3] + + if pubkey == "-" && input == "-" { + return errors.New("only the public key or the input can be read from stdin, not both") + } + + // Open device or simulator + dev, err := tpmk.OpenDevice(opt.device) + if err != nil { + return err + } + defer dev.Close() + + // Open the TPM key + priv, err := tpmk.NewRSAPrivateKey(dev, handle, opt.password) + if err != nil { + return err + } + + // Read the public key + var pubData io.Reader = os.Stdin + if pubkey != "-" { + f, err := os.Open(pubkey) + if err != nil { + return err + } + defer f.Close() + pubData = f + } + if opt.pubArmor { + block, err := armor.Decode(pubData) + if err != nil { + return err + } + if block.Type != openpgp.PublicKeyType { + return errors.New("not a public key") + } + pubData = block.Body + } + entity, err := tpmk.ReadOpenPGPEntity(packet.NewReader(pubData), priv) + if err != nil { + return err + } + + // Data reader + var r io.Reader = os.Stdin + if input != "-" { + f, err := os.Open(input) + if err != nil { + return err + } + defer f.Close() + r = f + } + if opt.armor { + block, err := armor.Decode(r) + if err != nil { + return err + } + r = block.Body + } + + // Output writer + var w io.Writer = os.Stdout + if output != "-" { + f, err := os.Create(output) + if err != nil { + return err + } + defer f.Close() + w = f + } + + // Decrypt the data, md.UnverifiedBody needs to be read whole before any signature or + // MAC is checked. + md, err := openpgp.ReadMessage(r, openpgp.EntityList([]*openpgp.Entity{entity}), nil, nil) + if err != nil { + return err + } + if _, err = io.Copy(w, md.UnverifiedBody); err != nil { + return err + } + return md.SignatureError +} diff --git a/cmd/tpmk/openpgpgen.go b/cmd/tpmk/openpgpgen.go new file mode 100644 index 0000000..84095aa --- /dev/null +++ b/cmd/tpmk/openpgpgen.go @@ -0,0 +1,101 @@ +package main + +import ( + "io" + "os" + + "github.com/folbricht/tpmk" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + + "github.com/spf13/cobra" +) + +type openpgpGenOptions struct { + armor bool + device string + password string + name string + comment string + email string +} + +func newOpenPGPGenCommand() *cobra.Command { + var opt openpgpGenOptions + + cmd := &cobra.Command{ + Use: "generate ", + Short: "Generate an public key", + Long: `Generate an OpenPGP public key using an existing private key in the TPM. +The key must already be present and be an RSA key. The generated public +key will contain one identity which must be provided with -n and -e. + +Use '-' to write the public key to STDOUT.`, + Example: ` tpmk openpgp generate -n Testing -e test@example.com 0x81000000 pub.pgp + tpmk openpgp generate -a -n Testing -e test@example.com 0x81000000 -`, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + return runOpenPGPGen(opt, args) + }, + SilenceUsage: true, + } + flags := cmd.Flags() + flags.BoolVarP(&opt.armor, "armor", "a", false, "Create ASCII armored output") + flags.StringVarP(&opt.name, "name", "n", "", "Identity name") + flags.StringVarP(&opt.comment, "comment", "c", "", "Identity comment") + flags.StringVarP(&opt.email, "email", "e", "", "Identity email") + flags.StringVarP(&opt.device, "device", "d", "/dev/tpmrm0", "TPM device, 'sim' for simulator") + flags.StringVarP(&opt.password, "password", "p", "", "Password for the TPM key") + _ = cmd.MarkFlagRequired("name") + _ = cmd.MarkFlagRequired("email") + return cmd +} + +func runOpenPGPGen(opt openpgpGenOptions, args []string) error { + handle, err := parseHandle(args[0]) + if err != nil { + return err + } + output := args[1] + + // Open device or simulator + dev, err := tpmk.OpenDevice(opt.device) + if err != nil { + return err + } + defer dev.Close() + + // Open the TPM key + priv, err := tpmk.NewRSAPrivateKey(dev, handle, opt.password) + if err != nil { + return err + } + + // Build an identity with the TPM key + entity, err := tpmk.NewOpenPGPEntity(opt.name, opt.comment, opt.email, nil, priv) + if err != nil { + return err + } + + var w io.Writer = os.Stdout + if output != "-" { + f, err := os.Create(output) + if err != nil { + return err + } + defer f.Close() + w = f + } + + if opt.armor { + encoder, err := armor.Encode(w, openpgp.PublicKeyType, nil) + if err != nil { + return err + } + defer encoder.Close() + w = encoder + } + + // Serialize the entity (public part) + return entity.Serialize(w) +} diff --git a/cmd/tpmk/openpgpsign.go b/cmd/tpmk/openpgpsign.go new file mode 100644 index 0000000..f8257d7 --- /dev/null +++ b/cmd/tpmk/openpgpsign.go @@ -0,0 +1,158 @@ +package main + +import ( + "io" + "os" + + "github.com/folbricht/tpmk" + "github.com/pkg/errors" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + "golang.org/x/crypto/openpgp/clearsign" + "golang.org/x/crypto/openpgp/packet" + + "github.com/spf13/cobra" +) + +type openpgpSignOptions struct { + armor bool + pubArmor bool + clearSigned bool + device string + password string +} + +func newOpenPGPSignCommand() *cobra.Command { + var opt openpgpSignOptions + + cmd := &cobra.Command{ + Use: "sign ", + Short: "Sign data with a TPM key", + Long: `Signs data using an existing private key in the TPM. +The key must already be present and be an RSA key. Generates a +signature for the data. Input can either be a file or '-' to +read the data from STDIN. Use '-' to write the signature to STDOUT. + +By default a detached signature is produced. Use --clear-sign to +generate a clear text signature instead. + +While the public key can also be read from STDIN, it is not possible +to read the input data from there at +the same time.`, + Example: ` tpmk openpgp sign 0x81000000 pub.pgp input.txt input.sig + tpmk openpgp sign -a 0x81000000 pup.pgp - - + tpmk openpgp sign -c -m 0x81000000 public.gpg input.txt input.txt.asc + tpmk openpgp sign -a -m 0x81000000 - input.txt -`, + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + return runOpenPGPSign(opt, args) + }, + SilenceUsage: true, + } + flags := cmd.Flags() + flags.BoolVarP(&opt.armor, "armor", "a", false, "Create ASCII armored output") + flags.BoolVarP(&opt.pubArmor, "public-armor", "m", false, "Public key is armored") + flags.BoolVarP(&opt.clearSigned, "clear-sign", "c", false, "Output a clear-text signature") + flags.StringVarP(&opt.device, "device", "d", "/dev/tpmrm0", "TPM device, 'sim' for simulator") + flags.StringVarP(&opt.password, "password", "p", "", "Password for the TPM key") + return cmd +} + +func runOpenPGPSign(opt openpgpSignOptions, args []string) error { + handle, err := parseHandle(args[0]) + if err != nil { + return err + } + pubkey := args[1] + input := args[2] + output := args[3] + + if pubkey == "-" && input == "-" { + return errors.New("only the public key or the input can be read from stdin, not both") + } + + if opt.clearSigned && opt.armor { + return errors.New("can't use --armor with --clear-sign, already armored") + } + + // Open device or simulator + dev, err := tpmk.OpenDevice(opt.device) + if err != nil { + return err + } + defer dev.Close() + + // Open the TPM key + priv, err := tpmk.NewRSAPrivateKey(dev, handle, opt.password) + if err != nil { + return err + } + + // Read the public key + var pubData io.Reader = os.Stdin + if pubkey != "-" { + f, err := os.Open(pubkey) + if err != nil { + return err + } + defer f.Close() + pubData = f + } + if opt.pubArmor { + block, err := armor.Decode(pubData) + if err != nil { + return err + } + if block.Type != openpgp.PublicKeyType { + return errors.New("not a public key") + } + pubData = block.Body + } + entity, err := tpmk.ReadOpenPGPEntity(packet.NewReader(pubData), priv) + if err != nil { + return err + } + + // Data reader + var r io.Reader = os.Stdin + if input != "-" { + f, err := os.Open(input) + if err != nil { + return err + } + defer f.Close() + r = f + } + + // Signature writer + var w io.Writer = os.Stdout + if output != "-" { + f, err := os.Create(output) + if err != nil { + return err + } + defer f.Close() + w = f + } + + if opt.armor { + encoder, err := armor.Encode(w, openpgp.SignatureType, nil) + if err != nil { + return err + } + defer encoder.Close() + w = encoder + } + + // Generate and write the signature + if opt.clearSigned { + wc, err := clearsign.Encode(w, entity.PrivateKey, nil) + if err != nil { + return err + } + defer wc.Close() + _, err = io.Copy(wc, r) + return err + } + return openpgp.DetachSign(w, entity, r, nil) +} diff --git a/go.mod b/go.mod index 993d022..32e1f96 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/folbricht/tpmk -go 1.11 +go 1.15 require ( github.com/folbricht/sshtest v0.1.0 @@ -11,6 +11,6 @@ require ( github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.3 // indirect github.com/stretchr/testify v1.3.0 - golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576 + golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d ) diff --git a/go.sum b/go.sum index 22eaad0..fd20884 100644 --- a/go.sum +++ b/go.sum @@ -19,14 +19,21 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576 h1:aUX/1G2gFSs4AsJJg2cL3HuoRhCSCz733FE5GUSuaT4= golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc h1:+q90ECDSAQirdykUN6sPEiBXBsp8Csjcca8Oy7bgLTA= +golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/openpgp.go b/openpgp.go new file mode 100644 index 0000000..fb62712 --- /dev/null +++ b/openpgp.go @@ -0,0 +1,109 @@ +package tpmk + +import ( + "crypto" + "crypto/rsa" + "fmt" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/errors" + "golang.org/x/crypto/openpgp/packet" + "golang.org/x/crypto/openpgp/s2k" +) + +func NewOpenPGPEntity(name, comment, email string, config *packet.Config, signer crypto.Signer) (*openpgp.Entity, error) { + now := config.Now() + + pub, ok := signer.Public().(*rsa.PublicKey) + if !ok { + return nil, errors.InvalidArgumentError("signer must be an rsa key") + } + + uid := packet.NewUserId(name, comment, email) + if uid == nil { + return nil, errors.InvalidArgumentError("user id field contained invalid characters") + } + + e := &openpgp.Entity{ + PrimaryKey: packet.NewRSAPublicKey(now, pub), + PrivateKey: packet.NewSignerPrivateKey(now, signer), + Identities: make(map[string]*openpgp.Identity), + } + isPrimaryId := true + e.Identities[uid.Id] = &openpgp.Identity{ + Name: uid.Id, + UserId: uid, + SelfSignature: &packet.Signature{ + CreationTime: now, + SigType: packet.SigTypePositiveCert, + PubKeyAlgo: packet.PubKeyAlgoRSA, + Hash: config.Hash(), + IsPrimaryId: &isPrimaryId, + FlagsValid: true, + FlagSign: true, + FlagCertify: true, + IssuerKeyId: &e.PrimaryKey.KeyId, + }, + } + if err := e.Identities[uid.Id].SelfSignature.SignUserId(uid.Id, e.PrimaryKey, e.PrivateKey, config); err != nil { + return nil, err + } + + // Set the PreferredHash for the SelfSignature if one was provided in the config + if config != nil && config.DefaultHash != 0 { + v, ok := s2k.HashToHashId(config.DefaultHash) + if !ok { + return nil, fmt.Errorf("unsupported hash: %s", config.DefaultHash.String()) + } + e.Identities[uid.Id].SelfSignature.PreferredHash = []uint8{v} + } + + // Set DefaultCipher if one was provided + if config != nil && config.DefaultCipher != 0 { + e.Identities[uid.Id].SelfSignature.PreferredSymmetric = []uint8{uint8(config.DefaultCipher)} + } + + e.Subkeys = []openpgp.Subkey{ + { + PublicKey: packet.NewRSAPublicKey(now, pub), + PrivateKey: packet.NewSignerPrivateKey(now, signer), + Sig: &packet.Signature{ + CreationTime: now, + SigType: packet.SigTypeSubkeyBinding, + PubKeyAlgo: packet.PubKeyAlgoRSA, + Hash: config.Hash(), + FlagsValid: true, + FlagEncryptStorage: true, + FlagEncryptCommunications: true, + IssuerKeyId: &e.PrimaryKey.KeyId, + }, + }, + } + e.Subkeys[0].PublicKey.IsSubkey = true + e.Subkeys[0].PrivateKey.IsSubkey = true + err := e.Subkeys[0].Sig.SignKey(e.Subkeys[0].PublicKey, e.PrivateKey, config) + return e, err +} + +// ReadOpenPGPEntity reads a public key and returns an openpgp.Entity with the provided crypto.Signer. +// The returned entity can be used for signing or decryption. The private key must match the primary +// public key. +func ReadOpenPGPEntity(packets *packet.Reader, signer crypto.Signer) (*openpgp.Entity, error) { + e, err := openpgp.ReadEntity(packets) + if err != nil { + return nil, err + } + privateKey := packet.NewSignerPrivateKey(e.PrimaryKey.CreationTime, signer) + + if privateKey.KeyId != e.PrimaryKey.KeyId { + return nil, fmt.Errorf("id of private key %q does not match public key %q", privateKey.KeyIdString(), e.PrimaryKey.KeyIdString()) + } + e.PrivateKey = privateKey + for i := range e.Subkeys { + if e.Subkeys[i].PublicKey.KeyId == privateKey.KeyId { + e.Subkeys[i].PrivateKey = privateKey + e.Subkeys[i].PrivateKey.IsSubkey = true + } + } + return e, nil +} diff --git a/openpgp_test.go b/openpgp_test.go new file mode 100644 index 0000000..f5cd1c8 --- /dev/null +++ b/openpgp_test.go @@ -0,0 +1,131 @@ +package tpmk + +import ( + "bytes" + "crypto" + "io" + "io/ioutil" + "strings" + "testing" + + "github.com/google/go-tpm-tools/simulator" + "github.com/google/go-tpm/tpm2" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/clearsign" + "golang.org/x/crypto/openpgp/packet" +) + +func TestOpenPGPSign(t *testing.T) { + dev, err := simulator.Get() + require.NoError(t, err) + defer dev.Close() + + const ( + handle = 0x81000000 + pw = "" + attr = tpm2.FlagSign | tpm2.FlagUserWithAuth | tpm2.FlagSensitiveDataOrigin + ) + + // Generate and use the key in the TPM for signing + _, err = GenRSAPrimaryKey(dev, handle, pw, pw, attr) + require.NoError(t, err) + priv, err := NewRSAPrivateKey(dev, handle, pw) + require.NoError(t, err) + + config := &packet.Config{ + DefaultHash: crypto.SHA256, + DefaultCipher: packet.CipherAES256, + DefaultCompressionAlgo: packet.CompressionZLIB, + CompressionConfig: &packet.CompressionConfig{ + Level: 9, + }, + } + + // Build an identity with the TPM key + entity, err := NewOpenPGPEntity("test", "comment", "test@example.com", config, priv) + require.NoError(t, err) + + // Serialize the entity (public part) then load it back from that + exported := new(bytes.Buffer) + err = entity.Serialize(exported) + require.NoError(t, err) + + entityRead, err := ReadOpenPGPEntity(packet.NewReader(exported), priv) + require.NoError(t, err) + + // Sign something with the TPM key producing a detached signature + sigDetached := new(bytes.Buffer) + msg := "signed message\n" + err = openpgp.DetachSign(sigDetached, entityRead, strings.NewReader(msg), config) + require.NoError(t, err) + + // Verify the signature + keyring := openpgp.EntityList{entityRead} + signedBy, err := openpgp.CheckDetachedSignature(keyring, strings.NewReader(msg), sigDetached) + require.NoError(t, err) + require.Equal(t, entityRead, signedBy) + + // Sign it with a clear text signature + sigClear := new(bytes.Buffer) + wc, err := clearsign.Encode(sigClear, entityRead.PrivateKey, config) + require.NoError(t, err) + _, err = io.Copy(wc, strings.NewReader(msg)) + require.NoError(t, err) + err = wc.Close() + require.NoError(t, err) + + block, rest := clearsign.Decode(sigClear.Bytes()) + require.Empty(t, rest) + require.Equal(t, msg, string(block.Plaintext)) +} + +func TestOpenPGPDecrypt(t *testing.T) { + dev, err := simulator.Get() + require.NoError(t, err) + defer dev.Close() + + const ( + handle = 0x81000000 + pw = "" + attr = tpm2.FlagSign | tpm2.FlagDecrypt | tpm2.FlagUserWithAuth | tpm2.FlagSensitiveDataOrigin + ) + + // Generate and use the key in the TPM for signing + _, err = GenRSAPrimaryKey(dev, handle, pw, pw, attr) + require.NoError(t, err) + priv, err := NewRSAPrivateKey(dev, handle, pw) + require.NoError(t, err) + + // Build an identity with the TPM key + config := &packet.Config{ + DefaultHash: crypto.SHA256, + DefaultCipher: packet.CipherAES256, + DefaultCompressionAlgo: packet.CompressionZLIB, + CompressionConfig: &packet.CompressionConfig{ + Level: 9, + }, + RSABits: 2048, + } + entity, err := NewOpenPGPEntity("test", "comment", "test@example.com", config, priv) + require.NoError(t, err) + + // Encrypt something for the TPM entity + msg := []byte("secret message") + ciphertext := new(bytes.Buffer) + wc, err := openpgp.Encrypt(ciphertext, []*openpgp.Entity{entity}, nil, nil, config) + require.NoError(t, err) + _, err = wc.Write(msg) + require.NoError(t, err) + err = wc.Close() + require.NoError(t, err) + + // Decrypt the message + md, err := openpgp.ReadMessage(ciphertext, openpgp.EntityList([]*openpgp.Entity{entity}), nil, config) + require.NoError(t, err) + decrypted, err := ioutil.ReadAll(md.UnverifiedBody) + require.NoError(t, err) + require.True(t, md.IsEncrypted) + require.Equal(t, entity, md.DecryptedWith.Entity) + require.Equal(t, msg, decrypted) +}