acme: add support for TLS-ALPN
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 <ddos@google.com> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
This commit is contained in:
Родитель
b47b158736
Коммит
8ac0e0d97c
81
acme/acme.go
81
acme/acme.go
|
@ -22,6 +22,8 @@ import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/asn1"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
@ -40,6 +42,9 @@ import (
|
||||||
// LetsEncryptURL is the Directory endpoint of Let's Encrypt CA.
|
// LetsEncryptURL is the Directory endpoint of Let's Encrypt CA.
|
||||||
const LetsEncryptURL = "https://acme-v01.api.letsencrypt.org/directory"
|
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 (
|
const (
|
||||||
maxChainLen = 5 // max depth and breadth of a certificate chain
|
maxChainLen = 5 // max depth and breadth of a certificate chain
|
||||||
maxCertSize = 1 << 20 // max size of a certificate, in bytes
|
maxCertSize = 1 << 20 // max size of a certificate, in bytes
|
||||||
|
@ -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.
|
// 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 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) {
|
func (c *Client) TLSSNI01ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) {
|
||||||
ka, err := keyAuth(c.Key.Public(), token)
|
ka, err := keyAuth(c.Key.Public(), token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -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.
|
// 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 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) {
|
func (c *Client) TLSSNI02ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) {
|
||||||
b := sha256.Sum256([]byte(token))
|
b := sha256.Sum256([]byte(token))
|
||||||
h := hex.EncodeToString(b[:])
|
h := hex.EncodeToString(b[:])
|
||||||
|
@ -574,6 +579,48 @@ func (c *Client) TLSSNI02ChallengeCert(token string, opt ...CertOption) (cert tl
|
||||||
return cert, sanA, nil
|
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.
|
// doReg sends all types of registration requests.
|
||||||
// The type of request is identified by typ argument, which is a "resource"
|
// The type of request is identified by typ argument, which is a "resource"
|
||||||
// in the ACME spec terms.
|
// in the ACME spec terms.
|
||||||
|
@ -795,15 +842,25 @@ func keyAuth(pub crypto.PublicKey, token string) (string, error) {
|
||||||
return fmt.Sprintf("%s.%s", token, th), nil
|
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
|
// tlsChallengeCert creates a temporary certificate for TLS-SNI challenges
|
||||||
// with the given SANs and auto-generated public/private key pair.
|
// with the given SANs and auto-generated public/private key pair.
|
||||||
// The Subject Common Name is set to the first SAN to aid debugging.
|
// 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.
|
// To create a cert with a custom key pair, specify WithKey option.
|
||||||
func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
|
func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
|
||||||
var (
|
var key crypto.Signer
|
||||||
key crypto.Signer
|
tmpl := defaultTLSChallengeCertTemplate()
|
||||||
tmpl *x509.Certificate
|
|
||||||
)
|
|
||||||
for _, o := range opt {
|
for _, o := range opt {
|
||||||
switch o := o.(type) {
|
switch o := o.(type) {
|
||||||
case *certOptKey:
|
case *certOptKey:
|
||||||
|
@ -812,7 +869,7 @@ func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
|
||||||
}
|
}
|
||||||
key = o.key
|
key = o.key
|
||||||
case *certOptTemplate:
|
case *certOptTemplate:
|
||||||
var t = *(*x509.Certificate)(o) // shallow copy is ok
|
t := *(*x509.Certificate)(o) // shallow copy is ok
|
||||||
tmpl = &t
|
tmpl = &t
|
||||||
default:
|
default:
|
||||||
// package's fault, if we let this happen:
|
// package's fault, if we let this happen:
|
||||||
|
@ -825,16 +882,6 @@ func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
|
||||||
return tls.Certificate{}, err
|
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
|
tmpl.DNSNames = san
|
||||||
if len(san) > 0 {
|
if len(san) > 0 {
|
||||||
tmpl.Subject.CommonName = san[0]
|
tmpl.Subject.CommonName = san[0]
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
@ -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) {
|
func TestTLSChallengeCertOpt(t *testing.T) {
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 512)
|
key, err := rsa.GenerateKey(rand.Reader, 512)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -296,8 +296,8 @@ func (e *wireError) error(h http.Header) *Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CertOption is an optional argument type for the TLSSNIxChallengeCert methods for
|
// CertOption is an optional argument type for the TLS ChallengeCert methods for
|
||||||
// customizing a temporary certificate for TLS-SNI challenges.
|
// customizing a temporary certificate for TLS-based challenges.
|
||||||
type CertOption interface {
|
type CertOption interface {
|
||||||
privateCertOpt()
|
privateCertOpt()
|
||||||
}
|
}
|
||||||
|
@ -317,7 +317,7 @@ func (*certOptKey) privateCertOpt() {}
|
||||||
// WithTemplate creates an option for specifying a certificate template.
|
// WithTemplate creates an option for specifying a certificate template.
|
||||||
// See x509.CreateCertificate for template usage details.
|
// 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.
|
// resulting in a self-signed certificate.
|
||||||
// The DNSNames field of t is always overwritten for tls-sni challenge certs.
|
// The DNSNames field of t is always overwritten for tls-sni challenge certs.
|
||||||
func WithTemplate(t *x509.Certificate) CertOption {
|
func WithTemplate(t *x509.Certificate) CertOption {
|
||||||
|
|
Загрузка…
Ссылка в новой задаче