forked from letsencrypt/boulder
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtlsalpn.go
298 lines (261 loc) · 10.5 KB
/
tlsalpn.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
package va
import (
"bytes"
"context"
"crypto/sha256"
"crypto/subtle"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/hex"
"errors"
"fmt"
"net"
"strconv"
"strings"
"github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/identifier"
)
const (
// ALPN protocol ID for TLS-ALPN-01 challenge
// https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-5.2
ACMETLS1Protocol = "acme-tls/1"
)
var (
// As defined in https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-04#section-5.1
// id-pe OID + 31 (acmeIdentifier)
IdPeAcmeIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}
// OID for the Subject Alternative Name extension, as defined in
// https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6
IdCeSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17}
)
// certAltNames collects up all of a certificate's subject names (Subject CN and
// Subject Alternate Names) and reduces them to a unique, sorted set, typically for an
// error message
func certAltNames(cert *x509.Certificate) []string {
var names []string
if cert.Subject.CommonName != "" {
names = append(names, cert.Subject.CommonName)
}
names = append(names, cert.DNSNames...)
names = append(names, cert.EmailAddresses...)
for _, id := range cert.IPAddresses {
names = append(names, id.String())
}
for _, id := range cert.URIs {
names = append(names, id.String())
}
names = core.UniqueLowerNames(names)
return names
}
func (va *ValidationAuthorityImpl) tryGetChallengeCert(
ctx context.Context,
identifier identifier.ACMEIdentifier,
tlsConfig *tls.Config,
) (*x509.Certificate, *tls.ConnectionState, core.ValidationRecord, error) {
allAddrs, resolvers, err := va.getAddrs(ctx, identifier.Value)
validationRecord := core.ValidationRecord{
DnsName: identifier.Value,
AddressesResolved: allAddrs,
Port: strconv.Itoa(va.tlsPort),
ResolverAddrs: resolvers,
}
if err != nil {
return nil, nil, validationRecord, err
}
// Split the available addresses into v4 and v6 addresses
v4, v6 := availableAddresses(allAddrs)
addresses := append(v4, v6...)
// This shouldn't happen, but be defensive about it anyway
if len(addresses) < 1 {
return nil, nil, validationRecord, berrors.MalformedError("no IP addresses found for %q", identifier.Value)
}
// If there is at least one IPv6 address then try it first
if len(v6) > 0 {
address := net.JoinHostPort(v6[0].String(), validationRecord.Port)
validationRecord.AddressUsed = v6[0]
cert, cs, err := va.getChallengeCert(ctx, address, identifier, tlsConfig)
// If there is no problem, return immediately
if err == nil {
return cert, cs, validationRecord, nil
}
// Otherwise, we note that we tried an address and fall back to trying IPv4
validationRecord.AddressesTried = append(validationRecord.AddressesTried, validationRecord.AddressUsed)
va.metrics.ipv4FallbackCounter.Inc()
}
// If there are no IPv4 addresses and we tried an IPv6 address return
// an error - there's nothing left to try
if len(v4) == 0 && len(validationRecord.AddressesTried) > 0 {
return nil, nil, validationRecord, berrors.MalformedError("Unable to contact %q at %q, no IPv4 addresses to try as fallback",
validationRecord.DnsName, validationRecord.AddressesTried[0])
} else if len(v4) == 0 && len(validationRecord.AddressesTried) == 0 {
// It shouldn't be possible that there are no IPv4 addresses and no previous
// attempts at an IPv6 address connection but be defensive about it anyway
return nil, nil, validationRecord, berrors.MalformedError("No IP addresses found for %q", validationRecord.DnsName)
}
// Otherwise if there are no IPv6 addresses, or there was an error
// talking to the first IPv6 address, try the first IPv4 address
validationRecord.AddressUsed = v4[0]
address := net.JoinHostPort(v4[0].String(), validationRecord.Port)
cert, cs, err := va.getChallengeCert(ctx, address, identifier, tlsConfig)
return cert, cs, validationRecord, err
}
func (va *ValidationAuthorityImpl) getChallengeCert(
ctx context.Context,
hostPort string,
identifier identifier.ACMEIdentifier,
config *tls.Config,
) (*x509.Certificate, *tls.ConnectionState, error) {
va.log.Info(fmt.Sprintf("%s [%s] Attempting to validate for %s %s", core.ChallengeTypeTLSALPN01, identifier, hostPort, config.ServerName))
// We expect a self-signed challenge certificate, do not verify it here.
config.InsecureSkipVerify = true
dialCtx, cancel := context.WithTimeout(ctx, va.singleDialTimeout)
defer cancel()
dialer := &tls.Dialer{Config: config}
conn, err := dialer.DialContext(dialCtx, "tcp", hostPort)
if err != nil {
va.log.Infof("%s connection failure for %s. err=[%#v] errStr=[%s]", core.ChallengeTypeTLSALPN01, identifier, err, err)
host, _, splitErr := net.SplitHostPort(hostPort)
if splitErr == nil && net.ParseIP(host) != nil {
// Wrap the validation error and the IP of the remote host in an
// IPError so we can display the IP in the problem details returned
// to the client.
return nil, nil, ipError{net.ParseIP(host), err}
}
return nil, nil, err
}
defer conn.Close()
// tls.Dialer.DialContext guarantees that the *net.Conn it returns is a *tls.Conn.
cs := conn.(*tls.Conn).ConnectionState()
certs := cs.PeerCertificates
if len(certs) == 0 {
va.log.Infof("%s challenge for %s resulted in no certificates", core.ChallengeTypeTLSALPN01, identifier.Value)
return nil, nil, berrors.UnauthorizedError("No certs presented for %s challenge", core.ChallengeTypeTLSALPN01)
}
for i, cert := range certs {
va.log.AuditInfof("%s challenge for %s received certificate (%d of %d): cert=[%s]",
core.ChallengeTypeTLSALPN01, identifier.Value, i+1, len(certs), hex.EncodeToString(cert.Raw))
}
return certs[0], &cs, nil
}
func checkExpectedSAN(cert *x509.Certificate, name identifier.ACMEIdentifier) error {
if len(cert.DNSNames) != 1 {
return errors.New("wrong number of dNSNames")
}
for _, ext := range cert.Extensions {
if IdCeSubjectAltName.Equal(ext.Id) {
expectedSANs, err := asn1.Marshal([]asn1.RawValue{
{Tag: 2, Class: 2, Bytes: []byte(cert.DNSNames[0])},
})
if err != nil || !bytes.Equal(expectedSANs, ext.Value) {
return errors.New("SAN extension does not match expected bytes")
}
}
}
if !strings.EqualFold(cert.DNSNames[0], name.Value) {
return errors.New("dNSName does not match expected identifier")
}
return nil
}
// Confirm that of the OIDs provided, all of them are in the provided list of
// extensions. Also confirms that of the extensions provided that none are
// repeated. Per RFC8737, allows unexpected extensions.
func checkAcceptableExtensions(exts []pkix.Extension, requiredOIDs []asn1.ObjectIdentifier) error {
oidSeen := make(map[string]bool)
for _, ext := range exts {
if oidSeen[ext.Id.String()] {
return fmt.Errorf("Extension OID %s seen twice", ext.Id)
}
oidSeen[ext.Id.String()] = true
}
for _, required := range requiredOIDs {
if !oidSeen[required.String()] {
return fmt.Errorf("Required extension OID %s is not present", required)
}
}
return nil
}
func (va *ValidationAuthorityImpl) validateTLSALPN01(ctx context.Context, identifier identifier.ACMEIdentifier, keyAuthorization string) ([]core.ValidationRecord, error) {
if identifier.Type != "dns" {
va.log.Info(fmt.Sprintf("Identifier type for TLS-ALPN-01 was not DNS: %s", identifier))
return nil, berrors.MalformedError("Identifier type for TLS-ALPN-01 was not DNS")
}
cert, cs, tvr, problem := va.tryGetChallengeCert(ctx, identifier, &tls.Config{
MinVersion: tls.VersionTLS12,
NextProtos: []string{ACMETLS1Protocol},
ServerName: identifier.Value,
})
// Copy the single validationRecord into the slice that we have to return, and
// get a reference to it so we can modify it if we have to.
validationRecords := []core.ValidationRecord{tvr}
validationRecord := &validationRecords[0]
if problem != nil {
return validationRecords, problem
}
if cs.NegotiatedProtocol != ACMETLS1Protocol {
return validationRecords, berrors.UnauthorizedError(
"Cannot negotiate ALPN protocol %q for %s challenge",
ACMETLS1Protocol,
core.ChallengeTypeTLSALPN01)
}
badCertErr := func(msg string) error {
hostPort := net.JoinHostPort(validationRecord.AddressUsed.String(), validationRecord.Port)
return berrors.UnauthorizedError(
"Incorrect validation certificate for %s challenge. "+
"Requested %s from %s. %s",
core.ChallengeTypeTLSALPN01, identifier.Value, hostPort, msg)
}
// The certificate must be self-signed.
err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature)
if err != nil || !bytes.Equal(cert.RawSubject, cert.RawIssuer) {
return validationRecords, badCertErr(
"Received certificate which is not self-signed.")
}
// The certificate must have the subjectAltName and acmeIdentifier
// extensions, and only one of each.
allowedOIDs := []asn1.ObjectIdentifier{
IdPeAcmeIdentifier, IdCeSubjectAltName,
}
err = checkAcceptableExtensions(cert.Extensions, allowedOIDs)
if err != nil {
return validationRecords, badCertErr(
fmt.Sprintf("Received certificate with unexpected extensions: %q", err))
}
// The certificate returned must have a subjectAltName extension containing
// only the dNSName being validated and no other entries.
err = checkExpectedSAN(cert, identifier)
if err != nil {
names := strings.Join(certAltNames(cert), ", ")
return validationRecords, badCertErr(
fmt.Sprintf("Received certificate with unexpected identifiers (%q): %q", names, err))
}
// Verify key authorization in acmeValidation extension
h := sha256.Sum256([]byte(keyAuthorization))
for _, ext := range cert.Extensions {
if IdPeAcmeIdentifier.Equal(ext.Id) {
va.metrics.tlsALPNOIDCounter.WithLabelValues(IdPeAcmeIdentifier.String()).Inc()
if !ext.Critical {
return validationRecords, badCertErr(
"Received certificate with acmeValidationV1 extension that is not Critical.")
}
var extValue []byte
rest, err := asn1.Unmarshal(ext.Value, &extValue)
if err != nil || len(rest) > 0 || len(h) != len(extValue) {
return validationRecords, badCertErr(
"Received certificate with malformed acmeValidationV1 extension value.")
}
if subtle.ConstantTimeCompare(h[:], extValue) != 1 {
return validationRecords, badCertErr(fmt.Sprintf(
"Received certificate with acmeValidationV1 extension value %s but expected %s.",
hex.EncodeToString(extValue),
hex.EncodeToString(h[:]),
))
}
return validationRecords, nil
}
}
return validationRecords, badCertErr(
"Received certificate with no acmeValidationV1 extension.")
}