1/*
2Copyright (c) 2016 VMware, Inc. All Rights Reserved.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package object
18
19import (
20	"crypto/sha256"
21	"crypto/tls"
22	"crypto/x509"
23	"crypto/x509/pkix"
24	"encoding/asn1"
25	"fmt"
26	"io"
27	"net/url"
28	"strings"
29	"text/tabwriter"
30
31	"github.com/vmware/govmomi/vim25/soap"
32	"github.com/vmware/govmomi/vim25/types"
33)
34
35// HostCertificateInfo provides helpers for types.HostCertificateManagerCertificateInfo
36type HostCertificateInfo struct {
37	types.HostCertificateManagerCertificateInfo
38
39	ThumbprintSHA1   string
40	ThumbprintSHA256 string
41
42	Err         error
43	Certificate *x509.Certificate `json:"-"`
44
45	subjectName *pkix.Name
46	issuerName  *pkix.Name
47}
48
49// FromCertificate converts x509.Certificate to HostCertificateInfo
50func (info *HostCertificateInfo) FromCertificate(cert *x509.Certificate) *HostCertificateInfo {
51	info.Certificate = cert
52	info.subjectName = &cert.Subject
53	info.issuerName = &cert.Issuer
54
55	info.Issuer = info.fromName(info.issuerName)
56	info.NotBefore = &cert.NotBefore
57	info.NotAfter = &cert.NotAfter
58	info.Subject = info.fromName(info.subjectName)
59
60	info.ThumbprintSHA1 = soap.ThumbprintSHA1(cert)
61
62	// SHA-256 for info purposes only, API fields all use SHA-1
63	sum := sha256.Sum256(cert.Raw)
64	hex := make([]string, len(sum))
65	for i, b := range sum {
66		hex[i] = fmt.Sprintf("%02X", b)
67	}
68	info.ThumbprintSHA256 = strings.Join(hex, ":")
69
70	if info.Status == "" {
71		info.Status = string(types.HostCertificateManagerCertificateInfoCertificateStatusUnknown)
72	}
73
74	return info
75}
76
77// FromURL connects to the given URL.Host via tls.Dial with the given tls.Config and populates the HostCertificateInfo
78// via tls.ConnectionState.  If the certificate was verified with the given tls.Config, the Err field will be nil.
79// Otherwise, Err will be set to the x509.UnknownAuthorityError or x509.HostnameError.
80// If tls.Dial returns an error of any other type, that error is returned.
81func (info *HostCertificateInfo) FromURL(u *url.URL, config *tls.Config) error {
82	addr := u.Host
83	if !(strings.LastIndex(addr, ":") > strings.LastIndex(addr, "]")) {
84		addr += ":443"
85	}
86
87	conn, err := tls.Dial("tcp", addr, config)
88	if err != nil {
89		switch err.(type) {
90		case x509.UnknownAuthorityError:
91		case x509.HostnameError:
92		default:
93			return err
94		}
95
96		info.Err = err
97
98		conn, err = tls.Dial("tcp", addr, &tls.Config{InsecureSkipVerify: true})
99		if err != nil {
100			return err
101		}
102	} else {
103		info.Status = string(types.HostCertificateManagerCertificateInfoCertificateStatusGood)
104	}
105
106	state := conn.ConnectionState()
107	_ = conn.Close()
108	info.FromCertificate(state.PeerCertificates[0])
109
110	return nil
111}
112
113var emailAddressOID = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1}
114
115func (info *HostCertificateInfo) fromName(name *pkix.Name) string {
116	var attrs []string
117
118	oids := map[string]string{
119		emailAddressOID.String(): "emailAddress",
120	}
121
122	for _, attr := range name.Names {
123		if key, ok := oids[attr.Type.String()]; ok {
124			attrs = append(attrs, fmt.Sprintf("%s=%s", key, attr.Value))
125		}
126	}
127
128	attrs = append(attrs, fmt.Sprintf("CN=%s", name.CommonName))
129
130	add := func(key string, vals []string) {
131		for _, val := range vals {
132			attrs = append(attrs, fmt.Sprintf("%s=%s", key, val))
133		}
134	}
135
136	elts := []struct {
137		key string
138		val []string
139	}{
140		{"OU", name.OrganizationalUnit},
141		{"O", name.Organization},
142		{"L", name.Locality},
143		{"ST", name.Province},
144		{"C", name.Country},
145	}
146
147	for _, elt := range elts {
148		add(elt.key, elt.val)
149	}
150
151	return strings.Join(attrs, ",")
152}
153
154func (info *HostCertificateInfo) toName(s string) *pkix.Name {
155	var name pkix.Name
156
157	for _, pair := range strings.Split(s, ",") {
158		attr := strings.SplitN(pair, "=", 2)
159		if len(attr) != 2 {
160			continue
161		}
162
163		v := attr[1]
164
165		switch strings.ToLower(attr[0]) {
166		case "cn":
167			name.CommonName = v
168		case "ou":
169			name.OrganizationalUnit = append(name.OrganizationalUnit, v)
170		case "o":
171			name.Organization = append(name.Organization, v)
172		case "l":
173			name.Locality = append(name.Locality, v)
174		case "st":
175			name.Province = append(name.Province, v)
176		case "c":
177			name.Country = append(name.Country, v)
178		case "emailaddress":
179			name.Names = append(name.Names, pkix.AttributeTypeAndValue{Type: emailAddressOID, Value: v})
180		}
181	}
182
183	return &name
184}
185
186// SubjectName parses Subject into a pkix.Name
187func (info *HostCertificateInfo) SubjectName() *pkix.Name {
188	if info.subjectName != nil {
189		return info.subjectName
190	}
191
192	return info.toName(info.Subject)
193}
194
195// IssuerName parses Issuer into a pkix.Name
196func (info *HostCertificateInfo) IssuerName() *pkix.Name {
197	if info.issuerName != nil {
198		return info.issuerName
199	}
200
201	return info.toName(info.Issuer)
202}
203
204// Write outputs info similar to the Chrome Certificate Viewer.
205func (info *HostCertificateInfo) Write(w io.Writer) error {
206	tw := tabwriter.NewWriter(w, 2, 0, 2, ' ', 0)
207
208	s := func(val string) string {
209		if val != "" {
210			return val
211		}
212		return "<Not Part Of Certificate>"
213	}
214
215	ss := func(val []string) string {
216		return s(strings.Join(val, ","))
217	}
218
219	name := func(n *pkix.Name) {
220		fmt.Fprintf(tw, "  Common Name (CN):\t%s\n", s(n.CommonName))
221		fmt.Fprintf(tw, "  Organization (O):\t%s\n", ss(n.Organization))
222		fmt.Fprintf(tw, "  Organizational Unit (OU):\t%s\n", ss(n.OrganizationalUnit))
223	}
224
225	status := info.Status
226	if info.Err != nil {
227		status = fmt.Sprintf("ERROR %s", info.Err)
228	}
229	fmt.Fprintf(tw, "Certificate Status:\t%s\n", status)
230
231	fmt.Fprintln(tw, "Issued To:\t")
232	name(info.SubjectName())
233
234	fmt.Fprintln(tw, "Issued By:\t")
235	name(info.IssuerName())
236
237	fmt.Fprintln(tw, "Validity Period:\t")
238	fmt.Fprintf(tw, "  Issued On:\t%s\n", info.NotBefore)
239	fmt.Fprintf(tw, "  Expires On:\t%s\n", info.NotAfter)
240
241	if info.ThumbprintSHA1 != "" {
242		fmt.Fprintln(tw, "Thumbprints:\t")
243		if info.ThumbprintSHA256 != "" {
244			fmt.Fprintf(tw, "  SHA-256 Thumbprint:\t%s\n", info.ThumbprintSHA256)
245		}
246		fmt.Fprintf(tw, "  SHA-1 Thumbprint:\t%s\n", info.ThumbprintSHA1)
247	}
248
249	return tw.Flush()
250}
251