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 {