Repository: vmware/govmomi
Pull request #3384 (2024-03-11 21:54:17)

Add support for SHA256 thumbprint based vCenter authentication
vSphere in general is moving from SHA1 to SHA256 based TLS certificate thumbprints. This change allows use of SHA256 thumbprint when connecting to vCenter.

govc: the about.cert command '-thumbprint' flag now outputs SHA256 instead of SHA1


dougm: Existing test coverage here: https://github.com/vmware/govmomi/blob/1fb3f623bc8498d098bc92e01ab4f881afd77c3c/govc/test/cli.bats#L75

Manual version of that test:
```console
% govc about.cert | grep Thumbprint
Thumbprints:                 
  SHA-256 Thumbprint:        D2:87:1C:36:1A:1C:88:AA:8E:14:14:4D:1E:6A:96:94:B3:D5:86:DF:5D:D3:AE:D3:BA:CC:D3:BA:8C:36:B4:35
  SHA-1 Thumbprint:          3D:20:E1:A4:24:69:61:06:FF:A4:F1:8A:85:38:3B:E4:5D:66:2D:5E

% govc about.cert -k=true -thumbprint | tee .govc_known_hosts
10.x.x.x D2:87:1C:36:1A:1C:88:AA:8E:14:14:4D:1E:6A:96:94:B3:D5:86:DF:5D:D3:AE:D3:BA:CC:D3:BA:8C:36:B4:35

% govc about -k=false
govc: Post "https://10.x.x.x.x/sdk#vcva70U3e": tls: failed to verify certificate: x509: “10.x.x.x.x” certificate is not trusted

% govc about -k=false -tls-known-hosts .govc_known_hosts 
FullName:     VMware vCenter Server 7.0.3 build-21958406
...
```

Patch:
diff --git a/govc/about/cert.go b/govc/about/cert.go
index 5a5af0f7..8ec65d0c 100644
--- a/govc/about/cert.go
+++ b/govc/about/cert.go
@@ -89,7 +89,7 @@ func (r *certResult) Write(w io.Writer) error {
 
 	if r.cmd.thumbprint {
 		u := r.cmd.Session.URL
-		_, err := fmt.Fprintf(w, "%s %s\n", u.Host, r.info.ThumbprintSHA1)
+		_, err := fmt.Fprintf(w, "%s %s\n", u.Host, r.info.ThumbprintSHA256)
 		return err
 	}
 
diff --git a/lookup/client.go b/lookup/client.go
index a807c75b..4cc73e0d 100644
--- a/lookup/client.go
+++ b/lookup/client.go
@@ -23,7 +23,6 @@ import (
 	"log"
 	"net/url"
 
-	"github.com/vmware/govmomi/internal"
 	"github.com/vmware/govmomi/lookup/methods"
 	"github.com/vmware/govmomi/lookup/types"
 	"github.com/vmware/govmomi/object"
@@ -56,9 +55,9 @@ type Client struct {
 
 // NewClient returns a client targeting the SSO Lookup Service API endpoint.
 func NewClient(ctx context.Context, c *vim25.Client) (*Client, error) {
+	// PSC may be external, attempt to derive from sts.uri
 	path := &url.URL{Path: Path}
-	// PSC may be external, attempt to derive from sts.uri if not using envoy sidecar
-	if !internal.UsingEnvoySidecar(c) && c.ServiceContent.Setting != nil {
+	if c.ServiceContent.Setting != nil {
 		m := object.NewOptionManager(c, *c.ServiceContent.Setting)
 		opts, err := m.Query(ctx, "config.vpxd.sso.sts.uri")
 		if err == nil && len(opts) == 1 {
diff --git a/object/host_certificate_info.go b/object/host_certificate_info.go
index fd9b370e..1a3a7fab 100644
--- a/object/host_certificate_info.go
+++ b/object/host_certificate_info.go
@@ -17,7 +17,6 @@ limitations under the License.
 package object
 
 import (
-	"crypto/sha256"
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509/pkix"
@@ -58,14 +57,7 @@ func (info *HostCertificateInfo) FromCertificate(cert *x509.Certificate) *HostCe
 	info.Subject = info.fromName(info.subjectName)
 
 	info.ThumbprintSHA1 = soap.ThumbprintSHA1(cert)
-
-	// SHA-256 for info purposes only, API fields all use SHA-1
-	sum := sha256.Sum256(cert.Raw)
-	hex := make([]string, len(sum))
-	for i, b := range sum {
-		hex[i] = fmt.Sprintf("%02X", b)
-	}
-	info.ThumbprintSHA256 = strings.Join(hex, ":")
+	info.ThumbprintSHA256 = soap.ThumbprintSHA256(cert)
 
 	if info.Status == "" {
 		info.Status = string(types.HostCertificateManagerCertificateInfoCertificateStatusUnknown)
diff --git a/vim25/soap/client.go b/vim25/soap/client.go
index bbf3d92f..a253ab5b 100644
--- a/vim25/soap/client.go
+++ b/vim25/soap/client.go
@@ -21,6 +21,7 @@ import (
 	"bytes"
 	"context"
 	"crypto/sha1"
+	"crypto/sha256"
 	"crypto/tls"
 	"crypto/x509"
 	"encoding/json"
@@ -387,6 +388,20 @@ func ThumbprintSHA1(cert *x509.Certificate) string {
 	return strings.Join(hex, ":")
 }
 
+// ThumbprintSHA256 returns the sha256 thumbprint of the given cert.
+func ThumbprintSHA256(cert *x509.Certificate) string {
+	sum := sha256.Sum256(cert.Raw)
+	hex := make([]string, len(sum))
+	for i, b := range sum {
+		hex[i] = fmt.Sprintf("%02X", b)
+	}
+	return strings.Join(hex, ":")
+}
+
+func thumbprintMatches(thumbprint string, cert *x509.Certificate) bool {
+	return thumbprint == ThumbprintSHA256(cert) || thumbprint == ThumbprintSHA1(cert)
+}
+
 func (c *Client) dialTLSContext(
 	ctx context.Context,
 	network, addr string) (net.Conn, error) {
@@ -418,14 +433,13 @@ func (c *Client) dialTLSContext(
 	}
 
 	cert := conn.ConnectionState().PeerCertificates[0]
-	peer := ThumbprintSHA1(cert)
-	if thumbprint != peer {
-		_ = conn.Close()
-
-		return nil, fmt.Errorf("host %q thumbprint does not match %q", addr, thumbprint)
+	if thumbprintMatches(thumbprint, cert) {
+		return conn, nil
 	}
 
-	return conn, nil
+	_ = conn.Close()
+
+	return nil, fmt.Errorf("host %q thumbprint does not match %q", addr, thumbprint)
 }
 
 // splitHostPort is similar to net.SplitHostPort,


Test patch:
diff --git a/govc/test/cli.bats b/govc/test/cli.bats
index 0ec72910..37bb7ce9 100755
--- a/govc/test/cli.bats
+++ b/govc/test/cli.bats
@@ -59,7 +59,7 @@ load test_helper
   run govc about.cert -show
   assert_success
 
-  # with -k=true we get thumbprint output and exit 0
+  # with -k=true we get sha256 thumbprint output and exit 0
   thumbprint=$(govc about.cert -k=true -thumbprint)
 
   # with -k=true we get thumbprint output and exit 60
@@ -77,6 +77,12 @@ load test_helper
 
   run govc about -k=false -tls-known-hosts <(echo "nope nope")
   assert_failure
+
+  # sha1 backwards compatibility
+  host=$(awk '{print $1}'<<<"$thumbprint")
+  sha1=$(govc about.cert -k=true -json | jq -r .thumbprintSHA1)
+  run govc about -k=false -tls-known-hosts <(echo "$host $sha1")
+  assert_success
 }
 
 @test "version" {