From 7a1054f3ac58191481dc500077c6b060f5d6c7e5 Mon Sep 17 00:00:00 2001 From: Alex Vaghin Date: Wed, 3 Aug 2016 21:58:22 +0200 Subject: [PATCH] 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 --- acme/internal/acme/acme.go | 76 +++++++++++++++++++++++++++++++++ acme/internal/acme/acme_test.go | 52 ++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/acme/internal/acme/acme.go b/acme/internal/acme/acme.go index 6dc1888..9b1a4ea 100644 --- a/acme/internal/acme/acme.go +++ b/acme/internal/acme/acme.go @@ -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 diff --git a/acme/internal/acme/acme_test.go b/acme/internal/acme/acme_test.go index eae8f66..45e9f30 100644 --- a/acme/internal/acme/acme_test.go +++ b/acme/internal/acme/acme_test.go @@ -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 | 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 | 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) + } +}