Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace TLS-SNI-02 with TLS-ALPN-01. #112

Merged
merged 3 commits into from
Apr 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions acme/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ const (

IdentifierDNS = "dns"

ChallengeHTTP01 = "http-01"
ChallengeTLSSNI02 = "tls-sni-02"
ChallengeDNS01 = "dns-01"
ChallengeHTTP01 = "http-01"
ChallengeTLSALPN01 = "tls-alpn-01"
ChallengeDNS01 = "dns-01"

HTTP01BaseURL = ".well-known/acme-challenge/"

ACMETLS1Protocol = "acme-tls/1"
)

type Identifier struct {
Expand Down
125 changes: 57 additions & 68 deletions va/va.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"crypto/subtle"
"crypto/tls"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/hex"
"fmt"
"io/ioutil"
"log"
Expand Down Expand Up @@ -46,6 +46,8 @@ const (
noSleepEnvVar = "PEBBLE_VA_NOSLEEP"
)

var IdPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1}

func userAgent() string {
return fmt.Sprintf(
"%s (%s; %s)",
Expand Down Expand Up @@ -198,8 +200,8 @@ func (va VAImpl) performValidation(task *vaTask, results chan<- *core.Validation
switch task.Challenge.Type {
case acme.ChallengeHTTP01:
results <- va.validateHTTP01(task)
case acme.ChallengeTLSSNI02:
results <- va.validateTLSSNI02(task)
case acme.ChallengeTLSALPN01:
results <- va.validateTLSALPN01(task)
case acme.ChallengeDNS01:
results <- va.validateDNS01(task)
default:
Expand Down Expand Up @@ -245,7 +247,7 @@ func (va VAImpl) validateDNS01(task *vaTask) *core.ValidationRecord {
return result
}

func (va VAImpl) validateTLSSNI02(task *vaTask) *core.ValidationRecord {
func (va VAImpl) validateTLSALPN01(task *vaTask) *core.ValidationRecord {
portString := strconv.Itoa(va.tlsPort)
hostPort := net.JoinHostPort(task.Identifier, portString)

Expand All @@ -254,96 +256,83 @@ func (va VAImpl) validateTLSSNI02(task *vaTask) *core.ValidationRecord {
ValidatedAt: va.clk.Now(),
}

const tlsSNITokenID = "token"
const tlsSNIKaID = "ka"
const tlsSNISuffix = "acme.invalid"

// Lock the challenge for reading while we validate
task.Challenge.RLock()
defer task.Challenge.RUnlock()

// Compute the digest for the SAN b that will appear in the certificate
ha := sha256.Sum256([]byte(task.Challenge.Token))
za := hex.EncodeToString(ha[:])
sanAName := fmt.Sprintf("%s.%s.%s.%s", za[:32], za[32:], tlsSNITokenID, tlsSNISuffix)

// Compute the digest for the SAN B that will appear in the certificate
expectedKeyAuthorization := task.Challenge.ExpectedKeyAuthorization(task.Account.Key)
hb := sha256.Sum256([]byte(expectedKeyAuthorization))
zb := hex.EncodeToString(hb[:])
sanBName := fmt.Sprintf("%s.%s.%s.%s", zb[:32], zb[32:], tlsSNIKaID, tlsSNISuffix)

// Perform the validation
result.Error = va.validateTLSSNI02WithNames(hostPort, sanAName, sanBName)
return result
}

func (va VAImpl) validateTLSSNI02WithNames(hostPort string, sanAName, sanBName string) *acme.ProblemDetails {
certs, problem := va.fetchCerts(hostPort, sanAName)
cs, problem := va.fetchConnectionState(hostPort, &tls.Config{
ServerName: task.Identifier,
NextProtos: []string{acme.ACMETLS1Protocol},
InsecureSkipVerify: true,
})
if problem != nil {
return problem
result.Error = problem
return result
}

leafCert := certs[0]
if len(leafCert.DNSNames) != 2 {
names := certNames(leafCert)
msg := fmt.Sprintf(
"%s challenge certificate doesn't include exactly 2 DNSName entries. "+
"Received %d certificate(s), first certificate had names %q",
acme.ChallengeTLSSNI02, len(certs), names)
return acme.MalformedProblem(msg)
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != acme.ACMETLS1Protocol {
result.Error = acme.UnauthorizedProblem(fmt.Sprintf(
"Cannot negotiate ALPN protocol %q for %s challenge",
acme.ACMETLS1Protocol,
acme.ChallengeTLSALPN01,
))
return result
}

var validSanAName, validSanBName bool
for _, name := range leafCert.DNSNames {
if subtle.ConstantTimeCompare([]byte(name), []byte(sanAName)) == 1 {
validSanAName = true
}

if subtle.ConstantTimeCompare([]byte(name), []byte(sanBName)) == 1 {
validSanBName = true
}
certs := cs.PeerCertificates
if len(certs) == 0 {
result.Error = acme.UnauthorizedProblem(fmt.Sprintf("No certs presented for %s challenge", acme.ChallengeTLSALPN01))
return result
}
leafCert := certs[0]

if !validSanAName || !validSanBName {
// Verify SNI - certificate returned must be issued only for the domain we are verifying.
if len(leafCert.DNSNames) != 1 || !strings.EqualFold(leafCert.DNSNames[0], task.Identifier) {
names := certNames(leafCert)
msg := fmt.Sprintf(
errText := fmt.Sprintf(
"Incorrect validation certificate for %s challenge. "+
"Requested %s from %s. Received %d certificate(s), "+
"first certificate had names %q",
acme.ChallengeTLSSNI02, sanAName, hostPort,
len(certs), names)
return acme.UnauthorizedProblem(msg)
acme.ChallengeTLSALPN01, task.Identifier, hostPort, len(certs), names)
result.Error = acme.UnauthorizedProblem(errText)
return result
}

return nil
// Verify key authorization in acmeValidation extension
expectedKeyAuthorization := task.Challenge.ExpectedKeyAuthorization(task.Account.Key)
h := sha256.Sum256([]byte(expectedKeyAuthorization))
for _, ext := range leafCert.Extensions {
if IdPeAcmeIdentifierV1.Equal(ext.Id) && ext.Critical {
if subtle.ConstantTimeCompare(h[:], ext.Value) == 1 {
return result
}
errText := fmt.Sprintf("Incorrect validation certificate for %s challenge. "+
"Invalid acmeValidationV1 extension value.", acme.ChallengeTLSALPN01)
result.Error = acme.UnauthorizedProblem(errText)
return result
}
}

errText := fmt.Sprintf(
"Incorrect validation certificate for %s challenge. "+
"Missing acmeValidationV1 extension.",
acme.ChallengeTLSALPN01)
result.Error = acme.UnauthorizedProblem(errText)
return result
}

func (va VAImpl) fetchCerts(hostPort string, sni string) ([]*x509.Certificate, *acme.ProblemDetails) {
conn, err := tls.DialWithDialer(
&net.Dialer{Timeout: time.Second * 5}, "tcp", hostPort,
&tls.Config{
ServerName: sni,
InsecureSkipVerify: true,
})
func (va VAImpl) fetchConnectionState(hostPort string, config *tls.Config) (*tls.ConnectionState, *acme.ProblemDetails) {
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: time.Second * 5}, "tcp", hostPort, config)

if err != nil {
// TODO(@cpu): Return better err - see parseHTTPConnError from boulder
return nil, acme.UnauthorizedProblem(
fmt.Sprintf("Failed to connect to %s for the %s challenge", hostPort, acme.ChallengeTLSSNI02))
fmt.Sprintf("Failed to connect to %s for the %s challenge", hostPort, acme.ChallengeTLSALPN01))
}

// close errors are not important here
defer func() {
_ = conn.Close()
}()

certs := conn.ConnectionState().PeerCertificates
if len(certs) == 0 {
return nil, acme.UnauthorizedProblem(
fmt.Sprintf("No certs presented for %s challenge", acme.ChallengeTLSSNI02))
}
return certs, nil
cs := conn.ConnectionState()
return &cs, nil
}

func (va VAImpl) validateHTTP01(task *vaTask) *core.ValidationRecord {
Expand Down
2 changes: 1 addition & 1 deletion wfe/wfe.go
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,7 @@ func (wfe *WebFrontEndImpl) makeChallenges(authz *core.Authorization, request *h
chals = []*core.Challenge{chal}
} else {
// Non-wildcard authorizations get all of the enabled challenge types
enabledChallenges := []string{acme.ChallengeHTTP01, acme.ChallengeTLSSNI02, acme.ChallengeDNS01}
enabledChallenges := []string{acme.ChallengeHTTP01, acme.ChallengeTLSALPN01, acme.ChallengeDNS01}
for _, chalType := range enabledChallenges {
chal, err := wfe.makeChallenge(chalType, authz, request)
if err != nil {
Expand Down