Jelajahi Sumber

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>
Alex Vaghin 9 tahun lalu
induk
melakukan
7a1054f3ac
2 mengubah file dengan 128 tambahan dan 0 penghapusan
  1. 76 0
      acme/internal/acme/acme.go
  2. 52 0
      acme/internal/acme/acme_test.go

+ 76 - 0
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

+ 52 - 0
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 <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)
+	}
+}