Add ACME DNS-01 provider for Akamai FastDNS

This commit is contained in:
Tom Wieczorek 2018-02-21 10:49:44 +01:00
parent 07cfb982aa
commit f681f5a6b1
No known key found for this signature in database
GPG Key ID: FE33A2282371E831
8 changed files with 875 additions and 0 deletions

View File

@ -142,6 +142,22 @@ cloudflare:
key: api-key
```
##### Akamai FastDNS
```yaml
akamai:
serviceConsumerDomain: akab-tho6xie2aiteip8p-poith5aej0ughaba.luna.akamaiapis.net
clientTokenSecretRef:
name: akamai-dns
key: clientToken
clientSecretSecretRef:
name: akamai-dns
key: clientSecret
accessTokenSecretRef:
name: akamai-dns
key: accessToken
```
## CA Configuration
CA Issuers issue certificates signed from a X509 signing keypair, stored in a

View File

@ -111,12 +111,22 @@ type ACMEIssuerDNS01Config struct {
type ACMEIssuerDNS01Provider struct {
Name string `json:"name"`
Akamai *ACMEIssuerDNS01ProviderAkamai `json:"akamai,omitempty"`
CloudDNS *ACMEIssuerDNS01ProviderCloudDNS `json:"clouddns,omitempty"`
Cloudflare *ACMEIssuerDNS01ProviderCloudflare `json:"cloudflare,omitempty"`
Route53 *ACMEIssuerDNS01ProviderRoute53 `json:"route53,omitempty"`
AzureDNS *ACMEIssuerDNS01ProviderAzureDNS `json:"azuredns,omitempty"`
}
// ACMEIssuerDNS01ProviderAkamai is a structure containing the DNS
// configuration for Akamai DNS—Zone Record Management API
type ACMEIssuerDNS01ProviderAkamai struct {
ServiceConsumerDomain string `json:"serviceConsumerDomain"`
ClientToken SecretKeySelector `json:"clientTokenSecretRef"`
ClientSecret SecretKeySelector `json:"clientSecretSecretRef"`
AccessToken SecretKeySelector `json:"accessTokenSecretRef"`
}
// ACMEIssuerDNS01ProviderCloudDNS is a structure containing the DNS
// configuration for Google Cloud DNS
type ACMEIssuerDNS01ProviderCloudDNS struct {

View File

@ -204,6 +204,15 @@ func (in *ACMEIssuerDNS01Config) DeepCopy() *ACMEIssuerDNS01Config {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ACMEIssuerDNS01Provider) DeepCopyInto(out *ACMEIssuerDNS01Provider) {
*out = *in
if in.Akamai != nil {
in, out := &in.Akamai, &out.Akamai
if *in == nil {
*out = nil
} else {
*out = new(ACMEIssuerDNS01ProviderAkamai)
**out = **in
}
}
if in.CloudDNS != nil {
in, out := &in.CloudDNS, &out.CloudDNS
if *in == nil {
@ -253,6 +262,25 @@ func (in *ACMEIssuerDNS01Provider) DeepCopy() *ACMEIssuerDNS01Provider {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ACMEIssuerDNS01ProviderAkamai) DeepCopyInto(out *ACMEIssuerDNS01ProviderAkamai) {
*out = *in
out.ClientToken = in.ClientToken
out.ClientSecret = in.ClientSecret
out.AccessToken = in.AccessToken
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ACMEIssuerDNS01ProviderAkamai.
func (in *ACMEIssuerDNS01ProviderAkamai) DeepCopy() *ACMEIssuerDNS01ProviderAkamai {
if in == nil {
return nil
}
out := new(ACMEIssuerDNS01ProviderAkamai)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ACMEIssuerDNS01ProviderAzureDNS) DeepCopyInto(out *ACMEIssuerDNS01ProviderAzureDNS) {
*out = *in

View File

@ -0,0 +1,272 @@
// Package akamai implements a DNS provider for solving the DNS-01
// challenge using Akamai FastDNS.
// See https://developer.akamai.com/api/luna/config-dns/overview.html
package akamai
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/golang/glog"
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/util"
"github.com/pkg/errors"
)
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
// serviceConsumerDomain as issued by Akamai Luna Control Center.
// The ServiceConsumerDomain is the base URL.
serviceConsumerDomain string
auth *EdgeGridAuth
transport http.RoundTripper
findHostedDomainByFqdn func(string) (string, error)
}
// NewDNSProvider returns a DNSProvider instance configured for Akamai.
func NewDNSProvider(serviceConsumerDomain, clientToken, clientSecret, accessToken string) (*DNSProvider, error) {
return &DNSProvider{
serviceConsumerDomain,
NewEdgeGridAuth(clientToken, clientSecret, accessToken),
http.DefaultTransport,
findHostedDomainByFqdn,
}, nil
}
func findHostedDomainByFqdn(fqdn string) (string, error) {
zone, err := util.FindZoneByFqdn(fqdn, util.RecursiveNameservers)
if err != nil {
return "", err
}
return util.UnFqdn(zone), nil
}
// Timeout returns the timeout and interval to use when checking for DNS
// propagation. Adjusting here to cope with spikes in propagation times.
func (a *DNSProvider) Timeout() (timeout, interval time.Duration) {
return 5 * time.Minute, 5 * time.Second
}
// Present creates a TXT record to fulfil the dns-01 challenge
func (a *DNSProvider) Present(domain, token, keyAuth string) error {
fqdn, value, ttl := util.DNS01Record(domain, keyAuth)
return a.setTxtRecord(fqdn, &dns01Record{value, ttl})
}
// CleanUp removes the TXT record matching the specified parameters
func (a *DNSProvider) CleanUp(domain, token, keyAuth string) error {
fqdn, _, _ := util.DNS01Record(domain, keyAuth)
return a.setTxtRecord(fqdn, nil)
}
type dns01Record struct {
value string
ttl int
}
func (a *DNSProvider) setTxtRecord(fqdn string, dns01Record *dns01Record) error {
hostedDomain, err := a.findHostedDomainByFqdn(fqdn)
if err != nil {
return errors.Wrapf(err, "failed to determine hosted domain for %q", fqdn)
}
zoneData, err := a.loadZoneData(hostedDomain)
if err != nil {
return errors.Wrapf(err, "failed to load zone data for %q", hostedDomain)
}
recordName, err := makeTxtRecordName(fqdn, hostedDomain)
if err != nil {
return errors.Wrapf(err, "failed to create TXT record name")
}
if updated, err := zoneData.setTxtRecord(recordName, dns01Record); !updated || err != nil {
if err != nil {
return errors.Wrapf(err, "failed to set TXT record in %q", hostedDomain)
}
return errors.Errorf("no %q TXT record found in %q", recordName, hostedDomain)
}
newSerial, err := zoneData.incSoaSerial()
if err != nil {
return errors.Wrapf(err, "failed to increment SOA serial for %q", hostedDomain)
}
if err := a.saveZoneData(hostedDomain, zoneData); err != nil {
return errors.Wrapf(err, "failed to save zone data for %q", hostedDomain)
}
glog.V(4).Infof("Updated Akamai TXT record for %q on %q using SOA serial of %d", recordName, hostedDomain, newSerial)
return nil
}
func makeTxtRecordName(fqdn, hostedDomain string) (string, error) {
if !strings.HasSuffix(fqdn, "."+hostedDomain+".") {
return "", errors.Errorf("fqdn %q is not part of %q", fqdn, hostedDomain)
}
return fqdn[0 : len(fqdn)-len(hostedDomain)-2], nil
}
func (a *DNSProvider) urlForDomain(domain string) string {
return fmt.Sprintf("https://%s/config-dns/v1/zones/%s", a.serviceConsumerDomain, domain)
}
func (a *DNSProvider) loadZoneData(domain string) (zoneData, error) {
url := a.urlForDomain(domain)
req, err := http.NewRequest(http.MethodGet, url, http.NoBody)
if err != nil {
return nil, errors.Wrap(err, "failed to create HTTP request")
}
responsePayload, err := a.makeRequest(req)
if err != nil {
return nil, err
}
var zoneData map[string]interface{}
err = json.NewDecoder(bytes.NewReader(responsePayload)).Decode(&zoneData)
if err != nil {
return nil, errors.Wrap(err, "failed to decode Akamai OPEN API response")
}
return zoneData, nil
}
func (a *DNSProvider) saveZoneData(domain string, data zoneData) error {
body, err := json.Marshal(data)
if err != nil {
return errors.Wrap(err, "failed to encode zone data")
}
url := a.urlForDomain(domain)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return errors.Wrap(err, "failed to create HTTP request")
}
req.Header.Set("Content-Type", "application/json")
if _, err := a.makeRequest(req); err != nil {
return err
}
return nil
}
func (a *DNSProvider) makeRequest(req *http.Request) ([]byte, error) {
if err := a.auth.SignRequest(req); err != nil {
return nil, errors.Wrap(err, "failed to sign HTTP request")
}
client := http.Client{
Transport: a.transport,
Timeout: 30 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "error querying Akamai OPEN API")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNoContent {
return nil, nil
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Akamai OPEN API returned %d %s", resp.StatusCode, resp.Status)
}
responsePayload, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "failed to read response payload")
}
return responsePayload, nil
}
type zoneData map[string]interface{}
func (z zoneData) setTxtRecord(name string, dns01Record *dns01Record) (bool, error) {
zone, ok := z["zone"].(map[string]interface{})
if !ok {
return false, errors.New("failed to retrieve zone from zone data")
}
var txtRecords []interface{}
if txtNode, ok := zone["txt"]; ok {
if txtRecords, ok = txtNode.([]interface{}); !ok {
return false, errors.New("failed to retrieve TXT records from zone data")
}
}
if dns01Record == nil {
if txtRecords = deleteRecord(txtRecords, name); txtRecords == nil {
return false, nil
}
} else {
txtRecords = updateRecord(txtRecords, name, map[string]interface{}{
"name": name,
"ttl": dns01Record.ttl,
"active": true,
"target": dns01Record.value,
})
}
if len(txtRecords) < 1 {
delete(zone, "txt")
} else {
zone["txt"] = txtRecords
}
return true, nil
}
func (z zoneData) incSoaSerial() (uint64, error) {
soa, ok := z["zone"].(map[string]interface{})["soa"].(map[string]interface{})
if !ok {
return 0, errors.New("failed to retrieve SOA record from zone data")
}
serial, ok := soa["serial"].(float64)
if !ok {
return 0, errors.New("failed to retrieve SOA serial from zone data")
}
newSerial := uint64(serial) + 1
soa["serial"] = newSerial
return newSerial, nil
}
func deleteRecord(records []interface{}, name string) []interface{} {
for pos := range records {
if recordName, ok := records[pos].(map[string]interface{})["name"]; ok && recordName == name {
return append(records[:pos], records[pos+1:]...)
}
}
return nil
}
func updateRecord(records []interface{}, name string, record map[string]interface{}) []interface{} {
for pos := range records {
if records[pos].(map[string]interface{})["name"] == name {
records[pos] = record
return records
}
}
return append(records, record)
}

View File

@ -0,0 +1,163 @@
package akamai
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
const sampleZoneData = `{
"token": "a184671d5307a388180fbf7f11dbdf46",
"zone": {
"name": "example.com",
"soa": {
"contact": "hostmaster.akamai.com.",
"expire": 604800,
"minimum": 180,
"originserver": "use4.akamai.com.",
"refresh": 900,
"retry": 300,
"serial": 1271354824,
"ttl": 900
},
"ns": [
{
"active": true,
"name": "",
"target": "use4.akam.net.",
"ttl": 3600
},
{
"active": true,
"name": "",
"target": "use3.akam.net.",
"ttl": 3600
}
]
}
}`
const sampleZoneDataWithTxt = `{
"token": "a184671d5307a388180fbf7f11dbdf46",
"zone": {
"name": "example.com",
"soa": {
"contact": "hostmaster.akamai.com.",
"expire": 604800,
"minimum": 180,
"originserver": "use4.akamai.com.",
"refresh": 900,
"retry": 300,
"serial": 1271354825,
"ttl": 900
},
"ns": [
{
"active": true,
"name": "",
"target": "use4.akam.net.",
"ttl": 3600
},
{
"active": true,
"name": "",
"target": "use3.akam.net.",
"ttl": 3600
}
],
"txt": [
{
"active": true,
"name" :"_acme-challenge.test",
"target": "dns01-key",
"ttl": 60
}
]
}
}`
type httpResponder func(req *http.Request) (*http.Response, error)
func (r httpResponder) RoundTrip(req *http.Request) (*http.Response, error) {
return r(req)
}
func TestPresent(t *testing.T) {
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token")
assert.NoError(t, err)
var response []byte
mockTransport(t, akamai, "example.com", sampleZoneData, &response)
assert.NoError(t, akamai.Present("test.example.com", "dns01-token", "dns01-key"))
var expected, actual map[string]interface{}
assert.NoError(t, json.Unmarshal([]byte(sampleZoneDataWithTxt), &expected))
assert.NoError(t, json.Unmarshal(response, &actual))
assert.EqualValues(t, expected, actual)
}
func TestCleanUp(t *testing.T) {
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token")
assert.NoError(t, err)
var response []byte
mockTransport(t, akamai, "example.com", sampleZoneDataWithTxt, &response)
assert.NoError(t, akamai.CleanUp("test.example.com", "dns01-token", "dns01-key"))
var expected, actual map[string]interface{}
assert.NoError(t, json.Unmarshal([]byte(sampleZoneData), &expected))
expected["zone"].(map[string]interface{})["soa"].(map[string]interface{})["serial"] = 1271354826.
assert.NoError(t, json.Unmarshal(response, &actual))
assert.EqualValues(t, expected, actual)
}
func mockTransport(t *testing.T, akamai *DNSProvider, domain, data string, response *[]byte) {
akamai.transport = httpResponder(func(req *http.Request) (*http.Response, error) {
defer req.Body.Close()
if req.URL.String() != "https://akamai.example.com/config-dns/v1/zones/"+domain {
return &http.Response{
StatusCode: http.StatusNotFound,
Body: http.NoBody,
}, nil
}
if req.Method == http.MethodGet {
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(data))),
}, nil
}
if req.Method == http.MethodPost {
if req.Header.Get("Content-Type") != "application/json" {
t.Fatalf("usupported Content Type: %v", req.Header.Get("Content-Type"))
}
var err error
*response, err = ioutil.ReadAll(req.Body)
assert.NoError(t, err)
return &http.Response{
StatusCode: http.StatusNoContent,
Body: http.NoBody,
}, nil
}
t.Fatalf("unexpected method: %v", req.Method)
return nil, nil
})
akamai.findHostedDomainByFqdn = func(fqdn string) (string, error) {
if !strings.HasSuffix(fqdn, domain+".") {
t.Fatalf("unexpected fqdn: %s", fqdn)
}
return domain, nil
}
}

View File

@ -0,0 +1,264 @@
package akamai
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"sort"
"strings"
"time"
"unicode"
)
// EdgeGridAuth holds all values required to perform Akamai API Client Authentication.
// See https://developer.akamai.com/introduction/Client_Auth.html.
type EdgeGridAuth struct {
ClientToken string
ClientSecret string
AccessToken string
HeadersToSign []string
MaxBody int
now func() time.Time
createNonce func() (string, error)
}
type signingData struct {
timestamp string
authHeader string
dataToSign string
}
// edgeGridAuthTimeFormat is used for timestamps in request signatures.
const edgeGridAuthTimeFormat = "20060102T15:04:05-0700" // yyyyMMddTHH:mm:ss+0000
const NoMaxBody = -1
// NewEdgeGridAuth returns a new request signer for Akamai EdgeGrid
func NewEdgeGridAuth(clientToken, clientSecret, accessToken string, headersToSign ...string) *EdgeGridAuth {
return &EdgeGridAuth{
ClientToken: clientToken,
ClientSecret: clientSecret,
AccessToken: accessToken,
HeadersToSign: headersToSign,
MaxBody: NoMaxBody,
now: time.Now,
createNonce: createRandomNonce,
}
}
// SignRequest calculates the signature for Akamai Open API and adds it as the Authorization header.
// The Authorization header starts with the signing algorithm moniker (name of the algorithm) used to sign the request.
// The moniker below identifies EdgeGrid V1, hash message authentication code, SHA256 as the hash standard.
// This moniker is then followed by a space and an ordered list of name value pairs with each field separated by a semicolon.
func (e *EdgeGridAuth) SignRequest(req *http.Request) error {
signingData, err := e.signingData(req)
if err != nil {
return err
}
req.Header.Set("Authorization", fmt.Sprintf(
"%ssignature=%s",
signingData.authHeader,
e.calculateRequestSignature(signingData)))
return nil
}
func (e *EdgeGridAuth) calculateRequestSignature(signingData *signingData) string {
return computeSignature(
signingData.dataToSign,
e.signingKey(signingData.timestamp))
}
func (e *EdgeGridAuth) signingData(req *http.Request) (*signingData, error) {
nonce, err := e.createNonce()
if err != nil {
return nil, err
}
timestamp := e.now().UTC().Format(edgeGridAuthTimeFormat)
authHeader := fmt.Sprintf("EG1-HMAC-SHA256 client_token=%s;access_token=%s;timestamp=%s;nonce=%s;",
e.ClientToken,
e.AccessToken,
timestamp,
nonce)
return &signingData{
timestamp: timestamp,
authHeader: authHeader,
dataToSign: e.dataToSign(req, authHeader),
}, nil
}
// dataToSign includes the information from the HTTP request that is relevant to ensuring that the request is authentic.
// This data set comprised of the request data combined with the authorization header value (excluding the signature field,
// but including the ; right before the signature field).
func (e *EdgeGridAuth) dataToSign(req *http.Request, authHeader string) string {
var buffer bytes.Buffer
buffer.WriteString(req.Method)
buffer.WriteRune('\t')
buffer.WriteString(req.URL.Scheme)
buffer.WriteRune('\t')
buffer.WriteString(req.URL.Host)
buffer.WriteRune('\t')
buffer.WriteString(relativeURL(req.URL))
buffer.WriteRune('\t')
buffer.WriteString(e.canonicalizedHeaders(req))
buffer.WriteRune('\t')
buffer.WriteString(e.computeBodyHash(req))
buffer.WriteRune('\t')
buffer.WriteString(authHeader)
return buffer.String()
}
// signingKey is derived from the client secret.
// The signing key is computed as the base64 encoding of the SHA256 HMAC of the timestamp string
// (the field value included in the HTTP authorization header described above) with the client secret as the key.
func (e *EdgeGridAuth) signingKey(timestamp string) string {
return computeSignature(timestamp, e.ClientSecret)
}
// realtiveURL is the part of the URL that starts from the root path and includes the query string, with the handling of following special cases:
// If the path is null or empty, set it to / (forward-slash).
// If the path does not start with /, add / to the beginning.
func relativeURL(url *url.URL) string {
relativeURL := url.Path
if relativeURL == "" {
return "/"
}
if relativeURL[0] != '/' {
relativeURL = "/" + relativeURL
}
if url.RawQuery != "" {
relativeURL += "?"
relativeURL += url.RawQuery
}
return relativeURL
}
// computeBodyHash returns the base64-encoded SHA256 hash of the POST body.
// For any other request methods, this field is empty. But the tac separator (\t) must be included.
// The size of the POST body must be less than or equal to the value specified by the service.
// Any request that does not meet this criteria SHOULD be rejected during the signing process,
// as the request will be rejected by EdgeGrid.
func (e *EdgeGridAuth) computeBodyHash(req *http.Request) string {
if req.Body != nil {
bodyBytes, _ := ioutil.ReadAll(req.Body)
req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
if req.Method == http.MethodPost && len(bodyBytes) > 0 {
dataToHash := bodyBytes
if e.MaxBody != NoMaxBody && len(dataToHash) > e.MaxBody {
dataToHash = dataToHash[0:e.MaxBody]
}
sha256Sum := sha256.Sum256(dataToHash)
return base64.StdEncoding.EncodeToString(sha256Sum[:])
}
}
return ""
}
// canonicalizedHeaders returns the request headers as a canonicalized string.
//
// The protocol does not support multiple request headers with the same header name.
// Such requests SHOULD be rejected during the signing process. Otherwise, EdgeGrid
// will not produce the intended results by rejecting such requests or removing all
// (but one) duplicated headers.
//
// Header names are case-insensitive per rfc2616.
//
// For each entry in the list of headers designated by the service provider to include
// in the signature in the specified order, the canonicalization of the request header
// is done as follows:
//
// Get the first header value for the name.
// Trim the leading and trailing white spaces.
// Replace all repeated white spaces with a single space.
// Concatenate the name:value pairs with the tab (\t) separator (name field is all in lower case).
// Terminate the headers with another tab (\t) separator.
//
// NOTE: The canonicalized data is used for creating the signature only, as this step
// might alter the header value. If a header in the list is not present in the request,
// or the header value is empty, nothing for that header, neither the name nor the tab
// separator, may be included.
func (e *EdgeGridAuth) canonicalizedHeaders(req *http.Request) string {
if len(e.HeadersToSign) < 1 {
return ""
}
var headerNamesToSign []string
for headerName := range req.Header {
for _, sign := range e.HeadersToSign {
if strings.EqualFold(sign, headerName) {
headerNamesToSign = append(headerNamesToSign, headerName)
break
}
}
}
if len(headerNamesToSign) < 1 {
return ""
}
sort.Strings(headerNamesToSign)
var buffer bytes.Buffer
for _, headerName := range headerNamesToSign {
for _, c := range headerName {
buffer.WriteRune(unicode.ToLower(c))
}
buffer.WriteRune(':')
white := false
empty := true
for _, c := range req.Header.Get(headerName) {
if unicode.IsSpace(c) {
white = true
} else {
if white && !empty {
buffer.WriteRune(' ')
}
buffer.WriteRune(unicode.ToLower(c))
empty = false
white = false
}
}
buffer.WriteRune('\t')
}
return buffer.String()
}
// calculateSignature is the base64-encoding of the SHA256 HMAC of the data to sign with the signing key.
func computeSignature(message string, secret string) string {
key := []byte(secret)
h := hmac.New(sha256.New, key)
h.Write([]byte(message))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func createRandomNonce() (string, error) {
bytes := make([]byte, 18)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes), nil
}

View File

@ -0,0 +1,81 @@
package akamai
import (
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestDataToSign(t *testing.T) {
req, err := http.NewRequest(
http.MethodGet,
"https://akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net/diagnostic-tools/v1/locations",
http.NoBody)
assert.NoError(t, err)
auth := NewEdgeGridAuth("ClientToken", "ClientSecret", "AccessToken")
auth.now = func() time.Time {
return time.Unix(1396461906, 0) // 20140402T18:05:06Z
}
auth.createNonce = func() (string, error) {
return "185f94eb-537c-4c01-b8cc-2fa5a06aee7f", nil
}
data, err := auth.signingData(req)
assert.NoError(t, err)
expected := "GET" +
"\thttps" +
"\takaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net" +
"\t/diagnostic-tools/v1/locations" +
"\t" + // headers
"\t" + // content hash
"\tEG1-HMAC-SHA256 " +
"client_token=ClientToken;" +
"access_token=AccessToken;" +
"timestamp=20140402T18:05:06+0000;" +
"nonce=185f94eb-537c-4c01-b8cc-2fa5a06aee7f;"
assert.EqualValues(t, expected, data.dataToSign)
}
func TestDataToSignWithHeaders(t *testing.T) {
req, err := http.NewRequest(
http.MethodGet,
"http://akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna-dev.akamaiapis.net/sample-api/v1/property/?fields=x&format=json&cpcode=1234",
http.NoBody)
assert.NoError(t, err)
req.Header.Set("x-a", "va")
req.Header.Set("x-c", "\" xc \"")
req.Header.Set("x-b", " w b")
auth := NewEdgeGridAuth(
"ClientToken", "ClientSecret", "AccessToken",
"x-c", "x-b", "x-a")
auth.now = func() time.Time {
return time.Unix(1376917283, 0) // 20130819T13:01:23Z
}
auth.createNonce = func() (string, error) {
return "ac392096-8aa1-44fd-8c3b-f797d35a6736", nil
}
data, err := auth.signingData(req)
assert.NoError(t, err)
expected := "GET" +
"\thttp" +
"\takaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna-dev.akamaiapis.net" +
"\t/sample-api/v1/property/?fields=x&format=json&cpcode=1234" +
"\tx-a:va\tx-b:w b\tx-c:\" xc \"\t" +
"\t" + // content hash
"\tEG1-HMAC-SHA256 " +
"client_token=ClientToken;" +
"access_token=AccessToken;" +
"timestamp=20130819T13:01:23+0000;" +
"nonce=ac392096-8aa1-44fd-8c3b-f797d35a6736;"
assert.EqualValues(t, expected, data.dataToSign)
}

View File

@ -9,7 +9,11 @@ import (
"k8s.io/client-go/kubernetes"
corev1listers "k8s.io/client-go/listers/core/v1"
"github.com/pkg/errors"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/akamai"
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/azuredns"
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/clouddns"
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/cloudflare"
@ -116,6 +120,30 @@ func (s *Solver) solverFor(crt *v1alpha1.Certificate, domain string) (solver, er
var impl solver
switch {
case providerConfig.Akamai != nil:
clientToken, err := s.loadSecretData(&providerConfig.Akamai.ClientToken)
if err != nil {
return nil, errors.Wrap(err, "error getting akamai client token")
}
clientSecret, err := s.loadSecretData(&providerConfig.Akamai.ClientSecret)
if err != nil {
return nil, errors.Wrap(err, "error getting akamai client secret")
}
accessToken, err := s.loadSecretData(&providerConfig.Akamai.AccessToken)
if err != nil {
return nil, errors.Wrap(err, "error getting akamai access token")
}
impl, err = akamai.NewDNSProvider(
providerConfig.Akamai.ServiceConsumerDomain,
string(clientToken),
string(clientSecret),
string(accessToken))
if err != nil {
return nil, errors.Wrap(err, "error instantiating akamai challenge solver")
}
case providerConfig.CloudDNS != nil:
saSecret, err := s.secretLister.Secrets(s.resourceNamespace).Get(providerConfig.CloudDNS.ServiceAccount.Name)
if err != nil {
@ -189,3 +217,16 @@ func (s *Solver) solverFor(crt *v1alpha1.Certificate, domain string) (solver, er
func NewSolver(issuer v1alpha1.GenericIssuer, client kubernetes.Interface, secretLister corev1listers.SecretLister, resourceNamespace string) *Solver {
return &Solver{issuer, client, secretLister, resourceNamespace}
}
func (s *Solver) loadSecretData(selector *v1alpha1.SecretKeySelector) ([]byte, error) {
secret, err := s.secretLister.Secrets(s.resourceNamespace).Get(selector.Name)
if err != nil {
return nil, errors.Wrapf(err, "failed to load secret with name %q", selector.Name)
}
if data, ok := secret.Data[selector.Key]; ok {
return data, nil
}
return nil, errors.Errorf("no key %q in secret %q", selector.Key, selector.Name)
}