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