Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TLS-ALPN-01 Challenge #572

Merged
merged 13 commits into from Jun 13, 2018
@@ -56,6 +56,7 @@ Otherwise the release will be tagged with the `dev` version identifier.
- Robust implementation of all ACME challenges
- HTTP (http-01)
- DNS (dns-01)
- TLS (tls-alpn-01)
- SAN certificate support
- Comes with multiple optional [DNS providers](https://github.com/xenolf/lego/tree/master/providers/dns)
- [Custom challenge solvers](https://github.com/xenolf/lego/wiki/Writing-a-Challenge-Solver)
@@ -75,9 +76,6 @@ NAME:
USAGE:
lego [global options] command [command options] [arguments...]
VERSION:
0.4.1
COMMANDS:
run Register an account, then create and install a certificate
revoke Revoke a certificate
@@ -91,12 +89,16 @@ GLOBAL OPTIONS:
--server value, -s value CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. (default: "https://acme-v02.api.letsencrypt.org/directory")
--email value, -m value Email used for registration and recovery contact.
--accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.
--eab Use External Account Binding for account registration. Requires --kid and --hmac.
--kid value Key identifier from External CA. Used for External Account Binding.
--hmac value MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.
--key-type value, -k value Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384 (default: "rsa2048")
--path value Directory to use for storing the data (default: "/.lego")
--exclude value, -x value Explicitly disallow solvers by name from being used. Solvers: "http-01", "dns-01",.
--path value Directory to use for storing the data (default: "./.lego")
--exclude value, -x value Explicitly disallow solvers by name from being used. Solvers: "http-01", "dns-01", "tls-alpn-01".
--webroot value Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge
--memcached-host value Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts.
--http value Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port
--tls value Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port
--dns value Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage.
--http-timeout value Set the HTTP timeout value to a specific value in seconds. The default is 10 seconds. (default: 0)
--dns-timeout value Set the DNS timeout value to a specific value in seconds. The default is 10 seconds. (default: 0)
@@ -130,7 +132,7 @@ HTTP Port:

TLS Port:

- All TLS handshakes on port 443 for the TLS-SNI challenge.
- All TLS handshakes on port 443 for the TLS-ALPN challenge.

This traffic redirection is only needed as long as lego solves challenges. As soon as you have received your certificates you can deactivate the forwarding.

@@ -10,4 +10,6 @@ const (
// DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns
// Note: DNS01Record returns a DNS record which will fulfill this challenge
DNS01 = Challenge("dns-01")
// TLSALPN01 is the "tls-alpn-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01
TLSALPN01 = Challenge("tls-alpn-01")
)
@@ -81,8 +81,10 @@ func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) {
// REVIEW: best possibility?
// Add all available solvers with the right index as per ACME
// spec to this map. Otherwise they won`t be found.
solvers := make(map[Challenge]solver)
solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}}
solvers := map[Challenge]solver{
HTTP01: &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}},
TLSALPN01: &tlsALPNChallenge{jws: jws, validate: validate, provider: &TLSALPNProviderServer{}},
}

return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil
}
@@ -94,8 +96,10 @@ func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider)
c.solvers[challenge] = &httpChallenge{jws: c.jws, validate: validate, provider: p}
case DNS01:
c.solvers[challenge] = &dnsChallenge{jws: c.jws, validate: validate, provider: p}
case TLSALPN01:
c.solvers[challenge] = &tlsALPNChallenge{jws: c.jws, validate: validate, provider: p}
default:
return fmt.Errorf("Unknown challenge %v", challenge)
return fmt.Errorf("unknown challenge %v", challenge)
}
return nil
}
@@ -119,6 +123,24 @@ func (c *Client) SetHTTPAddress(iface string) error {
return nil
}

// SetTLSAddress specifies a custom interface:port to be used for TLS based challenges.
// If this option is not used, the default port 443 and all interfaces will be used.
// To only specify a port and no interface use the ":port" notation.
//
// NOTE: This REPLACES any custom TLS-ALPN provider previously set by calling
// c.SetChallengeProvider with the default TLS-ALPN challenge provider.
func (c *Client) SetTLSAddress(iface string) error {
host, port, err := net.SplitHostPort(iface)
if err != nil {
return err
}

if chlng, ok := c.solvers[TLSALPN01]; ok {
chlng.(*tlsALPNChallenge).provider = NewTLSALPNProviderServer(host, port)
}
return nil
}

// ExcludeChallenges explicitly removes challenges from the pool for solving.
func (c *Client) ExcludeChallenges(challenges []Challenge) {
// Loop through all challenges and delete the requested one if found.
@@ -53,7 +53,7 @@ func TestNewClient(t *testing.T) {
t.Errorf("Expected keyType to be %s but was %s", keyType, client.keyType)
}

if expected, actual := 1, len(client.solvers); actual != expected {
if expected, actual := 2, len(client.solvers); actual != expected {
t.Fatalf("Expected %d solver(s), got %d", expected, actual)
}
}
@@ -303,16 +303,16 @@ func getCertExpiration(cert []byte) (time.Time, error) {
return pCert.NotAfter, nil
}

func generatePemCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) {
derBytes, err := generateDerCert(privKey, time.Time{}, domain)
func generatePemCert(privKey *rsa.PrivateKey, domain string, extensions []pkix.Extension) ([]byte, error) {
derBytes, err := generateDerCert(privKey, time.Time{}, domain, extensions)
if err != nil {
return nil, err
}

return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil
}

func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]byte, error) {
func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string, extensions []pkix.Extension) ([]byte, error) {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
@@ -334,6 +334,7 @@ func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain strin
KeyUsage: x509.KeyUsageKeyEncipherment,
BasicConstraintsValid: true,
DNSNames: []string{domain},
ExtraExtensions: extensions,
}

return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
@@ -60,7 +60,7 @@ func TestPEMCertExpiration(t *testing.T) {

expiration := time.Now().Add(365)
expiration = expiration.Round(time.Second)
certBytes, err := generateDerCert(privKey.(*rsa.PrivateKey), expiration, "test.com")
certBytes, err := generateDerCert(privKey.(*rsa.PrivateKey), expiration, "test.com", nil)
if err != nil {
t.Fatal("Error generating cert:", err)
}
@@ -0,0 +1,95 @@
package acme

import (
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"

"github.com/xenolf/lego/log"
)

// idPeAcmeIdentifierV1 is the SMI Security for PKIX Certification Extension
// OID referencing the ACME extension. Reference:
// https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-5.1
var idPeAcmeIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1}

type tlsALPNChallenge struct {
jws *jws
validate validateFunc
provider ChallengeProvider
}

// Solve manages the provider to validate and solve the challenge.
func (t *tlsALPNChallenge) Solve(chlng challenge, domain string) error {
log.Printf("[INFO][%s] acme: Trying to solve TLS-ALPN-01", domain)

// Generate the Key Authorization for the challenge
keyAuth, err := getKeyAuthorization(chlng.Token, t.jws.privKey)
if err != nil {
return err
}

err = t.provider.Present(domain, chlng.Token, keyAuth)
if err != nil {
return fmt.Errorf("[%s] error presenting token: %v", domain, err)
}
defer func() {
err := t.provider.CleanUp(domain, chlng.Token, keyAuth)
if err != nil {
log.Printf("[%s] error cleaning up: %v", domain, err)
}
}()

return t.validate(t.jws, domain, chlng.URL, challenge{Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
}

// TLSALPNChallengeCert returns a certificate with the acmeValidation-v1
// extension and domain name for the `tls-alpn-01` challenge.
func TLSALPNChallengeCert(domain, keyAuth string) (*tls.Certificate, error) {
// Generate a new RSA key for the certificates.
tempPrivKey, err := generatePrivateKey(RSA2048)
if err != nil {
return nil, err
}

// Encode the private key into a PEM format. We'll need to use it to
// generate the x509 keypair.
rsaPrivKey := tempPrivKey.(*rsa.PrivateKey)
rsaPrivPEM := pemEncode(rsaPrivKey)

// Compute the SHA-256 digest of the key authorization.
zBytes := sha256.Sum256([]byte(keyAuth))

value, err := asn1.Marshal(zBytes[:sha256.Size])
if err != nil {
return nil, err
}

// Add the keyAuth digest as the acmeValidation-v1 extension (marked as
// critical such that it won't be used by non-ACME software). Reference:
// https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-3
extensions := []pkix.Extension{
{
Id: idPeAcmeIdentifierV1,
Critical: true,
Value: value,
},
}

// Generate the PEM certificate using the provided private key, domain, and
// extra extensions.
tempCertPEM, err := generatePemCert(rsaPrivKey, domain, extensions)
if err != nil {
return nil, err
}

certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM)
if err != nil {
return nil, err
}

return &certificate, nil
}
@@ -0,0 +1,86 @@
package acme

import (
"crypto/tls"
"fmt"
"net"
"net/http"
)

const (
// acmeTLS1Protocol is the ALPN Protocol ID for the ACME-TLS/1 Protocol.
acmeTLS1Protocol = "acme-tls/1"

// defaultTLSPort is the port that the TLSALPNProviderServer will default to
// when no other port is provided.
defaultTLSPort = "443"
)

// TLSALPNProviderServer implements ChallengeProvider for `TLS-ALPN-01`
// challenge. It may be instantiated without using the NewTLSALPNProviderServer
// if you want only to use the default values.
type TLSALPNProviderServer struct {
iface string
port string
listener net.Listener
}

// NewTLSALPNProviderServer creates a new TLSALPNProviderServer on the selected
// interface and port. Setting iface and / or port to an empty string will make
// the server fall back to the "any" interface and port 443 respectively.
func NewTLSALPNProviderServer(iface, port string) *TLSALPNProviderServer {
return &TLSALPNProviderServer{iface: iface, port: port}
}

// Present generates a certificate with a SHA-256 digest of the keyAuth provided
// as the acmeValidation-v1 extension value to conform to the ACME-TLS-ALPN
// spec.
func (t *TLSALPNProviderServer) Present(domain, token, keyAuth string) error {
if t.port == "" {
// Fallback to port 443 if the port was not provided.
t.port = defaultTLSPort
}

// Generate the challenge certificate using the provided keyAuth and domain.
cert, err := TLSALPNChallengeCert(domain, keyAuth)
if err != nil {
return err
}

// Place the generated certificate with the extension into the TLS config
// so that it can serve the correct details.
tlsConf := new(tls.Config)
tlsConf.Certificates = []tls.Certificate{*cert}

// We must set that the `acme-tls/1` application level protocol is supported
// so that the protocol negotiation can succeed. Reference:
// https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-5.2
tlsConf.NextProtos = []string{acmeTLS1Protocol}

// Create the listener with the created tls.Config.
t.listener, err = tls.Listen("tcp", net.JoinHostPort(t.iface, t.port), tlsConf)
if err != nil {
return fmt.Errorf("could not start HTTPS server for challenge -> %v", err)
}

// Shut the server down when we're finished.
go func() {
http.Serve(t.listener, nil)
}()

return nil
}

// CleanUp closes the HTTPS server.
func (t *TLSALPNProviderServer) CleanUp(domain, token, keyAuth string) error {
if t.listener == nil {

This comment has been minimized.

Copy link
@mholt

mholt Jun 10, 2018

Contributor

Excellent, checking for a nil value just in case

return nil
}

// Server was created, close it.
if err := t.listener.Close(); err != nil && err != http.ErrServerClosed {
return err
}

return nil
}
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.