From 22607cd86a053e5e983857ecdc1701e3665e015e Mon Sep 17 00:00:00 2001 From: Doug MacEachern Date: Sun, 23 Jun 2024 16:51:21 -0700 Subject: [PATCH 1/5] fix: xml marshal byte array fields as vCenter does Go's encoding/xml package and vCenter marshal '[]byte' differently: - Go encodes the entire array in a single xml element, for example: hello - vCenter encodes each byte of the array in its own xml element, example with same data as above: 104 101 108 108 111 This behavior is hardwired, see the xml/encoding source for the handful of []byte special cases along the lines of: if reflect.Slice && slice element type == reflect.Uint8 While govmomi maintains a fork of Go's xml/encoding package, we prefer to further diverge. Proposed solution is to use a 'ByteSlice' type that implements xml marshaling as vCenter does, but otherwise behaves just as '[]byte' does. Commits that follow enhance vcsim and govc support around various []byte fields. Fixes #1977 Fixes #3469 --- gen/gen_from_vmodl.rb | 5 +- gen/vim_wsdl.rb | 6 +- govc/object/collect.go | 18 ++++-- govc/test/host.bats | 11 ++++ govc/test/object.bats | 22 ++++++++ object/host_system_test.go | 34 +++++++++++- simulator/customization_spec_manager.go | 17 ++++-- simulator/esx/host_config_info.go | 7 ++- simulator/esx/host_storage_device_info.go | 14 ++--- simulator/property_collector.go | 4 ++ vim25/mo/mo.go | 4 +- vim25/types/byte_slice.go | 67 +++++++++++++++++++++++ vim25/types/byte_slice_test.go | 45 +++++++++++++++ vim25/types/helpers.go | 4 +- vim25/types/helpers_test.go | 4 +- vim25/types/types.go | 20 +++---- 16 files changed, 245 insertions(+), 37 deletions(-) create mode 100644 vim25/types/byte_slice.go create mode 100644 vim25/types/byte_slice_test.go diff --git a/gen/gen_from_vmodl.rb b/gen/gen_from_vmodl.rb index 6f7609be4..61f51dd71 100644 --- a/gen/gen_from_vmodl.rb +++ b/gen/gen_from_vmodl.rb @@ -1,4 +1,4 @@ -# Copyright (c) 2014 VMware, Inc. All Rights Reserved. +# Copyright (c) 2014-2024 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. @@ -86,6 +86,9 @@ def var_type when "dateTime" type ="time.Time" when "byte" + if slice? + return "types.ByteSlice" + end when "double" type ="float64" when "float" diff --git a/gen/vim_wsdl.rb b/gen/vim_wsdl.rb index daf631948..9aa16a020 100644 --- a/gen/vim_wsdl.rb +++ b/gen/vim_wsdl.rb @@ -1,4 +1,4 @@ -# Copyright (c) 2014-2021 VMware, Inc. All Rights Reserved. +# Copyright (c) 2014-2024 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. @@ -321,6 +321,10 @@ def var_type self.need_omitempty = false end when "byte" + if slice? + prefix = "" + t = "#{pkg}ByteSlice" + end when "double" t = "float64" when "float" diff --git a/govc/object/collect.go b/govc/object/collect.go index 972f1f337..6a2e5b232 100644 --- a/govc/object/collect.go +++ b/govc/object/collect.go @@ -18,6 +18,7 @@ package object import ( "context" + "encoding/base64" "encoding/json" "flag" "fmt" @@ -111,8 +112,8 @@ Examples: govc object.collect -R create-filter-request.xml -O # convert filter to Go code govc object.collect -s vm/my-vm summary.runtime.host | xargs govc ls -L # inventory path of VM's host govc object.collect -dump -o "network/VM Network" # output Managed Object structure as Go code - govc object.collect -json $vm config | \ # use -json + jq to search array elements - jq -r '.[] | select(.val.hardware.device[].macAddress == "00:0c:29:0c:73:c0") | .val.name'`, atable) + govc object.collect -json -s $vm config | \ # use -json + jq to search array elements + jq -r 'select(.hardware.device[].macAddress == "00:50:56:99:c4:27") | .name'`, atable) } var stringer = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() @@ -123,11 +124,11 @@ type change struct { } func (pc *change) MarshalJSON() ([]byte, error) { - if len(pc.cmd.kind) == 0 { + if len(pc.cmd.kind) == 0 && !pc.cmd.simple { return json.Marshal(pc.Update.ChangeSet) } - return json.Marshal(pc.Update) + return json.Marshal(pc.Dump()) } func (pc *change) output(name string, rval reflect.Value, rtype reflect.Type) { @@ -154,6 +155,15 @@ func (pc *change) output(name string, rval reflect.Value, rtype reflect.Type) { etype := rtype.Elem() + if etype.Kind() == reflect.Uint8 { + if v, ok := rval.Interface().(types.ByteSlice); ok { + s = base64.StdEncoding.EncodeToString(v) // ArrayOfByte + } else { + s = fmt.Sprintf("%x", rval.Interface().([]byte)) // ArrayOfBase64Binary + } + break + } + if etype.Kind() != reflect.Interface && etype.Kind() != reflect.Struct || etype.Implements(stringer) { var val []string diff --git a/govc/test/host.bats b/govc/test/host.bats index 9364dc656..50c7cf932 100755 --- a/govc/test/host.bats +++ b/govc/test/host.bats @@ -159,6 +159,17 @@ load test_helper run govc host.storage.info -t hba assert_success + + names=$(govc host.storage.info -json | jq -r .storageDeviceInfo.scsiLun[].alternateName[].data) + # given data is hex encoded []byte and: + # [0] == encoding + # [1] == type + # [2] == ? + # [3] == length + # validate name is at least 2 char x 4 + for name in $names; do + [ "${#name}" -ge 8 ] + done } @test "host.options" { diff --git a/govc/test/object.bats b/govc/test/object.bats index 2da45061b..93133e8ad 100755 --- a/govc/test/object.bats +++ b/govc/test/object.bats @@ -338,6 +338,28 @@ load test_helper assert_success } +@test "object.collect bytes" { + vcsim_env + + host=$(govc find / -type h | head -1) + + # ArrayOfByte with PEM encoded cert + govc object.collect -s "$host" config.certificate | \ + base64 -d | openssl x509 -text + + # []byte field with PEM encoded cert + govc object.collect -s -json "$host" config | jq -r .certificate | \ + base64 -d | openssl x509 -text + + # ArrayOfByte with DER encoded cert + govc object.collect -s CustomizationSpecManager:CustomizationSpecManager encryptionKey | \ + base64 -d | openssl x509 -inform DER -text + + # []byte field with DER encoded cert + govc object.collect -o -json CustomizationSpecManager:CustomizationSpecManager | jq -r .encryptionKey | \ + base64 -d | openssl x509 -inform DER -text +} + @test "object.collect view" { vcsim_env -dc 2 -folder 1 diff --git a/object/host_system_test.go b/object/host_system_test.go index 31caf4c31..684a76b53 100644 --- a/object/host_system_test.go +++ b/object/host_system_test.go @@ -1,9 +1,12 @@ /* -Copyright (c) 2019 VMware, Inc. All Rights Reserved. +Copyright (c) 2019-2024 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 + +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. @@ -14,12 +17,16 @@ limitations under the License. package object_test import ( + "bytes" "context" + "encoding/pem" "testing" "github.com/vmware/govmomi/find" "github.com/vmware/govmomi/simulator" + "github.com/vmware/govmomi/simulator/esx" "github.com/vmware/govmomi/vim25" + "github.com/vmware/govmomi/vim25/mo" ) func TestHostSystemManagementIPs(t *testing.T) { @@ -59,3 +66,26 @@ func TestHostSystemManagementIPs(t *testing.T) { } }) } + +func TestHostSystemConfig(t *testing.T) { + simulator.Test(func(ctx context.Context, c *vim25.Client) { + host, err := find.NewFinder(c).HostSystem(ctx, "DC0_C0_H0") + if err != nil { + t.Fatal(err) + } + + var props mo.HostSystem + if err := host.Properties(ctx, host.Reference(), []string{"config"}, &props); err != nil { + t.Fatal(err) + } + + if !bytes.Equal(props.Config.Certificate, esx.HostConfigInfo.Certificate) { + t.Errorf("certificate=%s", string(props.Config.Certificate)) + } + + b, _ := pem.Decode(props.Config.Certificate) + if b == nil { + t.Error("failed to parse certificate") + } + }) +} diff --git a/simulator/customization_spec_manager.go b/simulator/customization_spec_manager.go index b522680e5..4ef7a51ee 100644 --- a/simulator/customization_spec_manager.go +++ b/simulator/customization_spec_manager.go @@ -1,11 +1,11 @@ /* -Copyright (c) 2019 VMware, Inc. All Rights Reserved. +Copyright (c) 2019-2024 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 +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, @@ -17,10 +17,12 @@ limitations under the License. package simulator import ( + "encoding/pem" "fmt" "sync/atomic" "time" + "github.com/vmware/govmomi/simulator/internal" "github.com/vmware/govmomi/vim25/methods" "github.com/vmware/govmomi/vim25/mo" "github.com/vmware/govmomi/vim25/soap" @@ -169,7 +171,7 @@ var DefaultCustomizationSpec = []types.CustomizationSpecItem{ }, }, }, - EncryptionKey: []uint8{0x30}, + EncryptionKey: nil, }, }, { @@ -236,7 +238,7 @@ var DefaultCustomizationSpec = []types.CustomizationSpecItem{ }, }, }, - EncryptionKey: []uint8{0x30}, + EncryptionKey: nil, }, }, } @@ -249,6 +251,13 @@ type CustomizationSpecManager struct { func (m *CustomizationSpecManager) init(r *Registry) { m.items = DefaultCustomizationSpec + + // Real VC is different DN, X509v3 extensions, etc. + // This is still useful for testing []byte of DER encoded cert over SOAP + if len(m.EncryptionKey) == 0 { + block, _ := pem.Decode(internal.LocalhostCert) + m.EncryptionKey = block.Bytes + } } var customizeNameCounter uint64 diff --git a/simulator/esx/host_config_info.go b/simulator/esx/host_config_info.go index b98f4cf35..ced5a5046 100644 --- a/simulator/esx/host_config_info.go +++ b/simulator/esx/host_config_info.go @@ -16,7 +16,10 @@ limitations under the License. package esx -import "github.com/vmware/govmomi/vim25/types" +import ( + "github.com/vmware/govmomi/simulator/internal" + "github.com/vmware/govmomi/vim25/types" +) // HostConfigInfo is the default template for the HostSystem config property. // Capture method: @@ -998,7 +1001,7 @@ var HostConfigInfo = types.HostConfigInfo{ Ipmi: (*types.HostIpmiInfo)(nil), SslThumbprintInfo: (*types.HostSslThumbprintInfo)(nil), SslThumbprintData: nil, - Certificate: []uint8{0x31, 0x30}, + Certificate: internal.LocalhostCert, PciPassthruInfo: nil, AuthenticationManagerInfo: &types.HostAuthenticationManagerInfo{ AuthConfig: []types.BaseHostAuthenticationStoreInfo{ diff --git a/simulator/esx/host_storage_device_info.go b/simulator/esx/host_storage_device_info.go index 79033344f..43d1f2ecd 100644 --- a/simulator/esx/host_storage_device_info.go +++ b/simulator/esx/host_storage_device_info.go @@ -1,5 +1,5 @@ /* -Copyright (c) 2017-2023 VMware, Inc. All Rights Reserved. +Copyright (c) 2017-2024 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. @@ -93,15 +93,15 @@ var HostStorageDeviceInfo = types.HostStorageDeviceInfo{ { Namespace: "GENERIC_VPD", NamespaceId: 0x5, - Data: []uint8{0x2d, 0x37, 0x39}, + Data: []uint8{0x0, 0x0, 0x0, 0x4, 0x0, 0xb0, 0xb1, 0xb2}, }, { Namespace: "GENERIC_VPD", NamespaceId: 0x5, - Data: []uint8{0x30}, + Data: []uint8{0x0, 0xb2, 0x0, 0x4, 0x1, 0x60, 0x0, 0x0}, }, }, - StandardInquiry: []uint8{0x30}, + StandardInquiry: []uint8{0x0, 0x0, 0x6, 0x2, 0x1f, 0x0, 0x0, 0x72}, QueueDepth: 0, OperationalState: []string{"ok"}, Capabilities: &types.ScsiLunCapabilities{}, @@ -143,15 +143,15 @@ var HostStorageDeviceInfo = types.HostStorageDeviceInfo{ { Namespace: "GENERIC_VPD", NamespaceId: 0x5, - Data: []uint8{0x2d, 0x37, 0x39}, + Data: []uint8{0x0, 0x0, 0x0, 0x4, 0x0, 0xb0, 0xb1, 0xb2}, }, { Namespace: "GENERIC_VPD", NamespaceId: 0x5, - Data: []uint8{0x30}, + Data: []uint8{0x0, 0xb2, 0x0, 0x4, 0x1, 0x60, 0x0, 0x0}, }, }, - StandardInquiry: []uint8{0x30}, + StandardInquiry: []uint8{0x0, 0x0, 0x6, 0x2, 0x1f, 0x0, 0x0, 0x72}, QueueDepth: 1024, OperationalState: []string{"ok"}, Capabilities: &types.ScsiLunCapabilities{}, diff --git a/simulator/property_collector.go b/simulator/property_collector.go index b50fc9a91..9d37f2e9a 100644 --- a/simulator/property_collector.go +++ b/simulator/property_collector.go @@ -131,6 +131,10 @@ func wrapValue(rval reflect.Value, rtype reflect.Type) interface{} { pval = &types.ArrayOfByte{ Byte: v, } + case types.ByteSlice: + pval = &types.ArrayOfByte{ + Byte: v, + } case []int16: pval = &types.ArrayOfShort{ Short: v, diff --git a/vim25/mo/mo.go b/vim25/mo/mo.go index fc6c96ff6..5b91ae47d 100644 --- a/vim25/mo/mo.go +++ b/vim25/mo/mo.go @@ -5,7 +5,7 @@ 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 +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, @@ -210,7 +210,7 @@ type CustomizationSpecManager struct { Self types.ManagedObjectReference `json:"self"` Info []types.CustomizationSpecInfo `json:"info"` - EncryptionKey []byte `json:"encryptionKey"` + EncryptionKey types.ByteSlice `json:"encryptionKey"` } func (m CustomizationSpecManager) Reference() types.ManagedObjectReference { diff --git a/vim25/types/byte_slice.go b/vim25/types/byte_slice.go new file mode 100644 index 000000000..a19a0bb23 --- /dev/null +++ b/vim25/types/byte_slice.go @@ -0,0 +1,67 @@ +/* +Copyright (c) 2024-2024 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 types + +import ( + "fmt" + "io" + "math" + "strconv" + + "github.com/vmware/govmomi/vim25/xml" +) + +// ByteSlice implements vCenter compatibile xml encoding and decoding for a byte slice. +// vCenter encodes each byte of the array in its own xml element, whereas +// Go encodes the entire byte array in a single xml element. +type ByteSlice []byte + +// MarshalXML implements xml.Marshaler +func (b ByteSlice) MarshalXML(e *xml.Encoder, field xml.StartElement) error { + start := xml.StartElement{ + Name: field.Name, + } + for i := range b { + if err := e.EncodeElement(b[i], start); err != nil { + return err + } + } + return nil +} + +// UnmarshalXML implements xml.Unmarshaler +func (b *ByteSlice) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + for { + t, err := d.Token() + if err == io.EOF { + break + } + + if c, ok := t.(xml.CharData); ok { + n, err := strconv.ParseInt(string(c), 10, 16) + if err != nil { + return err + } + if n > math.MaxUint8 { + return fmt.Errorf("parsing %q: uint8 overflow", start.Name.Local) + } + *b = append(*b, byte(n)) + } + } + + return nil +} diff --git a/vim25/types/byte_slice_test.go b/vim25/types/byte_slice_test.go new file mode 100644 index 000000000..c0adf19e6 --- /dev/null +++ b/vim25/types/byte_slice_test.go @@ -0,0 +1,45 @@ +/* +Copyright (c) 2024-2024 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 types + +import ( + "bytes" + "testing" + + "github.com/vmware/govmomi/vim25/xml" +) + +func TestByteSlice(t *testing.T) { + in := &ArrayOfByte{ + Byte: []byte("xmhell"), + } + + res, err := xml.Marshal(in) + if err != nil { + t.Fatal(err) + } + + var out ArrayOfByte + if err := xml.Unmarshal(res, &out); err != nil { + t.Logf("%s", string(res)) + t.Fatal(err) + } + + if !bytes.Equal(in.Byte, out.Byte) { + t.Errorf("out=%#v", out.Byte) + } +} diff --git a/vim25/types/helpers.go b/vim25/types/helpers.go index cbcdb637e..94fb50df4 100644 --- a/vim25/types/helpers.go +++ b/vim25/types/helpers.go @@ -1,11 +1,11 @@ /* -Copyright (c) 2015-2022 VMware, Inc. All Rights Reserved. +Copyright (c) 2015-2024 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 +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, diff --git a/vim25/types/helpers_test.go b/vim25/types/helpers_test.go index 960c4256e..f680945ed 100644 --- a/vim25/types/helpers_test.go +++ b/vim25/types/helpers_test.go @@ -1,11 +1,11 @@ /* -Copyright (c) 2022 VMware, Inc. All Rights Reserved. +Copyright (c) 2022-2024 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 +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, diff --git a/vim25/types/types.go b/vim25/types/types.go index a51550331..0f8dab11a 100644 --- a/vim25/types/types.go +++ b/vim25/types/types.go @@ -5,7 +5,7 @@ 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 +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, @@ -2431,7 +2431,7 @@ func init() { // A boxed array of `PrimitiveByte`. To be used in `Any` placeholders. type ArrayOfByte struct { - Byte []byte `xml:"byte,omitempty" json:"_value"` + Byte ByteSlice `xml:"byte,omitempty" json:"_value"` } func init() { @@ -18385,7 +18385,7 @@ type CustomizationSpec struct { // Both the client and the server can use this to determine if // stored passwords can be decrypted by the server or if the passwords need to be // re-entered and re-encrypted before the specification can be used. - EncryptionKey []byte `xml:"encryptionKey,omitempty" json:"encryptionKey,omitempty"` + EncryptionKey ByteSlice `xml:"encryptionKey,omitempty" json:"encryptionKey,omitempty"` } func init() { @@ -34873,7 +34873,7 @@ type HostConfigInfo struct { // SSL Thumbprints registered on this host. SslThumbprintData []HostSslThumbprintInfo `xml:"sslThumbprintData,omitempty" json:"sslThumbprintData,omitempty"` // Full Host Certificate in PEM format, if known - Certificate []byte `xml:"certificate,omitempty" json:"certificate,omitempty"` + Certificate ByteSlice `xml:"certificate,omitempty" json:"certificate,omitempty"` // PCI passthrough information. PciPassthruInfo []BaseHostPciPassthruInfo `xml:"pciPassthruInfo,omitempty,typeattr" json:"pciPassthruInfo,omitempty"` // Current authentication configuration. @@ -36351,7 +36351,7 @@ type HostDigestInfo struct { DigestMethod string `xml:"digestMethod" json:"digestMethod"` // The variable length byte array containing the digest value calculated by // the specified digestMethod. - DigestValue []byte `xml:"digestValue" json:"digestValue"` + DigestValue ByteSlice `xml:"digestValue" json:"digestValue"` // The name of the object from which this digest value is calcaulated. ObjectName string `xml:"objectName,omitempty" json:"objectName,omitempty"` } @@ -44877,7 +44877,7 @@ type HostSubSpecification struct { // Time at which the host sub specification was created. CreatedTime time.Time `xml:"createdTime" json:"createdTime"` // The host sub specification data - Data []byte `xml:"data,omitempty" json:"data,omitempty"` + Data ByteSlice `xml:"data,omitempty" json:"data,omitempty"` // The host sub specification data in Binary for wire efficiency. BinaryData []byte `xml:"binaryData,omitempty" json:"binaryData,omitempty"` } @@ -45374,7 +45374,7 @@ type HostTpmEventDetails struct { DynamicData // Value of the Platform Configuration Register (PCR) for this event. - DataHash []byte `xml:"dataHash" json:"dataHash"` + DataHash ByteSlice `xml:"dataHash" json:"dataHash"` // Method in which the digest hash is calculated. // // The set of possible @@ -45434,7 +45434,7 @@ type HostTpmOptionEventDetails struct { // This array exposes the raw contents of the settings file (or files) that were // passed to kernel during the boot up process, and, therefore, should be treated // accordingly. - BootOptions []byte `xml:"bootOptions,omitempty" json:"bootOptions,omitempty"` + BootOptions ByteSlice `xml:"bootOptions,omitempty" json:"bootOptions,omitempty"` } func init() { @@ -71958,7 +71958,7 @@ type ScsiLun struct { // For a SCSI-3 compliant device this property is derived from the // standard inquiry data. For devices that are not SCSI-3 compliant this // property is not defined. - StandardInquiry []byte `xml:"standardInquiry,omitempty" json:"standardInquiry,omitempty"` + StandardInquiry ByteSlice `xml:"standardInquiry,omitempty" json:"standardInquiry,omitempty"` // The queue depth of SCSI device. QueueDepth int32 `xml:"queueDepth,omitempty" json:"queueDepth,omitempty"` // The operational states of the LUN. @@ -72078,7 +72078,7 @@ type ScsiLunDurableName struct { // along with the payload for data obtained from page 83h, and is the // payload for data obtained from page 80h of the Vital Product Data // (VPD). - Data []byte `xml:"data,omitempty" json:"data,omitempty"` + Data ByteSlice `xml:"data,omitempty" json:"data,omitempty"` } func init() { From 7f0c9f0c48209e9bda5897403e609634c3834283 Mon Sep 17 00:00:00 2001 From: Doug MacEachern Date: Sun, 23 Jun 2024 16:54:28 -0700 Subject: [PATCH 2/5] govc: add host.cert.info -show flag --- govc/host/cert/info.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/govc/host/cert/info.go b/govc/host/cert/info.go index ffec1ab00..25dd933a1 100644 --- a/govc/host/cert/info.go +++ b/govc/host/cert/info.go @@ -1,11 +1,11 @@ /* -Copyright (c) 2016 VMware, Inc. All Rights Reserved. +Copyright (c) 2016-2024 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 +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, @@ -19,14 +19,18 @@ package cert import ( "context" "flag" + "fmt" "github.com/vmware/govmomi/govc/cli" "github.com/vmware/govmomi/govc/flags" + "github.com/vmware/govmomi/vim25/mo" ) type info struct { *flags.HostSystemFlag *flags.OutputFlag + + show bool } func init() { @@ -39,6 +43,8 @@ func (cmd *info) Register(ctx context.Context, f *flag.FlagSet) { cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx) cmd.OutputFlag.Register(ctx, f) + + f.BoolVar(&cmd.show, "show", false, "Show PEM encoded server certificate only") } func (cmd *info) Description() string { @@ -61,6 +67,16 @@ func (cmd *info) Run(ctx context.Context, f *flag.FlagSet) error { return err } + if cmd.show { + var props mo.HostSystem + err = host.Properties(ctx, host.Reference(), []string{"config.certificate"}, &props) + if err != nil { + return err + } + _, err = fmt.Fprint(cmd.Out, string(props.Config.Certificate)) + return err + } + m, err := host.ConfigManager().CertificateManager(ctx) if err != nil { return err From 072011c621686fcb0764122b03c21dae7df1e40b Mon Sep 17 00:00:00 2001 From: Doug MacEachern Date: Sun, 23 Jun 2024 17:00:18 -0700 Subject: [PATCH 3/5] vcsim: add HostCertificateManager --- govc/library/trust/info.go | 14 +--- govc/object/save.go | 15 +++- govc/test/host.bats | 28 +++---- object/host_certificate_info.go | 16 +++- object/host_certificate_manager.go | 13 ++- simulator/esx/host_system.go | 4 +- simulator/host_certificate_manager.go | 111 ++++++++++++++++++++++++++ simulator/host_system.go | 1 + simulator/model.go | 1 + vcsim/main.go | 3 +- 10 files changed, 169 insertions(+), 37 deletions(-) create mode 100644 simulator/host_certificate_manager.go diff --git a/govc/library/trust/info.go b/govc/library/trust/info.go index 5685187dd..f6e14f0c9 100644 --- a/govc/library/trust/info.go +++ b/govc/library/trust/info.go @@ -1,11 +1,11 @@ /* -Copyright (c) 2022-2022 VMware, Inc. All Rights Reserved. +Copyright (c) 2022-2024 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 +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, @@ -18,8 +18,6 @@ package trust import ( "context" - "crypto/x509" - "encoding/pem" "flag" "io" @@ -68,15 +66,11 @@ type infoResultsWriter struct { } func (r infoResultsWriter) Write(w io.Writer) error { - block, _ := pem.Decode([]byte(r.TrustedCertificateInfo.Text)) - x, err := x509.ParseCertificate(block.Bytes) + var info object.HostCertificateInfo + _, err := info.FromPEM([]byte(r.TrustedCertificateInfo.Text)) if err != nil { return err } - - var info object.HostCertificateInfo - info.FromCertificate(x) - return info.Write(w) } diff --git a/govc/object/save.go b/govc/object/save.go index 796b54900..fead3b62b 100644 --- a/govc/object/save.go +++ b/govc/object/save.go @@ -1,5 +1,5 @@ /* -Copyright (c) 2019-2023 VMware, Inc. All Rights Reserved. +Copyright (c) 2019-2024 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. @@ -191,7 +191,7 @@ func (cmd *save) save(content []types.ObjectContent) error { return err } dir := filepath.Join(cmd.dir, ref) - if err = os.Mkdir(dir, 0755); err != nil { + if err = os.MkdirAll(dir, 0755); err != nil { return err } for _, obj := range objs { @@ -327,6 +327,9 @@ func (cmd *save) Run(ctx context.Context, f *flag.FlagSet) error { &types.SelectionSpec{ Name: "hostVirtualNicManagerTraversalSpec", }, + &types.SelectionSpec{ + Name: "hostCertificateManagerTraversalSpec", + }, &types.SelectionSpec{ Name: "entityTraversalSpec", }, @@ -360,6 +363,13 @@ func (cmd *save) Run(ctx context.Context, f *flag.FlagSet) error { Type: "HostSystem", Path: "configManager.virtualNicManager", }, + &types.TraversalSpec{ + SelectionSpec: types.SelectionSpec{ + Name: "hostCertificateManagerTraversalSpec", + }, + Type: "HostSystem", + Path: "configManager.certificateManager", + }, &types.TraversalSpec{ SelectionSpec: types.SelectionSpec{ Name: "hostDatastoreSystemTraversalSpec", @@ -382,6 +392,7 @@ func (cmd *save) Run(ctx context.Context, f *flag.FlagSet) error { {Type: "HostDatastoreSystem", All: all}, {Type: "HostNetworkSystem", All: all}, {Type: "HostVirtualNicManager", All: all}, + {Type: "HostCertificateManager", All: all}, {Type: "ManagedEntity", All: all}, {Type: "Task", All: all}, }, diff --git a/govc/test/host.bats b/govc/test/host.bats index 50c7cf932..9c175d25e 100755 --- a/govc/test/host.bats +++ b/govc/test/host.bats @@ -202,7 +202,7 @@ load test_helper } @test "host.cert.info" { - esx_env + vcsim_env -esx run govc host.cert.info assert_success @@ -213,10 +213,16 @@ load test_helper expires=$(govc host.cert.info -json | jq -r .notAfter) about_expires=$(govc about.cert -json | jq -r .notAfter) assert_equal "$expires" "$about_expires" + + run govc host.cert.info -show + assert_success + + run openssl x509 -text <<<"$output" + assert_success } @test "host.cert.csr" { - esx_env + vcsim_env -esx # Requested Extensions: # X509v3 Subject Alternative Name: @@ -234,16 +240,10 @@ load test_helper } @test "host.cert.import" { - esx_env + vcsim_env -esx - issuer=$(govc host.cert.info -json | jq -r .issuer) expires=$(govc host.cert.info -json | jq -r .notAfter) - # only mess with the cert if its already been signed by our test CA - if [[ "$issuer" != CN=govc-ca,* ]] ; then - skip "host cert not signed by govc-ca" - fi - govc host.cert.csr -ip | ./host_cert_sign.sh | govc host.cert.import expires2=$(govc host.cert.info -json | jq -r .notAfter) @@ -251,16 +251,8 @@ load test_helper [ "$expires" != "$expires2" ] # verify hostd is using the new cert too - expires=$(govc about.cert -json | jq -r .notAfter) + expires=$(govc host.cert.info -json | jq -r .notAfter) assert_equal "$expires" "$expires2" - - # our cert is not trusted against the system CA list - status=$(govc about.cert | grep Status:) - assert_matches ERROR "$status" - - # with our CA trusted, the cert should be too - status=$(govc about.cert -tls-ca-certs ./govc_ca.pem | grep Status:) - assert_matches good "$status" } @test "host.date.info" { diff --git a/object/host_certificate_info.go b/object/host_certificate_info.go index 1a3a7fab5..0d56e7e7b 100644 --- a/object/host_certificate_info.go +++ b/object/host_certificate_info.go @@ -1,5 +1,5 @@ /* -Copyright (c) 2016-2023 VMware, Inc. All Rights Reserved. +Copyright (c) 2016-2024 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. @@ -21,6 +21,8 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/asn1" + "encoding/pem" + "errors" "fmt" "io" "net/url" @@ -66,6 +68,18 @@ func (info *HostCertificateInfo) FromCertificate(cert *x509.Certificate) *HostCe return info } +func (info *HostCertificateInfo) FromPEM(cert []byte) (*HostCertificateInfo, error) { + block, _ := pem.Decode(cert) + if block == nil { + return nil, errors.New("failed to pem.Decode cert") + } + x, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + return info.FromCertificate(x), nil +} + // FromURL connects to the given URL.Host via tls.Dial with the given tls.Config and populates the HostCertificateInfo // via tls.ConnectionState. If the certificate was verified with the given tls.Config, the Err field will be nil. // Otherwise, Err will be set to the x509.UnknownAuthorityError or x509.HostnameError. diff --git a/object/host_certificate_manager.go b/object/host_certificate_manager.go index ddf1d8c59..30787e7a4 100644 --- a/object/host_certificate_manager.go +++ b/object/host_certificate_manager.go @@ -1,11 +1,11 @@ /* -Copyright (c) 2016 VMware, Inc. All Rights Reserved. +Copyright (c) 2016-2024 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 +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, @@ -23,6 +23,7 @@ import ( "github.com/vmware/govmomi/vim25" "github.com/vmware/govmomi/vim25/methods" "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/soap" "github.com/vmware/govmomi/vim25/types" ) @@ -117,7 +118,13 @@ func (m HostCertificateManager) InstallServerCertificate(ctx context.Context, ce Req: &types.Refresh{This: m.Reference()}, } - return m.Client().RoundTrip(ctx, &body, &body) + err = m.Client().RoundTrip(ctx, &body, &body) + if err != nil && soap.IsSoapFault(err) { + if _, ok := soap.ToSoapFault(err).VimFault().(types.MethodNotFound); ok { + return nil + } + } + return err } // ListCACertificateRevocationLists returns the SSL CRLs of Certificate Authorities that are trusted by the host system. diff --git a/simulator/esx/host_system.go b/simulator/esx/host_system.go index 2cd0c685d..496f49d3a 100644 --- a/simulator/esx/host_system.go +++ b/simulator/esx/host_system.go @@ -1,5 +1,5 @@ /* -Copyright (c) 2017-2023 VMware, Inc. All Rights Reserved. +Copyright (c) 2017-2024 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. @@ -1736,7 +1736,7 @@ var HostSystem = mo.HostSystem{ OverallStatus: "gray", RebootRequired: false, CustomValue: nil, - ManagementServerIp: "", + ManagementServerIp: "127.0.0.1", MaxEVCModeKey: "", CurrentEVCModeKey: "", Gateway: (*types.HostListSummaryGatewaySummary)(nil), diff --git a/simulator/host_certificate_manager.go b/simulator/host_certificate_manager.go new file mode 100644 index 000000000..5e6a5fdc4 --- /dev/null +++ b/simulator/host_certificate_manager.go @@ -0,0 +1,111 @@ +/* +Copyright (c) 2024-2024 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 simulator + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "net" + + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25/methods" + "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/soap" + "github.com/vmware/govmomi/vim25/types" +) + +type HostCertificateManager struct { + mo.HostCertificateManager + + Host *mo.HostSystem +} + +func (m *HostCertificateManager) init(r *Registry) { + for _, obj := range r.objects { + if h, ok := obj.(*HostSystem); ok { + if h.ConfigManager.CertificateManager.Value == m.Self.Value { + m.Host = &h.HostSystem + } + } + } +} + +func NewHostCertificateManager(h *mo.HostSystem) *HostCertificateManager { + m := &HostCertificateManager{Host: h} + + _ = m.InstallServerCertificate(SpoofContext(), &types.InstallServerCertificate{ + Cert: string(m.Host.Config.Certificate), + }) + + return m +} + +func (m *HostCertificateManager) InstallServerCertificate(ctx *Context, req *types.InstallServerCertificate) soap.HasFault { + body := new(methods.InstallServerCertificateBody) + + var info object.HostCertificateInfo + cert := []byte(req.Cert) + _, err := info.FromPEM(cert) + if err != nil { + body.Fault_ = Fault(err.Error(), new(types.HostConfigFault)) + return body + } + + m.CertificateInfo = info.HostCertificateManagerCertificateInfo + + m.Host.Config.Certificate = cert + + body.Res = new(types.InstallServerCertificateResponse) + + return body +} + +func (m *HostCertificateManager) GenerateCertificateSigningRequest(ctx *Context, req *types.GenerateCertificateSigningRequest) soap.HasFault { + block, _ := pem.Decode(m.Host.Config.Certificate) + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + panic(err) + } + + csr := x509.CertificateRequest{ + Subject: cert.Subject, + SignatureAlgorithm: x509.SHA256WithRSA, + } + + if req.UseIpAddressAsCommonName { + csr.IPAddresses = []net.IP{net.ParseIP(m.Host.Summary.ManagementServerIp)} + } else { + csr.DNSNames = []string{m.Host.Name} + } + + key, _ := rsa.GenerateKey(rand.Reader, 2048) + der, _ := x509.CreateCertificateRequest(rand.Reader, &csr, key) + var buf bytes.Buffer + err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: der}) + if err != nil { + panic(err) + } + + return &methods.GenerateCertificateSigningRequestBody{ + Res: &types.GenerateCertificateSigningRequestResponse{ + Returnval: buf.String(), + }, + } +} diff --git a/simulator/host_system.go b/simulator/host_system.go index 6c7705e59..555b85a5a 100644 --- a/simulator/host_system.go +++ b/simulator/host_system.go @@ -100,6 +100,7 @@ func NewHostSystem(host mo.HostSystem) *HostSystem { {&hs.ConfigManager.AdvancedOption, NewOptionManager(nil, nil, &hs.Config.Option)}, {&hs.ConfigManager.FirewallSystem, NewHostFirewallSystem(&hs.HostSystem)}, {&hs.ConfigManager.StorageSystem, NewHostStorageSystem(&hs.HostSystem)}, + {&hs.ConfigManager.CertificateManager, NewHostCertificateManager(&hs.HostSystem)}, } for _, c := range config { diff --git a/simulator/model.go b/simulator/model.go index e14fd7508..e80d0f302 100644 --- a/simulator/model.go +++ b/simulator/model.go @@ -250,6 +250,7 @@ var kinds = map[string]reflect.Type{ "HostDatastoreBrowser": reflect.TypeOf((*HostDatastoreBrowser)(nil)).Elem(), "HostLocalAccountManager": reflect.TypeOf((*HostLocalAccountManager)(nil)).Elem(), "HostNetworkSystem": reflect.TypeOf((*HostNetworkSystem)(nil)).Elem(), + "HostCertificateManager": reflect.TypeOf((*HostCertificateManager)(nil)).Elem(), "HostSystem": reflect.TypeOf((*HostSystem)(nil)).Elem(), "IpPoolManager": reflect.TypeOf((*IpPoolManager)(nil)).Elem(), "LicenseManager": reflect.TypeOf((*LicenseManager)(nil)).Elem(), diff --git a/vcsim/main.go b/vcsim/main.go index f4379b15a..34ffbc0bd 100644 --- a/vcsim/main.go +++ b/vcsim/main.go @@ -1,5 +1,5 @@ /* -Copyright (c) 2017-2023 VMware, Inc. All Rights Reserved. +Copyright (c) 2017-2024 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. @@ -263,6 +263,7 @@ func updateHostTemplate(ip string) error { if err != nil { return err } + esx.HostSystem.Summary.ManagementServerIp = addr if port != "0" { // server starts after the model is created, skipping auto-selected ports for now n, err := strconv.Atoi(port) if err != nil { From a916df4e5a27543ec407a33f099b2656ab2cbb17 Mon Sep 17 00:00:00 2001 From: Doug MacEachern Date: Sun, 23 Jun 2024 17:01:16 -0700 Subject: [PATCH 4/5] govc: add host.tpm.info and host.tpm.report commands --- govc/host/tpm/info.go | 168 +++++++++++++++++++++++++++++++++++++++ govc/host/tpm/report.go | 128 +++++++++++++++++++++++++++++ govc/main.go | 3 +- govc/object/save.go | 9 +++ govc/test/host.bats | 10 +++ simulator/host_system.go | 8 ++ 6 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 govc/host/tpm/info.go create mode 100644 govc/host/tpm/report.go diff --git a/govc/host/tpm/info.go b/govc/host/tpm/info.go new file mode 100644 index 000000000..9c8666997 --- /dev/null +++ b/govc/host/tpm/info.go @@ -0,0 +1,168 @@ +/* +Copyright (c) 2024-2024 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 tpm + +import ( + "context" + "flag" + "fmt" + "io" + "strconv" + "strings" + "text/tabwriter" + "time" + + "github.com/vmware/govmomi/govc/cli" + "github.com/vmware/govmomi/govc/flags" + "github.com/vmware/govmomi/view" + "github.com/vmware/govmomi/vim25" + "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/types" +) + +type info struct { + *flags.DatacenterFlag +} + +func init() { + cli.Register("host.tpm.info", &info{}) +} + +func (cmd *info) Register(ctx context.Context, f *flag.FlagSet) { + cmd.DatacenterFlag, ctx = flags.NewDatacenterFlag(ctx) + cmd.DatacenterFlag.Register(ctx, f) +} + +func (cmd *info) Description() string { + return `Trusted Platform Module summary. + +Examples: + govc host.tpm.info + govc host.tpm.info -json` +} + +type TrustedPlatformModule struct { + Name string `json:"name"` + Supported bool `json:"supported"` + Version string `json:"version,omitempty"` + TxtEnabled bool `json:"txtEnabled,omitempty"` + Attestation *types.HostTpmAttestationInfo `json:"attestation,omitempty"` + StateEncryption *types.HostRuntimeInfoStateEncryptionInfo `json:"stateEncryption,omitempty"` +} + +func HostTrustedPlatformModule(ctx context.Context, c *vim25.Client, root types.ManagedObjectReference) ([]TrustedPlatformModule, error) { + v, err := view.NewManager(c).CreateContainerView(ctx, root, []string{"HostSystem"}, true) + if err != nil { + return nil, err + } + + defer v.Destroy(ctx) + + props := []string{ + "name", + "summary.tpmAttestation", + "summary.runtime.stateEncryption", + "capability.tpmSupported", + "capability.tpmVersion", + "capability.txtEnabled", + } + + var hosts []mo.HostSystem + err = v.Retrieve(ctx, []string{"HostSystem"}, props, &hosts) + if err != nil { + return nil, err + } + + tpm := make([]TrustedPlatformModule, len(hosts)) + + b := func(v *bool) bool { + if v == nil { + return false + } + return *v + } + + for i, host := range hosts { + m := TrustedPlatformModule{ + Name: host.Name, + Attestation: host.Summary.TpmAttestation, + } + if host.Capability != nil { + m.Supported = b(host.Capability.TpmSupported) + m.Version = host.Capability.TpmVersion + m.TxtEnabled = b(host.Capability.TxtEnabled) + } + if host.Summary.Runtime != nil { + m.StateEncryption = host.Summary.Runtime.StateEncryption + } + tpm[i] = m + } + + return tpm, nil +} + +func (cmd *info) Run(ctx context.Context, f *flag.FlagSet) error { + dc, err := cmd.DatacenterIfSpecified() + if err != nil { + return err + } + c, err := cmd.Client() + if err != nil { + return err + } + + root := c.ServiceContent.RootFolder + if dc != nil { + root = dc.Reference() + } + + tpm, err := HostTrustedPlatformModule(ctx, c, root) + if err != nil { + return err + } + + return cmd.WriteResult(infoResult(tpm)) +} + +type infoResult []TrustedPlatformModule + +func (r infoResult) Write(w io.Writer) error { + tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0) + + fields := []string{"Name", "Attestation", "Last Verified", "TPM version", "TXT", "Message"} + fmt.Fprintln(tw, strings.Join(fields, "\t")) + + for _, h := range r { + if h.Supported { + fields = []string{ + h.Name, + string(h.Attestation.Status), + h.Attestation.Time.Format(time.RFC3339), + h.Version, + strconv.FormatBool(h.TxtEnabled), + } + if m := h.Attestation.Message; m != nil { + fields = append(fields, m.Message) + } + } else { + fields = []string{h.Name, "N/A", "N/A", "N/A", "N/A"} + } + fmt.Fprintln(tw, strings.Join(fields, "\t")) + } + + return tw.Flush() +} diff --git a/govc/host/tpm/report.go b/govc/host/tpm/report.go new file mode 100644 index 000000000..1857b6566 --- /dev/null +++ b/govc/host/tpm/report.go @@ -0,0 +1,128 @@ +/* +Copyright (c) 2024-2024 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 tpm + +import ( + "context" + "flag" + "fmt" + "io" + "reflect" + "strings" + "text/tabwriter" + + "github.com/vmware/govmomi/govc/cli" + "github.com/vmware/govmomi/govc/flags" + "github.com/vmware/govmomi/vim25/methods" + "github.com/vmware/govmomi/vim25/types" +) + +type report struct { + *flags.HostSystemFlag + + e bool +} + +func init() { + cli.Register("host.tpm.report", &report{}) +} + +func (cmd *report) Register(ctx context.Context, f *flag.FlagSet) { + cmd.HostSystemFlag, ctx = flags.NewHostSystemFlag(ctx) + cmd.HostSystemFlag.Register(ctx, f) + + f.BoolVar(&cmd.e, "e", false, "Print events") +} + +func (cmd *report) Description() string { + return `Trusted Platform Module report. + +Examples: + govc host.tpm.report + govc host.tpm.report -e + govc host.tpm.report -json` +} + +func (cmd *report) Run(ctx context.Context, f *flag.FlagSet) error { + c, err := cmd.Client() + if err != nil { + return err + } + + host, err := cmd.HostSystem() + if err != nil { + return err + } + + query := types.QueryTpmAttestationReport{This: host.Reference()} + report, err := methods.QueryTpmAttestationReport(ctx, c, &query) + if err != nil { + return err + } + + return cmd.WriteResult(&reportResult{report.Returnval, cmd}) +} + +type reportResult struct { + Report *types.HostTpmAttestationReport + cmd *report +} + +func (r *reportResult) Write(w io.Writer) error { + if r.Report == nil { + return nil + } + + tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0) + + if r.cmd.e { + for _, e := range r.Report.TpmEvents { + pcr := e.PcrIndex + d := e.EventDetails.GetHostTpmEventDetails() + meth := d.DataHashMethod + hash := d.DataHash + var name string + + switch x := e.EventDetails.(type) { + case *types.HostTpmBootSecurityOptionEventDetails: + name = x.BootSecurityOption + case *types.HostTpmSoftwareComponentEventDetails: + name = x.ComponentName + case *types.HostTpmCommandEventDetails: + name = x.CommandLine + case *types.HostTpmSignerEventDetails: + name = x.BootSecurityOption + case *types.HostTpmVersionEventDetails: + name = fmt.Sprintf("%x", x.Version) + case *types.HostTpmOptionEventDetails: + name = x.OptionsFileName + case *types.HostTpmBootCompleteEventDetails: + } + + kind := reflect.ValueOf(e.EventDetails).Elem().Type().Name() + kind = strings.TrimPrefix(strings.TrimSuffix(kind, "EventDetails"), "HostTpm") + + fmt.Fprintf(tw, "%d\t%s\t%s\t%x\t%s\n", pcr, kind, meth, hash, name) + } + } else { + for _, e := range r.Report.TpmPcrValues { + fmt.Fprintf(tw, "PCR %d\t%s\t%x\t%s\n", e.PcrNumber, e.DigestMethod, e.DigestValue, e.ObjectName) + } + } + + return tw.Flush() +} diff --git a/govc/main.go b/govc/main.go index f4f6fd272..4943d3c5f 100644 --- a/govc/main.go +++ b/govc/main.go @@ -1,5 +1,5 @@ /* -Copyright (c) 2014-2023 VMware, Inc. All Rights Reserved. +Copyright (c) 2014-2024 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. @@ -67,6 +67,7 @@ import ( _ "github.com/vmware/govmomi/govc/host/portgroup" _ "github.com/vmware/govmomi/govc/host/service" _ "github.com/vmware/govmomi/govc/host/storage" + _ "github.com/vmware/govmomi/govc/host/tpm" _ "github.com/vmware/govmomi/govc/host/vnic" _ "github.com/vmware/govmomi/govc/host/vswitch" _ "github.com/vmware/govmomi/govc/importx" diff --git a/govc/object/save.go b/govc/object/save.go index fead3b62b..b6f40358f 100644 --- a/govc/object/save.go +++ b/govc/object/save.go @@ -147,11 +147,20 @@ func saveHostNetworkSystem(ctx context.Context, c *vim25.Client, ref types.Manag return []saveMethod{{"QueryNetworkHint", res}}, nil } +func saveHostSystem(ctx context.Context, c *vim25.Client, ref types.ManagedObjectReference) ([]saveMethod, error) { + res, err := methods.QueryTpmAttestationReport(ctx, c, &types.QueryTpmAttestationReport{This: ref}) + if err != nil { + return nil, err + } + return []saveMethod{{"QueryTpmAttestationReport", res}}, nil +} + // saveObjects maps object types to functions that can save data that isn't available via the PropertyCollector var saveObjects = map[string]func(context.Context, *vim25.Client, types.ManagedObjectReference) ([]saveMethod, error){ "VmwareDistributedVirtualSwitch": saveDVS, "EnvironmentBrowser": saveEnvironmentBrowser, "HostNetworkSystem": saveHostNetworkSystem, + "HostSystem": saveHostSystem, } func isNotConnected(err error) bool { diff --git a/govc/test/host.bats b/govc/test/host.bats index 9c175d25e..278b100b2 100755 --- a/govc/test/host.bats +++ b/govc/test/host.bats @@ -289,3 +289,13 @@ load test_helper status=$(govc host.info| grep -i "State"| awk '{print $2}') assert_equal 'connected' $status } + +@test "host.tpm" { + vcsim_env + + run govc host.tpm.info + assert_success + + run govc host.tpm.report + assert_success +} diff --git a/simulator/host_system.go b/simulator/host_system.go index 555b85a5a..367491a67 100644 --- a/simulator/host_system.go +++ b/simulator/host_system.go @@ -42,6 +42,8 @@ type HostSystem struct { mo.HostSystem sh *simHost + + types.QueryTpmAttestationReportResponse } func asHostSystemMO(obj mo.Reference) (*mo.HostSystem, bool) { @@ -588,3 +590,9 @@ func (h *HostSystem) ReconnectHostTask(ctx *Context, spec *types.ReconnectHost_T }, } } + +func (s *HostSystem) QueryTpmAttestationReport(req *types.QueryTpmAttestationReport) soap.HasFault { + return &methods.QueryTpmAttestationReportBody{ + Res: &s.QueryTpmAttestationReportResponse, + } +} From 43510fbc906d8f3f1075c69bc9bd36deaefabc03 Mon Sep 17 00:00:00 2001 From: Doug MacEachern Date: Sun, 23 Jun 2024 17:02:04 -0700 Subject: [PATCH 5/5] chore: make doc --- govc/USAGE.md | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/govc/USAGE.md b/govc/USAGE.md index f8ad98cf9..2dca0b77a 100644 --- a/govc/USAGE.md +++ b/govc/USAGE.md @@ -200,6 +200,8 @@ but appear via `govc $cmd -h`: - [host.storage.info](#hoststorageinfo) - [host.storage.mark](#hoststoragemark) - [host.storage.partition](#hoststoragepartition) + - [host.tpm.info](#hosttpminfo) + - [host.tpm.report](#hosttpmreport) - [host.vnic.change](#hostvnicchange) - [host.vnic.hint](#hostvnichint) - [host.vnic.info](#hostvnicinfo) @@ -2976,6 +2978,7 @@ Display SSL certificate info for HOST. Options: -host= Host system [GOVC_HOST] + -show=false Show PEM encoded server certificate only ``` ## host.date.change @@ -3293,6 +3296,37 @@ Options: -host= Host system [GOVC_HOST] ``` +## host.tpm.info + +``` +Usage: govc host.tpm.info [OPTIONS] + +Trusted Platform Module summary. + +Examples: + govc host.tpm.info + govc host.tpm.info -json + +Options: +``` + +## host.tpm.report + +``` +Usage: govc host.tpm.report [OPTIONS] + +Trusted Platform Module report. + +Examples: + govc host.tpm.report + govc host.tpm.report -e + govc host.tpm.report -json + +Options: + -e=false Print events + -host= Host system [GOVC_HOST] +``` + ## host.vnic.change ``` @@ -4611,8 +4645,8 @@ Examples: govc object.collect -R create-filter-request.xml -O # convert filter to Go code govc object.collect -s vm/my-vm summary.runtime.host | xargs govc ls -L # inventory path of VM's host govc object.collect -dump -o "network/VM Network" # output Managed Object structure as Go code - govc object.collect -json $vm config | \ # use -json + jq to search array elements - jq -r '.[] | select(.val.hardware.device[].macAddress == "00:0c:29:0c:73:c0") | .val.name' + govc object.collect -json -s $vm config | \ # use -json + jq to search array elements + jq -r 'select(.hardware.device[].macAddress == "00:50:56:99:c4:27") | .name' Options: -O=false Output the CreateFilter request itself