forked from TrueCloudLab/lego
Merge remote-tracking branch 'refs/remotes/xenolf/master'
This commit is contained in:
commit
eb773f17d2
39 changed files with 1172 additions and 617 deletions
13
.travis.yml
13
.travis.yml
|
@ -1 +1,14 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.4.3
|
||||
- 1.5.3
|
||||
- tip
|
||||
|
||||
install:
|
||||
- go get -t ./...
|
||||
- go get golang.org/x/tools/cmd/vet
|
||||
|
||||
script:
|
||||
- go vet ./...
|
||||
- go test -v ./...
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
### Added:
|
||||
- CLI: The `--dns` switch. To include the DNS challenge for consideration. Supported are the following solvers: cloudflare, digitalocean, dnsimple, route53, rfc2136 and manual.
|
||||
- CLI: The `--accept-tos` switch. Indicates your acceptance of the Let's Encrypt terms of service without prompting you.
|
||||
- lib: A new type for challenge identifiers `Challenge`
|
||||
- lib: A new interface for custom challenge providers `ChallengeProvider`
|
||||
- lib: SetChallengeProvider function. Pass a challenge identifier and a Provider to replace the default behaviour of a challenge.
|
||||
|
|
32
CONTRIBUTING.md
Normal file
32
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# How to contribute to lego
|
||||
|
||||
Contributions in the form of patches and proposals are essential to keep lego great and to make it even better.
|
||||
To ensure a great and easy experience for everyone, please review the few guidelines in this document.
|
||||
|
||||
## Bug reports
|
||||
|
||||
- Use the issue search to see if the issue has already been reported.
|
||||
- Also look for closed issues to see if your issue has already been fixed.
|
||||
- If both of the above do not apply create a new issue and include as much information as possible.
|
||||
|
||||
Bug reports should include all information a person could need to reproduce your problem without the need to
|
||||
follow up for more information. If possible, provide detailed steps for us to reproduce it, the expected behaviour and the actual behaviour.
|
||||
|
||||
## Feature proposals and requests
|
||||
|
||||
Feature requests are welcome and should be discussed in an issue.
|
||||
Please keep proposals focused on one thing at a time and be as detailed as possible.
|
||||
It is up to you to make a strong point about your proposal and convince us of the merits and the added complexity of this feature.
|
||||
|
||||
## Pull requests
|
||||
|
||||
Patches, new features and improvements are a great way to help the project.
|
||||
Please keep them focused on one thing and do not include unrelated commits.
|
||||
|
||||
All pull requests which alter the behaviour of the program, add new behaviour or somehow alter code in a non-trivial way should **always** include tests.
|
||||
|
||||
If you want to contribute a significant pull request (with a non-trivial workload for you) please **ask first**. We do not want you to spend
|
||||
a lot of time on something the project's developers might not want to merge into the project.
|
||||
|
||||
**IMPORTANT**: By submitting a patch, you agree to allow the project
|
||||
owners to license your work under the terms of the [MIT License](LICENSE).
|
|
@ -91,7 +91,7 @@ GLOBAL OPTIONS:
|
|||
--webroot Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge
|
||||
--http Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port
|
||||
--tls Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port
|
||||
--dns Enable the DNS challenge for solving using a provider.
|
||||
--dns Solve a DNS challenge using the specified provider. Disables all other solvers.
|
||||
Credentials for providers have to be passed through environment variables.
|
||||
For a more detailed explanation of the parameters, please see the online docs.
|
||||
Valid providers:
|
||||
|
@ -99,7 +99,7 @@ GLOBAL OPTIONS:
|
|||
digitalocean: DO_AUTH_TOKEN
|
||||
dnsimple: DNSIMPLE_EMAIL, DNSIMPLE_API_KEY
|
||||
route53: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION
|
||||
rfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_NAMESERVER, RFC2136_ZONE
|
||||
rfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER
|
||||
manual: none
|
||||
--help, -h show help
|
||||
--version, -v print the version
|
||||
|
@ -127,9 +127,11 @@ $ lego --email="foo@bar.com" --domains="example.com" renew
|
|||
Obtain a certificate using the DNS challenge and AWS Route 53:
|
||||
|
||||
```bash
|
||||
$ AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=my_id AWS_SECRET_ACCESS_KEY=my_key lego --email="foo@bar.com" --domains="example.com" --dns="route53" --exclude="http-01" --exclude="tls-sni-01" run
|
||||
$ AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=my_id AWS_SECRET_ACCESS_KEY=my_key lego --email="foo@bar.com" --domains="example.com" --dns="route53" run
|
||||
```
|
||||
|
||||
Note that `--dns=foo` implies `--exclude=http-01` and `--exclude=tls-sni-01`. lego will not attempt other challenges if you've told it to use DNS instead.
|
||||
|
||||
lego defaults to communicating with the production Let's Encrypt ACME server. If you'd like to test something without issuing real certificates, consider using the staging endpoint instead:
|
||||
|
||||
```bash
|
||||
|
|
16
account.go
16
account.go
|
@ -1,7 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
@ -13,7 +13,7 @@ import (
|
|||
// Account represents a users local saved credentials
|
||||
type Account struct {
|
||||
Email string `json:"email"`
|
||||
key *rsa.PrivateKey
|
||||
key crypto.PrivateKey
|
||||
Registration *acme.RegistrationResource `json:"registration"`
|
||||
|
||||
conf *Configuration
|
||||
|
@ -28,16 +28,18 @@ func NewAccount(email string, conf *Configuration) *Account {
|
|||
logger().Fatalf("Could not check/create directory for account %s: %v", email, err)
|
||||
}
|
||||
|
||||
var privKey *rsa.PrivateKey
|
||||
var privKey crypto.PrivateKey
|
||||
if _, err := os.Stat(accKeyPath); os.IsNotExist(err) {
|
||||
logger().Printf("No key found for account %s. Generating a %v bit key.", email, conf.RsaBits())
|
||||
privKey, err = generateRsaKey(conf.RsaBits(), accKeyPath)
|
||||
|
||||
logger().Printf("No key found for account %s. Generating a curve P384 EC key.", email)
|
||||
privKey, err = generatePrivateKey(accKeyPath)
|
||||
if err != nil {
|
||||
logger().Fatalf("Could not generate RSA private account key for account %s: %v", email, err)
|
||||
}
|
||||
|
||||
logger().Printf("Saved key to %s", accKeyPath)
|
||||
} else {
|
||||
privKey, err = loadRsaKey(accKeyPath)
|
||||
privKey, err = loadPrivateKey(accKeyPath)
|
||||
if err != nil {
|
||||
logger().Fatalf("Could not load RSA private key from file %s: %v", accKeyPath, err)
|
||||
}
|
||||
|
@ -73,7 +75,7 @@ func (a *Account) GetEmail() string {
|
|||
}
|
||||
|
||||
// GetPrivateKey returns the private RSA account key.
|
||||
func (a *Account) GetPrivateKey() *rsa.PrivateKey {
|
||||
func (a *Account) GetPrivateKey() crypto.PrivateKey {
|
||||
return a.key
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package acme
|
||||
|
||||
// Challenge is a string that identifies a particular type and version of ACME challenge.
|
||||
type Challenge string
|
||||
|
||||
const (
|
||||
|
|
|
@ -3,7 +3,6 @@ package acme
|
|||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
|
@ -38,7 +37,7 @@ func logf(format string, args ...interface{}) {
|
|||
type User interface {
|
||||
GetEmail() string
|
||||
GetRegistration() *RegistrationResource
|
||||
GetPrivateKey() *rsa.PrivateKey
|
||||
GetPrivateKey() crypto.PrivateKey
|
||||
}
|
||||
|
||||
// Interface for all challenge solvers to implement.
|
||||
|
@ -53,7 +52,7 @@ type Client struct {
|
|||
directory directory
|
||||
user User
|
||||
jws *jws
|
||||
keyBits int
|
||||
keyType KeyType
|
||||
issuerCert []byte
|
||||
solvers map[Challenge]solver
|
||||
}
|
||||
|
@ -61,16 +60,12 @@ type Client struct {
|
|||
// NewClient creates a new ACME client on behalf of the user. The client will depend on
|
||||
// the ACME directory located at caDirURL for the rest of its actions. It will
|
||||
// generate private keys for certificates of size keyBits.
|
||||
func NewClient(caDirURL string, user User, keyBits int) (*Client, error) {
|
||||
func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) {
|
||||
privKey := user.GetPrivateKey()
|
||||
if privKey == nil {
|
||||
return nil, errors.New("private key was nil")
|
||||
}
|
||||
|
||||
if err := privKey.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid private key: %v", err)
|
||||
}
|
||||
|
||||
var dir directory
|
||||
if _, err := getJSON(caDirURL, &dir); err != nil {
|
||||
return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err)
|
||||
|
@ -95,10 +90,10 @@ func NewClient(caDirURL string, user User, keyBits int) (*Client, error) {
|
|||
// 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}
|
||||
solvers[TLSSNI01] = &tlsSNIChallenge{jws: jws, validate: validate}
|
||||
solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}}
|
||||
solvers[TLSSNI01] = &tlsSNIChallenge{jws: jws, validate: validate, provider: &TLSProviderServer{}}
|
||||
|
||||
return &Client{directory: dir, user: user, jws: jws, keyBits: keyBits, solvers: solvers}, nil
|
||||
return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil
|
||||
}
|
||||
|
||||
// SetChallengeProvider specifies a custom provider that will make the solution available
|
||||
|
@ -126,7 +121,7 @@ func (c *Client) SetHTTPAddress(iface string) error {
|
|||
}
|
||||
|
||||
if chlng, ok := c.solvers[HTTP01]; ok {
|
||||
chlng.(*httpChallenge).provider = &httpChallengeServer{iface: host, port: port}
|
||||
chlng.(*httpChallenge).provider = NewHTTPProviderServer(host, port)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -142,7 +137,7 @@ func (c *Client) SetTLSAddress(iface string) error {
|
|||
}
|
||||
|
||||
if chlng, ok := c.solvers[TLSSNI01]; ok {
|
||||
chlng.(*tlsSNIChallenge).provider = &tlsSNIChallengeServer{iface: host, port: port}
|
||||
chlng.(*tlsSNIChallenge).provider = NewTLSProviderServer(host, port)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -197,8 +192,10 @@ func (c *Client) Register() (*RegistrationResource, error) {
|
|||
// AgreeToTOS updates the Client registration and sends the agreement to
|
||||
// the server.
|
||||
func (c *Client) AgreeToTOS() error {
|
||||
c.user.GetRegistration().Body.Agreement = c.user.GetRegistration().TosURL
|
||||
c.user.GetRegistration().Body.Resource = "reg"
|
||||
reg := c.user.GetRegistration()
|
||||
|
||||
reg.Body.Agreement = c.user.GetRegistration().TosURL
|
||||
reg.Body.Resource = "reg"
|
||||
_, err := postJSON(c.jws, c.user.GetRegistration().URI, c.user.GetRegistration().Body, nil)
|
||||
return err
|
||||
}
|
||||
|
@ -316,13 +313,12 @@ func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (Certif
|
|||
links := parseLinks(resp.Header["Link"])
|
||||
issuerCert, err := c.getIssuerCertificate(links["up"])
|
||||
if err != nil {
|
||||
// If we fail to aquire the issuer cert, return the issued certificate - do not fail.
|
||||
// If we fail to acquire the issuer cert, return the issued certificate - do not fail.
|
||||
logf("[ERROR][%s] acme: Could not bundle issuer certificate: %v", cert.Domain, err)
|
||||
} else {
|
||||
// Success - append the issuer cert to the issued cert.
|
||||
issuerCert = pemEncode(derCertificateBytes(issuerCert))
|
||||
issuedCert = append(issuedCert, issuerCert...)
|
||||
cert.Certificate = issuedCert
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -457,7 +453,7 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool,
|
|||
commonName := authz[0]
|
||||
var err error
|
||||
if privKey == nil {
|
||||
privKey, err = generatePrivateKey(rsakey, c.keyBits)
|
||||
privKey, err = generatePrivateKey(c.keyType)
|
||||
if err != nil {
|
||||
return CertificateResource{}, err
|
||||
}
|
||||
|
@ -471,7 +467,7 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool,
|
|||
}
|
||||
|
||||
// TODO: should the CSR be customizable?
|
||||
csr, err := generateCsr(privKey.(*rsa.PrivateKey), commonName.Domain, san)
|
||||
csr, err := generateCsr(privKey, commonName.Domain, san)
|
||||
if err != nil {
|
||||
return CertificateResource{}, err
|
||||
}
|
||||
|
@ -508,6 +504,7 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool,
|
|||
if len(cert) > 0 {
|
||||
|
||||
cerRes.CertStableURL = resp.Header.Get("Content-Location")
|
||||
cerRes.AccountRef = c.user.GetRegistration().URI
|
||||
|
||||
issuedCert := pemEncode(derCertificateBytes(cert))
|
||||
// If bundle is true, we want to return a certificate bundle.
|
||||
|
@ -518,7 +515,7 @@ func (c *Client) requestCertificate(authz []authorizationResource, bundle bool,
|
|||
links := parseLinks(resp.Header["Link"])
|
||||
issuerCert, err := c.getIssuerCertificate(links["up"])
|
||||
if err != nil {
|
||||
// If we fail to aquire the issuer cert, return the issued certificate - do not fail.
|
||||
// If we fail to acquire the issuer cert, return the issued certificate - do not fail.
|
||||
logf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", commonName.Domain, err)
|
||||
} else {
|
||||
// Success - append the issuer cert to the issued cert.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
|
@ -13,6 +14,7 @@ import (
|
|||
|
||||
func TestNewClient(t *testing.T) {
|
||||
keyBits := 32 // small value keeps test fast
|
||||
keyType := RSA2048
|
||||
key, err := rsa.GenerateKey(rand.Reader, keyBits)
|
||||
if err != nil {
|
||||
t.Fatal("Could not generate test key:", err)
|
||||
|
@ -28,7 +30,7 @@ func TestNewClient(t *testing.T) {
|
|||
w.Write(data)
|
||||
}))
|
||||
|
||||
client, err := NewClient(ts.URL, user, keyBits)
|
||||
client, err := NewClient(ts.URL, user, keyType)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create client: %v", err)
|
||||
}
|
||||
|
@ -40,8 +42,8 @@ func TestNewClient(t *testing.T) {
|
|||
t.Errorf("Expected jws.privKey to be %p but was %p", expected, actual)
|
||||
}
|
||||
|
||||
if client.keyBits != keyBits {
|
||||
t.Errorf("Expected keyBits to be %d but was %d", keyBits, client.keyBits)
|
||||
if client.keyType != keyType {
|
||||
t.Errorf("Expected keyType to be %s but was %s", keyType, client.keyType)
|
||||
}
|
||||
|
||||
if expected, actual := 2, len(client.solvers); actual != expected {
|
||||
|
@ -68,7 +70,7 @@ func TestClientOptPort(t *testing.T) {
|
|||
|
||||
optPort := "1234"
|
||||
optHost := ""
|
||||
client, err := NewClient(ts.URL, user, keyBits)
|
||||
client, err := NewClient(ts.URL, user, RSA2048)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create client: %v", err)
|
||||
}
|
||||
|
@ -82,10 +84,10 @@ func TestClientOptPort(t *testing.T) {
|
|||
if httpSolver.jws != client.jws {
|
||||
t.Error("Expected http-01 to have same jws as client")
|
||||
}
|
||||
if got := httpSolver.provider.(*httpChallengeServer).port; got != optPort {
|
||||
if got := httpSolver.provider.(*HTTPProviderServer).port; got != optPort {
|
||||
t.Errorf("Expected http-01 to have port %s but was %s", optPort, got)
|
||||
}
|
||||
if got := httpSolver.provider.(*httpChallengeServer).iface; got != optHost {
|
||||
if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost {
|
||||
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
|
||||
}
|
||||
|
||||
|
@ -96,10 +98,10 @@ func TestClientOptPort(t *testing.T) {
|
|||
if httpsSolver.jws != client.jws {
|
||||
t.Error("Expected tls-sni-01 to have same jws as client")
|
||||
}
|
||||
if got := httpsSolver.provider.(*tlsSNIChallengeServer).port; got != optPort {
|
||||
if got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort {
|
||||
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got)
|
||||
}
|
||||
if got := httpsSolver.provider.(*tlsSNIChallengeServer).iface; got != optHost {
|
||||
if got := httpsSolver.provider.(*TLSProviderServer).iface; got != optHost {
|
||||
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optHost, got)
|
||||
}
|
||||
|
||||
|
@ -108,10 +110,10 @@ func TestClientOptPort(t *testing.T) {
|
|||
client.SetHTTPAddress(net.JoinHostPort(optHost, optPort))
|
||||
client.SetTLSAddress(net.JoinHostPort(optHost, optPort))
|
||||
|
||||
if got := httpSolver.provider.(*httpChallengeServer).iface; got != optHost {
|
||||
if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost {
|
||||
t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got)
|
||||
}
|
||||
if got := httpsSolver.provider.(*tlsSNIChallengeServer).port; got != optPort {
|
||||
if got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort {
|
||||
t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got)
|
||||
}
|
||||
}
|
||||
|
@ -140,8 +142,8 @@ func TestValidate(t *testing.T) {
|
|||
}))
|
||||
defer ts.Close()
|
||||
|
||||
privKey, _ := generatePrivateKey(rsakey, 512)
|
||||
j := &jws{privKey: privKey.(*rsa.PrivateKey), directoryURL: ts.URL}
|
||||
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
||||
j := &jws{privKey: privKey, directoryURL: ts.URL}
|
||||
|
||||
tsts := []struct {
|
||||
name string
|
||||
|
@ -193,4 +195,4 @@ type mockUser struct {
|
|||
|
||||
func (u mockUser) GetEmail() string { return u.email }
|
||||
func (u mockUser) GetRegistration() *RegistrationResource { return u.regres }
|
||||
func (u mockUser) GetPrivateKey() *rsa.PrivateKey { return u.privatekey }
|
||||
func (u mockUser) GetPrivateKey() crypto.PrivateKey { return u.privatekey }
|
||||
|
|
103
acme/crypto.go
103
acme/crypto.go
|
@ -10,7 +10,6 @@ import (
|
|||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -22,15 +21,19 @@ import (
|
|||
"time"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
type keyType int
|
||||
// KeyType represents the key algo as well as the key size or curve to use.
|
||||
type KeyType string
|
||||
type derCertificateBytes []byte
|
||||
|
||||
// Constants for all key types we support.
|
||||
const (
|
||||
eckey keyType = iota
|
||||
rsakey
|
||||
EC256 = KeyType("P256")
|
||||
EC384 = KeyType("P384")
|
||||
RSA2048 = KeyType("2048")
|
||||
RSA4096 = KeyType("4096")
|
||||
RSA8192 = KeyType("8192")
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -56,14 +59,22 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
// We only got one certificate, means we have no issuer certificate - get it.
|
||||
// We expect the certificate slice to be ordered downwards the chain.
|
||||
// SRV CRT -> CA. We need to pull the leaf and issuer certs out of it,
|
||||
// which should always be the first two certificates. If there's no
|
||||
// OCSP server listed in the leaf cert, there's nothing to do. And if
|
||||
// we have only one certificate so far, we need to get the issuer cert.
|
||||
issuedCert := certificates[0]
|
||||
if len(issuedCert.OCSPServer) == 0 {
|
||||
return nil, nil, errors.New("no OCSP server specified in cert")
|
||||
}
|
||||
if len(certificates) == 1 {
|
||||
// TODO: build fallback. If this fails, check the remaining array entries.
|
||||
if len(certificates[0].IssuingCertificateURL) == 0 {
|
||||
if len(issuedCert.IssuingCertificateURL) == 0 {
|
||||
return nil, nil, errors.New("no issuing certificate URL")
|
||||
}
|
||||
|
||||
resp, err := httpGet(certificates[0].IssuingCertificateURL[0])
|
||||
resp, err := httpGet(issuedCert.IssuingCertificateURL[0])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
@ -83,17 +94,8 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
|
|||
// We want it ordered right SRV CRT -> CA
|
||||
certificates = append(certificates, issuerCert)
|
||||
}
|
||||
|
||||
// We expect the certificate slice to be ordered downwards the chain.
|
||||
// SRV CRT -> CA. We need to pull the cert and issuer cert out of it,
|
||||
// which should always be the last two certificates.
|
||||
issuedCert := certificates[0]
|
||||
issuerCert := certificates[1]
|
||||
|
||||
if len(issuedCert.OCSPServer) == 0 {
|
||||
return nil, nil, errors.New("no OCSP server specified in cert")
|
||||
}
|
||||
|
||||
// Finally kick off the OCSP request.
|
||||
ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil)
|
||||
if err != nil {
|
||||
|
@ -124,8 +126,16 @@ func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
|
|||
}
|
||||
|
||||
func getKeyAuthorization(token string, key interface{}) (string, error) {
|
||||
var publicKey crypto.PublicKey
|
||||
switch k := key.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
publicKey = k.Public()
|
||||
case *rsa.PrivateKey:
|
||||
publicKey = k.Public()
|
||||
}
|
||||
|
||||
// Generate the Key Authorization for the challenge
|
||||
jwk := keyAsJWK(key)
|
||||
jwk := keyAsJWK(publicKey)
|
||||
if jwk == nil {
|
||||
return "", errors.New("Could not generate JWK from key.")
|
||||
}
|
||||
|
@ -144,39 +154,6 @@ func getKeyAuthorization(token string, key interface{}) (string, error) {
|
|||
return token + "." + keyThumb, nil
|
||||
}
|
||||
|
||||
// Derive the shared secret according to acme spec 5.6
|
||||
func performECDH(priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, outLen int, label string) []byte {
|
||||
// Derive Z from the private and public keys according to SEC 1 Ver. 2.0 - 3.3.1
|
||||
Z, _ := priv.PublicKey.ScalarMult(pub.X, pub.Y, priv.D.Bytes())
|
||||
|
||||
if len(Z.Bytes())+len(label)+4 > 384 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if outLen < 384*(2^32-1) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Derive the shared secret key using the ANS X9.63 KDF - SEC 1 Ver. 2.0 - 3.6.1
|
||||
hasher := sha3.New384()
|
||||
buffer := make([]byte, outLen)
|
||||
bufferLen := 0
|
||||
for i := 0; i < outLen/384; i++ {
|
||||
hasher.Reset()
|
||||
|
||||
// Ki = Hash(Z || Counter || [SharedInfo])
|
||||
hasher.Write(Z.Bytes())
|
||||
binary.Write(hasher, binary.BigEndian, i)
|
||||
hasher.Write([]byte(label))
|
||||
|
||||
hash := hasher.Sum(nil)
|
||||
copied := copy(buffer[bufferLen:], hash)
|
||||
bufferLen += copied
|
||||
}
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
// parsePEMBundle parses a certificate bundle from top to bottom and returns
|
||||
// a slice of x509 certificates. This function will error if no certificates are found.
|
||||
func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
|
||||
|
@ -218,18 +195,25 @@ func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func generatePrivateKey(t keyType, keyLength int) (crypto.PrivateKey, error) {
|
||||
switch t {
|
||||
case eckey:
|
||||
func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {
|
||||
|
||||
switch keyType {
|
||||
case EC256:
|
||||
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
case EC384:
|
||||
return ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
case rsakey:
|
||||
return rsa.GenerateKey(rand.Reader, keyLength)
|
||||
case RSA2048:
|
||||
return rsa.GenerateKey(rand.Reader, 2048)
|
||||
case RSA4096:
|
||||
return rsa.GenerateKey(rand.Reader, 4096)
|
||||
case RSA8192:
|
||||
return rsa.GenerateKey(rand.Reader, 8192)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Invalid keytype: %d", t)
|
||||
return nil, fmt.Errorf("Invalid KeyType: %s", keyType)
|
||||
}
|
||||
|
||||
func generateCsr(privateKey *rsa.PrivateKey, domain string, san []string) ([]byte, error) {
|
||||
func generateCsr(privateKey crypto.PrivateKey, domain string, san []string) ([]byte, error) {
|
||||
template := x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: domain,
|
||||
|
@ -246,6 +230,9 @@ func generateCsr(privateKey *rsa.PrivateKey, domain string, san []string) ([]byt
|
|||
func pemEncode(data interface{}) []byte {
|
||||
var pemBlock *pem.Block
|
||||
switch key := data.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
keyBytes, _ := x509.MarshalECPrivateKey(key)
|
||||
pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
|
||||
case *rsa.PrivateKey:
|
||||
pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
|
||||
break
|
||||
|
|
|
@ -2,13 +2,14 @@ package acme
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGeneratePrivateKey(t *testing.T) {
|
||||
key, err := generatePrivateKey(rsakey, 32)
|
||||
key, err := generatePrivateKey(RSA2048)
|
||||
if err != nil {
|
||||
t.Error("Error generating private key:", err)
|
||||
}
|
||||
|
@ -18,12 +19,12 @@ func TestGeneratePrivateKey(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGenerateCSR(t *testing.T) {
|
||||
key, err := generatePrivateKey(rsakey, 512)
|
||||
key, err := rsa.GenerateKey(rand.Reader, 512)
|
||||
if err != nil {
|
||||
t.Fatal("Error generating private key:", err)
|
||||
}
|
||||
|
||||
csr, err := generateCsr(key.(*rsa.PrivateKey), "fizz.buzz", nil)
|
||||
csr, err := generateCsr(key, "fizz.buzz", nil)
|
||||
if err != nil {
|
||||
t.Error("Error generating CSR:", err)
|
||||
}
|
||||
|
@ -52,7 +53,7 @@ func TestPEMEncode(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPEMCertExpiration(t *testing.T) {
|
||||
privKey, err := generatePrivateKey(rsakey, 2048)
|
||||
privKey, err := generatePrivateKey(RSA2048)
|
||||
if err != nil {
|
||||
t.Fatal("Error generating private key:", err)
|
||||
}
|
||||
|
|
|
@ -6,17 +6,22 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
type preCheckDNSFunc func(domain, fqdn string) bool
|
||||
type preCheckDNSFunc func(fqdn, value string) (bool, error)
|
||||
|
||||
var preCheckDNS preCheckDNSFunc = checkDNS
|
||||
var (
|
||||
preCheckDNS preCheckDNSFunc = checkDNSPropagation
|
||||
fqdnToZone = map[string]string{}
|
||||
)
|
||||
|
||||
var preCheckDNSFallbackCount = 5
|
||||
var recursiveNameserver = "google-public-dns-a.google.com:53"
|
||||
|
||||
// DNS01Record returns a DNS record which will fulfill the `dns-01` challenge
|
||||
func DNS01Record(domain, keyAuth string) (fqdn string, value string, ttl int) {
|
||||
|
@ -44,7 +49,7 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
|
|||
}
|
||||
|
||||
// Generate the Key Authorization for the challenge
|
||||
keyAuth, err := getKeyAuthorization(chlng.Token, &s.jws.privKey.PublicKey)
|
||||
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -60,54 +65,182 @@ func (s *dnsChallenge) Solve(chlng challenge, domain string) error {
|
|||
}
|
||||
}()
|
||||
|
||||
fqdn, _, _ := DNS01Record(domain, keyAuth)
|
||||
fqdn, value, _ := DNS01Record(domain, keyAuth)
|
||||
|
||||
preCheckDNS(domain, fqdn)
|
||||
logf("[INFO][%s] Checking DNS record propagation...", domain)
|
||||
|
||||
err = WaitFor(60*time.Second, 2*time.Second, func() (bool, error) {
|
||||
return preCheckDNS(fqdn, value)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth})
|
||||
}
|
||||
|
||||
func checkDNS(domain, fqdn string) bool {
|
||||
// check if the expected DNS entry was created. If not wait for some time and try again.
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(domain+".", dns.TypeSOA)
|
||||
c := new(dns.Client)
|
||||
in, _, err := c.Exchange(m, "google-public-dns-a.google.com:53")
|
||||
// checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers.
|
||||
func checkDNSPropagation(fqdn, value string) (bool, error) {
|
||||
// Initial attempt to resolve at the recursive NS
|
||||
r, err := dnsQuery(fqdn, dns.TypeTXT, recursiveNameserver, true)
|
||||
if err != nil {
|
||||
return false
|
||||
return false, err
|
||||
}
|
||||
if r.Rcode == dns.RcodeSuccess {
|
||||
// If we see a CNAME here then use the alias
|
||||
for _, rr := range r.Answer {
|
||||
if cn, ok := rr.(*dns.CNAME); ok {
|
||||
if cn.Hdr.Name == fqdn {
|
||||
fqdn = cn.Target
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var authorativeNS string
|
||||
for _, answ := range in.Answer {
|
||||
soa := answ.(*dns.SOA)
|
||||
authorativeNS = soa.Ns
|
||||
authoritativeNss, err := lookupNameservers(fqdn)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
fallbackCnt := 0
|
||||
for fallbackCnt < preCheckDNSFallbackCount {
|
||||
m.SetQuestion(fqdn, dns.TypeTXT)
|
||||
in, _, err = c.Exchange(m, authorativeNS+":53")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(in.Answer) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
fallbackCnt++
|
||||
if fallbackCnt >= preCheckDNSFallbackCount {
|
||||
return false
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * time.Duration(fallbackCnt))
|
||||
}
|
||||
|
||||
return false
|
||||
return checkAuthoritativeNss(fqdn, value, authoritativeNss)
|
||||
}
|
||||
|
||||
// toFqdn converts the name into a fqdn appending a trailing dot.
|
||||
func toFqdn(name string) string {
|
||||
// checkAuthoritativeNss queries each of the given nameservers for the expected TXT record.
|
||||
func checkAuthoritativeNss(fqdn, value string, nameservers []string) (bool, error) {
|
||||
for _, ns := range nameservers {
|
||||
r, err := dnsQuery(fqdn, dns.TypeTXT, net.JoinHostPort(ns, "53"), false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if r.Rcode != dns.RcodeSuccess {
|
||||
return false, fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn)
|
||||
}
|
||||
|
||||
var found bool
|
||||
for _, rr := range r.Answer {
|
||||
if txt, ok := rr.(*dns.TXT); ok {
|
||||
if strings.Join(txt.Txt, "") == value {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return false, fmt.Errorf("NS %s did not return the expected TXT record", ns)
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// dnsQuery sends a DNS query to the given nameserver.
|
||||
// The nameserver should include a port, to facilitate testing where we talk to a mock dns server.
|
||||
func dnsQuery(fqdn string, rtype uint16, nameserver string, recursive bool) (in *dns.Msg, err error) {
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(fqdn, rtype)
|
||||
m.SetEdns0(4096, false)
|
||||
|
||||
if !recursive {
|
||||
m.RecursionDesired = false
|
||||
}
|
||||
|
||||
in, err = dns.Exchange(m, nameserver)
|
||||
if err == dns.ErrTruncated {
|
||||
tcp := &dns.Client{Net: "tcp"}
|
||||
in, _, err = tcp.Exchange(m, nameserver)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// lookupNameservers returns the authoritative nameservers for the given fqdn.
|
||||
func lookupNameservers(fqdn string) ([]string, error) {
|
||||
var authoritativeNss []string
|
||||
|
||||
zone, err := FindZoneByFqdn(fqdn, recursiveNameserver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r, err := dnsQuery(zone, dns.TypeNS, recursiveNameserver, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, rr := range r.Answer {
|
||||
if ns, ok := rr.(*dns.NS); ok {
|
||||
authoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns))
|
||||
}
|
||||
}
|
||||
|
||||
if len(authoritativeNss) > 0 {
|
||||
return authoritativeNss, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Could not determine authoritative nameservers")
|
||||
}
|
||||
|
||||
// FindZoneByFqdn determines the zone of the given fqdn
|
||||
func FindZoneByFqdn(fqdn, nameserver string) (string, error) {
|
||||
// Do we have it cached?
|
||||
if zone, ok := fqdnToZone[fqdn]; ok {
|
||||
return zone, nil
|
||||
}
|
||||
|
||||
// Query the authorative nameserver for a hopefully non-existing SOA record,
|
||||
// in the authority section of the reply it will have the SOA of the
|
||||
// containing zone. rfc2308 has this to say on the subject:
|
||||
// Name servers authoritative for a zone MUST include the SOA record of
|
||||
// the zone in the authority section of the response when reporting an
|
||||
// NXDOMAIN or indicating that no data (NODATA) of the requested type exists
|
||||
in, err := dnsQuery(fqdn, dns.TypeSOA, nameserver, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if in.Rcode != dns.RcodeNameError {
|
||||
if in.Rcode != dns.RcodeSuccess {
|
||||
return "", fmt.Errorf("NS %s returned %s for %s", nameserver, dns.RcodeToString[in.Rcode], fqdn)
|
||||
}
|
||||
// We have a success, so one of the answers has to be a SOA RR
|
||||
for _, ans := range in.Answer {
|
||||
if soa, ok := ans.(*dns.SOA); ok {
|
||||
zone := soa.Hdr.Name
|
||||
// If we ended up on one of the TLDs, it means the domain did not exist.
|
||||
publicsuffix, _ := publicsuffix.PublicSuffix(UnFqdn(zone))
|
||||
if publicsuffix == UnFqdn(zone) {
|
||||
return "", fmt.Errorf("Could not determine zone authoritatively")
|
||||
}
|
||||
fqdnToZone[fqdn] = zone
|
||||
return zone, nil
|
||||
}
|
||||
}
|
||||
// Or it is NODATA, fall through to NXDOMAIN
|
||||
}
|
||||
// Search the authority section for our precious SOA RR
|
||||
for _, ns := range in.Ns {
|
||||
if soa, ok := ns.(*dns.SOA); ok {
|
||||
zone := soa.Hdr.Name
|
||||
// If we ended up on one of the TLDs, it means the domain did not exist.
|
||||
publicsuffix, _ := publicsuffix.PublicSuffix(UnFqdn(zone))
|
||||
if publicsuffix == UnFqdn(zone) {
|
||||
return "", fmt.Errorf("Could not determine zone authoritatively")
|
||||
}
|
||||
fqdnToZone[fqdn] = zone
|
||||
return zone, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("NS %s did not return the expected SOA record in the authority section", nameserver)
|
||||
}
|
||||
|
||||
// ClearFqdnCache clears the cache of fqdn to zone mappings. Primarily used in testing.
|
||||
func ClearFqdnCache() {
|
||||
fqdnToZone = map[string]string{}
|
||||
}
|
||||
|
||||
// ToFqdn converts the name into a fqdn appending a trailing dot.
|
||||
func ToFqdn(name string) string {
|
||||
n := len(name)
|
||||
if n == 0 || name[n-1] == '.' {
|
||||
return name
|
||||
|
@ -115,31 +248,11 @@ func toFqdn(name string) string {
|
|||
return name + "."
|
||||
}
|
||||
|
||||
// unFqdn converts the fqdn into a name removing the trailing dot.
|
||||
func unFqdn(name string) string {
|
||||
// UnFqdn converts the fqdn into a name removing the trailing dot.
|
||||
func UnFqdn(name string) string {
|
||||
n := len(name)
|
||||
if n != 0 && name[n-1] == '.' {
|
||||
return name[:n-1]
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// waitFor polls the given function 'f', once per second, up to 'timeout' seconds.
|
||||
func waitFor(timeout int, f func() (bool, error)) error {
|
||||
start := time.Now().Second()
|
||||
for {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
if delta := time.Now().Second() - start; delta >= timeout {
|
||||
return fmt.Errorf("Time limit exceeded (%d seconds)", delta)
|
||||
}
|
||||
|
||||
stop, err := f()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stop {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,145 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/crackcomm/cloudflare"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// DNSProviderCloudFlare is an implementation of the DNSProvider interface
|
||||
type DNSProviderCloudFlare struct {
|
||||
client *cloudflare.Client
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewDNSProviderCloudFlare returns a DNSProviderCloudFlare instance with a configured cloudflare client.
|
||||
// Authentication is either done using the passed credentials or - when empty - using the environment
|
||||
// variables CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY.
|
||||
func NewDNSProviderCloudFlare(cloudflareEmail, cloudflareKey string) (*DNSProviderCloudFlare, error) {
|
||||
if cloudflareEmail == "" || cloudflareKey == "" {
|
||||
cloudflareEmail, cloudflareKey = cloudflareEnvAuth()
|
||||
if cloudflareEmail == "" || cloudflareKey == "" {
|
||||
return nil, fmt.Errorf("CloudFlare credentials missing")
|
||||
}
|
||||
}
|
||||
|
||||
c := &DNSProviderCloudFlare{
|
||||
client: cloudflare.New(&cloudflare.Options{cloudflareEmail, cloudflareKey}),
|
||||
ctx: context.Background(),
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Present creates a TXT record to fulfil the dns-01 challenge
|
||||
func (c *DNSProviderCloudFlare) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value, ttl := DNS01Record(domain, keyAuth)
|
||||
zoneID, err := c.getHostedZoneID(fqdn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
record := newTxtRecord(zoneID, fqdn, value, ttl)
|
||||
err = c.client.Records.Create(c.ctx, record)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CloudFlare API call failed: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters
|
||||
func (c *DNSProviderCloudFlare) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, _, _ := DNS01Record(domain, keyAuth)
|
||||
records, err := c.findTxtRecords(fqdn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rec := range records {
|
||||
err := c.client.Records.Delete(c.ctx, rec.ZoneID, rec.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DNSProviderCloudFlare) findTxtRecords(fqdn string) ([]*cloudflare.Record, error) {
|
||||
zoneID, err := c.getHostedZoneID(fqdn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var records []*cloudflare.Record
|
||||
result, err := c.client.Records.List(c.ctx, zoneID)
|
||||
if err != nil {
|
||||
return records, fmt.Errorf("CloudFlare API call has failed: %v", err)
|
||||
}
|
||||
|
||||
name := unFqdn(fqdn)
|
||||
for _, rec := range result {
|
||||
if rec.Name == name && rec.Type == "TXT" {
|
||||
records = append(records, rec)
|
||||
}
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (c *DNSProviderCloudFlare) getHostedZoneID(fqdn string) (string, error) {
|
||||
zones, err := c.client.Zones.List(c.ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("CloudFlare API call failed: %v", err)
|
||||
}
|
||||
|
||||
var hostedZone cloudflare.Zone
|
||||
for _, zone := range zones {
|
||||
name := toFqdn(zone.Name)
|
||||
if strings.HasSuffix(fqdn, name) {
|
||||
if len(zone.Name) > len(hostedZone.Name) {
|
||||
hostedZone = *zone
|
||||
}
|
||||
}
|
||||
}
|
||||
if hostedZone.ID == "" {
|
||||
return "", fmt.Errorf("No matching CloudFlare zone found for domain %s", fqdn)
|
||||
}
|
||||
|
||||
return hostedZone.ID, nil
|
||||
}
|
||||
|
||||
func newTxtRecord(zoneID, fqdn, value string, ttl int) *cloudflare.Record {
|
||||
name := unFqdn(fqdn)
|
||||
return &cloudflare.Record{
|
||||
Type: "TXT",
|
||||
Name: name,
|
||||
Content: value,
|
||||
TTL: sanitizeTTL(ttl),
|
||||
ZoneID: zoneID,
|
||||
}
|
||||
}
|
||||
|
||||
// TTL must be between 120 and 86400 seconds
|
||||
func sanitizeTTL(ttl int) int {
|
||||
switch {
|
||||
case ttl < 120:
|
||||
return 120
|
||||
case ttl > 86400:
|
||||
return 86400
|
||||
default:
|
||||
return ttl
|
||||
}
|
||||
}
|
||||
|
||||
func cloudflareEnvAuth() (email, apiKey string) {
|
||||
email = os.Getenv("CLOUDFLARE_EMAIL")
|
||||
apiKey = os.Getenv("CLOUDFLARE_API_KEY")
|
||||
if len(email) == 0 || len(apiKey) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
return
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/miekg/dns"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DNSProviderRFC2136 is an implementation of the ChallengeProvider interface that
|
||||
// uses dynamic DNS updates (RFC 2136) to create TXT records on a nameserver.
|
||||
type DNSProviderRFC2136 struct {
|
||||
nameserver string
|
||||
zone string
|
||||
tsigKey string
|
||||
tsigSecret string
|
||||
records map[string]string
|
||||
}
|
||||
|
||||
// NewDNSProviderRFC2136 returns a new DNSProviderRFC2136 instance.
|
||||
// To disable TSIG authentication 'tsigKey' and 'tsigSecret' must be set to the empty string.
|
||||
// 'nameserver' must be a network address in the the form "host:port". 'zone' must be the fully
|
||||
// qualified name of the zone.
|
||||
func NewDNSProviderRFC2136(nameserver, zone, tsigKey, tsigSecret string) (*DNSProviderRFC2136, error) {
|
||||
d := &DNSProviderRFC2136{
|
||||
nameserver: nameserver,
|
||||
zone: zone,
|
||||
records: make(map[string]string),
|
||||
}
|
||||
if len(tsigKey) > 0 && len(tsigSecret) > 0 {
|
||||
d.tsigKey = tsigKey
|
||||
d.tsigSecret = tsigSecret
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Present creates a TXT record using the specified parameters
|
||||
func (r *DNSProviderRFC2136) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value, ttl := DNS01Record(domain, keyAuth)
|
||||
r.records[fqdn] = value
|
||||
return r.changeRecord("INSERT", fqdn, value, ttl)
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters
|
||||
func (r *DNSProviderRFC2136) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, _, ttl := DNS01Record(domain, keyAuth)
|
||||
value := r.records[fqdn]
|
||||
return r.changeRecord("REMOVE", fqdn, value, ttl)
|
||||
}
|
||||
|
||||
func (r *DNSProviderRFC2136) changeRecord(action, fqdn, value string, ttl int) error {
|
||||
// Create RR
|
||||
rr := new(dns.TXT)
|
||||
rr.Hdr = dns.RR_Header{Name: fqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: uint32(ttl)}
|
||||
rr.Txt = []string{value}
|
||||
rrs := make([]dns.RR, 1)
|
||||
rrs[0] = rr
|
||||
|
||||
// Create dynamic update packet
|
||||
m := new(dns.Msg)
|
||||
m.SetUpdate(dns.Fqdn(r.zone))
|
||||
switch action {
|
||||
case "INSERT":
|
||||
m.Insert(rrs)
|
||||
case "REMOVE":
|
||||
m.Remove(rrs)
|
||||
default:
|
||||
return fmt.Errorf("Unexpected action: %s", action)
|
||||
}
|
||||
|
||||
// Setup client
|
||||
c := new(dns.Client)
|
||||
c.SingleInflight = true
|
||||
// TSIG authentication / msg signing
|
||||
if len(r.tsigKey) > 0 && len(r.tsigSecret) > 0 {
|
||||
m.SetTsig(dns.Fqdn(r.tsigKey), dns.HmacMD5, 300, time.Now().Unix())
|
||||
c.TsigSecret = map[string]string{dns.Fqdn(r.tsigKey): r.tsigSecret}
|
||||
}
|
||||
|
||||
// Send the query
|
||||
reply, _, err := c.Exchange(m, r.nameserver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("DNS update failed: %v", err)
|
||||
}
|
||||
if reply != nil && reply.Rcode != dns.RcodeSuccess {
|
||||
return fmt.Errorf("DNS update failed. Server replied: %s", dns.RcodeToString[reply.Rcode])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -2,19 +2,86 @@ package acme
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var lookupNameserversTestsOK = []struct {
|
||||
fqdn string
|
||||
nss []string
|
||||
}{
|
||||
{"books.google.com.ng.",
|
||||
[]string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."},
|
||||
},
|
||||
{"www.google.com.",
|
||||
[]string{"ns1.google.com.", "ns2.google.com.", "ns3.google.com.", "ns4.google.com."},
|
||||
},
|
||||
{"physics.georgetown.edu.",
|
||||
[]string{"ns1.georgetown.edu.", "ns2.georgetown.edu.", "ns3.georgetown.edu."},
|
||||
},
|
||||
}
|
||||
|
||||
var lookupNameserversTestsErr = []struct {
|
||||
fqdn string
|
||||
error string
|
||||
}{
|
||||
// invalid tld
|
||||
{"_null.n0n0.",
|
||||
"Could not determine zone authoritatively",
|
||||
},
|
||||
// invalid domain
|
||||
{"_null.com.",
|
||||
"Could not determine zone authoritatively",
|
||||
},
|
||||
// invalid domain
|
||||
{"in-valid.co.uk.",
|
||||
"Could not determine zone authoritatively",
|
||||
},
|
||||
}
|
||||
|
||||
var checkAuthoritativeNssTests = []struct {
|
||||
fqdn, value string
|
||||
ns []string
|
||||
ok bool
|
||||
}{
|
||||
// TXT RR w/ expected value
|
||||
{"8.8.8.8.asn.routeviews.org.", "151698.8.8.024", []string{"asnums.routeviews.org."},
|
||||
true,
|
||||
},
|
||||
// No TXT RR
|
||||
{"ns1.google.com.", "", []string{"ns2.google.com."},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
var checkAuthoritativeNssTestsErr = []struct {
|
||||
fqdn, value string
|
||||
ns []string
|
||||
error string
|
||||
}{
|
||||
// TXT RR /w unexpected value
|
||||
{"8.8.8.8.asn.routeviews.org.", "fe01=", []string{"asnums.routeviews.org."},
|
||||
"did not return the expected TXT record",
|
||||
},
|
||||
// No TXT RR
|
||||
{"ns1.google.com.", "fe01=", []string{"ns2.google.com."},
|
||||
"did not return the expected TXT record",
|
||||
},
|
||||
}
|
||||
|
||||
func TestDNSValidServerResponse(t *testing.T) {
|
||||
preCheckDNS = func(domain, fqdn string) bool {
|
||||
return true
|
||||
preCheckDNS = func(fqdn, value string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
privKey, _ := generatePrivateKey(rsakey, 512)
|
||||
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Replay-Nonce", "12345")
|
||||
|
@ -22,7 +89,7 @@ func TestDNSValidServerResponse(t *testing.T) {
|
|||
}))
|
||||
|
||||
manualProvider, _ := NewDNSProviderManual()
|
||||
jws := &jws{privKey: privKey.(*rsa.PrivateKey), directoryURL: ts.URL}
|
||||
jws := &jws{privKey: privKey, directoryURL: ts.URL}
|
||||
solver := &dnsChallenge{jws: jws, validate: validate, provider: manualProvider}
|
||||
clientChallenge := challenge{Type: "dns01", Status: "pending", URI: ts.URL, Token: "http8"}
|
||||
|
||||
|
@ -39,7 +106,60 @@ func TestDNSValidServerResponse(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPreCheckDNS(t *testing.T) {
|
||||
if !preCheckDNS("api.letsencrypt.org", "acme-staging.api.letsencrypt.org") {
|
||||
ok, err := preCheckDNS("acme-staging.api.letsencrypt.org", "fe01=")
|
||||
if err != nil || !ok {
|
||||
t.Errorf("preCheckDNS failed for acme-staging.api.letsencrypt.org")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupNameserversOK(t *testing.T) {
|
||||
for _, tt := range lookupNameserversTestsOK {
|
||||
nss, err := lookupNameservers(tt.fqdn)
|
||||
if err != nil {
|
||||
t.Fatalf("#%s: got %q; want nil", tt.fqdn, err)
|
||||
}
|
||||
|
||||
sort.Strings(nss)
|
||||
sort.Strings(tt.nss)
|
||||
|
||||
if !reflect.DeepEqual(nss, tt.nss) {
|
||||
t.Errorf("#%s: got %v; want %v", tt.fqdn, nss, tt.nss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupNameserversErr(t *testing.T) {
|
||||
for _, tt := range lookupNameserversTestsErr {
|
||||
_, err := lookupNameservers(tt.fqdn)
|
||||
if err == nil {
|
||||
t.Fatalf("#%s: expected %q (error); got <nil>", tt.fqdn, tt.error)
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), tt.error) {
|
||||
t.Errorf("#%s: expected %q (error); got %q", tt.fqdn, tt.error, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckAuthoritativeNss(t *testing.T) {
|
||||
for _, tt := range checkAuthoritativeNssTests {
|
||||
ok, _ := checkAuthoritativeNss(tt.fqdn, tt.value, tt.ns)
|
||||
if ok != tt.ok {
|
||||
t.Errorf("%s: got %t; want %t", tt.fqdn, ok, tt.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckAuthoritativeNssErr(t *testing.T) {
|
||||
for _, tt := range checkAuthoritativeNssTestsErr {
|
||||
_, err := checkAuthoritativeNss(tt.fqdn, tt.value, tt.ns)
|
||||
if err == nil {
|
||||
t.Fatalf("#%s: expected %q (error); got <nil>", tt.fqdn, tt.error)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.error) {
|
||||
t.Errorf("#%s: expected %q (error); got %q", tt.fqdn, tt.error, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
10
acme/http.go
10
acme/http.go
|
@ -8,11 +8,15 @@ import (
|
|||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UserAgent (if non-empty) will be tacked onto the User-Agent string in requests.
|
||||
var UserAgent string
|
||||
|
||||
// defaultClient is an HTTP client with a reasonable timeout value.
|
||||
var defaultClient = http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
const (
|
||||
// defaultGoUserAgent is the Go HTTP package user agent string. Too
|
||||
// bad it isn't exported. If it changes, we should update it here, too.
|
||||
|
@ -32,7 +36,7 @@ func httpHead(url string) (resp *http.Response, err error) {
|
|||
|
||||
req.Header.Set("User-Agent", userAgent())
|
||||
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
resp, err = defaultClient.Do(req)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
@ -50,7 +54,7 @@ func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response,
|
|||
req.Header.Set("Content-Type", bodyType)
|
||||
req.Header.Set("User-Agent", userAgent())
|
||||
|
||||
return http.DefaultClient.Do(req)
|
||||
return defaultClient.Do(req)
|
||||
}
|
||||
|
||||
// httpGet performs a GET request with a proper User-Agent string.
|
||||
|
@ -62,7 +66,7 @@ func httpGet(url string) (resp *http.Response, err error) {
|
|||
}
|
||||
req.Header.Set("User-Agent", userAgent())
|
||||
|
||||
return http.DefaultClient.Do(req)
|
||||
return defaultClient.Do(req)
|
||||
}
|
||||
|
||||
// getJSON performs an HTTP GET request and parses the response body
|
||||
|
|
|
@ -21,15 +21,11 @@ func (s *httpChallenge) Solve(chlng challenge, domain string) error {
|
|||
logf("[INFO][%s] acme: Trying to solve HTTP-01", domain)
|
||||
|
||||
// Generate the Key Authorization for the challenge
|
||||
keyAuth, err := getKeyAuthorization(chlng.Token, &s.jws.privKey.PublicKey)
|
||||
keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.provider == nil {
|
||||
s.provider = &httpChallengeServer{}
|
||||
}
|
||||
|
||||
err = s.provider.Present(domain, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] error presenting token: %v", domain, err)
|
||||
|
|
|
@ -7,16 +7,25 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// httpChallengeServer implements ChallengeProvider for `http-01` challenge
|
||||
type httpChallengeServer struct {
|
||||
// HTTPProviderServer implements ChallengeProvider for `http-01` challenge
|
||||
// It may be instantiated without using the NewHTTPProviderServer function if
|
||||
// you want only to use the default values.
|
||||
type HTTPProviderServer struct {
|
||||
iface string
|
||||
port string
|
||||
done chan bool
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
// Present makes the token available at `HTTP01ChallengePath(token)`
|
||||
func (s *httpChallengeServer) Present(domain, token, keyAuth string) error {
|
||||
// NewHTTPProviderServer creates a new HTTPProviderServer 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 80 respectively.
|
||||
func NewHTTPProviderServer(iface, port string) *HTTPProviderServer {
|
||||
return &HTTPProviderServer{iface: iface, port: port}
|
||||
}
|
||||
|
||||
// Present starts a web server and makes the token available at `HTTP01ChallengePath(token)` for web requests.
|
||||
func (s *HTTPProviderServer) Present(domain, token, keyAuth string) error {
|
||||
if s.port == "" {
|
||||
s.port = "80"
|
||||
}
|
||||
|
@ -32,7 +41,8 @@ func (s *httpChallengeServer) Present(domain, token, keyAuth string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *httpChallengeServer) CleanUp(domain, token, keyAuth string) error {
|
||||
// CleanUp closes the HTTP server and removes the token from `HTTP01ChallengePath(token)`
|
||||
func (s *HTTPProviderServer) CleanUp(domain, token, keyAuth string) error {
|
||||
if s.listener == nil {
|
||||
return nil
|
||||
}
|
||||
|
@ -41,7 +51,7 @@ func (s *httpChallengeServer) CleanUp(domain, token, keyAuth string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *httpChallengeServer) serve(domain, token, keyAuth string) {
|
||||
func (s *HTTPProviderServer) serve(domain, token, keyAuth string) {
|
||||
path := HTTP01ChallengePath(token)
|
||||
|
||||
// The handler validates the HOST header and request type.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
@ -9,8 +10,8 @@ import (
|
|||
)
|
||||
|
||||
func TestHTTPChallenge(t *testing.T) {
|
||||
privKey, _ := generatePrivateKey(rsakey, 512)
|
||||
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
|
||||
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
||||
j := &jws{privKey: privKey}
|
||||
clientChallenge := challenge{Type: HTTP01, Token: "http1"}
|
||||
mockValidate := func(_ *jws, _, _ string, chlng challenge) error {
|
||||
uri := "http://localhost:23457/.well-known/acme-challenge/" + chlng.Token
|
||||
|
@ -36,7 +37,7 @@ func TestHTTPChallenge(t *testing.T) {
|
|||
|
||||
return nil
|
||||
}
|
||||
solver := &httpChallenge{jws: j, validate: mockValidate, provider: &httpChallengeServer{port: "23457"}}
|
||||
solver := &httpChallenge{jws: j, validate: mockValidate, provider: &HTTPProviderServer{port: "23457"}}
|
||||
|
||||
if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil {
|
||||
t.Errorf("Solve error: got %v, want nil", err)
|
||||
|
@ -44,10 +45,10 @@ func TestHTTPChallenge(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHTTPChallengeInvalidPort(t *testing.T) {
|
||||
privKey, _ := generatePrivateKey(rsakey, 128)
|
||||
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
|
||||
privKey, _ := rsa.GenerateKey(rand.Reader, 128)
|
||||
j := &jws{privKey: privKey}
|
||||
clientChallenge := challenge{Type: HTTP01, Token: "http2"}
|
||||
solver := &httpChallenge{jws: j, validate: stubValidate, provider: &httpChallengeServer{port: "123456"}}
|
||||
solver := &httpChallenge{jws: j, validate: stubValidate, provider: &HTTPProviderServer{port: "123456"}}
|
||||
|
||||
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
|
||||
t.Errorf("Solve error: got %v, want error", err)
|
||||
|
|
20
acme/jws.go
20
acme/jws.go
|
@ -2,7 +2,9 @@ package acme
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
@ -12,7 +14,7 @@ import (
|
|||
|
||||
type jws struct {
|
||||
directoryURL string
|
||||
privKey *rsa.PrivateKey
|
||||
privKey crypto.PrivateKey
|
||||
nonces []string
|
||||
}
|
||||
|
||||
|
@ -46,8 +48,20 @@ func (j *jws) post(url string, content []byte) (*http.Response, error) {
|
|||
}
|
||||
|
||||
func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) {
|
||||
// TODO: support other algorithms - RS512
|
||||
signer, err := jose.NewSigner(jose.RS256, j.privKey)
|
||||
|
||||
var alg jose.SignatureAlgorithm
|
||||
switch k := j.privKey.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
alg = jose.RS256
|
||||
case *ecdsa.PrivateKey:
|
||||
if k.Curve == elliptic.P256() {
|
||||
alg = jose.ES256
|
||||
} else if k.Curve == elliptic.P384() {
|
||||
alg = jose.ES384
|
||||
}
|
||||
}
|
||||
|
||||
signer, err := jose.NewSigner(alg, j.privKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -28,17 +28,13 @@ type registrationMessage struct {
|
|||
// Registration is returned by the ACME server after the registration
|
||||
// The client implementation should save this registration somewhere.
|
||||
type Registration struct {
|
||||
Resource string `json:"resource,omitempty"`
|
||||
ID int `json:"id"`
|
||||
Key struct {
|
||||
Kty string `json:"kty"`
|
||||
N string `json:"n"`
|
||||
E string `json:"e"`
|
||||
} `json:"key"`
|
||||
Contact []string `json:"contact"`
|
||||
Agreement string `json:"agreement,omitempty"`
|
||||
Authorizations string `json:"authorizations,omitempty"`
|
||||
Certificates string `json:"certificates,omitempty"`
|
||||
Resource string `json:"resource,omitempty"`
|
||||
ID int `json:"id"`
|
||||
Key jose.JsonWebKey `json:"key"`
|
||||
Contact []string `json:"contact"`
|
||||
Agreement string `json:"agreement,omitempty"`
|
||||
Authorizations string `json:"authorizations,omitempty"`
|
||||
Certificates string `json:"certificates,omitempty"`
|
||||
// RecoveryKey recoveryKeyMessage `json:"recoveryKey,omitempty"`
|
||||
}
|
||||
|
||||
|
@ -113,6 +109,7 @@ type CertificateResource struct {
|
|||
Domain string `json:"domain"`
|
||||
CertURL string `json:"certUrl"`
|
||||
CertStableURL string `json:"certStableUrl"`
|
||||
AccountRef string `json:"accountRef,omitempty"`
|
||||
PrivateKey []byte `json:"-"`
|
||||
Certificate []byte `json:"-"`
|
||||
}
|
||||
|
|
|
@ -22,15 +22,11 @@ func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error {
|
|||
logf("[INFO][%s] acme: Trying to solve TLS-SNI-01", domain)
|
||||
|
||||
// Generate the Key Authorization for the challenge
|
||||
keyAuth, err := getKeyAuthorization(chlng.Token, &t.jws.privKey.PublicKey)
|
||||
keyAuth, err := getKeyAuthorization(chlng.Token, t.jws.privKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if t.provider == nil {
|
||||
t.provider = &tlsSNIChallengeServer{}
|
||||
}
|
||||
|
||||
err = t.provider.Present(domain, chlng.Token, keyAuth)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] error presenting token: %v", domain, err)
|
||||
|
@ -47,7 +43,7 @@ func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error {
|
|||
// TLSSNI01ChallengeCert returns a certificate for the `tls-sni-01` challenge
|
||||
func TLSSNI01ChallengeCert(keyAuth string) (tls.Certificate, error) {
|
||||
// generate a new RSA key for the certificates
|
||||
tempPrivKey, err := generatePrivateKey(rsakey, 2048)
|
||||
tempPrivKey, err := generatePrivateKey(RSA2048)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
|
|
|
@ -7,16 +7,25 @@ import (
|
|||
"net/http"
|
||||
)
|
||||
|
||||
// tlsSNIChallengeServer implements ChallengeProvider for `TLS-SNI-01` challenge
|
||||
type tlsSNIChallengeServer struct {
|
||||
// TLSProviderServer implements ChallengeProvider for `TLS-SNI-01` challenge
|
||||
// It may be instantiated without using the NewTLSProviderServer function if
|
||||
// you want only to use the default values.
|
||||
type TLSProviderServer struct {
|
||||
iface string
|
||||
port string
|
||||
done chan bool
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
// NewTLSProviderServer creates a new TLSProviderServer 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 NewTLSProviderServer(iface, port string) *TLSProviderServer {
|
||||
return &TLSProviderServer{iface: iface, port: port}
|
||||
}
|
||||
|
||||
// Present makes the keyAuth available as a cert
|
||||
func (s *tlsSNIChallengeServer) Present(domain, token, keyAuth string) error {
|
||||
func (s *TLSProviderServer) Present(domain, token, keyAuth string) error {
|
||||
if s.port == "" {
|
||||
s.port = "443"
|
||||
}
|
||||
|
@ -42,7 +51,8 @@ func (s *tlsSNIChallengeServer) Present(domain, token, keyAuth string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *tlsSNIChallengeServer) CleanUp(domain, token, keyAuth string) error {
|
||||
// CleanUp closes the HTTP server.
|
||||
func (s *TLSProviderServer) CleanUp(domain, token, keyAuth string) error {
|
||||
if s.listener == nil {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
|
@ -11,8 +12,8 @@ import (
|
|||
)
|
||||
|
||||
func TestTLSSNIChallenge(t *testing.T) {
|
||||
privKey, _ := generatePrivateKey(rsakey, 512)
|
||||
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
|
||||
privKey, _ := rsa.GenerateKey(rand.Reader, 512)
|
||||
j := &jws{privKey: privKey}
|
||||
clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni1"}
|
||||
mockValidate := func(_ *jws, _, _ string, chlng challenge) error {
|
||||
conn, err := tls.Dial("tcp", "localhost:23457", &tls.Config{
|
||||
|
@ -43,7 +44,7 @@ func TestTLSSNIChallenge(t *testing.T) {
|
|||
|
||||
return nil
|
||||
}
|
||||
solver := &tlsSNIChallenge{jws: j, validate: mockValidate, provider: &tlsSNIChallengeServer{port: "23457"}}
|
||||
solver := &tlsSNIChallenge{jws: j, validate: mockValidate, provider: &TLSProviderServer{port: "23457"}}
|
||||
|
||||
if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil {
|
||||
t.Errorf("Solve error: got %v, want nil", err)
|
||||
|
@ -51,10 +52,10 @@ func TestTLSSNIChallenge(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTLSSNIChallengeInvalidPort(t *testing.T) {
|
||||
privKey, _ := generatePrivateKey(rsakey, 128)
|
||||
j := &jws{privKey: privKey.(*rsa.PrivateKey)}
|
||||
privKey, _ := rsa.GenerateKey(rand.Reader, 128)
|
||||
j := &jws{privKey: privKey}
|
||||
clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni2"}
|
||||
solver := &tlsSNIChallenge{jws: j, validate: stubValidate, provider: &tlsSNIChallengeServer{port: "123456"}}
|
||||
solver := &tlsSNIChallenge{jws: j, validate: stubValidate, provider: &TLSProviderServer{port: "123456"}}
|
||||
|
||||
if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil {
|
||||
t.Errorf("Solve error: got %v, want error", err)
|
||||
|
|
29
acme/utils.go
Normal file
29
acme/utils.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WaitFor polls the given function 'f', once every 'interval', up to 'timeout'.
|
||||
func WaitFor(timeout, interval time.Duration, f func() (bool, error)) error {
|
||||
var lastErr string
|
||||
timeup := time.After(timeout)
|
||||
for {
|
||||
select {
|
||||
case <-timeup:
|
||||
return fmt.Errorf("Time limit exceeded. Last error: %s", lastErr)
|
||||
default:
|
||||
}
|
||||
|
||||
stop, err := f()
|
||||
if stop {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
lastErr = err.Error()
|
||||
}
|
||||
|
||||
time.Sleep(interval)
|
||||
}
|
||||
}
|
26
acme/utils_test.go
Normal file
26
acme/utils_test.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package acme
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWaitForTimeout(t *testing.T) {
|
||||
c := make(chan error)
|
||||
go func() {
|
||||
err := WaitFor(3*time.Second, 1*time.Second, func() (bool, error) {
|
||||
return false, nil
|
||||
})
|
||||
c <- err
|
||||
}()
|
||||
|
||||
timeout := time.After(4 * time.Second)
|
||||
select {
|
||||
case <-timeout:
|
||||
t.Fatal("timeout exceeded")
|
||||
case err := <-c:
|
||||
if err == nil {
|
||||
t.Errorf("expected timeout error; got %v", err)
|
||||
}
|
||||
}
|
||||
}
|
26
cli.go
26
cli.go
|
@ -50,6 +50,12 @@ func main() {
|
|||
Name: "run",
|
||||
Usage: "Register an account, then create and install a certificate",
|
||||
Action: run,
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "no-bundle",
|
||||
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "revoke",
|
||||
|
@ -70,6 +76,10 @@ func main() {
|
|||
Name: "reuse-key",
|
||||
Usage: "Used to indicate you want to reuse your current private key for the new certificate.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-bundle",
|
||||
Usage: "Do not create a certificate bundle by adding the issuers certificate to the new certificate.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -88,10 +98,14 @@ func main() {
|
|||
Name: "email, m",
|
||||
Usage: "Email used for registration and recovery contact.",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "rsa-key-size, B",
|
||||
Value: 2048,
|
||||
Usage: "Size of the RSA key.",
|
||||
cli.BoolFlag{
|
||||
Name: "accept-tos, a",
|
||||
Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "key-type, k",
|
||||
Value: "rsa2048",
|
||||
Usage: "Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "path",
|
||||
|
@ -116,7 +130,7 @@ func main() {
|
|||
},
|
||||
cli.StringFlag{
|
||||
Name: "dns",
|
||||
Usage: "Enable the DNS challenge for solving using a provider." +
|
||||
Usage: "Solve a DNS challenge using the specified provider. Disables all other challenges." +
|
||||
"\n\tCredentials for providers have to be passed through environment variables." +
|
||||
"\n\tFor a more detailed explanation of the parameters, please see the online docs." +
|
||||
"\n\tValid providers:" +
|
||||
|
@ -124,7 +138,7 @@ func main() {
|
|||
"\n\tdigitalocean: DO_AUTH_TOKEN" +
|
||||
"\n\tdnsimple: DNSIMPLE_EMAIL, DNSIMPLE_API_KEY" +
|
||||
"\n\troute53: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION" +
|
||||
"\n\trfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_NAMESERVER, RFC2136_ZONE" +
|
||||
"\n\trfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER" +
|
||||
"\n\tmanual: none",
|
||||
},
|
||||
}
|
||||
|
|
114
cli_handlers.go
114
cli_handlers.go
|
@ -11,6 +11,11 @@ import (
|
|||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/xenolf/lego/acme"
|
||||
"github.com/xenolf/lego/providers/dns/cloudflare"
|
||||
"github.com/xenolf/lego/providers/dns/digitalocean"
|
||||
"github.com/xenolf/lego/providers/dns/dnsimple"
|
||||
"github.com/xenolf/lego/providers/dns/rfc2136"
|
||||
"github.com/xenolf/lego/providers/dns/route53"
|
||||
)
|
||||
|
||||
func checkFolder(path string) error {
|
||||
|
@ -23,7 +28,7 @@ func checkFolder(path string) error {
|
|||
func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
|
||||
err := checkFolder(c.GlobalString("path"))
|
||||
if err != nil {
|
||||
logger().Fatalf("Cound not check/create path: %s", err.Error())
|
||||
logger().Fatalf("Could not check/create path: %s", err.Error())
|
||||
}
|
||||
|
||||
conf := NewConfiguration(c)
|
||||
|
@ -34,7 +39,12 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
|
|||
//TODO: move to account struct? Currently MUST pass email.
|
||||
acc := NewAccount(c.GlobalString("email"), conf)
|
||||
|
||||
client, err := acme.NewClient(c.GlobalString("server"), acc, conf.RsaBits())
|
||||
keyType, err := conf.KeyType()
|
||||
if err != nil {
|
||||
logger().Fatal(err.Error())
|
||||
}
|
||||
|
||||
client, err := acme.NewClient(c.GlobalString("server"), acc, keyType)
|
||||
if err != nil {
|
||||
logger().Fatalf("Could not create client: %s", err.Error())
|
||||
}
|
||||
|
@ -52,10 +62,16 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
|
|||
client.SetChallengeProvider(acme.HTTP01, provider)
|
||||
}
|
||||
if c.GlobalIsSet("http") {
|
||||
if strings.Index(c.GlobalString("http"), ":") == -1 {
|
||||
logger().Fatalf("The --http switch only accepts interface:port or :port for its argument.")
|
||||
}
|
||||
client.SetHTTPAddress(c.GlobalString("http"))
|
||||
}
|
||||
|
||||
if c.GlobalIsSet("tls") {
|
||||
if strings.Index(c.GlobalString("tls"), ":") == -1 {
|
||||
logger().Fatalf("The --tls switch only accepts interface:port or :port for its argument.")
|
||||
}
|
||||
client.SetTLSAddress(c.GlobalString("tls"))
|
||||
}
|
||||
|
||||
|
@ -64,23 +80,23 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
|
|||
var provider acme.ChallengeProvider
|
||||
switch c.GlobalString("dns") {
|
||||
case "cloudflare":
|
||||
provider, err = acme.NewDNSProviderCloudFlare("", "")
|
||||
provider, err = cloudflare.NewDNSProvider("", "")
|
||||
case "digitalocean":
|
||||
authToken := os.Getenv("DO_AUTH_TOKEN")
|
||||
|
||||
provider, err = acme.NewDNSProviderDigitalOcean(authToken)
|
||||
provider, err = digitalocean.NewDNSProvider(authToken)
|
||||
case "dnsimple":
|
||||
provider, err = acme.NewDNSProviderDNSimple("", "")
|
||||
provider, err = dnsimple.NewDNSProvider("", "")
|
||||
case "route53":
|
||||
awsRegion := os.Getenv("AWS_REGION")
|
||||
provider, err = acme.NewDNSProviderRoute53("", "", awsRegion)
|
||||
provider, err = route53.NewDNSProvider("", "", awsRegion)
|
||||
case "rfc2136":
|
||||
nameserver := os.Getenv("RFC2136_NAMESERVER")
|
||||
zone := os.Getenv("RFC2136_ZONE")
|
||||
tsigAlgorithm := os.Getenv("RFC2136_TSIG_ALGORITHM")
|
||||
tsigKey := os.Getenv("RFC2136_TSIG_KEY")
|
||||
tsigSecret := os.Getenv("RFC2136_TSIG_SECRET")
|
||||
|
||||
provider, err = acme.NewDNSProviderRFC2136(nameserver, zone, tsigKey, tsigSecret)
|
||||
provider, err = rfc2136.NewDNSProvider(nameserver, tsigAlgorithm, tsigKey, tsigSecret)
|
||||
case "manual":
|
||||
provider, err = acme.NewDNSProviderManual()
|
||||
}
|
||||
|
@ -90,6 +106,10 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
|
|||
}
|
||||
|
||||
client.SetChallengeProvider(acme.DNS01, provider)
|
||||
|
||||
// --dns=foo indicates that the user specifically want to do a DNS challenge
|
||||
// infer that the user also wants to exclude all other challenges
|
||||
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
|
||||
}
|
||||
|
||||
return conf, acc, client
|
||||
|
@ -123,6 +143,47 @@ func saveCertRes(certRes acme.CertificateResource, conf *Configuration) {
|
|||
}
|
||||
}
|
||||
|
||||
func handleTOS(c *cli.Context, client *acme.Client, acc *Account) {
|
||||
// Check for a global accept override
|
||||
if c.GlobalBool("accept-tos") {
|
||||
err := client.AgreeToTOS()
|
||||
if err != nil {
|
||||
logger().Fatalf("Could not agree to TOS: %s", err.Error())
|
||||
}
|
||||
|
||||
acc.Save()
|
||||
return
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
logger().Printf("Please review the TOS at %s", acc.Registration.TosURL)
|
||||
|
||||
for {
|
||||
logger().Println("Do you accept the TOS? Y/n")
|
||||
text, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
logger().Fatalf("Could not read from console: %s", err.Error())
|
||||
}
|
||||
|
||||
text = strings.Trim(text, "\r\n")
|
||||
|
||||
if text == "n" {
|
||||
logger().Fatal("You did not accept the TOS. Unable to proceed.")
|
||||
}
|
||||
|
||||
if text == "Y" || text == "y" || text == "" {
|
||||
err = client.AgreeToTOS()
|
||||
if err != nil {
|
||||
logger().Fatalf("Could not agree to TOS: %s", err.Error())
|
||||
}
|
||||
acc.Save()
|
||||
break
|
||||
}
|
||||
|
||||
logger().Println("Your input was invalid. Please answer with one of Y/y, n or by pressing enter.")
|
||||
}
|
||||
}
|
||||
|
||||
func run(c *cli.Context) {
|
||||
conf, acc, client := setup(c)
|
||||
if acc.Registration == nil {
|
||||
|
@ -145,41 +206,16 @@ func run(c *cli.Context) {
|
|||
|
||||
}
|
||||
|
||||
// If the agreement URL is empty, the account still needs to accept the LE TOS.
|
||||
if acc.Registration.Body.Agreement == "" {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
logger().Printf("Please review the TOS at %s", acc.Registration.TosURL)
|
||||
|
||||
for {
|
||||
logger().Println("Do you accept the TOS? Y/n")
|
||||
text, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
logger().Fatalf("Could not read from console -> %s", err.Error())
|
||||
}
|
||||
|
||||
text = strings.Trim(text, "\r\n")
|
||||
|
||||
if text == "n" {
|
||||
logger().Fatal("You did not accept the TOS. Unable to proceed.")
|
||||
}
|
||||
|
||||
if text == "Y" || text == "y" || text == "" {
|
||||
err = client.AgreeToTOS()
|
||||
if err != nil {
|
||||
logger().Fatalf("Could not agree to tos -> %s", err)
|
||||
}
|
||||
acc.Save()
|
||||
break
|
||||
}
|
||||
|
||||
logger().Println("Your input was invalid. Please answer with one of Y/y, n or by pressing enter.")
|
||||
}
|
||||
handleTOS(c, client, acc)
|
||||
}
|
||||
|
||||
if len(c.GlobalStringSlice("domains")) == 0 {
|
||||
logger().Fatal("Please specify --domains or -d")
|
||||
}
|
||||
|
||||
cert, failures := client.ObtainCertificate(c.GlobalStringSlice("domains"), true, nil)
|
||||
cert, failures := client.ObtainCertificate(c.GlobalStringSlice("domains"), !c.Bool("no-bundle"), nil)
|
||||
if len(failures) > 0 {
|
||||
for k, v := range failures {
|
||||
logger().Printf("[%s] Could not obtain certificates\n\t%s", k, v.Error())
|
||||
|
@ -193,7 +229,7 @@ func run(c *cli.Context) {
|
|||
|
||||
err := checkFolder(conf.CertPath())
|
||||
if err != nil {
|
||||
logger().Fatalf("Cound not check/create path: %s", err.Error())
|
||||
logger().Fatalf("Could not check/create path: %s", err.Error())
|
||||
}
|
||||
|
||||
saveCertRes(cert, conf)
|
||||
|
@ -205,7 +241,7 @@ func revoke(c *cli.Context) {
|
|||
|
||||
err := checkFolder(conf.CertPath())
|
||||
if err != nil {
|
||||
logger().Fatalf("Cound not check/create path: %s", err.Error())
|
||||
logger().Fatalf("Could not check/create path: %s", err.Error())
|
||||
}
|
||||
|
||||
for _, domain := range c.GlobalStringSlice("domains") {
|
||||
|
@ -276,7 +312,7 @@ func renew(c *cli.Context) {
|
|||
|
||||
certRes.Certificate = certBytes
|
||||
|
||||
newCert, err := client.RenewCertificate(certRes, true)
|
||||
newCert, err := client.RenewCertificate(certRes, !c.Bool("no-bundle"))
|
||||
if err != nil {
|
||||
logger().Fatalf("%s", err.Error())
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
|
@ -20,11 +21,25 @@ func NewConfiguration(c *cli.Context) *Configuration {
|
|||
return &Configuration{context: c}
|
||||
}
|
||||
|
||||
// RsaBits returns the current set RSA bit length for private keys
|
||||
func (c *Configuration) RsaBits() int {
|
||||
return c.context.GlobalInt("rsa-key-size")
|
||||
// KeyType the type from which private keys should be generated
|
||||
func (c *Configuration) KeyType() (acme.KeyType, error) {
|
||||
switch strings.ToUpper(c.context.GlobalString("key-type")) {
|
||||
case "RSA2048":
|
||||
return acme.RSA2048, nil
|
||||
case "RSA4096":
|
||||
return acme.RSA4096, nil
|
||||
case "RSA8192":
|
||||
return acme.RSA8192, nil
|
||||
case "EC256":
|
||||
return acme.EC256, nil
|
||||
case "EC384":
|
||||
return acme.EC384, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Unsupported KeyType: %s", c.context.GlobalString("key-type"))
|
||||
}
|
||||
|
||||
// ExcludedSolvers is a list of solvers that are to be excluded.
|
||||
func (c *Configuration) ExcludedSolvers() (cc []acme.Challenge) {
|
||||
for _, s := range c.context.GlobalStringSlice("exclude") {
|
||||
cc = append(cc, acme.Challenge(s))
|
||||
|
@ -39,6 +54,7 @@ func (c *Configuration) ServerPath() string {
|
|||
return strings.Replace(srvStr, "/", string(os.PathSeparator), -1)
|
||||
}
|
||||
|
||||
// CertPath gets the path for certificates.
|
||||
func (c *Configuration) CertPath() string {
|
||||
return path.Join(c.context.GlobalString("path"), "certificates")
|
||||
}
|
||||
|
@ -54,7 +70,7 @@ func (c *Configuration) AccountPath(acc string) string {
|
|||
return path.Join(c.AccountsPath(), acc)
|
||||
}
|
||||
|
||||
// AccountPath returns the OS dependent path to the keys of a particular account
|
||||
// AccountKeysPath returns the OS dependent path to the keys of a particular account
|
||||
func (c *Configuration) AccountKeysPath(acc string) string {
|
||||
return path.Join(c.AccountPath(acc), "keys")
|
||||
}
|
||||
|
|
29
crypto.go
29
crypto.go
|
@ -1,21 +1,30 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
func generateRsaKey(length int, file string) (*rsa.PrivateKey, error) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, length)
|
||||
func generatePrivateKey(file string) (crypto.PrivateKey, error) {
|
||||
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pemKey := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}
|
||||
keyBytes, err := x509.MarshalECPrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pemKey := pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}
|
||||
|
||||
certOut, err := os.Create(file)
|
||||
if err != nil {
|
||||
|
@ -28,12 +37,20 @@ func generateRsaKey(length int, file string) (*rsa.PrivateKey, error) {
|
|||
return privateKey, nil
|
||||
}
|
||||
|
||||
func loadRsaKey(file string) (*rsa.PrivateKey, error) {
|
||||
func loadPrivateKey(file string) (crypto.PrivateKey, error) {
|
||||
keyBytes, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyBlock, _ := pem.Decode(keyBytes)
|
||||
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||||
|
||||
switch keyBlock.Type {
|
||||
case "RSA PRIVATE KEY":
|
||||
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||||
case "EC PRIVATE KEY":
|
||||
return x509.ParseECPrivateKey(keyBlock.Bytes)
|
||||
}
|
||||
|
||||
return nil, errors.New("Unknown private key type.")
|
||||
}
|
||||
|
|
212
providers/dns/cloudflare/cloudflare.go
Normal file
212
providers/dns/cloudflare/cloudflare.go
Normal file
|
@ -0,0 +1,212 @@
|
|||
// Package cloudflare implements a DNS provider for solving the DNS-01 challenge using cloudflare DNS.
|
||||
package cloudflare
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// CloudFlareAPIURL represents the API endpoint to call.
|
||||
// TODO: Unexport?
|
||||
const CloudFlareAPIURL = "https://api.cloudflare.com/client/v4"
|
||||
|
||||
// DNSProvider is an implementation of the acme.ChallengeProvider interface
|
||||
type DNSProvider struct {
|
||||
authEmail string
|
||||
authKey string
|
||||
}
|
||||
|
||||
// NewDNSProvider returns a DNSProvider instance with a configured cloudflare client.
|
||||
// Credentials can either be passed as arguments or through CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY env vars.
|
||||
func NewDNSProvider(cloudflareEmail, cloudflareKey string) (*DNSProvider, error) {
|
||||
if cloudflareEmail == "" || cloudflareKey == "" {
|
||||
cloudflareEmail, cloudflareKey = cloudflareEnvAuth()
|
||||
if cloudflareEmail == "" || cloudflareKey == "" {
|
||||
return nil, fmt.Errorf("CloudFlare credentials missing")
|
||||
}
|
||||
}
|
||||
|
||||
return &DNSProvider{
|
||||
authEmail: cloudflareEmail,
|
||||
authKey: cloudflareKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Present creates a TXT record to fulfil the dns-01 challenge
|
||||
func (c *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
|
||||
zoneID, err := c.getHostedZoneID(fqdn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rec := cloudFlareRecord{
|
||||
Type: "TXT",
|
||||
Name: acme.UnFqdn(fqdn),
|
||||
Content: value,
|
||||
TTL: 120,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(rec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.makeRequest("POST", fmt.Sprintf("/zones/%s/dns_records", zoneID), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters
|
||||
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
|
||||
|
||||
record, err := c.findTxtRecord(fqdn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.makeRequest("DELETE", fmt.Sprintf("/zones/%s/dns_records/%s", record.ZoneID, record.ID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
|
||||
// HostedZone represents a CloudFlare DNS zone
|
||||
type HostedZone struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
result, err := c.makeRequest("GET", "/zones?per_page=1000", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var zones []HostedZone
|
||||
err = json.Unmarshal(result, &zones)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var hostedZone HostedZone
|
||||
for _, zone := range zones {
|
||||
name := acme.ToFqdn(zone.Name)
|
||||
if strings.HasSuffix(fqdn, name) {
|
||||
if len(zone.Name) > len(hostedZone.Name) {
|
||||
hostedZone = zone
|
||||
}
|
||||
}
|
||||
}
|
||||
if hostedZone.ID == "" {
|
||||
return "", fmt.Errorf("No matching CloudFlare zone found for %s", fqdn)
|
||||
}
|
||||
|
||||
return hostedZone.ID, nil
|
||||
}
|
||||
|
||||
func (c *DNSProvider) findTxtRecord(fqdn string) (*cloudFlareRecord, error) {
|
||||
zoneID, err := c.getHostedZoneID(fqdn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := c.makeRequest("GET", fmt.Sprintf("/zones/%s/dns_records?per_page=1000", zoneID), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var records []cloudFlareRecord
|
||||
err = json.Unmarshal(result, &records)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, rec := range records {
|
||||
if rec.Name == acme.UnFqdn(fqdn) && rec.Type == "TXT" {
|
||||
return &rec, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("No existing record found for %s", fqdn)
|
||||
}
|
||||
|
||||
func (c *DNSProvider) makeRequest(method, uri string, body io.Reader) (json.RawMessage, error) {
|
||||
// APIError contains error details for failed requests
|
||||
type APIError struct {
|
||||
Code int `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// APIResponse represents a response from CloudFlare API
|
||||
type APIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Errors []*APIError `json:"errors"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, fmt.Sprintf("%s%s", CloudFlareAPIURL, uri), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("X-Auth-Email", c.authEmail)
|
||||
req.Header.Set("X-Auth-Key", c.authKey)
|
||||
//req.Header.Set("User-Agent", userAgent())
|
||||
|
||||
client := http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error querying API -> %v", err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
var r APIResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !r.Success {
|
||||
if len(r.Errors) > 0 {
|
||||
return nil, fmt.Errorf("API error -> %d: %s", r.Errors[0].Code, r.Errors[0].Message)
|
||||
}
|
||||
return nil, fmt.Errorf("API error")
|
||||
}
|
||||
|
||||
return r.Result, nil
|
||||
}
|
||||
|
||||
func cloudflareEnvAuth() (email, apiKey string) {
|
||||
email = os.Getenv("CLOUDFLARE_EMAIL")
|
||||
apiKey = os.Getenv("CLOUDFLARE_API_KEY")
|
||||
if len(email) == 0 || len(apiKey) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// cloudFlareRecord represents a CloudFlare DNS record
|
||||
type cloudFlareRecord struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
ID string `json:"id,omitempty"`
|
||||
TTL int `json:"ttl,omitempty"`
|
||||
ZoneID string `json:"zone_id,omitempty"`
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package acme
|
||||
package cloudflare
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
@ -29,26 +29,26 @@ func restoreCloudFlareEnv() {
|
|||
os.Setenv("CLOUDFLARE_API_KEY", cflareAPIKey)
|
||||
}
|
||||
|
||||
func TestNewDNSProviderCloudFlareValid(t *testing.T) {
|
||||
func TestNewDNSProviderValid(t *testing.T) {
|
||||
os.Setenv("CLOUDFLARE_EMAIL", "")
|
||||
os.Setenv("CLOUDFLARE_API_KEY", "")
|
||||
_, err := NewDNSProviderCloudFlare("123", "123")
|
||||
_, err := NewDNSProvider("123", "123")
|
||||
assert.NoError(t, err)
|
||||
restoreCloudFlareEnv()
|
||||
}
|
||||
|
||||
func TestNewDNSProviderCloudFlareValidEnv(t *testing.T) {
|
||||
func TestNewDNSProviderValidEnv(t *testing.T) {
|
||||
os.Setenv("CLOUDFLARE_EMAIL", "test@example.com")
|
||||
os.Setenv("CLOUDFLARE_API_KEY", "123")
|
||||
_, err := NewDNSProviderCloudFlare("", "")
|
||||
_, err := NewDNSProvider("", "")
|
||||
assert.NoError(t, err)
|
||||
restoreCloudFlareEnv()
|
||||
}
|
||||
|
||||
func TestNewDNSProviderCloudFlareMissingCredErr(t *testing.T) {
|
||||
func TestNewDNSProviderMissingCredErr(t *testing.T) {
|
||||
os.Setenv("CLOUDFLARE_EMAIL", "")
|
||||
os.Setenv("CLOUDFLARE_API_KEY", "")
|
||||
_, err := NewDNSProviderCloudFlare("", "")
|
||||
_, err := NewDNSProvider("", "")
|
||||
assert.EqualError(t, err, "CloudFlare credentials missing")
|
||||
restoreCloudFlareEnv()
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ func TestCloudFlarePresent(t *testing.T) {
|
|||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
provider, err := NewDNSProviderCloudFlare(cflareEmail, cflareAPIKey)
|
||||
provider, err := NewDNSProvider(cflareEmail, cflareAPIKey)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = provider.Present(cflareDomain, "", "123d==")
|
||||
|
@ -70,9 +70,9 @@ func TestCloudFlareCleanUp(t *testing.T) {
|
|||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 1)
|
||||
time.Sleep(time.Second * 2)
|
||||
|
||||
provider, err := NewDNSProviderCloudFlare(cflareEmail, cflareAPIKey)
|
||||
provider, err := NewDNSProvider(cflareEmail, cflareAPIKey)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = provider.CleanUp(cflareDomain, "", "123d==")
|
|
@ -1,4 +1,5 @@
|
|||
package acme
|
||||
// Package digitalocean implements a DNS provider for solving the DNS-01 challenge using digitalocean DNS.
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -6,28 +7,30 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// DNSProviderDigitalOcean is an implementation of the DNSProvider interface
|
||||
// DNSProvider is an implementation of the acme.ChallengeProvider interface
|
||||
// that uses DigitalOcean's REST API to manage TXT records for a domain.
|
||||
type DNSProviderDigitalOcean struct {
|
||||
type DNSProvider struct {
|
||||
apiAuthToken string
|
||||
recordIDs map[string]int
|
||||
recordIDsMu sync.Mutex
|
||||
}
|
||||
|
||||
// NewDNSProviderDigitalOcean returns a new DNSProviderDigitalOcean instance.
|
||||
// NewDNSProvider returns a new DNSProvider instance.
|
||||
// apiAuthToken is the personal access token created in the DigitalOcean account
|
||||
// control panel, and it will be sent in bearer authorization headers.
|
||||
func NewDNSProviderDigitalOcean(apiAuthToken string) (*DNSProviderDigitalOcean, error) {
|
||||
return &DNSProviderDigitalOcean{
|
||||
func NewDNSProvider(apiAuthToken string) (*DNSProvider, error) {
|
||||
return &DNSProvider{
|
||||
apiAuthToken: apiAuthToken,
|
||||
recordIDs: make(map[string]int),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Present creates a TXT record using the specified parameters
|
||||
func (d *DNSProviderDigitalOcean) Present(domain, token, keyAuth string) error {
|
||||
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
// txtRecordRequest represents the request body to DO's API to make a TXT record
|
||||
type txtRecordRequest struct {
|
||||
RecordType string `json:"type"`
|
||||
|
@ -45,7 +48,7 @@ func (d *DNSProviderDigitalOcean) Present(domain, token, keyAuth string) error {
|
|||
} `json:"domain_record"`
|
||||
}
|
||||
|
||||
fqdn, value, _ := DNS01Record(domain, keyAuth)
|
||||
fqdn, value, _ := acme.DNS01Record(domain, keyAuth)
|
||||
|
||||
reqURL := fmt.Sprintf("%s/v2/domains/%s/records", digitalOceanBaseURL, domain)
|
||||
reqData := txtRecordRequest{RecordType: "TXT", Name: fqdn, Data: value}
|
||||
|
@ -87,8 +90,8 @@ func (d *DNSProviderDigitalOcean) Present(domain, token, keyAuth string) error {
|
|||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters
|
||||
func (d *DNSProviderDigitalOcean) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, _, _ := DNS01Record(domain, keyAuth)
|
||||
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
|
||||
|
||||
// get the record's unique ID from when we created it
|
||||
d.recordIDsMu.Lock()
|
|
@ -1,4 +1,4 @@
|
|||
package acme
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -53,7 +53,7 @@ func TestDigitalOceanPresent(t *testing.T) {
|
|||
defer mock.Close()
|
||||
digitalOceanBaseURL = mock.URL
|
||||
|
||||
doprov, err := NewDNSProviderDigitalOcean(fakeDigitalOceanAuth)
|
||||
doprov, err := NewDNSProvider(fakeDigitalOceanAuth)
|
||||
if doprov == nil {
|
||||
t.Fatal("Expected non-nil DigitalOcean provider, but was nil")
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ func TestDigitalOceanCleanUp(t *testing.T) {
|
|||
defer mock.Close()
|
||||
digitalOceanBaseURL = mock.URL
|
||||
|
||||
doprov, err := NewDNSProviderDigitalOcean(fakeDigitalOceanAuth)
|
||||
doprov, err := NewDNSProvider(fakeDigitalOceanAuth)
|
||||
if doprov == nil {
|
||||
t.Fatal("Expected non-nil DigitalOcean provider, but was nil")
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
package acme
|
||||
// Package dnsimple implements a DNS provider for solving the DNS-01 challenge using dnsimple DNS.
|
||||
package dnsimple
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -6,34 +7,35 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/weppos/dnsimple-go/dnsimple"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// DNSProviderDNSimple is an implementation of the DNSProvider interface.
|
||||
type DNSProviderDNSimple struct {
|
||||
// DNSProvider is an implementation of the acme.ChallengeProvider interface.
|
||||
type DNSProvider struct {
|
||||
client *dnsimple.Client
|
||||
}
|
||||
|
||||
// NewDNSProviderDNSimple returns a DNSProviderDNSimple instance with a configured dnsimple client.
|
||||
// NewDNSProvider returns a DNSProvider instance with a configured dnsimple client.
|
||||
// Authentication is either done using the passed credentials or - when empty - using the environment
|
||||
// variables DNSIMPLE_EMAIL and DNSIMPLE_API_KEY.
|
||||
func NewDNSProviderDNSimple(dnsimpleEmail, dnsimpleApiKey string) (*DNSProviderDNSimple, error) {
|
||||
if dnsimpleEmail == "" || dnsimpleApiKey == "" {
|
||||
dnsimpleEmail, dnsimpleApiKey = dnsimpleEnvAuth()
|
||||
if dnsimpleEmail == "" || dnsimpleApiKey == "" {
|
||||
func NewDNSProvider(dnsimpleEmail, dnsimpleAPIKey string) (*DNSProvider, error) {
|
||||
if dnsimpleEmail == "" || dnsimpleAPIKey == "" {
|
||||
dnsimpleEmail, dnsimpleAPIKey = dnsimpleEnvAuth()
|
||||
if dnsimpleEmail == "" || dnsimpleAPIKey == "" {
|
||||
return nil, fmt.Errorf("DNSimple credentials missing")
|
||||
}
|
||||
}
|
||||
|
||||
c := &DNSProviderDNSimple{
|
||||
client: dnsimple.NewClient(dnsimpleApiKey, dnsimpleEmail),
|
||||
c := &DNSProvider{
|
||||
client: dnsimple.NewClient(dnsimpleAPIKey, dnsimpleEmail),
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Present creates a TXT record to fulfil the dns-01 challenge.
|
||||
func (c *DNSProviderDNSimple) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value, ttl := DNS01Record(domain, keyAuth)
|
||||
func (c *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
||||
|
||||
zoneID, zoneName, err := c.getHostedZone(domain)
|
||||
if err != nil {
|
||||
|
@ -50,8 +52,8 @@ func (c *DNSProviderDNSimple) Present(domain, token, keyAuth string) error {
|
|||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters.
|
||||
func (c *DNSProviderDNSimple) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, _, _ := DNS01Record(domain, keyAuth)
|
||||
func (c *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, _, _ := acme.DNS01Record(domain, keyAuth)
|
||||
|
||||
records, err := c.findTxtRecords(domain, fqdn)
|
||||
if err != nil {
|
||||
|
@ -67,7 +69,7 @@ func (c *DNSProviderDNSimple) CleanUp(domain, token, keyAuth string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *DNSProviderDNSimple) getHostedZone(domain string) (string, string, error) {
|
||||
func (c *DNSProvider) getHostedZone(domain string) (string, string, error) {
|
||||
domains, _, err := c.client.Domains.List()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("DNSimple API call failed: %v", err)
|
||||
|
@ -88,7 +90,7 @@ func (c *DNSProviderDNSimple) getHostedZone(domain string) (string, string, erro
|
|||
return fmt.Sprintf("%v", hostedDomain.Id), hostedDomain.Name, nil
|
||||
}
|
||||
|
||||
func (c *DNSProviderDNSimple) findTxtRecords(domain, fqdn string) ([]dnsimple.Record, error) {
|
||||
func (c *DNSProvider) findTxtRecords(domain, fqdn string) ([]dnsimple.Record, error) {
|
||||
zoneID, zoneName, err := c.getHostedZone(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -110,7 +112,7 @@ func (c *DNSProviderDNSimple) findTxtRecords(domain, fqdn string) ([]dnsimple.Re
|
|||
return records, nil
|
||||
}
|
||||
|
||||
func (c *DNSProviderDNSimple) newTxtRecord(zone, fqdn, value string, ttl int) *dnsimple.Record {
|
||||
func (c *DNSProvider) newTxtRecord(zone, fqdn, value string, ttl int) *dnsimple.Record {
|
||||
name := c.extractRecordName(fqdn, zone)
|
||||
|
||||
return &dnsimple.Record{
|
||||
|
@ -121,8 +123,8 @@ func (c *DNSProviderDNSimple) newTxtRecord(zone, fqdn, value string, ttl int) *d
|
|||
}
|
||||
}
|
||||
|
||||
func (c *DNSProviderDNSimple) extractRecordName(fqdn, domain string) string {
|
||||
name := unFqdn(fqdn)
|
||||
func (c *DNSProvider) extractRecordName(fqdn, domain string) string {
|
||||
name := acme.UnFqdn(fqdn)
|
||||
if idx := strings.Index(name, "."+domain); idx != -1 {
|
||||
return name[:idx]
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package acme
|
||||
package dnsimple
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
@ -29,25 +29,25 @@ func restoreDNSimpleEnv() {
|
|||
os.Setenv("DNSIMPLE_API_KEY", dnsimpleAPIKey)
|
||||
}
|
||||
|
||||
func TestNewDNSProviderDNSimpleValid(t *testing.T) {
|
||||
func TestNewDNSProviderValid(t *testing.T) {
|
||||
os.Setenv("DNSIMPLE_EMAIL", "")
|
||||
os.Setenv("DNSIMPLE_API_KEY", "")
|
||||
_, err := NewDNSProviderDNSimple("example@example.com", "123")
|
||||
_, err := NewDNSProvider("example@example.com", "123")
|
||||
assert.NoError(t, err)
|
||||
restoreDNSimpleEnv()
|
||||
}
|
||||
func TestNewDNSProviderDNSimpleValidEnv(t *testing.T) {
|
||||
func TestNewDNSProviderValidEnv(t *testing.T) {
|
||||
os.Setenv("DNSIMPLE_EMAIL", "example@example.com")
|
||||
os.Setenv("DNSIMPLE_API_KEY", "123")
|
||||
_, err := NewDNSProviderDNSimple("", "")
|
||||
_, err := NewDNSProvider("", "")
|
||||
assert.NoError(t, err)
|
||||
restoreDNSimpleEnv()
|
||||
}
|
||||
|
||||
func TestNewDNSProviderDNSimpleMissingCredErr(t *testing.T) {
|
||||
func TestNewDNSProviderMissingCredErr(t *testing.T) {
|
||||
os.Setenv("DNSIMPLE_EMAIL", "")
|
||||
os.Setenv("DNSIMPLE_API_KEY", "")
|
||||
_, err := NewDNSProviderDNSimple("", "")
|
||||
_, err := NewDNSProvider("", "")
|
||||
assert.EqualError(t, err, "DNSimple credentials missing")
|
||||
restoreDNSimpleEnv()
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ func TestLiveDNSimplePresent(t *testing.T) {
|
|||
t.Skip("skipping live test")
|
||||
}
|
||||
|
||||
provider, err := NewDNSProviderDNSimple(dnsimpleEmail, dnsimpleAPIKey)
|
||||
provider, err := NewDNSProvider(dnsimpleEmail, dnsimpleAPIKey)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = provider.Present(dnsimpleDomain, "", "123d==")
|
||||
|
@ -71,7 +71,7 @@ func TestLiveDNSimpleCleanUp(t *testing.T) {
|
|||
|
||||
time.Sleep(time.Second * 1)
|
||||
|
||||
provider, err := NewDNSProviderDNSimple(cflareEmail, cflareAPIKey)
|
||||
provider, err := NewDNSProvider(dnsimpleEmail, dnsimpleAPIKey)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = provider.CleanUp(dnsimpleDomain, "", "123d==")
|
108
providers/dns/rfc2136/rfc2136.go
Normal file
108
providers/dns/rfc2136/rfc2136.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
// Package rfc2136 implements a DNS provider for solving the DNS-01 challenge using the rfc2136 dynamic update.
|
||||
package rfc2136
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// DNSProvider is an implementation of the acme.ChallengeProvider interface that
|
||||
// uses dynamic DNS updates (RFC 2136) to create TXT records on a nameserver.
|
||||
type DNSProvider struct {
|
||||
nameserver string
|
||||
tsigAlgorithm string
|
||||
tsigKey string
|
||||
tsigSecret string
|
||||
}
|
||||
|
||||
// NewDNSProvider returns a new DNSProvider instance.
|
||||
// To disable TSIG authentication 'tsigAlgorithm, 'tsigKey' and 'tsigSecret' must be set to the empty string.
|
||||
// 'nameserver' must be a network address in the the form "host" or "host:port".
|
||||
func NewDNSProvider(nameserver, tsigAlgorithm, tsigKey, tsigSecret string) (*DNSProvider, error) {
|
||||
// Append the default DNS port if none is specified.
|
||||
if _, _, err := net.SplitHostPort(nameserver); err != nil {
|
||||
if strings.Contains(err.Error(), "missing port") {
|
||||
nameserver = net.JoinHostPort(nameserver, "53")
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
d := &DNSProvider{
|
||||
nameserver: nameserver,
|
||||
}
|
||||
if tsigAlgorithm == "" {
|
||||
tsigAlgorithm = dns.HmacMD5
|
||||
}
|
||||
d.tsigAlgorithm = tsigAlgorithm
|
||||
if len(tsigKey) > 0 && len(tsigSecret) > 0 {
|
||||
d.tsigKey = tsigKey
|
||||
d.tsigSecret = tsigSecret
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Present creates a TXT record using the specified parameters
|
||||
func (r *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
||||
return r.changeRecord("INSERT", fqdn, value, ttl)
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters
|
||||
func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
||||
return r.changeRecord("REMOVE", fqdn, value, ttl)
|
||||
}
|
||||
|
||||
func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
|
||||
// Find the zone for the given fqdn
|
||||
zone, err := acme.FindZoneByFqdn(fqdn, r.nameserver)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create RR
|
||||
rr := new(dns.TXT)
|
||||
rr.Hdr = dns.RR_Header{Name: fqdn, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: uint32(ttl)}
|
||||
rr.Txt = []string{value}
|
||||
rrs := []dns.RR{rr}
|
||||
|
||||
// Create dynamic update packet
|
||||
m := new(dns.Msg)
|
||||
m.SetUpdate(zone)
|
||||
switch action {
|
||||
case "INSERT":
|
||||
// Always remove old challenge left over from who knows what.
|
||||
m.RemoveRRset(rrs)
|
||||
m.Insert(rrs)
|
||||
case "REMOVE":
|
||||
m.Remove(rrs)
|
||||
default:
|
||||
return fmt.Errorf("Unexpected action: %s", action)
|
||||
}
|
||||
|
||||
// Setup client
|
||||
c := new(dns.Client)
|
||||
c.SingleInflight = true
|
||||
// TSIG authentication / msg signing
|
||||
if len(r.tsigKey) > 0 && len(r.tsigSecret) > 0 {
|
||||
m.SetTsig(dns.Fqdn(r.tsigKey), r.tsigAlgorithm, 300, time.Now().Unix())
|
||||
c.TsigSecret = map[string]string{dns.Fqdn(r.tsigKey): r.tsigSecret}
|
||||
}
|
||||
|
||||
// Send the query
|
||||
reply, _, err := c.Exchange(m, r.nameserver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("DNS update failed: %v", err)
|
||||
}
|
||||
if reply != nil && reply.Rcode != dns.RcodeSuccess {
|
||||
return fmt.Errorf("DNS update failed. Server replied: %s", dns.RcodeToString[reply.Rcode])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
package acme
|
||||
package rfc2136
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -9,6 +10,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -25,6 +27,7 @@ var (
|
|||
var reqChan = make(chan *dns.Msg, 10)
|
||||
|
||||
func TestRFC2136CanaryLocalTestServer(t *testing.T) {
|
||||
acme.ClearFqdnCache()
|
||||
dns.HandleFunc("example.com.", serverHandlerHello)
|
||||
defer dns.HandleRemove("example.com.")
|
||||
|
||||
|
@ -48,6 +51,7 @@ func TestRFC2136CanaryLocalTestServer(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRFC2136ServerSuccess(t *testing.T) {
|
||||
acme.ClearFqdnCache()
|
||||
dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess)
|
||||
defer dns.HandleRemove(rfc2136TestZone)
|
||||
|
||||
|
@ -57,9 +61,9 @@ func TestRFC2136ServerSuccess(t *testing.T) {
|
|||
}
|
||||
defer server.Shutdown()
|
||||
|
||||
provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, "", "")
|
||||
provider, err := NewDNSProvider(addrstr, "", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err)
|
||||
t.Fatalf("Expected NewDNSProvider() to return no error but the error was -> %v", err)
|
||||
}
|
||||
if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err != nil {
|
||||
t.Errorf("Expected Present() to return no error but the error was -> %v", err)
|
||||
|
@ -67,6 +71,7 @@ func TestRFC2136ServerSuccess(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRFC2136ServerError(t *testing.T) {
|
||||
acme.ClearFqdnCache()
|
||||
dns.HandleFunc(rfc2136TestZone, serverHandlerReturnErr)
|
||||
defer dns.HandleRemove(rfc2136TestZone)
|
||||
|
||||
|
@ -76,9 +81,9 @@ func TestRFC2136ServerError(t *testing.T) {
|
|||
}
|
||||
defer server.Shutdown()
|
||||
|
||||
provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, "", "")
|
||||
provider, err := NewDNSProvider(addrstr, "", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err)
|
||||
t.Fatalf("Expected NewDNSProvider() to return no error but the error was -> %v", err)
|
||||
}
|
||||
if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err == nil {
|
||||
t.Errorf("Expected Present() to return an error but it did not.")
|
||||
|
@ -88,6 +93,7 @@ func TestRFC2136ServerError(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRFC2136TsigClient(t *testing.T) {
|
||||
acme.ClearFqdnCache()
|
||||
dns.HandleFunc(rfc2136TestZone, serverHandlerReturnSuccess)
|
||||
defer dns.HandleRemove(rfc2136TestZone)
|
||||
|
||||
|
@ -97,9 +103,9 @@ func TestRFC2136TsigClient(t *testing.T) {
|
|||
}
|
||||
defer server.Shutdown()
|
||||
|
||||
provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, rfc2136TestTsigKey, rfc2136TestTsigSecret)
|
||||
provider, err := NewDNSProvider(addrstr, "", rfc2136TestTsigKey, rfc2136TestTsigSecret)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err)
|
||||
t.Fatalf("Expected NewDNSProvider() to return no error but the error was -> %v", err)
|
||||
}
|
||||
if err := provider.Present(rfc2136TestDomain, "", rfc2136TestKeyAuth); err != nil {
|
||||
t.Errorf("Expected Present() to return no error but the error was -> %v", err)
|
||||
|
@ -107,6 +113,7 @@ func TestRFC2136TsigClient(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRFC2136ValidUpdatePacket(t *testing.T) {
|
||||
acme.ClearFqdnCache()
|
||||
dns.HandleFunc(rfc2136TestZone, serverHandlerPassBackRequest)
|
||||
defer dns.HandleRemove(rfc2136TestZone)
|
||||
|
||||
|
@ -116,18 +123,11 @@ func TestRFC2136ValidUpdatePacket(t *testing.T) {
|
|||
}
|
||||
defer server.Shutdown()
|
||||
|
||||
rr := new(dns.TXT)
|
||||
rr.Hdr = dns.RR_Header{
|
||||
Name: rfc2136TestFqdn,
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(rfc2136TestTTL),
|
||||
}
|
||||
rr.Txt = []string{rfc2136TestValue}
|
||||
rrs := make([]dns.RR, 1)
|
||||
rrs[0] = rr
|
||||
txtRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN TXT %s", rfc2136TestFqdn, rfc2136TestTTL, rfc2136TestValue))
|
||||
rrs := []dns.RR{txtRR}
|
||||
m := new(dns.Msg)
|
||||
m.SetUpdate(dns.Fqdn(rfc2136TestZone))
|
||||
m.SetUpdate(rfc2136TestZone)
|
||||
m.RemoveRRset(rrs)
|
||||
m.Insert(rrs)
|
||||
expectstr := m.String()
|
||||
expect, err := m.Pack()
|
||||
|
@ -135,9 +135,9 @@ func TestRFC2136ValidUpdatePacket(t *testing.T) {
|
|||
t.Fatalf("Error packing expect msg: %v", err)
|
||||
}
|
||||
|
||||
provider, err := NewDNSProviderRFC2136(addrstr, rfc2136TestZone, "", "")
|
||||
provider, err := NewDNSProvider(addrstr, "", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected NewDNSProviderRFC2136() to return no error but the error was -> %v", err)
|
||||
t.Fatalf("Expected NewDNSProvider() to return no error but the error was -> %v", err)
|
||||
}
|
||||
|
||||
if err := provider.Present(rfc2136TestDomain, "", "1234d=="); err != nil {
|
||||
|
@ -198,6 +198,11 @@ func serverHandlerHello(w dns.ResponseWriter, req *dns.Msg) {
|
|||
func serverHandlerReturnSuccess(w dns.ResponseWriter, req *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
if req.Opcode == dns.OpcodeQuery && req.Question[0].Qtype == dns.TypeSOA && req.Question[0].Qclass == dns.ClassINET {
|
||||
// Return SOA to appease findZoneByFqdn()
|
||||
soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", rfc2136TestZone, rfc2136TestTTL, rfc2136TestZone, rfc2136TestZone))
|
||||
m.Answer = []dns.RR{soaRR}
|
||||
}
|
||||
|
||||
if t := req.IsTsig(); t != nil {
|
||||
if w.TsigStatus() == nil {
|
||||
|
@ -218,6 +223,11 @@ func serverHandlerReturnErr(w dns.ResponseWriter, req *dns.Msg) {
|
|||
func serverHandlerPassBackRequest(w dns.ResponseWriter, req *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
if req.Opcode == dns.OpcodeQuery && req.Question[0].Qtype == dns.TypeSOA && req.Question[0].Qclass == dns.ClassINET {
|
||||
// Return SOA to appease findZoneByFqdn()
|
||||
soaRR, _ := dns.NewRR(fmt.Sprintf("%s %d IN SOA ns1.%s admin.%s 2016022801 28800 7200 2419200 1200", rfc2136TestZone, rfc2136TestTTL, rfc2136TestZone, rfc2136TestZone))
|
||||
m.Answer = []dns.RR{soaRR}
|
||||
}
|
||||
|
||||
if t := req.IsTsig(); t != nil {
|
||||
if w.TsigStatus() == nil {
|
||||
|
@ -227,5 +237,8 @@ func serverHandlerPassBackRequest(w dns.ResponseWriter, req *dns.Msg) {
|
|||
}
|
||||
|
||||
w.WriteMsg(m)
|
||||
reqChan <- req
|
||||
if req.Opcode != dns.OpcodeQuery || req.Question[0].Qtype != dns.TypeSOA || req.Question[0].Qclass != dns.ClassINET {
|
||||
// Only talk back when it is not the SOA RR.
|
||||
reqChan <- req
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
package acme
|
||||
// Package route53 implements a DNS provider for solving the DNS-01 challenge using route53 DNS.
|
||||
package route53
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -7,20 +8,21 @@ import (
|
|||
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
"github.com/mitchellh/goamz/route53"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// DNSProviderRoute53 is an implementation of the DNSProvider interface
|
||||
type DNSProviderRoute53 struct {
|
||||
// DNSProvider is an implementation of the acme.ChallengeProvider interface
|
||||
type DNSProvider struct {
|
||||
client *route53.Route53
|
||||
}
|
||||
|
||||
// NewDNSProviderRoute53 returns a DNSProviderRoute53 instance with a configured route53 client.
|
||||
// NewDNSProvider returns a DNSProvider instance with a configured route53 client.
|
||||
// Authentication is either done using the passed credentials or - when empty - falling back to
|
||||
// the customary AWS credential mechanisms, including the file refernced by $AWS_CREDENTIAL_FILE
|
||||
// the customary AWS credential mechanisms, including the file referenced by $AWS_CREDENTIAL_FILE
|
||||
// (defaulting to $HOME/.aws/credentials) optionally scoped to $AWS_PROFILE, credentials
|
||||
// supplied by the environment variables AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY [ + AWS_SECURITY_TOKEN ],
|
||||
// and finally credentials available via the EC2 instance metadata service.
|
||||
func NewDNSProviderRoute53(awsAccessKey, awsSecretKey, awsRegionName string) (*DNSProviderRoute53, error) {
|
||||
func NewDNSProvider(awsAccessKey, awsSecretKey, awsRegionName string) (*DNSProvider, error) {
|
||||
region, ok := aws.Regions[awsRegionName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Invalid AWS region name %s", awsRegionName)
|
||||
|
@ -32,35 +34,36 @@ func NewDNSProviderRoute53(awsAccessKey, awsSecretKey, awsRegionName string) (*D
|
|||
// - uses AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY and optionally AWS_SECURITY_TOKEN, if provided
|
||||
// - uses EC2 instance metadata credentials (http://169.254.169.254/latest/meta-data/…), if available
|
||||
// ...and otherwise returns an error
|
||||
if auth, err := aws.GetAuth(awsAccessKey, awsSecretKey); err != nil {
|
||||
auth, err := aws.GetAuth(awsAccessKey, awsSecretKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
client := route53.New(auth, region)
|
||||
return &DNSProviderRoute53{client: client}, nil
|
||||
}
|
||||
|
||||
client := route53.New(auth, region)
|
||||
return &DNSProvider{client: client}, nil
|
||||
}
|
||||
|
||||
// Present creates a TXT record using the specified parameters
|
||||
func (r *DNSProviderRoute53) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value, ttl := DNS01Record(domain, keyAuth)
|
||||
func (r *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
||||
value = `"` + value + `"`
|
||||
return r.changeRecord("UPSERT", fqdn, value, ttl)
|
||||
}
|
||||
|
||||
// CleanUp removes the TXT record matching the specified parameters
|
||||
func (r *DNSProviderRoute53) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, value, ttl := DNS01Record(domain, keyAuth)
|
||||
func (r *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
fqdn, value, ttl := acme.DNS01Record(domain, keyAuth)
|
||||
value = `"` + value + `"`
|
||||
return r.changeRecord("DELETE", fqdn, value, ttl)
|
||||
}
|
||||
|
||||
func (r *DNSProviderRoute53) changeRecord(action, fqdn, value string, ttl int) error {
|
||||
func (r *DNSProvider) changeRecord(action, fqdn, value string, ttl int) error {
|
||||
hostedZoneID, err := r.getHostedZoneID(fqdn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recordSet := newTXTRecordSet(fqdn, value, ttl)
|
||||
update := route53.Change{action, recordSet}
|
||||
update := route53.Change{Action: action, Record: recordSet}
|
||||
changes := []route53.Change{update}
|
||||
req := route53.ChangeResourceRecordSetsRequest{Comment: "Created by Lego", Changes: changes}
|
||||
resp, err := r.client.ChangeResourceRecordSets(hostedZoneID, &req)
|
||||
|
@ -68,7 +71,7 @@ func (r *DNSProviderRoute53) changeRecord(action, fqdn, value string, ttl int) e
|
|||
return err
|
||||
}
|
||||
|
||||
return waitFor(90, func() (bool, error) {
|
||||
return acme.WaitFor(90*time.Second, 5*time.Second, func() (bool, error) {
|
||||
status, err := r.client.GetChange(resp.ChangeInfo.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
@ -80,7 +83,7 @@ func (r *DNSProviderRoute53) changeRecord(action, fqdn, value string, ttl int) e
|
|||
})
|
||||
}
|
||||
|
||||
func (r *DNSProviderRoute53) getHostedZoneID(fqdn string) (string, error) {
|
||||
func (r *DNSProvider) getHostedZoneID(fqdn string) (string, error) {
|
||||
zones := []route53.HostedZone{}
|
||||
zoneResp, err := r.client.ListHostedZones("", 0)
|
||||
if err != nil {
|
|
@ -1,8 +1,10 @@
|
|||
package acme
|
||||
package route53
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
"github.com/mitchellh/goamz/route53"
|
||||
|
@ -63,9 +65,9 @@ var GetChangeAnswer = `<?xml version="1.0" encoding="UTF-8"?>
|
|||
</GetChangeResponse>`
|
||||
|
||||
var serverResponseMap = testutil.ResponseMap{
|
||||
"/2013-04-01/hostedzone/": testutil.Response{200, nil, ListHostedZonesAnswer},
|
||||
"/2013-04-01/hostedzone/Z2K123214213123/rrset": testutil.Response{200, nil, ChangeResourceRecordSetsAnswer},
|
||||
"/2013-04-01/change/asdf": testutil.Response{200, nil, GetChangeAnswer},
|
||||
"/2013-04-01/hostedzone/": testutil.Response{Status: 200, Headers: nil, Body: ListHostedZonesAnswer},
|
||||
"/2013-04-01/hostedzone/Z2K123214213123/rrset": testutil.Response{Status: 200, Headers: nil, Body: ChangeResourceRecordSetsAnswer},
|
||||
"/2013-04-01/change/asdf": testutil.Response{Status: 200, Headers: nil, Body: GetChangeAnswer},
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -89,40 +91,49 @@ func makeRoute53TestServer() *testutil.HTTPServer {
|
|||
return testServer
|
||||
}
|
||||
|
||||
func makeRoute53Provider(server *testutil.HTTPServer) *DNSProviderRoute53 {
|
||||
auth := aws.Auth{"abc", "123", ""}
|
||||
func makeRoute53Provider(server *testutil.HTTPServer) *DNSProvider {
|
||||
auth := aws.Auth{AccessKey: "abc", SecretKey: "123", Token: ""}
|
||||
client := route53.NewWithClient(auth, aws.Region{Route53Endpoint: server.URL}, testutil.DefaultClient)
|
||||
return &DNSProviderRoute53{client: client}
|
||||
return &DNSProvider{client: client}
|
||||
}
|
||||
|
||||
func TestNewDNSProviderRoute53Valid(t *testing.T) {
|
||||
func TestNewDNSProviderValid(t *testing.T) {
|
||||
os.Setenv("AWS_ACCESS_KEY_ID", "")
|
||||
os.Setenv("AWS_SECRET_ACCESS_KEY", "")
|
||||
_, err := NewDNSProviderRoute53("123", "123", "us-east-1")
|
||||
_, err := NewDNSProvider("123", "123", "us-east-1")
|
||||
assert.NoError(t, err)
|
||||
restoreRoute53Env()
|
||||
}
|
||||
|
||||
func TestNewDNSProviderRoute53ValidEnv(t *testing.T) {
|
||||
func TestNewDNSProviderValidEnv(t *testing.T) {
|
||||
os.Setenv("AWS_ACCESS_KEY_ID", "123")
|
||||
os.Setenv("AWS_SECRET_ACCESS_KEY", "123")
|
||||
_, err := NewDNSProviderRoute53("", "", "us-east-1")
|
||||
_, err := NewDNSProvider("", "", "us-east-1")
|
||||
assert.NoError(t, err)
|
||||
restoreRoute53Env()
|
||||
}
|
||||
|
||||
func TestNewDNSProviderRoute53MissingAuthErr(t *testing.T) {
|
||||
func TestNewDNSProviderMissingAuthErr(t *testing.T) {
|
||||
os.Setenv("AWS_ACCESS_KEY_ID", "")
|
||||
os.Setenv("AWS_SECRET_ACCESS_KEY", "")
|
||||
os.Setenv("AWS_CREDENTIAL_FILE", "") // in case test machine has this variable set
|
||||
os.Setenv("HOME", "/") // in case test machine has ~/.aws/credentials
|
||||
_, err := NewDNSProviderRoute53("", "", "us-east-1")
|
||||
|
||||
// The default AWS HTTP client retries three times with a deadline of 10 seconds.
|
||||
// Replace the default HTTP client with one that does not retry and has a low timeout.
|
||||
awsClient := aws.RetryingClient
|
||||
aws.RetryingClient = &http.Client{Timeout: time.Millisecond}
|
||||
|
||||
_, err := NewDNSProvider("", "", "us-east-1")
|
||||
assert.EqualError(t, err, "No valid AWS authentication found")
|
||||
restoreRoute53Env()
|
||||
|
||||
// restore default AWS HTTP client
|
||||
aws.RetryingClient = awsClient
|
||||
}
|
||||
|
||||
func TestNewDNSProviderRoute53InvalidRegionErr(t *testing.T) {
|
||||
_, err := NewDNSProviderRoute53("123", "123", "us-east-3")
|
||||
func TestNewDNSProviderInvalidRegionErr(t *testing.T) {
|
||||
_, err := NewDNSProvider("123", "123", "us-east-3")
|
||||
assert.EqualError(t, err, "Invalid AWS region name us-east-3")
|
||||
}
|
||||
|
Loading…
Reference in a new issue