diff --git a/govc/USAGE.md b/govc/USAGE.md index 530110645..440650e11 100644 --- a/govc/USAGE.md +++ b/govc/USAGE.md @@ -215,6 +215,10 @@ but appear via `govc $cmd -h`: - [library.subscriber.ls](#librarysubscriberls) - [library.subscriber.rm](#librarysubscriberrm) - [library.sync](#librarysync) + - [library.trust.create](#librarytrustcreate) + - [library.trust.info](#librarytrustinfo) + - [library.trust.ls](#librarytrustls) + - [library.trust.rm](#librarytrustrm) - [library.update](#libraryupdate) - [library.vmtx.info](#libraryvmtxinfo) - [license.add](#licenseadd) @@ -3502,6 +3506,62 @@ Options: -vmtx= Sync subscribed library to local library as VM Templates ``` +## library.trust.create + +``` +Usage: govc library.trust.create [OPTIONS] FILE + +Add a certificate to content library trust store. + +If FILE name is "-", read certificate from stdin. + +Examples: + govc library.trust.create cert.pem + govc about.cert -show -u wp-content-int.vmware.com | govc library.trust.create - + +Options: +``` + +## library.trust.info + +``` +Usage: govc library.trust.info [OPTIONS] ID + +Display trusted certificate info. + +Examples: + govc library.trust.info vmware_signed + +Options: +``` + +## library.trust.ls + +``` +Usage: govc library.trust.ls [OPTIONS] + +List trusted certificates for content libraries. + +Examples: + govc library.trust.ls + govc library.trust.ls -json + +Options: +``` + +## library.trust.rm + +``` +Usage: govc library.trust.rm [OPTIONS] ID + +Remove certificate ID from trusted certificates. + +Examples: + govc library.trust.rm $id + +Options: +``` + ## library.update ``` diff --git a/govc/library/info.go b/govc/library/info.go index 86a703c22..1f59eaf61 100644 --- a/govc/library/info.go +++ b/govc/library/info.go @@ -222,7 +222,12 @@ func (r infoResultsWriter) writeItem( fmt.Fprintf(w, " Created:\t%s\n", v.CreationTime.Format(time.ANSIC)) fmt.Fprintf(w, " Modified:\t%s\n", v.LastModifiedTime.Format(time.ANSIC)) fmt.Fprintf(w, " Version:\t%s\n", v.Version) - + if v.SecurityCompliance != nil { + fmt.Fprintf(w, " Security Compliance:\t%t\n", *v.SecurityCompliance) + } + if v.CertificateVerification != nil { + fmt.Fprintf(w, " Certificate Status:\t%s\n", v.CertificateVerification.Status) + } if r.cmd.long { fmt.Fprintf(w, " Datastore Path:\t%s\n", r.cmd.getDatastorePath(res)) } diff --git a/govc/library/trust/create.go b/govc/library/trust/create.go new file mode 100644 index 000000000..75c72b3eb --- /dev/null +++ b/govc/library/trust/create.go @@ -0,0 +1,84 @@ +/* +Copyright (c) 2022-2022 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 trust + +import ( + "bytes" + "context" + "flag" + "io" + "io/ioutil" + "os" + "path/filepath" + + "github.com/vmware/govmomi/govc/cli" + "github.com/vmware/govmomi/govc/flags" + "github.com/vmware/govmomi/vapi/library" +) + +type create struct { + *flags.ClientFlag +} + +func init() { + cli.Register("library.trust.create", &create{}) +} + +func (cmd *create) Register(ctx context.Context, f *flag.FlagSet) { + cmd.ClientFlag, ctx = flags.NewClientFlag(ctx) + cmd.ClientFlag.Register(ctx, f) +} + +func (cmd *create) Usage() string { + return "FILE" +} + +func (cmd *create) Description() string { + return `Add a certificate to content library trust store. + +If FILE name is "-", read certificate from stdin. + +Examples: + govc library.trust.create cert.pem + govc about.cert -show -u wp-content-int.vmware.com | govc library.trust.create -` +} + +func (cmd *create) Run(ctx context.Context, f *flag.FlagSet) error { + c, err := cmd.RestClient() + if err != nil { + return err + } + + var cert string + + name := f.Arg(0) + if name == "-" || name == "" { + var buf bytes.Buffer + if _, err := io.Copy(&buf, os.Stdin); err != nil { + return err + } + cert = buf.String() + } else { + b, err := ioutil.ReadFile(filepath.Clean(name)) + if err != nil { + return err + } + cert = string(b) + } + + return library.NewManager(c).CreateTrustedCertificate(ctx, cert) +} diff --git a/govc/library/trust/info.go b/govc/library/trust/info.go new file mode 100644 index 000000000..5685187dd --- /dev/null +++ b/govc/library/trust/info.go @@ -0,0 +1,102 @@ +/* +Copyright (c) 2022-2022 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 trust + +import ( + "context" + "crypto/x509" + "encoding/pem" + "flag" + "io" + + "github.com/vmware/govmomi/govc/cli" + "github.com/vmware/govmomi/govc/flags" + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vapi/library" +) + +type info struct { + *flags.ClientFlag + *flags.OutputFlag +} + +func init() { + cli.Register("library.trust.info", &info{}) +} + +func (cmd *info) Register(ctx context.Context, f *flag.FlagSet) { + cmd.ClientFlag, ctx = flags.NewClientFlag(ctx) + cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx) + cmd.ClientFlag.Register(ctx, f) + cmd.OutputFlag.Register(ctx, f) +} + +func (cmd *info) Process(ctx context.Context) error { + if err := cmd.ClientFlag.Process(ctx); err != nil { + return err + } + return nil +} + +func (cmd *info) Usage() string { + return "ID" +} + +func (cmd *info) Description() string { + return `Display trusted certificate info. + +Examples: + govc library.trust.info vmware_signed` +} + +type infoResultsWriter struct { + TrustedCertificateInfo *library.TrustedCertificate `json:"info,omitempty"` +} + +func (r infoResultsWriter) Write(w io.Writer) error { + block, _ := pem.Decode([]byte(r.TrustedCertificateInfo.Text)) + x, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return err + } + + var info object.HostCertificateInfo + info.FromCertificate(x) + + return info.Write(w) +} + +func (r infoResultsWriter) Dump() interface{} { + return r.TrustedCertificateInfo +} + +func (cmd *info) Run(ctx context.Context, f *flag.FlagSet) error { + if f.NArg() != 1 { + return flag.ErrHelp + } + + c, err := cmd.RestClient() + if err != nil { + return err + } + + cert, err := library.NewManager(c).GetTrustedCertificate(ctx, f.Arg(0)) + if err != nil { + return err + } + return cmd.WriteResult(&infoResultsWriter{cert}) +} diff --git a/govc/library/trust/ls.go b/govc/library/trust/ls.go new file mode 100644 index 000000000..7c5ddf6a6 --- /dev/null +++ b/govc/library/trust/ls.go @@ -0,0 +1,102 @@ +/* +Copyright (c) 2022-2022 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 trust + +import ( + "context" + "crypto/x509" + "encoding/pem" + "flag" + "fmt" + "io" + "text/tabwriter" + + "github.com/vmware/govmomi/govc/cli" + "github.com/vmware/govmomi/govc/flags" + "github.com/vmware/govmomi/vapi/library" +) + +type ls struct { + *flags.ClientFlag + *flags.OutputFlag +} + +func init() { + cli.Register("library.trust.ls", &ls{}) +} + +func (cmd *ls) Register(ctx context.Context, f *flag.FlagSet) { + cmd.ClientFlag, ctx = flags.NewClientFlag(ctx) + cmd.OutputFlag, ctx = flags.NewOutputFlag(ctx) + cmd.ClientFlag.Register(ctx, f) + cmd.OutputFlag.Register(ctx, f) +} + +func (cmd *ls) Process(ctx context.Context) error { + if err := cmd.ClientFlag.Process(ctx); err != nil { + return err + } + return nil +} + +func (cmd *ls) Description() string { + return `List trusted certificates for content libraries. + +Examples: + govc library.trust.ls + govc library.trust.ls -json` +} + +type lsResultsWriter struct { + TrustedCertificates []library.TrustedCertificateSummary `json:"certificates,omitempty"` +} + +func (r lsResultsWriter) Write(w io.Writer) error { + tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0) + + for _, cert := range r.TrustedCertificates { + block, _ := pem.Decode([]byte(cert.Text)) + x, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return err + } + + x.Subject.Names = nil // trim x.Subject.String() output + + fmt.Fprintf(tw, "%s\t%s\n", cert.ID, x.Subject) + } + + return tw.Flush() +} + +func (r lsResultsWriter) Dump() interface{} { + return r.TrustedCertificates +} + +func (cmd *ls) Run(ctx context.Context, _ *flag.FlagSet) error { + c, err := cmd.RestClient() + if err != nil { + return err + } + + certs, err := library.NewManager(c).ListTrustedCertificates(ctx) + if err != nil { + return err + } + + return cmd.WriteResult(&lsResultsWriter{certs}) +} diff --git a/govc/library/trust/rm.go b/govc/library/trust/rm.go new file mode 100644 index 000000000..33d91f49f --- /dev/null +++ b/govc/library/trust/rm.go @@ -0,0 +1,63 @@ +/* +Copyright (c) 2022-2022 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 trust + +import ( + "context" + "flag" + + "github.com/vmware/govmomi/govc/cli" + "github.com/vmware/govmomi/govc/flags" + "github.com/vmware/govmomi/vapi/library" +) + +type rm struct { + *flags.ClientFlag +} + +func init() { + cli.Register("library.trust.rm", &rm{}) +} + +func (cmd *rm) Register(ctx context.Context, f *flag.FlagSet) { + cmd.ClientFlag, ctx = flags.NewClientFlag(ctx) + cmd.ClientFlag.Register(ctx, f) +} + +func (cmd *rm) Usage() string { + return "ID" +} + +func (cmd *rm) Description() string { + return `Remove certificate ID from trusted certificates. + +Examples: + govc library.trust.rm $id` +} + +func (cmd *rm) Run(ctx context.Context, f *flag.FlagSet) error { + if f.NArg() != 1 { + return flag.ErrHelp + } + + c, err := cmd.RestClient() + if err != nil { + return err + } + + return library.NewManager(c).DeleteTrustedCertificate(ctx, f.Arg(0)) +} diff --git a/govc/main.go b/govc/main.go index 58e10a098..6f7385642 100644 --- a/govc/main.go +++ b/govc/main.go @@ -69,6 +69,7 @@ import ( _ "github.com/vmware/govmomi/govc/library/policy" _ "github.com/vmware/govmomi/govc/library/session" _ "github.com/vmware/govmomi/govc/library/subscriber" + _ "github.com/vmware/govmomi/govc/library/trust" _ "github.com/vmware/govmomi/govc/license" _ "github.com/vmware/govmomi/govc/logs" _ "github.com/vmware/govmomi/govc/ls" diff --git a/govc/test/library.bats b/govc/test/library.bats index df0186062..a294b24e0 100755 --- a/govc/test/library.bats +++ b/govc/test/library.bats @@ -469,6 +469,16 @@ EOF library_secpol=$(govc library.info -json secure-content | jq '.[].security_policy_id' -r) assert_equal "$library_secpol" "$policy_id" + run govc library.import secure-content "$GOVC_IMAGES/ttylinux-latest.ova" + assert_success + + run govc library.info -json secure-content/ttylinux-latest + assert_success + + assert_equal false "$(jq -r <<<"$output" .[].security_compliance)" + + assert_equal NOT_AVAILABLE "$(jq -r <<<"$output" .[].certificate_verification_info.status)" + run govc library.rm secure-content assert_success } @@ -504,3 +514,40 @@ EOF n=$(govc library.info my-content | grep -c Name:) [ "$n" == 1 ] } + +@test "library.trust" { + vcsim_env + + run govc library.trust.ls + assert_success + + run govc library.trust.info enoent + assert_failure # id does not exist + + run govc library.trust.rm enoent + assert_failure # id does not exist + + pem=$(new_id) + run govc extension.setcert -cert-pem ++ -org govc-library-trust "$pem" # generate a cert for testing + assert_success + + run govc library.trust.create "$pem.crt" + assert_success + + id=$(govc library.trust.ls | grep O=govc-library-trust | awk '{print $1}') + run govc library.trust.info "$id" + assert_success + + run govc library.trust.rm "$id" + assert_success + + run govc library.trust.info "$id" + assert_failure # id does not exist + + date > "$pem.crt" + run govc library.trust.create "$id.crt" + assert_failure # invalid cert + + # remove generated cert and key + rm "$pem".{crt,key} +} diff --git a/vapi/internal/internal.go b/vapi/internal/internal.go index fe66d7c40..f6584c569 100644 --- a/vapi/internal/internal.go +++ b/vapi/internal/internal.go @@ -40,6 +40,7 @@ const ( SecurityPoliciesPath = "/api/content/security-policies" SubscribedLibraryItem = "/com/vmware/content/library/subscribed-item" Subscriptions = "/com/vmware/content/library/subscriptions" + TrustedCertificatesPath = "/api/content/trusted-certificates" VCenterOVFLibraryItem = "/com/vmware/vcenter/ovf/library-item" VCenterVMTXLibraryItem = "/vcenter/vm-template/library-items" VCenterVM = "/vcenter/vm" diff --git a/vapi/library/library_item.go b/vapi/library/library_item.go index c596ec128..adc2101ab 100644 --- a/vapi/library/library_item.go +++ b/vapi/library/library_item.go @@ -1,5 +1,5 @@ /* -Copyright (c) 2018 VMware, Inc. All Rights Reserved. +Copyright (c) 2018-2022 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. @@ -47,6 +47,15 @@ type Item struct { SourceID string `json:"source_id,omitempty"` Type string `json:"type,omitempty"` Version string `json:"version,omitempty"` + + SecurityCompliance *bool `json:"security_compliance,omitempty"` + CertificateVerification *ItemCertificateVerification `json:"certificate_verification_info,omitempty"` +} + +// ItemCertificateVerification contains the certificate verification status and item's signing certificate +type ItemCertificateVerification struct { + Status string `json:"status"` + CertChain []string `json:"cert_chain,omitempty"` } // Patch merges updates from the given src. diff --git a/vapi/library/trusted_certificates.go b/vapi/library/trusted_certificates.go new file mode 100644 index 000000000..33e25a065 --- /dev/null +++ b/vapi/library/trusted_certificates.go @@ -0,0 +1,70 @@ +/* +Copyright (c) 2022-2022 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 library + +import ( + "context" + "net/http" + "path" + + "github.com/vmware/govmomi/vapi/internal" +) + +// TrustedCertificate contains a trusted certificate in Base64 encoded PEM format +type TrustedCertificate struct { + Text string `json:"cert_text"` +} + +// TrustedCertificateSummary contains a trusted certificate in Base64 encoded PEM format and its id +type TrustedCertificateSummary struct { + TrustedCertificate + ID string `json:"certificate"` +} + +// ListTrustedCertificates retrieves all content library's trusted certificates +func (c *Manager) ListTrustedCertificates(ctx context.Context) ([]TrustedCertificateSummary, error) { + url := c.Resource(internal.TrustedCertificatesPath) + var res struct { + Certificates []TrustedCertificateSummary `json:"certificates"` + } + err := c.Do(ctx, url.Request(http.MethodGet), &res) + return res.Certificates, err +} + +// GetTrustedCertificate retrieves a trusted certificate for a given certificate id +func (c *Manager) GetTrustedCertificate(ctx context.Context, id string) (*TrustedCertificate, error) { + url := c.Resource(path.Join(internal.TrustedCertificatesPath, id)) + var res TrustedCertificate + err := c.Do(ctx, url.Request(http.MethodGet), &res) + if err != nil { + return nil, err + } + return &res, nil +} + +// CreateTrustedCertificate adds a certificate to content library trust store +func (c *Manager) CreateTrustedCertificate(ctx context.Context, cert string) error { + url := c.Resource(internal.TrustedCertificatesPath) + body := TrustedCertificate{Text: cert} + return c.Do(ctx, url.Request(http.MethodPost, body), nil) +} + +// DeleteTrustedCertificate deletes the trusted certificate from content library's trust store for the given id +func (c *Manager) DeleteTrustedCertificate(ctx context.Context, id string) error { + url := c.Resource(path.Join(internal.TrustedCertificatesPath, id)) + return c.Do(ctx, url.Request(http.MethodDelete), nil) +} diff --git a/vapi/simulator/simulator.go b/vapi/simulator/simulator.go index c11f36b40..c43494f2b 100644 --- a/vapi/simulator/simulator.go +++ b/vapi/simulator/simulator.go @@ -21,8 +21,10 @@ import ( "bytes" "context" "crypto/tls" + "crypto/x509" "encoding/base64" "encoding/json" + "encoding/pem" "errors" "fmt" "io" @@ -96,6 +98,7 @@ type handler struct { Update map[string]update Download map[string]download Policies []library.ContentSecurityPoliciesInfo + Trust map[string]library.TrustedCertificate } func init() { @@ -122,6 +125,7 @@ func New(u *url.URL, settings []vim.BaseOptionValue) ([]string, http.Handler) { Update: make(map[string]update), Download: make(map[string]download), Policies: defaultSecurityPolicies(), + Trust: make(map[string]library.TrustedCertificate), } handlers := []struct { @@ -166,6 +170,8 @@ func New(u *url.URL, settings []vim.BaseOptionValue) ([]string, http.Handler) { {internal.DebugEcho, s.debugEcho}, // /api/ patterns. {internal.SecurityPoliciesPath, s.librarySecurityPolicies}, + {internal.TrustedCertificatesPath, s.libraryTrustedCertificates}, + {internal.TrustedCertificatesPath + "/", s.libraryTrustedCertificatesID}, } for i := range handlers { @@ -1075,6 +1081,13 @@ func (s *handler) libraryItem(w http.ResponseWriter, r *http.Request) { spec.Item.ID = id spec.Item.CreationTime = types.NewTime(time.Now()) spec.Item.LastModifiedTime = types.NewTime(time.Now()) + if l.SecurityPolicyID != "" { + // TODO: verify signed items + spec.Item.SecurityCompliance = types.NewBool(false) + spec.Item.CertificateVerification = &library.ItemCertificateVerification{ + Status: "NOT_AVAILABLE", + } + } l.Item[id] = &item{Item: &spec.Item} OK(w, id) } @@ -2190,6 +2203,68 @@ func (s *handler) isValidSecurityPolicy(policy string) bool { return false } +func (s *handler) libraryTrustedCertificates(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + var res struct { + Certificates []library.TrustedCertificateSummary `json:"certificates"` + } + for id, cert := range s.Trust { + res.Certificates = append(res.Certificates, library.TrustedCertificateSummary{ + TrustedCertificate: cert, + ID: id, + }) + } + + StatusOK(w, &res) + case http.MethodPost: + var info library.TrustedCertificate + if s.decode(r, w, &info) { + block, _ := pem.Decode([]byte(info.Text)) + if block == nil { + s.error(w, errors.New("invalid certificate")) + return + } + _, err := x509.ParseCertificate(block.Bytes) + if err != nil { + s.error(w, err) + return + } + + id := uuid.New().String() + for x, cert := range s.Trust { + if info.Text == cert.Text { + id = x // existing certificate + break + } + } + s.Trust[id] = info + + w.WriteHeader(http.StatusCreated) + } + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +func (s *handler) libraryTrustedCertificatesID(w http.ResponseWriter, r *http.Request) { + id := path.Base(r.URL.Path) + cert, ok := s.Trust[id] + if !ok { + http.NotFound(w, r) + return + } + + switch r.Method { + case http.MethodGet: + StatusOK(w, &cert) + case http.MethodDelete: + delete(s.Trust, id) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + func (s *handler) vmID(w http.ResponseWriter, r *http.Request) { id := path.Base(r.URL.Path)