Skip to content

Commit

Permalink
Merge pull request #112 from mdebski/tlsalpn
Browse files Browse the repository at this point in the history
This pull request contains a simple TLS-ALPN-01 implementation, as described in draft [0]

[0] https://github.com/rolandshoemaker/acme-tls-alpn
  • Loading branch information
Roland Bracewell Shoemaker authored Apr 16, 2018
2 parents 1688a1b + 60d2fd7 commit a6a406e
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 72 deletions.
8 changes: 5 additions & 3 deletions acme/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,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 @@ -897,7 +897,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

0 comments on commit a6a406e

Please sign in to comment.