diff --git a/go.mod b/go.mod index 34fe7217b..31013e1ee 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/a8m/tree v0.0.0-20210115125333-10a5fd5b637d github.com/dougm/pretty v0.0.0-20171025230240-2ee9d7453c02 github.com/google/go-cmp v0.5.9 + github.com/google/go-tpm v0.0.0-00010101000000-000000000000 github.com/google/uuid v1.3.1 github.com/rasky/go-xdr v0.0.0-20170217172119-4930550ba2e2 github.com/stretchr/testify v1.8.4 @@ -17,6 +18,9 @@ require ( github.com/kr/pretty v0.3.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.8.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/google/go-tpm => github.com/akutz/go-tpm v0.0.0-20230904203701-1d12d24e581e diff --git a/go.sum b/go.sum index 34ede8dab..5853fd93f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/a8m/tree v0.0.0-20210115125333-10a5fd5b637d h1:4E8RufAN3UQ/weB6AnQ4y5miZCO0Yco8ZdGId41WuQs= github.com/a8m/tree v0.0.0-20210115125333-10a5fd5b637d/go.mod h1:FSdwKX97koS5efgm8WevNf7XS3PqtyFkKDDXrz778cg= +github.com/akutz/go-tpm v0.0.0-20230904203701-1d12d24e581e h1:3+q0gcMmpQWxi4UI+KEmyC1OUBD4m2/O7Bcb2J/5AQ4= +github.com/akutz/go-tpm v0.0.0-20230904203701-1d12d24e581e/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -26,6 +28,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728 h1:sH9mEk+flyDxiUa5BuPiuhDETMbzrt9A20I2wktMvRQ= github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728/go.mod h1:x9oS4Wk2s2u4tS29nEaDLdzvuHdB19CvSGJjPgkZJNk= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/govc/USAGE.md b/govc/USAGE.md index d5d73cc69..a823f2fa5 100644 --- a/govc/USAGE.md +++ b/govc/USAGE.md @@ -358,6 +358,9 @@ but appear via `govc $cmd -h`: - [vm.rdm.attach](#vmrdmattach) - [vm.rdm.ls](#vmrdmls) - [vm.register](#vmregister) + - [vm.tpm2.cert.get](#vmtpm2certget) + - [vm.tpm2.cert.ls](#vmtpm2certls) + - [vm.tpm2.seal](#vmtpm2seal) - [vm.unregister](#vmunregister) - [vm.upgrade](#vmupgrade) - [vm.vnc](#vmvnc) @@ -6284,6 +6287,55 @@ Options: -template=false Mark VM as template ``` +## vm.tpm2.cert.get + +``` +Usage: govc vm.tpm2.cert.get [OPTIONS] + +Get certificate by fingerprint. + +Examples: + govc vm.tpm2.cert.get -vm VM -fingerprint 41:5D:F1:AE:B9:F2:B1:22:9F:79:B7:FF:DA:55:5B:86 + +Options: + -fingerprint= Fingerprint of cert to get. Use vm.tpm2.ls to see available certs." + -vm= Virtual machine [GOVC_VM] +``` + +## vm.tpm2.cert.ls + +``` +Usage: govc vm.tpm2.cert.ls [OPTIONS] + +List endorsement key certificates. + +Examples: + govc vm.tpm2.cert.ls -vm VM + govc vm.tpm2.cert.ls -vm VM -G ecc + +Options: + -G= Public key algorithm. Either "rsa", "ecc", or "ecdsa" + -vm= Virtual machine [GOVC_VM] +``` + +## vm.tpm2.seal + +``` +Usage: govc vm.tpm2.seal [OPTIONS] + +Seal plain-text data to the VM's TPM2 endorsement key. + +Examples: + govc vm.tpm2.seal -vm VM -in plain.txt + echo "Hello, world" | govc vm.tpm2.seal -vm VM + echo "Seal with ECC." | govc vm.tpm2.seal -vm VM -G ecc + +Options: + -G=rsa Public key algorithm. Either "rsa", "ecc", or "ecdsa" + -in=- Input data. Defaults to STDIN via "-" + -vm= Virtual machine [GOVC_VM] +``` + ## vm.unregister ``` diff --git a/govc/main.go b/govc/main.go index 643db42a2..d99fff391 100644 --- a/govc/main.go +++ b/govc/main.go @@ -109,6 +109,8 @@ import ( _ "github.com/vmware/govmomi/govc/vm/option" _ "github.com/vmware/govmomi/govc/vm/rdm" _ "github.com/vmware/govmomi/govc/vm/snapshot" + _ "github.com/vmware/govmomi/govc/vm/tpm2" + _ "github.com/vmware/govmomi/govc/vm/tpm2/cert" _ "github.com/vmware/govmomi/govc/volume" _ "github.com/vmware/govmomi/govc/volume/snapshot" _ "github.com/vmware/govmomi/govc/vsan" diff --git a/govc/vm/tpm2/cert/get.go b/govc/vm/tpm2/cert/get.go new file mode 100644 index 000000000..6790e1e2c --- /dev/null +++ b/govc/vm/tpm2/cert/get.go @@ -0,0 +1,162 @@ +/* +Copyright (c) 2023-2023 VMware, Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cert + +import ( + "bytes" + "context" + "crypto/md5" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "flag" + "fmt" + "io" + + "github.com/vmware/govmomi/govc/cli" + "github.com/vmware/govmomi/govc/flags" + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/types" +) + +type get struct { + *flags.VirtualMachineFlag + *flags.OutputFlag + + fingerprint string +} + +func init() { + cli.Register("vm.tpm2.cert.get", &get{}) +} + +func (cmd *get) Register(ctx context.Context, f *flag.FlagSet) { + + cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx) + cmd.VirtualMachineFlag.Register(ctx, f) + cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx) + cmd.OutputFlag.Register(ctx, f) + + f.StringVar(&cmd.fingerprint, "fingerprint", "", + `Fingerprint of cert to get. Use vm.tpm2.ls to see available certs."`) +} + +func (cmd *get) Description() string { + return `Get certificate by fingerprint. + +Examples: + govc vm.tpm2.cert.get -vm VM -fingerprint 41:5D:F1:AE:B9:F2:B1:22:9F:79:B7:FF:DA:55:5B:86` +} + +func (cmd *get) Process(ctx context.Context) error { + + if err := cmd.VirtualMachineFlag.Process(ctx); err != nil { + return err + } + if err := cmd.OutputFlag.Process(ctx); err != nil { + return err + } + return nil +} + +func (cmd *get) Run(ctx context.Context, f *flag.FlagSet) error { + if cmd.fingerprint == "" { + return flag.ErrHelp + } + + vm, err := cmd.VirtualMachine() + if err != nil { + return err + } + + if vm == nil { + return flag.ErrHelp + } + + // Get the VM's EK. + var moVM mo.VirtualMachine + if err := vm.Properties( + ctx, + vm.Reference(), + []string{"config.hardware.device"}, + &moVM); err != nil { + return err + } + + devices := object.VirtualDeviceList(moVM.Config.Hardware.Device) + selectedDevices := devices.SelectByType(&types.VirtualTPM{}) + if len(selectedDevices) == 0 { + return fmt.Errorf("no VirtualTPM devices found") + } + if len(selectedDevices) > 1 { + return fmt.Errorf("multiple VirtualTPM devices found") + } + + vtpmDev := selectedDevices[0].(*types.VirtualTPM) + + var result getResult + for i := range vtpmDev.EndorsementKeyCertificate { + // Use DecodeString as Decode complains about trailing data. + derString := string(vtpmDev.EndorsementKeyCertificate[i]) + derData, err := base64.StdEncoding.DecodeString(derString) + if err != nil { + return err + } + cert, err := x509.ParseCertificate(derData) + if err != nil { + return err + } + + fingerprint := md5.Sum(cert.Raw) + var fingerprintBuf bytes.Buffer + for i, f := range fingerprint { + if i > 0 { + fmt.Fprintf(&fingerprintBuf, ":") + } + fmt.Fprintf(&fingerprintBuf, "%02X", f) + } + + if cmd.fingerprint == fingerprintBuf.String() { + var pemBuf bytes.Buffer + block := &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + } + if err := pem.Encode(&pemBuf, block); err != nil { + return err + } + result.PubCertPEM = pemBuf.Bytes() + break + } + } + + if len(result.PubCertPEM) == 0 { + return fmt.Errorf("fingerprint %s not found", cmd.fingerprint) + } + + return cmd.WriteResult(result) +} + +type getResult struct { + PubCertPEM []byte `json:"pubCertPEM"` +} + +func (r getResult) Write(w io.Writer) error { + _, err := fmt.Fprint(w, string(r.PubCertPEM)) + return err +} diff --git a/govc/vm/tpm2/cert/ls.go b/govc/vm/tpm2/cert/ls.go new file mode 100644 index 000000000..528c595c4 --- /dev/null +++ b/govc/vm/tpm2/cert/ls.go @@ -0,0 +1,183 @@ +/* +Copyright (c) 2023-2023 VMware, Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cert + +import ( + "bytes" + "context" + "crypto/md5" + "crypto/x509" + "encoding/base64" + "flag" + "fmt" + "io" + "os" + "strings" + "text/tabwriter" + + "github.com/vmware/govmomi/govc/cli" + "github.com/vmware/govmomi/govc/flags" + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/types" +) + +type ls struct { + *flags.VirtualMachineFlag + *flags.OutputFlag + + alg string +} + +func init() { + cli.Register("vm.tpm2.cert.ls", &ls{}) +} + +func (cmd *ls) Register(ctx context.Context, f *flag.FlagSet) { + + cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx) + cmd.VirtualMachineFlag.Register(ctx, f) + cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx) + cmd.OutputFlag.Register(ctx, f) + + f.StringVar(&cmd.alg, "G", "", + `Public key algorithm. Either "rsa", "ecc", or "ecdsa"`) +} + +func (cmd *ls) Description() string { + return `List endorsement key certificates. + +Examples: + govc vm.tpm2.cert.ls -vm VM + govc vm.tpm2.cert.ls -vm VM -G ecc` +} + +func (cmd *ls) Process(ctx context.Context) error { + + if err := cmd.VirtualMachineFlag.Process(ctx); err != nil { + return err + } + if err := cmd.OutputFlag.Process(ctx); err != nil { + return err + } + return nil +} + +func (cmd *ls) Run(ctx context.Context, f *flag.FlagSet) error { + var alg x509.PublicKeyAlgorithm + switch strings.ToLower(cmd.alg) { + case "": + alg = x509.UnknownPublicKeyAlgorithm + case "rsa": + alg = x509.RSA + case "ecc", "ecdsa": + alg = x509.ECDSA + default: + return flag.ErrHelp + } + + vm, err := cmd.VirtualMachine() + if err != nil { + return err + } + + if vm == nil { + return flag.ErrHelp + } + + // Get the VM's EK. + var moVM mo.VirtualMachine + if err := vm.Properties( + ctx, + vm.Reference(), + []string{"config.hardware.device"}, + &moVM); err != nil { + return err + } + + devices := object.VirtualDeviceList(moVM.Config.Hardware.Device) + selectedDevices := devices.SelectByType(&types.VirtualTPM{}) + if len(selectedDevices) == 0 { + return fmt.Errorf("no VirtualTPM devices found") + } + if len(selectedDevices) > 1 { + return fmt.Errorf("multiple VirtualTPM devices found") + } + + vtpmDev := selectedDevices[0].(*types.VirtualTPM) + + var result lsResult + for i := range vtpmDev.EndorsementKeyCertificate { + // Use DecodeString as Decode complains about trailing data. + derString := string(vtpmDev.EndorsementKeyCertificate[i]) + derData, err := base64.StdEncoding.DecodeString(derString) + if err != nil { + return err + } + cert, err := x509.ParseCertificate(derData) + if err != nil { + return err + } + + var skipCert bool + switch { + case alg == x509.RSA && cert.PublicKeyAlgorithm != x509.RSA: + skipCert = true + case alg == x509.ECDSA && cert.PublicKeyAlgorithm != x509.ECDSA: + skipCert = true + } + if skipCert { + continue + } + + fingerprint := md5.Sum(cert.Raw) + var buf bytes.Buffer + for i, f := range fingerprint { + if i > 0 { + fmt.Fprintf(&buf, ":") + } + fmt.Fprintf(&buf, "%02X", f) + } + + info := certInfo{Fingerprint: buf.String()} + switch cert.PublicKeyAlgorithm { + case x509.RSA: + info.Algorithm = "rsa" + case x509.ECDSA: + info.Algorithm = "ecc" + } + result = append(result, info) + } + + return cmd.WriteResult(result) +} + +type certInfo struct { + Algorithm string `json:"algorithm"` + Fingerprint string `json:"fingerprint"` +} + +type lsResult []certInfo + +func (r lsResult) Write(w io.Writer) error { + tw := tabwriter.NewWriter(os.Stdout, 2, 0, 2, ' ', 0) + fmt.Fprintf(tw, "Algorithm\tFingerprint\n") + for i := range r { + fmt.Fprintf(tw, "%s\t%s\n", r[i].Algorithm, r[i].Fingerprint) + } + return tw.Flush() +} diff --git a/govc/vm/tpm2/seal.go b/govc/vm/tpm2/seal.go new file mode 100644 index 000000000..27f7f2c22 --- /dev/null +++ b/govc/vm/tpm2/seal.go @@ -0,0 +1,196 @@ +/* +Copyright (c) 2023-2023 VMware, Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tpm2 + +import ( + "context" + "crypto/x509" + "encoding/base64" + "flag" + "fmt" + "io" + "os" + "strings" + + "github.com/vmware/govmomi/govc/cli" + "github.com/vmware/govmomi/govc/flags" + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/types" + + "github.com/google/go-tpm/tpm2" +) + +type seal struct { + *flags.VirtualMachineFlag + *flags.OutputFlag + + input string + alg string +} + +func init() { + cli.Register("vm.tpm2.seal", &seal{}) +} + +func (cmd *seal) Register(ctx context.Context, f *flag.FlagSet) { + cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx) + cmd.VirtualMachineFlag.Register(ctx, f) + cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx) + cmd.OutputFlag.Register(ctx, f) + + f.StringVar(&cmd.input, "in", "-", + `Input data. Defaults to STDIN via "-"`) + f.StringVar(&cmd.alg, "G", "rsa", + `Public key algorithm. Either "rsa", "ecc", or "ecdsa"`) +} + +func (cmd *seal) Description() string { + return `Seal plain-text data to the VM's TPM2 endorsement key. + +Examples: + govc vm.tpm2.seal -vm VM -in plain.txt + echo "Hello, world" | govc vm.tpm2.seal -vm VM + echo "Seal with ECC." | govc vm.tpm2.seal -vm VM -G ecc` +} + +func (cmd *seal) Process(ctx context.Context) error { + if err := cmd.VirtualMachineFlag.Process(ctx); err != nil { + return err + } + if err := cmd.OutputFlag.Process(ctx); err != nil { + return err + } + return nil +} + +func (cmd *seal) Run(ctx context.Context, f *flag.FlagSet) error { + + var alg x509.PublicKeyAlgorithm + switch strings.ToLower(cmd.alg) { + case "rsa": + alg = x509.RSA + case "ecc", "ecdsa": + alg = x509.ECDSA + default: + return flag.ErrHelp + } + + vm, err := cmd.VirtualMachine() + if err != nil { + return err + } + + if vm == nil { + return flag.ErrHelp + } + + // Read the plain-text data. + var plainTextFile *os.File + if cmd.input == "-" { + plainTextFile = os.Stdin + } else { + f, err := os.Open(cmd.input) + if err != nil { + return err + } + plainTextFile = f + defer f.Close() + } + plainTextData, err := io.ReadAll(plainTextFile) + if err != nil { + return err + } + + // Get the VM's EK. + var moVM mo.VirtualMachine + if err := vm.Properties( + ctx, + vm.Reference(), + []string{"config.hardware.device"}, + &moVM); err != nil { + return err + } + + devices := object.VirtualDeviceList(moVM.Config.Hardware.Device) + selectedDevices := devices.SelectByType(&types.VirtualTPM{}) + if len(selectedDevices) == 0 { + return fmt.Errorf("no VirtualTPM devices found") + } + if len(selectedDevices) > 1 { + return fmt.Errorf("multiple VirtualTPM devices found") + } + + vtpmDev := selectedDevices[0].(*types.VirtualTPM) + + var ekCert *x509.Certificate + for i := range vtpmDev.EndorsementKeyCertificate { + // Use DecodeString as Decode complains about trailing data. + derString := string(vtpmDev.EndorsementKeyCertificate[i]) + derData, err := base64.StdEncoding.DecodeString(derString) + if err != nil { + return err + } + cert, err := x509.ParseCertificate(derData) + if err != nil { + return err + } + + if alg == x509.RSA && cert.PublicKeyAlgorithm == x509.RSA { + ekCert = cert + } else if alg == x509.ECDSA && cert.PublicKeyAlgorithm == x509.ECDSA { + ekCert = cert + } + } + + if ekCert == nil { + return fmt.Errorf("unable to find ek certificate") + } + + ek, err := tpm2.EKCertToTPMTPublic(*ekCert) + if err != nil { + return err + } + + pub, priv, seed, err := tpm2.EKSeal(ek, plainTextData) + if err != nil { + return err + } + + return cmd.WriteResult(sealResult{ + Public: base64.StdEncoding.EncodeToString(tpm2.Marshal(pub)), + Private: base64.StdEncoding.EncodeToString(tpm2.Marshal(priv)), + Seed: base64.StdEncoding.EncodeToString(tpm2.Marshal(seed)), + }) +} + +type sealResult struct { + Public string `json:"public"` + Private string `json:"private"` + Seed string `json:"seed"` +} + +func (r sealResult) Write(w io.Writer) error { + _, err := fmt.Fprintf( + w, + "%s@@NULL@@%s@@NULL@@%s", + r.Public, + r.Private, + r.Seed, + ) + return err +}