diff --git a/tpm_encrypt.go b/tpm_encrypt.go new file mode 100644 index 0000000..f8ae3ab --- /dev/null +++ b/tpm_encrypt.go @@ -0,0 +1,103 @@ +package tpm + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "fmt" + "hash" + + "github.com/folbricht/tpmk" + "github.com/pkg/errors" +) + +// DecodeBlob decodes a blob using a key stored in the TPM +func DecodeBlob(blob []byte, opts ...TPMOption) ([]byte, error) { + o, err := DefaultTPMOption(opts...) + if err != nil { + return []byte{}, err + } + + // Open device or simulator + dev, err := getTPMDevice(o) + if err != nil { + return []byte{}, err + } + if !o.emulated { + defer dev.Close() + } + + private, err := tpmk.NewRSAPrivateKey(dev, o.index, o.password) + if err != nil { + return []byte{}, fmt.Errorf("loading private key: '%w'", err) + } + return private.Decrypt(rand.Reader, blob, &rsa.OAEPOptions{Hash: o.hash}) +} + +func EncodeBlob(blob []byte, opts ...TPMOption) ([]byte, error) { + o, err := DefaultTPMOption(opts...) + if err != nil { + return []byte{}, err + } + + // Open device or simulator + dev, err := getTPMDevice(o) + if err != nil { + return []byte{}, err + } + if !o.emulated { + defer dev.Close() + } + + // Get a list of keys + keys, err := tpmk.KeyList(dev) + if err != nil { + return []byte{}, errors.Wrap(err, "reading key list") + } + + exists := false + // Print the key handles in hex notation + for _, hh := range keys { + if o.index == hh { + exists = true + } + } + + var pub crypto.PublicKey + if !exists { + // Generate the key if doesn't exist + pub, err = tpmk.GenRSAPrimaryKey(dev, o.index, "", o.password, o.keyAttr) + if err != nil { + return []byte{}, err + } + } else { + // Re-Use the private key in the TPM + private, err := tpmk.NewRSAPrivateKey(dev, o.index, o.password) + if err != nil { + return []byte{}, err + } + pub = private.Public() + } + + p, ok := pub.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("keypair returned by TPM is malformed: pubkey is not an RSA public key") + } + + return encryptWithPublicKey(blob, p, o.hash) +} + +// encryptWithPublicKey encrypts data with public key +func encryptWithPublicKey(msg []byte, pub *rsa.PublicKey, c crypto.Hash) ([]byte, error) { + var h hash.Hash + + switch c { + case crypto.SHA256: + h = sha256.New() + default: + return []byte{}, fmt.Errorf("unsupported encryption type") + } + + return rsa.EncryptOAEP(h, rand.Reader, pub, msg, nil) +} diff --git a/tpm_encrypt_options.go b/tpm_encrypt_options.go new file mode 100644 index 0000000..22472a2 --- /dev/null +++ b/tpm_encrypt_options.go @@ -0,0 +1,153 @@ +package tpm + +import ( + "crypto" + "fmt" + "io" + "strconv" + "strings" + + "github.com/folbricht/tpmk" + "github.com/google/go-tpm-tools/simulator" + "github.com/google/go-tpm/tpm2" + "github.com/google/go-tpm/tpmutil" +) + +type TPMOptions struct { + device string + index tpmutil.Handle + keyAttr tpm2.KeyProp + nvAttr tpm2.NVAttr + password string + emulated bool + + hash crypto.Hash +} + +var emulatedDevice io.ReadWriteCloser + +func CloseEmulatedDevice() { emulatedDevice.Close(); emulatedDevice = nil } + +func getTPMDevice(o *TPMOptions) (io.ReadWriteCloser, error) { + if o.emulated { + if emulatedDevice == nil { + var err error + emulatedDevice, err = simulator.Get() + if err != nil { + return nil, err + } + } + return emulatedDevice, nil + } + dev, err := tpmk.OpenDevice(o.device) + if err != nil { + return dev, err + } + return dev, err +} + +func DefaultTPMOption(opts ...TPMOption) (*TPMOptions, error) { + o := &TPMOptions{} + + defaults := []TPMOption{ + WithAttributes("sign|decrypt|userwithauth|sensitivedataorigin"), + WithNVAttributes("ownerwrite|ownerread|authread|ppread"), + WithIndex("0x81000008"), + WithDevice("/dev/tpmrm0"), + WithHash(crypto.SHA256), + } + + return o, o.Apply(append(defaults, opts...)...) +} + +type TPMOption func(t *TPMOptions) error + +func (t *TPMOptions) Apply(opts ...TPMOption) error { + for _, o := range opts { + if err := o(t); err != nil { + return err + } + } + + return nil +} + +var EmulatedTPM TPMOption = func(t *TPMOptions) error { + t.emulated = true + return nil +} + +func WithHash(c crypto.Hash) TPMOption { + return func(t *TPMOptions) (err error) { + t.hash = c + return + } +} + +func WithPassword(s string) TPMOption { + return func(t *TPMOptions) error { + t.password = s + return nil + } +} + +func WithDevice(s string) TPMOption { + return func(t *TPMOptions) (err error) { + t.device = s + return + } +} + +func WithAttributes(s string) TPMOption { + return func(t *TPMOptions) (err error) { + t.keyAttr, err = parseKeyAttributes(s) + return + } +} + +func WithIndex(s string) TPMOption { + return func(t *TPMOptions) (err error) { + t.index, err = parseHandle(s) + return + } +} + +func WithNVAttributes(s string) TPMOption { + return func(t *TPMOptions) (err error) { + t.nvAttr, err = parseNVAttributes(s) + return + } +} + +func parseHandle(s string) (tpmutil.Handle, error) { + i, err := strconv.ParseUint(s, 0, 32) + return tpmutil.Handle(i), err +} + +func parseNVAttributes(s string) (tpm2.NVAttr, error) { + var nvAttr tpm2.NVAttr + s = strings.Replace(s, " ", "", -1) + for _, prop := range strings.Split(s, "|") { + v, ok := stringToNVAttribute[prop] + if !ok { + return nvAttr, fmt.Errorf("unknown attribute '%s'", prop) + } + nvAttr |= v + } + + return nvAttr, nil +} + +func parseKeyAttributes(s string) (tpm2.KeyProp, error) { + var keyProp tpm2.KeyProp + s = strings.Replace(s, " ", "", -1) + for _, prop := range strings.Split(s, "|") { + v, ok := stringToKeyAttribute[prop] + if !ok { + return keyProp, fmt.Errorf("unknown attribute property '%s'", prop) + } + keyProp |= v + } + + return keyProp, nil +} diff --git a/tpm_encrypt_test.go b/tpm_encrypt_test.go new file mode 100644 index 0000000..f6ff7c8 --- /dev/null +++ b/tpm_encrypt_test.go @@ -0,0 +1,26 @@ +package tpm_test + +import ( + . "github.com/kairos-io/tpm-helpers" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("TPM Encryption", func() { + Context("Blob", func() { + It("encrypts a blob", func() { + var encodedBlob []byte + var err error + By("Encoding the blob", func() { + encodedBlob, err = EncodeBlob([]byte("foo"), EmulatedTPM) + Expect(err).ToNot(HaveOccurred()) + }) + By("Decoding the blob", func() { + foo, err := DecodeBlob(encodedBlob, EmulatedTPM) + Expect(err).ToNot(HaveOccurred()) + Expect(foo).To(Equal([]byte("foo"))) + }) + CloseEmulatedDevice() + }) + }) +}) diff --git a/tpm_flags.go b/tpm_flags.go new file mode 100644 index 0000000..d286b71 --- /dev/null +++ b/tpm_flags.go @@ -0,0 +1,40 @@ +package tpm + +import "github.com/google/go-tpm/tpm2" + +// This is from github.com/folbricht/tpmk +var stringToKeyAttribute = map[string]tpm2.KeyProp{ + "fixedtpm": tpm2.FlagFixedTPM, + "fixedparent": tpm2.FlagFixedParent, + "sensitivedataorigin": tpm2.FlagSensitiveDataOrigin, + "userwithauth": tpm2.FlagUserWithAuth, + "adminwithpolicy": tpm2.FlagAdminWithPolicy, + "noda": tpm2.FlagNoDA, + "restricted": tpm2.FlagRestricted, + "decrypt": tpm2.FlagDecrypt, + "sign": tpm2.FlagSign, +} + +var stringToNVAttribute = map[string]tpm2.NVAttr{ + "ppwrite": tpm2.AttrPPWrite, + "ownerwrite": tpm2.AttrOwnerWrite, + "authwrite": tpm2.AttrAuthWrite, + "policywrite": tpm2.AttrPolicyWrite, + "policydelete": tpm2.AttrPolicyDelete, + "writelocked": tpm2.AttrWriteLocked, + "writeall": tpm2.AttrWriteAll, + "writedefine": tpm2.AttrWriteDefine, + "writestclear": tpm2.AttrWriteSTClear, + "globallock": tpm2.AttrGlobalLock, + "ppread": tpm2.AttrPPRead, + "ownerread": tpm2.AttrOwnerRead, + "authread": tpm2.AttrAuthRead, + "policyread": tpm2.AttrPolicyRead, + "noda": tpm2.AttrNoDA, + "orderly": tpm2.AttrOrderly, + "clearstclear": tpm2.AttrClearSTClear, + "readlocked": tpm2.AttrReadLocked, + "written": tpm2.AttrWritten, + "platformcreate": tpm2.AttrPlatformCreate, + "readstclear": tpm2.AttrReadSTClear, +} diff --git a/tpm_nv.go b/tpm_nv.go new file mode 100644 index 0000000..0ca464d --- /dev/null +++ b/tpm_nv.go @@ -0,0 +1,43 @@ +package tpm + +import ( + "github.com/folbricht/tpmk" +) + +func StoreBlob(blob []byte, opts ...TPMOption) error { + o, err := DefaultTPMOption(opts...) + if err != nil { + return err + } + + // Open device or simulator + dev, err := getTPMDevice(o) + if err != nil { + return err + } + if !o.emulated { + defer dev.Close() + } + + // Write to the index + return tpmk.NVWrite(dev, o.index, blob, o.password, o.nvAttr) +} + +func ReadBlob(opts ...TPMOption) ([]byte, error) { + o, err := DefaultTPMOption(opts...) + if err != nil { + return []byte{}, err + } + + // Open device or simulator + dev, err := getTPMDevice(o) + if err != nil { + return []byte{}, err + } + if !o.emulated { + defer dev.Close() + } + + // Read the data + return tpmk.NVRead(dev, o.index, o.password) +} diff --git a/tpm_nv_test.go b/tpm_nv_test.go new file mode 100644 index 0000000..7f165bf --- /dev/null +++ b/tpm_nv_test.go @@ -0,0 +1,25 @@ +package tpm_test + +import ( + . "github.com/kairos-io/tpm-helpers" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("TPM NV", func() { + Context("NV store", func() { + It("stores a blob and get it back", func() { + By("Storing the blob", func() { + // authwrite matters here! + err := StoreBlob([]byte("foo"), EmulatedTPM, WithIndex("0x1500000")) + Expect(err).ToNot(HaveOccurred()) + }) + By("Reading the blob", func() { + foo, err := ReadBlob(WithIndex("0x1500000"), EmulatedTPM) + Expect(err).ToNot(HaveOccurred()) + Expect(foo).To(Equal([]byte("foo"))) + }) + CloseEmulatedDevice() + }) + }) +})