Repository: vmware/govmomi
Pull request #3559 (2024-09-20 21:34:41)

govc: add native kms provider support
api: add vapi/crypto package with support for native KMS
api: add rest.Client.DownloadAttachment method



Patch:
diff --git a/crypto/doc.go b/crypto/doc.go
new file mode 100644
index 00000000..c7c33dcb
--- /dev/null
+++ b/crypto/doc.go
@@ -0,0 +1,21 @@
+/*
+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 crypto provides access to CryptoManagerKmip methods used to manage cryptographic key providers.
+For creating and delete native providers, see package vapi/crypto.
+*/
+package crypto
diff --git a/govc/USAGE.md b/govc/USAGE.md
index f4f59780..30ff0800 100644
--- a/govc/USAGE.md
+++ b/govc/USAGE.md
@@ -220,6 +220,7 @@ but appear via `govc $cmd -h`:
  - [import.vmdk](#importvmdk)
  - [kms.add](#kmsadd)
  - [kms.default](#kmsdefault)
+ - [kms.export](#kmsexport)
  - [kms.ls](#kmsls)
  - [kms.rm](#kmsrm)
  - [kms.trust](#kmstrust)
@@ -3597,12 +3598,15 @@ Add KMS cluster.
 Server name and address are required, port defaults to 5696.
 
 Examples:
+  govc kms.add -N knp
   govc kms.add -n my-server -a kms.example.com my-kp
 
 Options:
+  -N=false               Add native key provider
   -a=                    Server address
   -n=                    Server name
   -p=5696                Server port
+  -tpm=true              Use only with TPM protected ESXi hosts (native only)
 ```
 
 ## kms.default
@@ -3622,6 +3626,22 @@ Options:
   -e=                    Set entity default KMS cluster (cluster or host folder)
 ```
 
+## kms.export
+
+```
+Usage: govc kms.export [OPTIONS] NAME
+
+Export KMS cluster for backup.
+
+Examples:
+  govc kms.export my-kp
+  govc kms.export -f my-backup.p12 my-kp
+
+Options:
+  -f=                    File name
+  -p=                    Password
+```
+
 ## kms.ls
 
 ```
diff --git a/govc/kms/add.go b/govc/kms/add.go
index 12963832..1814e716 100644
--- a/govc/kms/add.go
+++ b/govc/kms/add.go
@@ -23,6 +23,7 @@ import (
 	"github.com/vmware/govmomi/crypto"
 	"github.com/vmware/govmomi/govc/cli"
 	"github.com/vmware/govmomi/govc/flags"
+	vapicrypto "github.com/vmware/govmomi/vapi/crypto"
 	"github.com/vmware/govmomi/vim25/types"
 )
 
@@ -30,6 +31,8 @@ type add struct {
 	*flags.ClientFlag
 
 	types.KmipServerSpec
+	native vapicrypto.KmsProviderCreateSpec
+	nkp    bool
 }
 
 func init() {
@@ -44,6 +47,9 @@ func (cmd *add) Register(ctx context.Context, f *flag.FlagSet) {
 	f.StringVar(&cmd.Info.Address, "a", "", "Server address")
 	cmd.Info.Port = 5696 // default
 	f.Var(flags.NewInt32(&cmd.Info.Port), "p", "Server port")
+
+	f.BoolVar(&cmd.nkp, "N", false, "Add native key provider")
+	f.BoolVar(&cmd.native.Constraints.TpmRequired, "tpm", true, "Use only with TPM protected ESXi hosts (native only)")
 }
 
 func (cmd *add) Usage() string {
@@ -56,6 +62,7 @@ func (cmd *add) Description() string {
 Server name and address are required, port defaults to 5696.
 
 Examples:
+  govc kms.add -N knp
   govc kms.add -n my-server -a kms.example.com my-kp`
 }
 
@@ -65,6 +72,15 @@ func (cmd *add) Run(ctx context.Context, f *flag.FlagSet) error {
 		return flag.ErrHelp
 	}
 
+	if cmd.nkp {
+		rc, err := cmd.RestClient()
+		if err != nil {
+			return err
+		}
+		cmd.native.Provider = id
+		return vapicrypto.NewManager(rc).KmsProviderCreate(ctx, cmd.native)
+	}
+
 	c, err := cmd.Client()
 	if err != nil {
 		return err
diff --git a/govc/kms/export.go b/govc/kms/export.go
new file mode 100644
index 00000000..3176eba9
--- /dev/null
+++ b/govc/kms/export.go
@@ -0,0 +1,89 @@
+/*
+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 kms
+
+import (
+	"context"
+	"flag"
+	"fmt"
+
+	"github.com/vmware/govmomi/govc/cli"
+	"github.com/vmware/govmomi/govc/flags"
+	"github.com/vmware/govmomi/vapi/crypto"
+)
+
+type export struct {
+	*flags.ClientFlag
+
+	spec crypto.KmsProviderExportSpec
+	file string
+}
+
+func init() {
+	cli.Register("kms.export", &export{})
+}
+
+func (cmd *export) Register(ctx context.Context, f *flag.FlagSet) {
+	cmd.ClientFlag, ctx = flags.NewClientFlag(ctx)
+	cmd.ClientFlag.Register(ctx, f)
+
+	f.StringVar(&cmd.file, "f", "", "File name")
+	f.StringVar(&cmd.spec.Password, "p", "", "Password")
+}
+
+func (cmd *export) Usage() string {
+	return "NAME"
+}
+
+func (cmd *export) Description() string {
+	return `Export KMS cluster for backup.
+
+Examples:
+  govc kms.export my-kp
+  govc kms.export -f my-backup.p12 my-kp`
+}
+
+func (cmd *export) Run(ctx context.Context, f *flag.FlagSet) error {
+	id := f.Arg(0)
+	if id == "" {
+		return flag.ErrHelp
+	}
+
+	rc, err := cmd.RestClient()
+	if err != nil {
+		return err
+	}
+
+	m := crypto.NewManager(rc)
+
+	cmd.spec.Provider = id
+	export, err := m.KmsProviderExport(ctx, cmd.spec)
+	if err != nil {
+		return err
+	}
+
+	if export.Type != "LOCATION" {
+		return fmt.Errorf("unsupported export type: %s", export.Type)
+	}
+
+	req, err := m.KmsProviderExportRequest(ctx, export.Location)
+	if err != nil {
+		return err
+	}
+
+	return rc.DownloadAttachment(ctx, req, cmd.file)
+}
diff --git a/govc/kms/rm.go b/govc/kms/rm.go
index ef7ad9e8..5604bd8f 100644
--- a/govc/kms/rm.go
+++ b/govc/kms/rm.go
@@ -23,6 +23,7 @@ import (
 	"github.com/vmware/govmomi/crypto"
 	"github.com/vmware/govmomi/govc/cli"
 	"github.com/vmware/govmomi/govc/flags"
+	vapicrypto "github.com/vmware/govmomi/vapi/crypto"
 )
 
 type rm struct {
@@ -70,6 +71,18 @@ func (cmd *rm) Run(ctx context.Context, f *flag.FlagSet) error {
 		return err
 	}
 
+	native, err := m.IsNativeProvider(ctx, id)
+	if err != nil {
+		return err
+	}
+	if native {
+		rc, err := cmd.RestClient()
+		if err != nil {
+			return err
+		}
+		return vapicrypto.NewManager(rc).KmsProviderDelete(ctx, id)
+	}
+
 	if cmd.server != "" {
 		return m.RemoveKmipServer(ctx, id, cmd.server)
 	}
diff --git a/govc/namespace/cluster/logs.go b/govc/namespace/cluster/logs.go
index 80808f73..663afd0f 100644
--- a/govc/namespace/cluster/logs.go
+++ b/govc/namespace/cluster/logs.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2020 VMware, Inc. All Rights Reserved.
+Copyright (c) 2020-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,10 +19,6 @@ package cluster
 import (
 	"context"
 	"flag"
-	"io"
-	"mime"
-	"net/http"
-	"os"
 
 	"github.com/vmware/govmomi/govc/cli"
 	"github.com/vmware/govmomi/govc/flags"
@@ -90,29 +86,5 @@ func (cmd *logs) Run(ctx context.Context, f *flag.FlagSet) error {
 		req.Header.Set("vmware-api-session-id", id)
 	}
 
-	return c.Client.Do(ctx, req, func(res *http.Response) error {
-		if name == "" {
-			d := res.Header.Get("Content-Disposition")
-			_, params, err := mime.ParseMediaType(d)
-			if err == nil {
-				name = params["filename"]
-			}
-		}
-
-		var w io.Writer
-
-		if name == "-" {
-			w = os.Stdout
-		} else {
-			f, err := os.Create(name)
-			if err != nil {
-				return err
-			}
-			defer f.Close()
-			w = f
-		}
-
-		_, err = io.Copy(w, res.Body)
-		return err
-	})
+	return c.DownloadAttachment(ctx, req, name)
 }
diff --git a/vapi/crypto/crypto.go b/vapi/crypto/crypto.go
new file mode 100644
index 00000000..9d7ceb08
--- /dev/null
+++ b/vapi/crypto/crypto.go
@@ -0,0 +1,104 @@
+/*
+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 crypto
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/vmware/govmomi/vapi/crypto/internal"
+	"github.com/vmware/govmomi/vapi/rest"
+)
+
+// Manager extends rest.Client, adding crypto related methods.
+// Currently providing create, delete and export only.
+// See crypto.ManagerKmip for getting provider details.
+type Manager struct {
+	*rest.Client
+}
+
+// NewManager creates a new Manager instance with the given client.
+func NewManager(client *rest.Client) *Manager {
+	return &Manager{
+		Client: client,
+	}
+}
+
+type KmsProviderConstraints struct {
+	TpmRequired bool `json:"tpm_required"`
+}
+
+type KmsProviderCreateSpec struct {
+	Provider    string                 `json:"provider"`
+	Constraints KmsProviderConstraints `json:"constraints"`
+}
+
+type KmsProviderExportSpec struct {
+	Provider string `json:"provider"`
+	Password string `json:"password,omitempty"`
+}
+
+type KmsProviderDownloadToken struct {
+	Token  string `json:"token"`
+	Expiry string `json:"expiry"`
+}
+
+type KmsProviderExportLocation struct {
+	URL           string                   `json:"url"`
+	DownloadToken KmsProviderDownloadToken `json:"download_token"`
+}
+
+type KmsProviderExport struct {
+	Type     string                     `json:"type"`
+	Location *KmsProviderExportLocation `json:"location,omitempty"`
+}
+
+func (c *Manager) KmsProviderCreate(ctx context.Context, spec KmsProviderCreateSpec) error {
+	resource := c.Resource(internal.KmsProvidersPath)
+	request := resource.Request(http.MethodPost, spec)
+	return c.Do(ctx, request, nil)
+}
+
+func (c *Manager) KmsProviderDelete(ctx context.Context, provider string) error {
+	resource := c.Resource(internal.KmsProvidersPath).WithSubpath(provider)
+	request := resource.Request(http.MethodDelete)
+	return c.Do(ctx, request, nil)
+}
+
+func (c *Manager) KmsProviderExport(ctx context.Context, spec KmsProviderExportSpec) (*KmsProviderExport, error) {
+	resource := c.Resource(internal.KmsProvidersPath).WithParam("action", "export")
+	request := resource.Request(http.MethodPost, spec)
+
+	var res KmsProviderExport
+	if err := c.Do(ctx, request, &res); err != nil {
+		return nil, err
+	}
+
+	return &res, nil
+}
+
+func (c *Manager) KmsProviderExportRequest(ctx context.Context, export *KmsProviderExportLocation) (*http.Request, error) {
+	req, err := http.NewRequest(http.MethodGet, export.URL, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", export.DownloadToken.Token))
+
+	return req, nil
+}
diff --git a/vapi/crypto/doc.go b/vapi/crypto/doc.go
new file mode 100644
index 00000000..ba082f0a
--- /dev/null
+++ b/vapi/crypto/doc.go
@@ -0,0 +1,23 @@
+/*
+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 vapi/crypto provides access to the Crypto Manager REST APIs that are not available in the SOAP API.
+Currently for creating and deleting native providers only.
+See the top-level package crypto for getting provider details via crypto.ManagerKmip.
+See also: https://blogs.vmware.com/code/2023/07/30/automate-vsphere-native-key-providers/
+*/
+package crypto
diff --git a/vapi/crypto/internal/internal.go b/vapi/crypto/internal/internal.go
new file mode 100644
index 00000000..6c9903d2
--- /dev/null
+++ b/vapi/crypto/internal/internal.go
@@ -0,0 +1,21 @@
+/*
+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 internal
+
+const (
+	KmsProvidersPath = "/api/vcenter/crypto-manager/kms/providers"
+)
diff --git a/vapi/crypto/simulator/simulator.go b/vapi/crypto/simulator/simulator.go
new file mode 100644
index 00000000..73f9232a
--- /dev/null
+++ b/vapi/crypto/simulator/simulator.go
@@ -0,0 +1,183 @@
+/*
+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 (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+	"path"
+	"time"
+
+	"github.com/google/uuid"
+
+	"github.com/vmware/govmomi/simulator"
+	"github.com/vmware/govmomi/vapi/crypto"
+	"github.com/vmware/govmomi/vapi/crypto/internal"
+	vapi "github.com/vmware/govmomi/vapi/simulator"
+	"github.com/vmware/govmomi/vim25/types"
+)
+
+const (
+	typeNativeProvider = string(types.KmipClusterInfoKmsManagementTypeNativeProvider)
+	backupPath         = "/cryptomanager/kms/"
+)
+
+func init() {
+	simulator.RegisterEndpoint(func(s *simulator.Service, r *simulator.Registry) {
+		New(r, s.Listen).Register(s, r)
+	})
+}
+
+// Handler implements the Cluster Modules API simulator
+type Handler struct {
+	URL *url.URL
+	Map *simulator.Registry
+}
+
+// New creates a Handler instance
+func New(r *simulator.Registry, u *url.URL) *Handler {
+	return &Handler{
+		Map: r,
+		URL: u,
+	}
+}
+
+// Register Namespace Management API paths with the vapi simulator's http.ServeMux
+func (h *Handler) Register(s *simulator.Service, r *simulator.Registry) {
+	if r.IsVPX() {
+		s.HandleFunc(internal.KmsProvidersPath, h.providers)
+		s.HandleFunc(internal.KmsProvidersPath+"/", h.providersID)
+		s.HandleFunc(backupPath, h.backup)
+	}
+}
+
+// We need to use the simulator objects directly when updating fields (e.g. HasBackup)
+// Skipping the trouble of locking for now, as existing use-cases would not race.
+func (h *Handler) find(id string) *types.KmipClusterInfo {
+	m := h.Map.CryptoManager()
+	for i := range m.KmipServers {
+		p := &m.KmipServers[i]
+		if p.ClusterId.Id == id {
+			return p
+		}
+	}
+	return nil
+}
+
+func (h *Handler) backup(w http.ResponseWriter, r *http.Request) {
+	id := path.Base(r.RequestURI)
+	p := h.find(id)
+	if p == nil {
+		vapi.ApiErrorNotFound(w)
+		return
+	}
+
+	// Content of the simulated backup does not matter for the use-case we're covering:
+	// Export sets HasBackup=true, which sets CryptoManagerKmipClusterStatus.OverallStatus=green
+	p.HasBackup = types.NewBool(true)
+
+	name := fmt.Sprintf("%s%s.p12", id, time.Now().Format(time.RFC3339))
+
+	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name))
+	_ = json.NewEncoder(w).Encode(p)
+}
+
+func (h *Handler) providers(w http.ResponseWriter, r *http.Request) {
+	switch r.Method {
+	case http.MethodPost:
+		switch r.URL.Query().Get("action") {
+		case "":
+			var spec crypto.KmsProviderCreateSpec
+
+			if vapi.Decode(r, w, &spec) {
+				if h.find(spec.Provider) != nil {
+					vapi.ApiErrorAlreadyExists(w)
+					return
+				}
+			}
+
+			m := h.Map.CryptoManager()
+			m.KmipServers = append(m.KmipServers, types.KmipClusterInfo{
+				ClusterId:      types.KeyProviderId{Id: spec.Provider},
+				ManagementType: typeNativeProvider,
+				TpmRequired:    &spec.Constraints.TpmRequired,
+				HasBackup:      types.NewBool(false),
+			})
+		case "export":
+			var spec crypto.KmsProviderExportSpec
+			var p *types.KmipClusterInfo
+
+			if vapi.Decode(r, w, &spec) {
+				if p = h.find(spec.Provider); p == nil {
+					vapi.ApiErrorNotFound(w)
+					return
+				}
+				if p.ManagementType != typeNativeProvider {
+					vapi.ApiErrorUnsupported(w)
+					return
+				}
+
+				u := url.URL{
+					Scheme: h.URL.Scheme,
+					Host:   h.URL.Host,
+					Path:   backupPath + spec.Provider,
+				}
+
+				res := crypto.KmsProviderExport{
+					Type: "LOCATION",
+					Location: &crypto.KmsProviderExportLocation{
+						URL: u.String(),
+						DownloadToken: crypto.KmsProviderDownloadToken{
+							Token:  uuid.NewString(),
+							Expiry: time.Now().Add(time.Minute).Format(time.RFC3339),
+						},
+					},
+				}
+
+				vapi.StatusOK(w, res)
+			}
+		}
+	default:
+		w.WriteHeader(http.StatusMethodNotAllowed)
+	}
+}
+
+func (h *Handler) providersID(w http.ResponseWriter, r *http.Request) {
+	switch r.Method {
+	case http.MethodDelete:
+		id := path.Base(r.RequestURI)
+		p := h.find(id)
+		if p == nil {
+			vapi.ApiErrorNotFound(w)
+			return
+		}
+		if p.ManagementType != typeNativeProvider {
+			vapi.ApiErrorUnsupported(w)
+			return
+		}
+		m := h.Map.CryptoManager()
+		_ = m.UnregisterKmsCluster(simulator.SpoofContext(), &types.UnregisterKmsCluster{
+			This:      m.Self,
+			ClusterId: types.KeyProviderId{Id: id},
+		})
+		vapi.StatusOK(w)
+	default:
+		w.WriteHeader(http.StatusMethodNotAllowed)
+	}
+}
diff --git a/vapi/rest/client.go b/vapi/rest/client.go
index 52552bf0..048b65f3 100644
--- a/vapi/rest/client.go
+++ b/vapi/rest/client.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2018 VMware, Inc. All Rights Reserved.
+Copyright (c) 2018-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,
@@ -22,8 +22,10 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"mime"
 	"net/http"
 	"net/url"
+	"os"
 	"strings"
 	"sync"
 	"time"
@@ -273,6 +275,36 @@ func (c *Client) DownloadFile(ctx context.Context, file string, u *url.URL, para
 	return c.Client.DownloadFile(ctx, file, u, &p)
 }
 
+// DownloadAttachment writes the response to given filename, defaulting to Content-Disposition filename in the response.
+// A filename of "-" writes the response to stdout.
+func (c *Client) DownloadAttachment(ctx context.Context, req *http.Request, filename string) error {
+	return c.Client.Do(ctx, req, func(res *http.Response) error {
+		if filename == "" {
+			d := res.Header.Get("Content-Disposition")
+			_, params, err := mime.ParseMediaType(d)
+			if err == nil {
+				filename = params["filename"]
+			}
+		}
+
+		var w io.Writer
+
+		if filename == "-" {
+			w = os.Stdout
+		} else {
+			f, err := os.Create(filename)
+			if err != nil {
+				return err
+			}
+			defer f.Close()
+			w = f
+		}
+
+		_, err := io.Copy(w, res.Body)
+		return err
+	})
+}
+
 // Upload wraps soap.Client.Upload, adding the REST authentication header
 func (c *Client) Upload(ctx context.Context, f io.Reader, u *url.URL, param *soap.Upload) error {
 	p := *param
diff --git a/vcsim/main.go b/vcsim/main.go
index 34ffbc0b..97332a51 100644
--- a/vcsim/main.go
+++ b/vcsim/main.go
@@ -49,6 +49,7 @@ import (
 	_ "github.com/vmware/govmomi/vapi/appliance/simulator"
 	_ "github.com/vmware/govmomi/vapi/cis/tasks/simulator"
 	_ "github.com/vmware/govmomi/vapi/cluster/simulator"
+	_ "github.com/vmware/govmomi/vapi/crypto/simulator"
 	_ "github.com/vmware/govmomi/vapi/esx/settings/simulator"
 	_ "github.com/vmware/govmomi/vapi/namespace/simulator"
 	_ "github.com/vmware/govmomi/vapi/simulator"


Test patch:
diff --git a/govc/test/kms.bats b/govc/test/kms.bats
index 071ed9b9..75b6dd26 100755
--- a/govc/test/kms.bats
+++ b/govc/test/kms.bats
@@ -2,7 +2,7 @@
 
 load test_helper
 
-@test "kms" {
+@test "kms standard" {
   vcsim_env
 
   run govc kms.ls
@@ -47,9 +47,36 @@ load test_helper
   run govc kms.add -n my-server -a "$host" vcsim-kp
   assert_success
 
+  run govc kms.export vcsim-kp
+  assert_failure # export is only supported for native
+  assert_matches "400 Bad Request"
+
+  run govc session.login -r -X DELETE "/api/vcenter/crypto-manager/kms/providers/vcsim-kp"
+  assert_failure # vapi can only delete native providers
+  assert_matches "400 Bad Request"
+
   run govc kms.rm vcsim-kp
   assert_success
 
   run govc kms.rm vcsim-kp
   assert_failure # does not exist
 }
+
+@test "kms native" {
+  vcsim_env
+
+  run govc kms.add -N nkp
+  assert_success
+
+  run govc kms.ls nkp
+  assert_success
+
+  run govc kms.export -f /dev/null nkp
+  assert_success
+
+  run govc kms.default nkp
+  assert_success
+
+  run govc kms.rm nkp
+  assert_success
+}