acme: TLS-SNI challenges implementation

Current and N-1 versions of the spec adopted TLS-SNI-02, but Let's
Encrypt seems to support TLS-SNI-01, devised in v1 of the spec.

This change implements both, older 01 and current 02 versions of TLS-SNI
challenges.

Change-Id: I2bb76ad852e105098f703cb6113d3996c0e0b118
Reviewed-on: https://go-review.googlesource.com/25450
Reviewed-by: Alex Vaghin <ddos@google.com>
This commit is contained in:
Alex Vaghin
2016-08-04 08:26:12 +00:00
parent 611beeb3d5
commit 7a1054f3ac
2 changed files with 128 additions and 0 deletions
+76
View File
@@ -11,12 +11,19 @@ package acme
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"math/big"
"net/http"
"strconv"
"strings"
@@ -351,6 +358,45 @@ func (c *Client) HTTP01Handler(token string) http.Handler {
})
}
// TLSSNI01ChallengeCert creates a certificate for TLS-SNI-01 challenge response.
// Servers can present the certificate to validate the challenge and prove control
// over a domain name.
//
// The implementation is incomplete in that the returned value is a single certificate,
// computed only for Z0 of the key authorization. ACME CAs are expected to update
// their implementations to use the newer version, TLS-SNI-02.
// For more details on TLS-SNI-01 see https://tools.ietf.org/html/draft-ietf-acme-acme-01#section-7.3.
//
// The token argument is a Challenge.Token value.
// The returned certificate is valid for the next 24 hours.
func (c *Client) TLSSNI01ChallengeCert(token string) (tls.Certificate, error) {
ka := keyAuth(&c.Key.PublicKey, token)
b := sha256.Sum256([]byte(ka))
h := hex.EncodeToString(b[:])
name := fmt.Sprintf("%s.%s.acme.invalid", h[:32], h[32:])
return tlsChallengeCert(name)
}
// TLSSNI02ChallengeCert creates a certificate for TLS-SNI-02 challenge response.
// Servers can present the certificate to validate the challenge and prove control
// over a domain name. For more details on TLS-SNI-02 see
// https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.3.
//
// The token argument is a Challenge.Token value.
// The returned certificate is valid for the next 24 hours.
func (c *Client) TLSSNI02ChallengeCert(token string) (tls.Certificate, error) {
b := sha256.Sum256([]byte(token))
h := hex.EncodeToString(b[:])
sanA := fmt.Sprintf("%s.%s.token.acme.invalid", h[:32], h[32:])
ka := keyAuth(&c.Key.PublicKey, token)
b = sha256.Sum256([]byte(ka))
h = hex.EncodeToString(b[:])
sanB := fmt.Sprintf("%s.%s.ka.acme.invalid", h[:32], h[32:])
return tlsChallengeCert(sanA, sanB)
}
func (c *Client) httpClient() *http.Client {
if c.HTTPClient != nil {
return c.HTTPClient
@@ -531,5 +577,35 @@ func keyAuth(pub *rsa.PublicKey, token string) string {
return fmt.Sprintf("%s.%s", token, JWKThumbprint(pub))
}
// tlsChallengeCert creates a temporary certificate for TLS-SNI challenges
// with the given SANs.
func tlsChallengeCert(san ...string) (tls.Certificate, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return tls.Certificate{}, err
}
t := x509.Certificate{
SerialNumber: big.NewInt(1),
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageKeyEncipherment,
DNSNames: san,
}
der, err := x509.CreateCertificate(rand.Reader, &t, &t, &key.PublicKey, key)
if err != nil {
return tls.Certificate{}, err
}
cert := encodePEM("CERTIFICATE", der)
keyp := encodePEM("RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(key))
return tls.X509KeyPair(cert, keyp)
}
// encodePEM returns b encoded as PEM with block of type typ.
func encodePEM(typ string, b []byte) []byte {
pb := &pem.Block{Type: typ, Bytes: b}
return pem.EncodeToMemory(pb)
}
// timeNow is useful for testing for fixed current time.
var timeNow = time.Now
+52
View File
@@ -766,3 +766,55 @@ func TestErrorResponse(t *testing.T) {
t.Errorf("v.Header = %+v; want %+v", v.Header, res.Header)
}
}
func TestTLSSNI01ChallengeCert(t *testing.T) {
const (
token = "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"
// echo -n <token.testKeyThumbprint> | shasum -a 256
san = "b6ddc3df57802969e2e0b88eb548d4be.febc5bd6cf3690eb526081b5d10deda4.acme.invalid"
)
client := &Client{Key: testKey}
tlscert, err := client.TLSSNI01ChallengeCert(token)
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)
}
if len(cert.DNSNames) != 1 || cert.DNSNames[0] != san {
t.Errorf("cert.DNSNames = %v; want %q", cert.DNSNames, san)
}
}
func TestTLSSNI02ChallengeCert(t *testing.T) {
const (
token = "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"
// echo -n evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA | shasum -a 256
sanA = "7ea0aaa69214e71e02cebb18bb867736.09b730209baabf60e43d4999979ff139.token.acme.invalid"
// echo -n <token.testKeyThumbprint> | shasum -a 256
sanB = "b6ddc3df57802969e2e0b88eb548d4be.febc5bd6cf3690eb526081b5d10deda4.ka.acme.invalid"
)
client := &Client{Key: testKey}
tlscert, err := client.TLSSNI02ChallengeCert(token)
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{sanA, sanB}
if !reflect.DeepEqual(cert.DNSNames, names) {
t.Errorf("cert.DNSNames = %v;\nwant %v", cert.DNSNames, names)
}
}