Skip to content

Commit

Permalink
acme: add support for TLS-ALPN
Browse files Browse the repository at this point in the history
This adds support for the new challenge type, as described in
https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01

Updates golang/go#25013

Change-Id: I81b335ff4b4e89e705a70e7d38dd21c3d5f5c25f
Reviewed-on: https://go-review.googlesource.com/116995
Reviewed-by: Alex Vaghin <[email protected]>
Reviewed-by: Brad Fitzpatrick <[email protected]>
  • Loading branch information
mdebski authored and Alex Vaghin committed Jun 8, 2018
1 parent b47b158 commit 8ac0e0d
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 20 deletions.
81 changes: 64 additions & 17 deletions acme/acme.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/hex"
"encoding/json"
Expand All @@ -40,6 +42,9 @@ import (
// LetsEncryptURL is the Directory endpoint of Let's Encrypt CA.
const LetsEncryptURL = "https://acme-v01.api.letsencrypt.org/directory"

// idPeACMEIdentifierV1 is the OID for the ACME extension for the TLS-ALPN challenge.
var idPeACMEIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1}

const (
maxChainLen = 5 // max depth and breadth of a certificate chain
maxCertSize = 1 << 20 // max size of a certificate, in bytes
Expand Down Expand Up @@ -526,7 +531,7 @@ func (c *Client) HTTP01ChallengePath(token string) string {
// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve.
//
// The returned certificate is valid for the next 24 hours and must be presented only when
// the server name of the client hello matches exactly the returned name value.
// the server name of the TLS ClientHello matches exactly the returned name value.
func (c *Client) TLSSNI01ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) {
ka, err := keyAuth(c.Key.Public(), token)
if err != nil {
Expand All @@ -553,7 +558,7 @@ func (c *Client) TLSSNI01ChallengeCert(token string, opt ...CertOption) (cert tl
// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve.
//
// The returned certificate is valid for the next 24 hours and must be presented only when
// the server name in the client hello matches exactly the returned name value.
// the server name in the TLS ClientHello matches exactly the returned name value.
func (c *Client) TLSSNI02ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) {
b := sha256.Sum256([]byte(token))
h := hex.EncodeToString(b[:])
Expand All @@ -574,6 +579,48 @@ func (c *Client) TLSSNI02ChallengeCert(token string, opt ...CertOption) (cert tl
return cert, sanA, nil
}

// TLSALPN01ChallengeCert creates a certificate for TLS-ALPN-01 challenge response.
// Servers can present the certificate to validate the challenge and prove control
// over a domain name. For more details on TLS-ALPN-01 see
// https://tools.ietf.org/html/draft-shoemaker-acme-tls-alpn-00#section-3
//
// The token argument is a Challenge.Token value.
// If a WithKey option is provided, its private part signs the returned cert,
// and the public part is used to specify the signee.
// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve.
//
// The returned certificate is valid for the next 24 hours and must be presented only when
// the server name in the TLS ClientHello matches the domain, and the special acme-tls/1 ALPN protocol
// has been specified.
func (c *Client) TLSALPN01ChallengeCert(token, domain string, opt ...CertOption) (cert tls.Certificate, err error) {
ka, err := keyAuth(c.Key.Public(), token)
if err != nil {
return tls.Certificate{}, err
}
shasum := sha256.Sum256([]byte(ka))
acmeExtension := pkix.Extension{
Id: idPeACMEIdentifierV1,
Critical: true,
Value: shasum[:],
}

tmpl := defaultTLSChallengeCertTemplate()

var newOpt []CertOption
for _, o := range opt {
switch o := o.(type) {
case *certOptTemplate:
t := *(*x509.Certificate)(o) // shallow copy is ok
tmpl = &t
default:
newOpt = append(newOpt, o)
}
}
tmpl.ExtraExtensions = append(tmpl.ExtraExtensions, acmeExtension)
newOpt = append(newOpt, WithTemplate(tmpl))
return tlsChallengeCert([]string{domain}, newOpt)
}

// doReg sends all types of registration requests.
// The type of request is identified by typ argument, which is a "resource"
// in the ACME spec terms.
Expand Down Expand Up @@ -795,15 +842,25 @@ func keyAuth(pub crypto.PublicKey, token string) (string, error) {
return fmt.Sprintf("%s.%s", token, th), nil
}

// defaultTLSChallengeCertTemplate is a template used to create challenge certs for TLS challenges.
func defaultTLSChallengeCertTemplate() *x509.Certificate {
return &x509.Certificate{
SerialNumber: big.NewInt(1),
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
}

// tlsChallengeCert creates a temporary certificate for TLS-SNI challenges
// with the given SANs and auto-generated public/private key pair.
// The Subject Common Name is set to the first SAN to aid debugging.
// To create a cert with a custom key pair, specify WithKey option.
func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
var (
key crypto.Signer
tmpl *x509.Certificate
)
var key crypto.Signer
tmpl := defaultTLSChallengeCertTemplate()
for _, o := range opt {
switch o := o.(type) {
case *certOptKey:
Expand All @@ -812,7 +869,7 @@ func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
}
key = o.key
case *certOptTemplate:
var t = *(*x509.Certificate)(o) // shallow copy is ok
t := *(*x509.Certificate)(o) // shallow copy is ok
tmpl = &t
default:
// package's fault, if we let this happen:
Expand All @@ -825,16 +882,6 @@ func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
return tls.Certificate{}, err
}
}
if tmpl == nil {
tmpl = &x509.Certificate{
SerialNumber: big.NewInt(1),
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
}
tmpl.DNSNames = san
if len(san) > 0 {
tmpl.Subject.CommonName = san[0]
Expand Down
53 changes: 53 additions & 0 deletions acme/acme_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"math/big"
Expand Down Expand Up @@ -1160,6 +1161,58 @@ func TestTLSSNI02ChallengeCert(t *testing.T) {
}
}

func TestTLSALPN01ChallengeCert(t *testing.T) {
const (
token = "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"
keyAuth = "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA." + testKeyECThumbprint
// echo -n <token.testKeyECThumbprint> | shasum -a 256
h = "dbbd5eefe7b4d06eb9d1d9f5acb4c7cda27d320e4b30332f0b6cb441734ad7b0"
domain = "example.com"
)

extValue, err := hex.DecodeString(h)
if err != nil {
t.Fatal(err)
}

client := &Client{Key: testKeyEC}
tlscert, err := client.TLSALPN01ChallengeCert(token, domain)
if err != nil {
t.Fatal(err)
}

if n := len(tlscert.Certificate); n != 1 {
t.Fatalf("len(tlscert.Certificate) = %d; want 1", n)
}
cert, err := x509.ParseCertificate(tlscert.Certificate[0])
if err != nil {
t.Fatal(err)
}
names := []string{domain}
if !reflect.DeepEqual(cert.DNSNames, names) {
t.Fatalf("cert.DNSNames = %v;\nwant %v", cert.DNSNames, names)
}
if cn := cert.Subject.CommonName; cn != domain {
t.Errorf("CommonName = %q; want %q", cn, domain)
}
acmeExts := []pkix.Extension{}
for _, ext := range cert.Extensions {
if idPeACMEIdentifierV1.Equal(ext.Id) {
acmeExts = append(acmeExts, ext)
}
}
if len(acmeExts) != 1 {
t.Errorf("acmeExts = %v; want exactly one", acmeExts)
}
if !acmeExts[0].Critical {
t.Errorf("acmeExt.Critical = %v; want true", acmeExts[0].Critical)
}
if bytes.Compare(acmeExts[0].Value, extValue) != 0 {
t.Errorf("acmeExt.Value = %v; want %v", acmeExts[0].Value, extValue)
}

}

func TestTLSChallengeCertOpt(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 512)
if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions acme/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,8 @@ func (e *wireError) error(h http.Header) *Error {
}
}

// CertOption is an optional argument type for the TLSSNIxChallengeCert methods for
// customizing a temporary certificate for TLS-SNI challenges.
// CertOption is an optional argument type for the TLS ChallengeCert methods for
// customizing a temporary certificate for TLS-based challenges.
type CertOption interface {
privateCertOpt()
}
Expand All @@ -317,7 +317,7 @@ func (*certOptKey) privateCertOpt() {}
// WithTemplate creates an option for specifying a certificate template.
// See x509.CreateCertificate for template usage details.
//
// In TLSSNIxChallengeCert methods, the template is also used as parent,
// In TLS ChallengeCert methods, the template is also used as parent,
// resulting in a self-signed certificate.
// The DNSNames field of t is always overwritten for tls-sni challenge certs.
func WithTemplate(t *x509.Certificate) CertOption {
Expand Down

0 comments on commit 8ac0e0d

Please sign in to comment.