251 lines
6.3 KiB
Go
251 lines
6.3 KiB
Go
|
/*
|
||
|
Copyright (c) 2016 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 object
|
||
|
|
||
|
import (
|
||
|
"crypto/sha256"
|
||
|
"crypto/tls"
|
||
|
"crypto/x509"
|
||
|
"crypto/x509/pkix"
|
||
|
"encoding/asn1"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"net/url"
|
||
|
"strings"
|
||
|
"text/tabwriter"
|
||
|
|
||
|
"github.com/vmware/govmomi/vim25/soap"
|
||
|
"github.com/vmware/govmomi/vim25/types"
|
||
|
)
|
||
|
|
||
|
// HostCertificateInfo provides helpers for types.HostCertificateManagerCertificateInfo
|
||
|
type HostCertificateInfo struct {
|
||
|
types.HostCertificateManagerCertificateInfo
|
||
|
|
||
|
ThumbprintSHA1 string
|
||
|
ThumbprintSHA256 string
|
||
|
|
||
|
Err error
|
||
|
Certificate *x509.Certificate `json:"-"`
|
||
|
|
||
|
subjectName *pkix.Name
|
||
|
issuerName *pkix.Name
|
||
|
}
|
||
|
|
||
|
// FromCertificate converts x509.Certificate to HostCertificateInfo
|
||
|
func (info *HostCertificateInfo) FromCertificate(cert *x509.Certificate) *HostCertificateInfo {
|
||
|
info.Certificate = cert
|
||
|
info.subjectName = &cert.Subject
|
||
|
info.issuerName = &cert.Issuer
|
||
|
|
||
|
info.Issuer = info.fromName(info.issuerName)
|
||
|
info.NotBefore = &cert.NotBefore
|
||
|
info.NotAfter = &cert.NotAfter
|
||
|
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, ":")
|
||
|
|
||
|
if info.Status == "" {
|
||
|
info.Status = string(types.HostCertificateManagerCertificateInfoCertificateStatusUnknown)
|
||
|
}
|
||
|
|
||
|
return info
|
||
|
}
|
||
|
|
||
|
// 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.
|
||
|
// If tls.Dial returns an error of any other type, that error is returned.
|
||
|
func (info *HostCertificateInfo) FromURL(u *url.URL, config *tls.Config) error {
|
||
|
addr := u.Host
|
||
|
if !(strings.LastIndex(addr, ":") > strings.LastIndex(addr, "]")) {
|
||
|
addr += ":443"
|
||
|
}
|
||
|
|
||
|
conn, err := tls.Dial("tcp", addr, config)
|
||
|
if err != nil {
|
||
|
switch err.(type) {
|
||
|
case x509.UnknownAuthorityError:
|
||
|
case x509.HostnameError:
|
||
|
default:
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
info.Err = err
|
||
|
|
||
|
conn, err = tls.Dial("tcp", addr, &tls.Config{InsecureSkipVerify: true})
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
} else {
|
||
|
info.Status = string(types.HostCertificateManagerCertificateInfoCertificateStatusGood)
|
||
|
}
|
||
|
|
||
|
state := conn.ConnectionState()
|
||
|
_ = conn.Close()
|
||
|
info.FromCertificate(state.PeerCertificates[0])
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
var emailAddressOID = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}
|
||
|
|
||
|
func (info *HostCertificateInfo) fromName(name *pkix.Name) string {
|
||
|
var attrs []string
|
||
|
|
||
|
oids := map[string]string{
|
||
|
emailAddressOID.String(): "emailAddress",
|
||
|
}
|
||
|
|
||
|
for _, attr := range name.Names {
|
||
|
if key, ok := oids[attr.Type.String()]; ok {
|
||
|
attrs = append(attrs, fmt.Sprintf("%s=%s", key, attr.Value))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
attrs = append(attrs, fmt.Sprintf("CN=%s", name.CommonName))
|
||
|
|
||
|
add := func(key string, vals []string) {
|
||
|
for _, val := range vals {
|
||
|
attrs = append(attrs, fmt.Sprintf("%s=%s", key, val))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
elts := []struct {
|
||
|
key string
|
||
|
val []string
|
||
|
}{
|
||
|
{"OU", name.OrganizationalUnit},
|
||
|
{"O", name.Organization},
|
||
|
{"L", name.Locality},
|
||
|
{"ST", name.Province},
|
||
|
{"C", name.Country},
|
||
|
}
|
||
|
|
||
|
for _, elt := range elts {
|
||
|
add(elt.key, elt.val)
|
||
|
}
|
||
|
|
||
|
return strings.Join(attrs, ",")
|
||
|
}
|
||
|
|
||
|
func (info *HostCertificateInfo) toName(s string) *pkix.Name {
|
||
|
var name pkix.Name
|
||
|
|
||
|
for _, pair := range strings.Split(s, ",") {
|
||
|
attr := strings.SplitN(pair, "=", 2)
|
||
|
if len(attr) != 2 {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
v := attr[1]
|
||
|
|
||
|
switch strings.ToLower(attr[0]) {
|
||
|
case "cn":
|
||
|
name.CommonName = v
|
||
|
case "ou":
|
||
|
name.OrganizationalUnit = append(name.OrganizationalUnit, v)
|
||
|
case "o":
|
||
|
name.Organization = append(name.Organization, v)
|
||
|
case "l":
|
||
|
name.Locality = append(name.Locality, v)
|
||
|
case "st":
|
||
|
name.Province = append(name.Province, v)
|
||
|
case "c":
|
||
|
name.Country = append(name.Country, v)
|
||
|
case "emailaddress":
|
||
|
name.Names = append(name.Names, pkix.AttributeTypeAndValue{Type: emailAddressOID, Value: v})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return &name
|
||
|
}
|
||
|
|
||
|
// SubjectName parses Subject into a pkix.Name
|
||
|
func (info *HostCertificateInfo) SubjectName() *pkix.Name {
|
||
|
if info.subjectName != nil {
|
||
|
return info.subjectName
|
||
|
}
|
||
|
|
||
|
return info.toName(info.Subject)
|
||
|
}
|
||
|
|
||
|
// IssuerName parses Issuer into a pkix.Name
|
||
|
func (info *HostCertificateInfo) IssuerName() *pkix.Name {
|
||
|
if info.issuerName != nil {
|
||
|
return info.issuerName
|
||
|
}
|
||
|
|
||
|
return info.toName(info.Issuer)
|
||
|
}
|
||
|
|
||
|
// Write outputs info similar to the Chrome Certificate Viewer.
|
||
|
func (info *HostCertificateInfo) Write(w io.Writer) error {
|
||
|
tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0)
|
||
|
|
||
|
s := func(val string) string {
|
||
|
if val != "" {
|
||
|
return val
|
||
|
}
|
||
|
return "<Not Part Of Certificate>"
|
||
|
}
|
||
|
|
||
|
ss := func(val []string) string {
|
||
|
return s(strings.Join(val, ","))
|
||
|
}
|
||
|
|
||
|
name := func(n *pkix.Name) {
|
||
|
fmt.Fprintf(tw, " Common Name (CN):\t%s\n", s(n.CommonName))
|
||
|
fmt.Fprintf(tw, " Organization (O):\t%s\n", ss(n.Organization))
|
||
|
fmt.Fprintf(tw, " Organizational Unit (OU):\t%s\n", ss(n.OrganizationalUnit))
|
||
|
}
|
||
|
|
||
|
status := info.Status
|
||
|
if info.Err != nil {
|
||
|
status = fmt.Sprintf("ERROR %s", info.Err)
|
||
|
}
|
||
|
fmt.Fprintf(tw, "Certificate Status:\t%s\n", status)
|
||
|
|
||
|
fmt.Fprintln(tw, "Issued To:\t")
|
||
|
name(info.SubjectName())
|
||
|
|
||
|
fmt.Fprintln(tw, "Issued By:\t")
|
||
|
name(info.IssuerName())
|
||
|
|
||
|
fmt.Fprintln(tw, "Validity Period:\t")
|
||
|
fmt.Fprintf(tw, " Issued On:\t%s\n", info.NotBefore)
|
||
|
fmt.Fprintf(tw, " Expires On:\t%s\n", info.NotAfter)
|
||
|
|
||
|
if info.ThumbprintSHA1 != "" {
|
||
|
fmt.Fprintln(tw, "Thumbprints:\t")
|
||
|
if info.ThumbprintSHA256 != "" {
|
||
|
fmt.Fprintf(tw, " SHA-256 Thumbprint:\t%s\n", info.ThumbprintSHA256)
|
||
|
}
|
||
|
fmt.Fprintf(tw, " SHA-1 Thumbprint:\t%s\n", info.ThumbprintSHA1)
|
||
|
}
|
||
|
|
||
|
return tw.Flush()
|
||
|
}
|