1package connect
2
3import (
4	"crypto/rand"
5	"encoding/binary"
6	"fmt"
7	"regexp"
8	"strconv"
9	"strings"
10)
11
12var invalidDNSNameChars = regexp.MustCompile(`[^a-z0-9]`)
13
14const (
15	// 64 = max length of a certificate common name
16	// 21 = 7 bytes for ".consul", 9 bytes for .<trust domain> and 5 bytes for ".svc."
17	// This ends up being 43 bytes
18	maxServiceAndNamespaceLen = 64 - 21
19	minServiceNameLen         = 15
20	minNamespaceNameLen       = 15
21)
22
23// trucateServiceAndNamespace will take a service name and namespace name and truncate
24// them appropriately so that they would fit within the space alloted for them in the
25// Common Name field of the x509 certificate. That field is capped at 64 characters
26// in length and there is other data that must be a part of the name too. This function
27// takes all of that into account.
28func truncateServiceAndNamespace(serviceName, namespace string) (string, string) {
29	svcLen := len(serviceName)
30	nsLen := len(namespace)
31	totalLen := svcLen + nsLen
32
33	// quick exit when the entirety of both can fit
34	if totalLen <= maxServiceAndNamespaceLen {
35		return serviceName, namespace
36	}
37
38	toRemove := totalLen - maxServiceAndNamespaceLen
39	// now we must figure out how to truncate each one, we need to ensure we don't remove all of either one.
40	if svcLen <= minServiceNameLen {
41		// only remove bytes from the namespace
42		return serviceName, truncateTo(namespace, nsLen-toRemove)
43	} else if nsLen <= minNamespaceNameLen {
44		// only remove bytes from the service name
45		return truncateTo(serviceName, svcLen-toRemove), namespace
46	} else {
47		// we can remove an "equal" amount from each. If the number of bytes to remove is odd we give it to the namespace
48		svcTruncate := svcLen - (toRemove / 2) - (toRemove % 2)
49		nsTruncate := nsLen - (toRemove / 2)
50
51		// checks to ensure we don't reduce one side too much when they are not roughly balanced in length.
52		if svcTruncate <= minServiceNameLen {
53			svcTruncate = minServiceNameLen
54			nsTruncate = maxServiceAndNamespaceLen - minServiceNameLen
55		} else if nsTruncate <= minNamespaceNameLen {
56			svcTruncate = maxServiceAndNamespaceLen - minNamespaceNameLen
57			nsTruncate = minNamespaceNameLen
58		}
59
60		return truncateTo(serviceName, svcTruncate), truncateTo(namespace, nsTruncate)
61	}
62}
63
64// ServiceCN returns the common name for a service's certificate. We can't use
65// SPIFFE URIs because some CAs require valid FQDN format. We can't use SNI
66// values because they are often too long than the 64 bytes allowed by
67// CommonNames. We could attempt to encode more information into this to make
68// identifying which instance/node it was issued to in a management tool easier
69// but that just introduces more complications around length. It's also strange
70// that the Common Name would encode more information than the actual
71// identifying URI we use to assert anything does and my lead to bad assumptions
72// that the common name is in some way "secure" or verified - there is nothing
73// inherently provable here except that the requestor had ACLs for that service
74// name in that DC.
75//
76// Format is:
77//   <sanitized_service_name>.svc.<trust_domain_first_8>.consul
78//
79//   service name is sanitized by removing any chars that are not legal in a DNS
80//   name and lower casing. It is truncated to the first X chars to keep the
81//   total at 64.
82//
83//   trust domain is truncated to keep the whole name short
84func ServiceCN(serviceName, namespace, trustDomain string) string {
85	svc := invalidDNSNameChars.ReplaceAllString(strings.ToLower(serviceName), "")
86
87	svc, namespace = truncateServiceAndNamespace(svc, namespace)
88	return fmt.Sprintf("%s.svc.%s.%s.consul",
89		svc, namespace, truncateTo(trustDomain, 8))
90}
91
92// AgentCN returns the common name for an agent certificate. See ServiceCN for
93// more details on rationale.
94//
95// Format is:
96//   <sanitized_node_name>.agnt.<trust_domain_first_8>.consul
97//
98//   node name is sanitized by removing any chars that are not legal in a DNS
99//   name and lower casing. It is truncated to the first X chars to keep the
100//   total at 64.
101//
102//   trust domain is truncated to keep the whole name short
103func AgentCN(node, trustDomain string) string {
104	nodeSan := invalidDNSNameChars.ReplaceAllString(strings.ToLower(node), "")
105	// 21 = 7 bytes for ".consul", 8 bytes for trust domain, 6 bytes for ".agnt."
106	return fmt.Sprintf("%s.agnt.%s.consul",
107		truncateTo(nodeSan, 64-21), truncateTo(trustDomain, 8))
108}
109
110// CompactUID returns a crypto random Unique Identifier string consiting of 8
111// characters of base36 encoded random value. This has roughly 41 bits of
112// entropy so is suitable for infrequently occuring events with low probability
113// of collision. It is not suitable for UUIDs for very frequent events. It's
114// main purpose is to assign unique values to CA certificate Common Names which
115// need to be unique in some providers - see CACN - but without using up large
116// amounts of the limited 64 character Common Name. It also makes the values
117// more easily digestable by humans considering there are likely to be few of
118// them ever in use.
119func CompactUID() (string, error) {
120	// 48 bits (6 bytes) is enough to fill 8 bytes in base36 but it's simpler to
121	// have a whole uint8 to convert from.
122	var raw [8]byte
123	_, err := rand.Read(raw[:])
124	if err != nil {
125		return "", err
126	}
127
128	i := binary.LittleEndian.Uint64(raw[:])
129	return truncateTo(strconv.FormatInt(int64(i), 36), 8), nil
130}
131
132// CACN returns the common name for a CA certificate. See ServiceCN for more
133// details on rationale. A uniqueID is requires because some providers (e.g.
134// Vault) cache by subject and so produce incorrect results - for example they
135// won't cross-sign an older CA certificate with the same common name since they
136// think they already have a valid cert for that CN and just return the current
137// root.
138//
139// This can be generated by any means but will be truncated to 8 chars and
140// sanitised to DNS-safe chars. CompactUID generates suitable UIDs for this
141// specific purpose.
142//
143// Format is:
144//   {provider}-{uniqueID_first8}.{pri|sec}.ca.<trust_domain_first_8>.consul
145//
146//   trust domain is truncated to keep the whole name short
147func CACN(provider, uniqueID, trustDomain string, primaryDC bool) string {
148	providerSan := invalidDNSNameChars.ReplaceAllString(strings.ToLower(provider), "")
149	typ := "pri"
150	if !primaryDC {
151		typ = "sec"
152	}
153	// 32 = 7 bytes for ".consul", 8 bytes for trust domain, 8 bytes for
154	// ".pri.ca.", 9 bytes for "-{uniqueID-8-b36}"
155	uidSAN := invalidDNSNameChars.ReplaceAllString(strings.ToLower(uniqueID), "")
156	return fmt.Sprintf("%s-%s.%s.ca.%s.consul", typ, truncateTo(uidSAN, 8),
157		truncateTo(providerSan, 64-32), truncateTo(trustDomain, 8))
158}
159
160func truncateTo(s string, n int) string {
161	if len(s) > n {
162		return s[:n]
163	}
164	return s
165}
166
167// CNForCertURI returns the correct common name for a given cert URI type. It
168// doesn't work for CA Signing IDs since more context is needed and CA Providers
169// always know their CN from their own context.
170func CNForCertURI(uri CertURI) (string, error) {
171	// Even though leafs should be from our own CSRs which should have the same CN
172	// logic as here, override anyway to account for older version clients that
173	// didn't include the Common Name in the CSR.
174	switch id := uri.(type) {
175	case *SpiffeIDService:
176		return ServiceCN(id.Service, id.Namespace, id.Host), nil
177	case *SpiffeIDAgent:
178		return AgentCN(id.Agent, id.Host), nil
179	case *SpiffeIDSigning:
180		return "", fmt.Errorf("CertURI is a SpiffeIDSigning, not enough context to generate Common Name")
181	default:
182		return "", fmt.Errorf("CertURI type not recognized")
183	}
184}
185