pax_global_header00006660000000000000000000000064135517223060014516gustar00rootroot0000000000000052 comment=4c466ab079c59d00344630cb861d075f3a75bc56 golang-gopkg-hlandau-acmeapi.v2-2.0.1/000077500000000000000000000000001355172230600174275ustar00rootroot00000000000000golang-gopkg-hlandau-acmeapi.v2-2.0.1/.drone.yml000066400000000000000000000004211355172230600213340ustar00rootroot00000000000000pipeline: test: image: golang:latest commands: - export GOPATH=/drone - go get ./... github.com/letsencrypt/pebble/cmd/pebble - go install ./... github.com/letsencrypt/pebble/cmd/pebble - .drone/with-pebble go test -tags integration ./... golang-gopkg-hlandau-acmeapi.v2-2.0.1/.drone/000077500000000000000000000000001355172230600206145ustar00rootroot00000000000000golang-gopkg-hlandau-acmeapi.v2-2.0.1/.drone/with-pebble000077500000000000000000000014331355172230600227450ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail export PEBBLE_WFE_NONCEREJECT=80 [[ -z "$GOPATH" ]] && { echo >&2 'with-pebble: must have a $GOPATH set.' exit 1 } [[ -x "$GOPATH/bin/pebble" ]] || { echo >&2 'with-pebble: must have $GOPATH/bin/pebble installed (from github.com/letsencrypt/pebble/cmd/pebble).' exit 1 } OLD_PWD="$(pwd)" cd "$GOPATH/src/github.com/letsencrypt/pebble" echo > pebble.log "$GOPATH/bin/pebble" -strict &> pebble.log & PEBBLE_PID=$! function cleanup { echo "with-pebble: killing $PEBBLE_PID" kill "$PEBBLE_PID" } trap cleanup EXIT echo "with-pebble: waiting for pebble (${PEBBLE_PID}) to come up..." while ((1)); do kill -0 "$PEBBLE_PID" || exit 1 grep -q 'listening on: ' pebble.log && break sleep 1 done echo with-pebble: up cd "$OLD_PWD" time "$@" golang-gopkg-hlandau-acmeapi.v2-2.0.1/README.md000066400000000000000000000006531355172230600207120ustar00rootroot00000000000000acmeapi ======= This is an ACMEv2 protocol client library written in Go. [Godoc.](https://godoc.org/git.devever.net/hlandau/acmeapi) It targets the final version of the ACME specification. It deprecates [hlandau/acme/acmeapi](https://github.com/hlandau/acme), which implemented the `-02` version of the specification initially implemented by Let's Encrypt. © 2017-2018 Hugo Landau MIT License golang-gopkg-hlandau-acmeapi.v2-2.0.1/acmeendpoints/000077500000000000000000000000001355172230600222605ustar00rootroot00000000000000golang-gopkg-hlandau-acmeapi.v2-2.0.1/acmeendpoints/endpoint.go000066400000000000000000000043121355172230600244270ustar00rootroot00000000000000// Package acmeendpoints provides information on known ACME servers. package acmeendpoints import ( "fmt" "regexp" "sync" ) // Provides information on a known ACME endpoint. type Endpoint struct { // Friendly name for the provider. Should be a short, single-line, title case // human readable description of the endpoint. Title string // Short unique endpoint identifier. Must match ^[a-zA-Z][a-zA-Z0-9_]*$ and // should use CamelCase. Code string // The ACME directory URL. Must be an HTTPS URL and typically ends in // "/directory". DirectoryURL string // If this is not "", this is a regexp which must be matched iff an OCSP // endpoint URL as found in a certificate implies that a certificate was // issued by this endpoint. OCSPURLRegexp string ocspURLRegexp *regexp.Regexp // Whether the endpoint gives live certificates. Live bool // If not "", this is a regexp matching deprecated directory URLs which this // endpoint supercedes. We use this to upgrade seamlessly to ACMEv2 without // requiring server administrators to change their ACME directory URLs. DeprecatedDirectoryURLRegexp string deprecatedDirectoryURLRegexp *regexp.Regexp initOnce sync.Once } func (e *Endpoint) String() string { return fmt.Sprintf("Endpoint(%v)", e.DirectoryURL) } func (e *Endpoint) init() { e.initOnce.Do(func() { if e.OCSPURLRegexp != "" { e.ocspURLRegexp = regexp.MustCompile(e.OCSPURLRegexp) } //if e.CertificateURLRegexp != "" { // e.certificateURLRegexp = regexp.MustCompile(e.CertificateURLRegexp) //} //if e.CertificateURLTemplate != "" { // e.certificateURLTemplate = template.Must(template.New("certificate-url").Parse(e.CertificateURLTemplate)) //} if e.DeprecatedDirectoryURLRegexp != "" { e.deprecatedDirectoryURLRegexp = regexp.MustCompile(e.DeprecatedDirectoryURLRegexp) } }) } var endpoints []*Endpoint // Visit all registered endpoints. func Visit(f func(p *Endpoint) error) error { for _, p := range endpoints { err := f(p) if err != nil { return err } } return nil } // Register a new endpoint. func RegisterEndpoint(p *Endpoint) { p.init() endpoints = append(endpoints, p) } func init() { for _, p := range builtinEndpoints { RegisterEndpoint(p) } } golang-gopkg-hlandau-acmeapi.v2-2.0.1/acmeendpoints/endpoint_test.go000066400000000000000000000012001355172230600254570ustar00rootroot00000000000000package acmeendpoints import ( "fmt" "testing" ) func TestVisit(t *testing.T) { ep := map[*Endpoint]struct{}{} err := Visit(func(e *Endpoint) error { ep[e] = struct{}{} return nil }) if err != nil { t.Fail() } _, ok := ep[&LetsEncryptLiveV2] if !ok { t.Fail() } _, ok = ep[&LetsEncryptStagingV2] if !ok { t.Fail() } ep = map[*Endpoint]struct{}{} e1 := fmt.Errorf("e1") i := 0 err = Visit(func(e *Endpoint) error { if i == 1 { return e1 } i++ ep[e] = struct{}{} return nil }) if err != e1 { t.Fail() } if len(ep) != 1 { t.Fail() } _, ok = ep[&LetsEncryptLiveV2] if !ok { t.Fail() } } golang-gopkg-hlandau-acmeapi.v2-2.0.1/acmeendpoints/endpoints.go000066400000000000000000000017401355172230600246140ustar00rootroot00000000000000package acmeendpoints var ( // Let's Encrypt (Live v2) LetsEncryptLiveV2 = Endpoint{ Code: "LetsEncryptLiveV2", Title: "Let's Encrypt (Live v2)", DirectoryURL: "https://acme-v02.api.letsencrypt.org/directory", OCSPURLRegexp: `^http://ocsp\.int-[^.]+\.letsencrypt\.org\.?/.*$`, DeprecatedDirectoryURLRegexp: `^https://acme-v01\.api\.letsencrypt\.org/directory$`, Live: true, } // Let's Encrypt (Staging v2) LetsEncryptStagingV2 = Endpoint{ Code: "LetsEncryptStagingV2", Title: "Let's Encrypt (Staging v2)", DirectoryURL: "https://acme-staging-v02.api.letsencrypt.org/directory", OCSPURLRegexp: `^http://ocsp\.(staging|stg-int)-[^.]+\.letsencrypt\.org\.?/.*$`, Live: false, } ) // Suggested default endpoint. var DefaultEndpoint = &LetsEncryptLiveV2 var builtinEndpoints = []*Endpoint{ &LetsEncryptLiveV2, &LetsEncryptStagingV2, } golang-gopkg-hlandau-acmeapi.v2-2.0.1/acmeendpoints/url.go000066400000000000000000000031211355172230600234060ustar00rootroot00000000000000package acmeendpoints import ( "crypto/sha256" "errors" "fmt" "github.com/hlandau/xlog" ) var log, Log = xlog.New("acme.endpoints") // Returned when no matching endpoint can be found. var ErrNotFound = errors.New("no corresponding endpoint found") // Finds an endpoint with the given directory URL. If no such endpoint is // found, returns ErrNotFound. func ByDirectoryURL(directoryURL string) (*Endpoint, error) { for _, e := range endpoints { if directoryURL == e.DirectoryURL { return e, nil } if e.deprecatedDirectoryURLRegexp != nil && e.deprecatedDirectoryURLRegexp.MatchString(directoryURL) { return e, nil } } return nil, ErrNotFound } // If an endpoint exists with the given directory URL, returns it. // // Otherwise, tries to create a new endpoint for the directory URL. Where // possible, endpoint parameters are guessed. Currently boulder is supported. // Non-boulder based endpoints will not have any parameters set other than the // directory URL, which means some operations on the endpoint will not succeed. // // It is acceptable to change the fields of the returned endpoint. // By default, the title of the endpoint is the directory URL. func CreateByDirectoryURL(directoryURL string) (*Endpoint, error) { e, err := ByDirectoryURL(directoryURL) if err == nil { return e, nil } // Make a code for the endpoint by hashing the directory URL... h := sha256.New() h.Write([]byte(directoryURL)) code := fmt.Sprintf("Temp%08x", h.Sum(nil)[0:4]) e = &Endpoint{ Title: directoryURL, DirectoryURL: directoryURL, Code: code, } return e, nil } golang-gopkg-hlandau-acmeapi.v2-2.0.1/acmeendpoints/url_test.go000066400000000000000000000105421355172230600244520ustar00rootroot00000000000000package acmeendpoints import ( "github.com/hlandau/acmeapi/acmeutils" "testing" ) const leStagingTestCert = ` -----BEGIN CERTIFICATE----- MIIE6DCCA9CgAwIBAgITAPo8NeGtZ2xhrKoeMR+onLNgFzANBgkqhkiG9w0BAQsF ADAfMR0wGwYDVQQDDBRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNjAxMTcxNjAz MDBaFw0xNjA0MTYxNjAzMDBaMB4xHDAaBgNVBAMTE2FxMS5saGguZGV2ZXZlci5u ZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTP6pFjvAzkVohGaGH hIJ746SGTdw2cjDfqZimiBc1Yrjl1AFlLfHHLZ7Uyt3b7EYlYao6P6Vx9wKigCI+ vaeAudlZNerJa8fWNJXf4eqYoYH7vf+xnZP7TYUmiWLSGES9p8QBRCHwWPycP7mm X4kneqo/oF/asQnOmUy0hi2VyCCT/XQ93ApN5pHz8dg7A3OtOGlHXd38rJ3uBJ0N JXM6Dx5Oj833nDaa2ndkBxq5m0SLnOimE5GsqX7bWNfllMeZXqH5/3E25cgh2YTR 6JBDLqpzO9ZvFOOWcOVk0QG+zfXhHVx++6I6fs36p3/+DN58WB/JP4CLV3JvC6cE NyuvAgMBAAGjggIcMIICGDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYB BQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFNB3WkfIcwYM bXABE4q5k3/o1vNHMB8GA1UdIwQYMBaAFPt4TxL5YBWDLJ8XfzQZsy426kGJMHgG CCsGAQUFBwEBBGwwajAzBggrBgEFBQcwAYYnaHR0cDovL29jc3Auc3RhZ2luZy14 MS5sZXRzZW5jcnlwdC5vcmcvMDMGCCsGAQUFBzAChidodHRwOi8vY2VydC5zdGFn aW5nLXgxLmxldHNlbmNyeXB0Lm9yZy8wHgYDVR0RBBcwFYITYXExLmxoaC5kZXZl dmVyLm5ldDCB/gYDVR0gBIH2MIHzMAgGBmeBDAECATCB5gYLKwYBBAGC3xMBAQEw gdYwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQub3JnMIGrBggr BgEFBQcCAjCBngyBm1RoaXMgQ2VydGlmaWNhdGUgbWF5IG9ubHkgYmUgcmVsaWVk IHVwb24gYnkgUmVseWluZyBQYXJ0aWVzIGFuZCBvbmx5IGluIGFjY29yZGFuY2Ug d2l0aCB0aGUgQ2VydGlmaWNhdGUgUG9saWN5IGZvdW5kIGF0IGh0dHBzOi8vbGV0 c2VuY3J5cHQub3JnL3JlcG9zaXRvcnkvMA0GCSqGSIb3DQEBCwUAA4IBAQABcut7 1jVicQnHvSkQgY1CRiGSmlHyOyEKimNtCuyaAVwm3cavV/wpGTDFnePyNds4cst/ 8BcL0QaKLmE1an/oeGmfs0U8maiKbL69Yun0qTNTKOaqJP/iitwAbliQ3TzO2kOZ +a2RkPKx0/zYlZb0GzhfIwHE4Qd7/P0qLphu2UaaEpzBnRlT1F9k+cGe4DZYb4XL BZHnOmXeZrhfPeeTw4VYAEtZ7fpwRhirBjshU8kRbO7KgZh4Id+v26FQpBE3eMQ2 CWV8q8XThKcX3OaMOkLOIB2xZA7Fpj3JoDcsLPEKn5sxVgkxfjs03glTWd839qcE YAC6drs6Fev1cVa9 -----END CERTIFICATE-----` const leLiveTestCert = ` -----BEGIN CERTIFICATE----- MIIFCzCCA/OgAwIBAgISAaoIVMlVWnr9Vfrj+Ak2new4MA0GCSqGSIb3DQEBCwUA MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMTAeFw0xNjAxMTEwNDQ0MDBaFw0x NjA0MTAwNDQ0MDBaMBYxFDASBgNVBAMTC2RldmV2ZXIubmV0MIIBIjANBgkqhkiG 9w0BAQEFAAOCAQ8AMIIBCgKCAQEArBzKQy0inr2oheVRuCDS2prucTF+8xQW66WP D5ZNzoypPFB9uvFSJN1QzMeq7fdLGWn3QIFj9HlntYxI7Sy47nFeciHG2lN7zfGL Lex0vREZ21ST3IfUuD/LogkAMqgjcymBiMdrO5hcPf0OIkboBe96BrBAKXTFVlme guwkdexNjedlFQ4egtzKZ2YrJXR4z9VOW0qaRNqk+9zvjLGG2mIay+NN0aTHomOk Ow+y8bFFJ9wrkMtn+/IwP1uIbyMEgF2qmKnB/G6H/Qdq52IBF1rCC5xlpWNB0w/3 aJd512AqC5WFC/yFy8ksFS7EjIhQeyqBx1unyaz13C3yrRimbwIDAQABo4ICHTCC AhkwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD AjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQY0ADTmNDEDmrqY45CJCJdwvHp7DAf BgNVHSMEGDAWgBSoSmpjBH3duubRObemRWXv86jsoTBwBggrBgEFBQcBAQRkMGIw LwYIKwYBBQUHMAGGI2h0dHA6Ly9vY3NwLmludC14MS5sZXRzZW5jcnlwdC5vcmcv MC8GCCsGAQUFBzAChiNodHRwOi8vY2VydC5pbnQteDEubGV0c2VuY3J5cHQub3Jn LzAnBgNVHREEIDAeggtkZXZldmVyLm5ldIIPd3d3LmRldmV2ZXIubmV0MIH+BgNV HSAEgfYwgfMwCAYGZ4EMAQIBMIHmBgsrBgEEAYLfEwEBATCB1jAmBggrBgEFBQcC ARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwgasGCCsGAQUFBwICMIGeDIGb VGhpcyBDZXJ0aWZpY2F0ZSBtYXkgb25seSBiZSByZWxpZWQgdXBvbiBieSBSZWx5 aW5nIFBhcnRpZXMgYW5kIG9ubHkgaW4gYWNjb3JkYW5jZSB3aXRoIHRoZSBDZXJ0 aWZpY2F0ZSBQb2xpY3kgZm91bmQgYXQgaHR0cHM6Ly9sZXRzZW5jcnlwdC5vcmcv cmVwb3NpdG9yeS8wDQYJKoZIhvcNAQELBQADggEBAHcD+3AjdbfZylPHFYyYSIWk no90p+rWZwh3sDnWC5KzZ8jm7uMynCvr7NK0BBxIzuqlWQ0vjKq41KFkTA+GllS/ a4/1XnzrKIJ8udX698Ofsn6HEqxoT0/sAQhxGChrXDRl33QDowqquHWh8HGXx1ke jV1U4H69KjWYRNx7EN2kbik4GDznwOGpkAUPFCiW2g40zs8Lw4+RiTGPHNELzm7c TMAyWtPi4eJpMz87jYxv+jB6a4Zy5gAdEySejtGwerhGrrmntkliR8MKZQ6Lisd8 h6xyLde4iNUiXtPOr9I87FBLC1U2AnP+GldAKYB3PO1qPHy6u/a15Xg34FrD8SM= -----END CERTIFICATE-----` type urlTestCase struct { Cert string Endpoint *Endpoint } var urlTestCases = []*urlTestCase{ { Cert: leStagingTestCert, Endpoint: &LetsEncryptStagingV2, }, { Cert: leLiveTestCert, Endpoint: &LetsEncryptLiveV2, }, } func TestURL(t *testing.T) { _, err := ByDirectoryURL("https://unknown/directory") if err != ErrNotFound { t.Fail() } for _, tc := range urlTestCases { e, err := ByDirectoryURL(tc.Endpoint.DirectoryURL) if err != nil { t.Fatalf("cannot get by directory URL") } if e != tc.Endpoint { t.Fatalf("got wrong endpoint: %v != %v", e, tc.Endpoint) } _, err = acmeutils.LoadCertificates([]byte(tc.Cert)) if err != nil { t.Fatalf("cannot load test certificate") } } } golang-gopkg-hlandau-acmeapi.v2-2.0.1/acmeutils/000077500000000000000000000000001355172230600214155ustar00rootroot00000000000000golang-gopkg-hlandau-acmeapi.v2-2.0.1/acmeutils/hostname.go000066400000000000000000000015461355172230600235700ustar00rootroot00000000000000package acmeutils import ( "fmt" "golang.org/x/net/idna" "regexp" "strings" ) var reHostname = regexp.MustCompilePOSIX(`^([a-z0-9_-]+\.)*[a-z0-9_-]+$`) // Normalizes the hostname given. If the hostname is not valid, returns "" and // an error. func NormalizeHostname(name string) (string, error) { isWildcard := strings.HasPrefix(name, "*.") if isWildcard { name = name[2:] } name = strings.TrimSuffix(strings.ToLower(name), ".") name, err := idna.ToASCII(name) if err != nil { return "", fmt.Errorf("IDN error: %#v: %v", name, err) } if !reHostname.MatchString(name) { return "", fmt.Errorf("invalid hostname: %#v", name) } if isWildcard { name = "*." + name } return name, nil } // Returns true iff the given string is a valid hostname. func ValidateHostname(name string) bool { _, err := NormalizeHostname(name) return err == nil } golang-gopkg-hlandau-acmeapi.v2-2.0.1/acmeutils/hostname_test.go000066400000000000000000000014401355172230600246200ustar00rootroot00000000000000package acmeutils import "testing" func TestHostname(t *testing.T) { type entry struct { Input, Output string Valid bool } var entries = []entry{ {"example.com", "example.com", true}, {"example.com.", "example.com", true}, {"example.com..", "", false}, {"ex ample.com.", "", false}, {"む.com", "xn--dbk.com", true}, {"む..com.", "", false}, {"*.example.com", "*.example.com", true}, {"*.*.example.com", "", false}, {"foo.*.example.com", "", false}, } for _, e := range entries { out, err := NormalizeHostname(e.Input) if e.Valid != (err == nil) { t.Logf("hostname fail: expected valid=%v, got err=%v", e.Valid, err) t.Fail() } if out != e.Output { t.Logf("hostname fail: for input %q, %q != %q", e.Input, out, e.Output) t.Fail() } } } golang-gopkg-hlandau-acmeapi.v2-2.0.1/acmeutils/keyauth.go000066400000000000000000000053521355172230600234230ustar00rootroot00000000000000package acmeutils import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/sha256" "crypto/x509" "crypto/x509/pkix" "encoding/base64" "encoding/hex" "gopkg.in/square/go-jose.v1" "math/big" "time" ) // Calculates the base64 thumbprint of a public or private key. Returns an // error if the key is of an unknown type. func Base64Thumbprint(key interface{}) (string, error) { k := jose.JsonWebKey{Key: key} thumbprint, err := k.Thumbprint(crypto.SHA256) if err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(thumbprint), nil } // Calculates a key authorization using the given account public or private key // and the token to prefix. func KeyAuthorization(accountKey interface{}, token string) (string, error) { thumbprint, err := Base64Thumbprint(accountKey) if err != nil { return "", err } return token + "." + thumbprint, nil } // Calculates a key authorization which is then hashed and base64 encoded as is // required for the DNS challenge. func DNSKeyAuthorization(accountKey interface{}, token string) (string, error) { ka, err := KeyAuthorization(accountKey, token) if err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(sha256Bytes([]byte(ka))), nil } // Determines the hostname which must appear in a TLS-SNI challenge // certificate. func TLSSNIHostname(accountKey interface{}, token string) (string, error) { ka, err := KeyAuthorization(accountKey, token) if err != nil { return "", err } kaHex := sha256BytesHex([]byte(ka)) return kaHex[0:32] + "." + kaHex[32:64] + ".acme.invalid", nil } // Creates a self-signed certificate and matching private key suitable for // responding to a TLS-SNI challenge. hostname should be a hostname returned by // TLSSNIHostname. func CreateTLSSNICertificate(hostname string) (certDER []byte, privateKey crypto.PrivateKey, err error) { crt := x509.Certificate{ Subject: pkix.Name{ CommonName: hostname, }, Issuer: pkix.Name{ CommonName: hostname, }, SerialNumber: big.NewInt(1), NotBefore: time.Now().Add(-24 * time.Hour), NotAfter: time.Now().Add(365 * 24 * time.Hour), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, DNSNames: []string{hostname}, } pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return } certDER, err = x509.CreateCertificate(rand.Reader, &crt, &crt, &pk.PublicKey, pk) privateKey = pk return } func sha256Bytes(b []byte) []byte { h := sha256.New() h.Write(b) return h.Sum(nil) } func sha256BytesHex(b []byte) string { return hex.EncodeToString(sha256Bytes(b)) } golang-gopkg-hlandau-acmeapi.v2-2.0.1/acmeutils/keyauth_test.go000066400000000000000000000017371355172230600244650ustar00rootroot00000000000000package acmeutils import "testing" func TestKeyAuthorization(t *testing.T) { pk, err := LoadPrivateKey([]byte(testKey)) if err != nil { t.Fatal() } ka, err := KeyAuthorization(pk, "foo") if err != nil { t.Fatal() } if ka != "foo.UOn6kBbQDrwoTc2BcjGS1_JeF5rDIVYrZmBhs5bgXWo" { t.Fatal() } pk, err = LoadPrivateKey([]byte(testECKey)) if err != nil { t.Fatal() } ka, err = KeyAuthorization(pk, "foo") if err != nil { t.Fatal() } if ka != "foo.S8MUz-12EEFgpVWWfDpvolnpTkuD9yVV6qHdzFuJyj8" { t.Fatalf("%v", ka) } ka, err = DNSKeyAuthorization(pk, "foo") if err != nil { t.Fatal() } if ka != "efdLQjp7LK3TpMZ4b5UsX-vVaexjtxTNfn1M3Shfqjo" { t.Fatalf("%v", ka) } hostname, err := TLSSNIHostname(pk, "foo") if err != nil { t.Fatal() } if hostname != "79f74b423a7b2cadd3a4c6786f952c5f.ebd569ec63b714cd7e7d4cdd285faa3a.acme.invalid" { t.Fatalf("%#v", hostname) } _, _, err = CreateTLSSNICertificate(hostname) if err != nil { t.Fatal() } } golang-gopkg-hlandau-acmeapi.v2-2.0.1/acmeutils/load.go000066400000000000000000000057371355172230600226770ustar00rootroot00000000000000// Package acmeutils provides miscellaneous ACME-related utility functions. package acmeutils import ( "crypto" "crypto/ecdsa" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "io" "strings" ) // Load one or more certificates from a sequence of PEM-encoded certificates. func LoadCertificates(pemBlock []byte) ([][]byte, error) { var derBlock *pem.Block var certs [][]byte for { derBlock, pemBlock = pem.Decode(pemBlock) if derBlock == nil { break } if derBlock.Type != "CERTIFICATE" { return nil, fmt.Errorf("is not a certificate") } certs = append(certs, derBlock.Bytes) } if len(certs) == 0 { return nil, fmt.Errorf("no certificates found") } return certs, nil } // Writes one or more DER-formatted certificates in PEM format. func SaveCertificates(w io.Writer, certificates ...[]byte) error { for _, c := range certificates { err := pem.Encode(w, &pem.Block{ Type: "CERTIFICATE", Bytes: c, }) if err != nil { return err } } return nil } // Load a PEM private key from a stream. func LoadPrivateKey(keyPEMBlock []byte) (crypto.PrivateKey, error) { var keyDERBlock *pem.Block for { keyDERBlock, keyPEMBlock = pem.Decode(keyPEMBlock) if keyDERBlock == nil { return nil, fmt.Errorf("failed to parse key PEM data") } if keyDERBlock.Type == "PRIVATE KEY" || strings.HasSuffix(keyDERBlock.Type, " PRIVATE KEY") { break } } pk, err := LoadPrivateKeyDER(keyDERBlock.Bytes) if err != nil { return nil, err } return pk, nil } // Parse a DER private key. The key can be RSA or ECDSA. PKCS8 containers are // supported. func LoadPrivateKeyDER(der []byte) (crypto.PrivateKey, error) { pk, err := x509.ParsePKCS1PrivateKey(der) if err == nil { return pk, nil } pk2, err := x509.ParsePKCS8PrivateKey(der) if err == nil { switch pk2 := pk2.(type) { case *rsa.PrivateKey, *ecdsa.PrivateKey: return pk2, nil default: return nil, fmt.Errorf("unknown private key type") } } epk, err := x509.ParseECPrivateKey(der) if err == nil { return epk, nil } return nil, fmt.Errorf("failed to parse private key") } // Write a private key in PEM form. func SavePrivateKey(w io.Writer, pk crypto.PrivateKey) error { var kb []byte var hdr string var err error switch v := pk.(type) { case *rsa.PrivateKey: kb = x509.MarshalPKCS1PrivateKey(v) hdr = "RSA PRIVATE KEY" case *ecdsa.PrivateKey: kb, err = x509.MarshalECPrivateKey(v) hdr = "EC PRIVATE KEY" default: return fmt.Errorf("unsupported private key type: %T", pk) } if err != nil { return err } err = pem.Encode(w, &pem.Block{ Type: hdr, Bytes: kb, }) if err != nil { return err } return nil } // Load a PEM CSR from a stream and return it in DER form. func LoadCSR(pemBlock []byte) ([]byte, error) { var derBlock *pem.Block for { derBlock, pemBlock = pem.Decode(pemBlock) if derBlock == nil { return nil, fmt.Errorf("failed to parse CSR PEM data") } if derBlock.Type == "CERTIFICATE REQUEST" { break } } return derBlock.Bytes, nil } golang-gopkg-hlandau-acmeapi.v2-2.0.1/acmeutils/load_test.go000066400000000000000000000762601355172230600237350ustar00rootroot00000000000000package acmeutils import ( "bytes" "crypto/ecdsa" "crypto/elliptic" "crypto/rsa" "math/big" "reflect" "testing" ) const testCerts = `-----BEGIN CERTIFICATE----- MIIEmzCCA4OgAwIBAgITAP8qTldZTvtEDieNbQA7/6O6mDANBgkqhkiG9w0BAQsF ADAfMR0wGwYDVQQDExRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNTExMjcxMTE1 MDBaFw0xNjAyMjUxMTE1MDBaMCkxJzAlBgNVBAMTHmRvbTEuYWNtZXRvb2wtdGVz dC5kZXZldmVyLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMKQ an2mcZAIWqu7J5/6n+KQ521+VlsJEe3189hLJfzTbxpDxag/DiJo2xcl4jZeEAkr FR4JZxs693lh92YnbSfWwZ0IvjEUNOYJGs9L59wADTu7wEoM2xAWWSUh85ziKwpb P/JE7i1D4+Mlll/xEcfbzpbYdlGKND76wAGyapJ4ozdXr2Bjtp4rEiMGjyOfgbur ex20Fyv1A40UwdaKdypnmtEyXIdP++/dj4yWGX3rszyO1erGzsCIng9EDhPreL3c 7uZVnZJrrlUPS7mPJzPfjUZhCWK9BQniAoSHiZiB0lAB2TUNU4gaqGgj4TDv1KPa pqP1spdcIWGqA+40j70CAwEAAaOCAcQwggHAMA4GA1UdDwEB/wQEAwIFoDAdBgNV HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E FgQUO1JtuzcEAU6kv0VQlEcL59SvNOQwHwYDVR0jBBgwFoAU+3hPEvlgFYMsnxd/ NBmzLjbqQYkwagYIKwYBBQUHAQEEXjBcMCYGCCsGAQUFBzABhhpodHRwOi8vMTI3 LjAuMC4xOjQwMDIvb2NzcDAyBggrBgEFBQcwAoYmaHR0cDovLzEyNy4wLjAuMTo0 MDAwL2FjbWUvaXNzdWVyLWNlcnQwSQYDVR0RBEIwQIIeZG9tMS5hY21ldG9vbC10 ZXN0LmRldmV2ZXIubmV0gh5kb20yLmFjbWV0b29sLXRlc3QuZGV2ZXZlci5uZXQw JwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL2V4YW1wbGUuY29tL2NybDBhBgNVHSAE WjBYMAgGBmeBDAECATBMBgMqAwQwRTAiBggrBgEFBQcCARYWaHR0cDovL2V4YW1w bGUuY29tL2NwczAfBggrBgEFBQcCAjATDBFEbyBXaGF0IFRob3UgV2lsdDANBgkq hkiG9w0BAQsFAAOCAQEAhpchBW1k++LrbyaCG0Y8dpJY01TDhKxNoMrEGTd7UH0F 7Ar+kBPOcf0gglvX6gcZzcJkILQTbBct5Lvqta+j/JMkseAoAFr31GWP7SdeOsmi txzWmbL+Mm256jqXPYewUiK1k9HpmPT9CajS6T/f2Q0RvRQgRD4e2B61kTRMt3t4 p7u0/wF5PDzsj8oC0D/yUSBU7icWHPkzuKhw+zjYZPoVdyEe0CoLMtTskPZooZby M+ngFyg3Boy1R4px/mmdV1fds5nHdZ+R6g2qe7FT7LB8KUVsUMEyCUiDIzeu1WLn vWa/xVfwBbKvgMjnHs31qODbsSzRRpNgX0NWE14jew== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEijCCA3KgAwIBAgICEk0wDQYJKoZIhvcNAQELBQAwKzEpMCcGA1UEAwwgY2Fj a2xpbmcgY3J5cHRvZ3JhcGhlciBmYWtlIFJPT1QwHhcNMTUxMDIxMjAxMTUyWhcN MjAxMDE5MjAxMTUyWjAfMR0wGwYDVQQDExRoYXBweSBoYWNrZXIgZmFrZSBDQTCC ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMIKR3maBcUSsncXYzQT13D5 Nr+Z3mLxMMh3TUdt6sACmqbJ0btRlgXfMtNLM2OU1I6a3Ju+tIZSdn2v21JBwvxU zpZQ4zy2cimIiMQDZCQHJwzC9GZn8HaW091iz9H0Go3A7WDXwYNmsdLNRi00o14U joaVqaPsYrZWvRKaIRqaU0hHmS0AWwQSvN/93iMIXuyiwywmkwKbWnnxCQ/gsctK FUtcNrwEx9Wgj6KlhwDTyI1QWSBbxVYNyUgPFzKxrSmwMO0yNff7ho+QT9x5+Y/7 XE59S4Mc4ZXxcXKew/gSlN9U5mvT+D2BhDtkCupdfsZNCQWp27A+b/DmrFI9NqsC AwEAAaOCAcIwggG+MBIGA1UdEwEB/wQIMAYBAf8CAQAwQwYDVR0eBDwwOqE4MAaC BC5taWwwCocIAAAAAAAAAAAwIocgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAwDgYDVR0PAQH/BAQDAgGGMH8GCCsGAQUFBwEBBHMwcTAyBggrBgEFBQcw AYYmaHR0cDovL2lzcmcudHJ1c3RpZC5vY3NwLmlkZW50cnVzdC5jb20wOwYIKwYB BQUHMAKGL2h0dHA6Ly9hcHBzLmlkZW50cnVzdC5jb20vcm9vdHMvZHN0cm9vdGNh eDMucDdjMB8GA1UdIwQYMBaAFOmkP+6epeby1dd5YDyTpi4kjpeqMFQGA1UdIARN MEswCAYGZ4EMAQIBMD8GCysGAQQBgt8TAQEBMDAwLgYIKwYBBQUHAgEWImh0dHA6 Ly9jcHMucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcwPAYDVR0fBDUwMzAxoC+gLYYr aHR0cDovL2NybC5pZGVudHJ1c3QuY29tL0RTVFJPT1RDQVgzQ1JMLmNybDAdBgNV HQ4EFgQU+3hPEvlgFYMsnxd/NBmzLjbqQYkwDQYJKoZIhvcNAQELBQADggEBAA0Y AeLXOklx4hhCikUUl+BdnFfn1g0W5AiQLVNIOL6PnqXu0wjnhNyhqdwnfhYMnoy4 idRh4lB6pz8Gf9pnlLd/DnWSV3gS+/I/mAl1dCkKby6H2V790e6IHmIK2KYm3jm+ U++FIdGpBdsQTSdmiX/rAyuxMDM0adMkNBwTfQmZQCz6nGHw1QcSPZMvZpsC8Skv ekzxsjF1otOrMUPNPQvtTWrVx8GlR2qfx/4xbQa1v2frNvFBCmO59goz+jnWvfTt j2NjwDZ7vlMBsPm16dbKYC840uvRoZjxqsdc3ChCZjqimFqlNG/xoPA8+dTicZzC XE9ijPIcvW6y1aa3bGw= -----END CERTIFICATE-----` var testCertsDER = [][]byte{ { 0x30, 0x82, 0x04, 0x9b, 0x30, 0x82, 0x03, 0x83, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x13, 0x00, 0xff, 0x2a, 0x4e, 0x57, 0x59, 0x4e, 0xfb, 0x44, 0x0e, 0x27, 0x8d, 0x6d, 0x00, 0x3b, 0xff, 0xa3, 0xba, 0x98, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x30, 0x1f, 0x31, 0x1d, 0x30, 0x1b, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x14, 0x68, 0x61, 0x70, 0x70, 0x79, 0x20, 0x68, 0x61, 0x63, 0x6b, 0x65, 0x72, 0x20, 0x66, 0x61, 0x6b, 0x65, 0x20, 0x43, 0x41, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x35, 0x31, 0x31, 0x32, 0x37, 0x31, 0x31, 0x31, 0x35, 0x30, 0x30, 0x5a, 0x17, 0x0d, 0x31, 0x36, 0x30, 0x32, 0x32, 0x35, 0x31, 0x31, 0x31, 0x35, 0x30, 0x30, 0x5a, 0x30, 0x29, 0x31, 0x27, 0x30, 0x25, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x1e, 0x64, 0x6f, 0x6d, 0x31, 0x2e, 0x61, 0x63, 0x6d, 0x65, 0x74, 0x6f, 0x6f, 0x6c, 0x2d, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x64, 0x65, 0x76, 0x65, 0x76, 0x65, 0x72, 0x2e, 0x6e, 0x65, 0x74, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xc2, 0x90, 0x6a, 0x7d, 0xa6, 0x71, 0x90, 0x08, 0x5a, 0xab, 0xbb, 0x27, 0x9f, 0xfa, 0x9f, 0xe2, 0x90, 0xe7, 0x6d, 0x7e, 0x56, 0x5b, 0x09, 0x11, 0xed, 0xf5, 0xf3, 0xd8, 0x4b, 0x25, 0xfc, 0xd3, 0x6f, 0x1a, 0x43, 0xc5, 0xa8, 0x3f, 0x0e, 0x22, 0x68, 0xdb, 0x17, 0x25, 0xe2, 0x36, 0x5e, 0x10, 0x09, 0x2b, 0x15, 0x1e, 0x09, 0x67, 0x1b, 0x3a, 0xf7, 0x79, 0x61, 0xf7, 0x66, 0x27, 0x6d, 0x27, 0xd6, 0xc1, 0x9d, 0x08, 0xbe, 0x31, 0x14, 0x34, 0xe6, 0x09, 0x1a, 0xcf, 0x4b, 0xe7, 0xdc, 0x00, 0x0d, 0x3b, 0xbb, 0xc0, 0x4a, 0x0c, 0xdb, 0x10, 0x16, 0x59, 0x25, 0x21, 0xf3, 0x9c, 0xe2, 0x2b, 0x0a, 0x5b, 0x3f, 0xf2, 0x44, 0xee, 0x2d, 0x43, 0xe3, 0xe3, 0x25, 0x96, 0x5f, 0xf1, 0x11, 0xc7, 0xdb, 0xce, 0x96, 0xd8, 0x76, 0x51, 0x8a, 0x34, 0x3e, 0xfa, 0xc0, 0x01, 0xb2, 0x6a, 0x92, 0x78, 0xa3, 0x37, 0x57, 0xaf, 0x60, 0x63, 0xb6, 0x9e, 0x2b, 0x12, 0x23, 0x06, 0x8f, 0x23, 0x9f, 0x81, 0xbb, 0xab, 0x7b, 0x1d, 0xb4, 0x17, 0x2b, 0xf5, 0x03, 0x8d, 0x14, 0xc1, 0xd6, 0x8a, 0x77, 0x2a, 0x67, 0x9a, 0xd1, 0x32, 0x5c, 0x87, 0x4f, 0xfb, 0xef, 0xdd, 0x8f, 0x8c, 0x96, 0x19, 0x7d, 0xeb, 0xb3, 0x3c, 0x8e, 0xd5, 0xea, 0xc6, 0xce, 0xc0, 0x88, 0x9e, 0x0f, 0x44, 0x0e, 0x13, 0xeb, 0x78, 0xbd, 0xdc, 0xee, 0xe6, 0x55, 0x9d, 0x92, 0x6b, 0xae, 0x55, 0x0f, 0x4b, 0xb9, 0x8f, 0x27, 0x33, 0xdf, 0x8d, 0x46, 0x61, 0x09, 0x62, 0xbd, 0x05, 0x09, 0xe2, 0x02, 0x84, 0x87, 0x89, 0x98, 0x81, 0xd2, 0x50, 0x01, 0xd9, 0x35, 0x0d, 0x53, 0x88, 0x1a, 0xa8, 0x68, 0x23, 0xe1, 0x30, 0xef, 0xd4, 0xa3, 0xda, 0xa6, 0xa3, 0xf5, 0xb2, 0x97, 0x5c, 0x21, 0x61, 0xaa, 0x03, 0xee, 0x34, 0x8f, 0xbd, 0x02, 0x03, 0x01, 0x00, 0x01, 0xa3, 0x82, 0x01, 0xc4, 0x30, 0x82, 0x01, 0xc0, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x1d, 0x0f, 0x01, 0x01, 0xff, 0x04, 0x04, 0x03, 0x02, 0x05, 0xa0, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x25, 0x04, 0x16, 0x30, 0x14, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02, 0x30, 0x0c, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, 0x02, 0x30, 0x00, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, 0x16, 0x04, 0x14, 0x3b, 0x52, 0x6d, 0xbb, 0x37, 0x04, 0x01, 0x4e, 0xa4, 0xbf, 0x45, 0x50, 0x94, 0x47, 0x0b, 0xe7, 0xd4, 0xaf, 0x34, 0xe4, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, 0x14, 0xfb, 0x78, 0x4f, 0x12, 0xf9, 0x60, 0x15, 0x83, 0x2c, 0x9f, 0x17, 0x7f, 0x34, 0x19, 0xb3, 0x2e, 0x36, 0xea, 0x41, 0x89, 0x30, 0x6a, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x01, 0x04, 0x5e, 0x30, 0x5c, 0x30, 0x26, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x86, 0x1a, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x32, 0x37, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31, 0x3a, 0x34, 0x30, 0x30, 0x32, 0x2f, 0x6f, 0x63, 0x73, 0x70, 0x30, 0x32, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x02, 0x86, 0x26, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x32, 0x37, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31, 0x3a, 0x34, 0x30, 0x30, 0x30, 0x2f, 0x61, 0x63, 0x6d, 0x65, 0x2f, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x2d, 0x63, 0x65, 0x72, 0x74, 0x30, 0x49, 0x06, 0x03, 0x55, 0x1d, 0x11, 0x04, 0x42, 0x30, 0x40, 0x82, 0x1e, 0x64, 0x6f, 0x6d, 0x31, 0x2e, 0x61, 0x63, 0x6d, 0x65, 0x74, 0x6f, 0x6f, 0x6c, 0x2d, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x64, 0x65, 0x76, 0x65, 0x76, 0x65, 0x72, 0x2e, 0x6e, 0x65, 0x74, 0x82, 0x1e, 0x64, 0x6f, 0x6d, 0x32, 0x2e, 0x61, 0x63, 0x6d, 0x65, 0x74, 0x6f, 0x6f, 0x6c, 0x2d, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x64, 0x65, 0x76, 0x65, 0x76, 0x65, 0x72, 0x2e, 0x6e, 0x65, 0x74, 0x30, 0x27, 0x06, 0x03, 0x55, 0x1d, 0x1f, 0x04, 0x20, 0x30, 0x1e, 0x30, 0x1c, 0xa0, 0x1a, 0xa0, 0x18, 0x86, 0x16, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x72, 0x6c, 0x30, 0x61, 0x06, 0x03, 0x55, 0x1d, 0x20, 0x04, 0x5a, 0x30, 0x58, 0x30, 0x08, 0x06, 0x06, 0x67, 0x81, 0x0c, 0x01, 0x02, 0x01, 0x30, 0x4c, 0x06, 0x03, 0x2a, 0x03, 0x04, 0x30, 0x45, 0x30, 0x22, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x01, 0x16, 0x16, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x70, 0x73, 0x30, 0x1f, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x02, 0x30, 0x13, 0x0c, 0x11, 0x44, 0x6f, 0x20, 0x57, 0x68, 0x61, 0x74, 0x20, 0x54, 0x68, 0x6f, 0x75, 0x20, 0x57, 0x69, 0x6c, 0x74, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x86, 0x97, 0x21, 0x05, 0x6d, 0x64, 0xfb, 0xe2, 0xeb, 0x6f, 0x26, 0x82, 0x1b, 0x46, 0x3c, 0x76, 0x92, 0x58, 0xd3, 0x54, 0xc3, 0x84, 0xac, 0x4d, 0xa0, 0xca, 0xc4, 0x19, 0x37, 0x7b, 0x50, 0x7d, 0x05, 0xec, 0x0a, 0xfe, 0x90, 0x13, 0xce, 0x71, 0xfd, 0x20, 0x82, 0x5b, 0xd7, 0xea, 0x07, 0x19, 0xcd, 0xc2, 0x64, 0x20, 0xb4, 0x13, 0x6c, 0x17, 0x2d, 0xe4, 0xbb, 0xea, 0xb5, 0xaf, 0xa3, 0xfc, 0x93, 0x24, 0xb1, 0xe0, 0x28, 0x00, 0x5a, 0xf7, 0xd4, 0x65, 0x8f, 0xed, 0x27, 0x5e, 0x3a, 0xc9, 0xa2, 0xb7, 0x1c, 0xd6, 0x99, 0xb2, 0xfe, 0x32, 0x6d, 0xb9, 0xea, 0x3a, 0x97, 0x3d, 0x87, 0xb0, 0x52, 0x22, 0xb5, 0x93, 0xd1, 0xe9, 0x98, 0xf4, 0xfd, 0x09, 0xa8, 0xd2, 0xe9, 0x3f, 0xdf, 0xd9, 0x0d, 0x11, 0xbd, 0x14, 0x20, 0x44, 0x3e, 0x1e, 0xd8, 0x1e, 0xb5, 0x91, 0x34, 0x4c, 0xb7, 0x7b, 0x78, 0xa7, 0xbb, 0xb4, 0xff, 0x01, 0x79, 0x3c, 0x3c, 0xec, 0x8f, 0xca, 0x02, 0xd0, 0x3f, 0xf2, 0x51, 0x20, 0x54, 0xee, 0x27, 0x16, 0x1c, 0xf9, 0x33, 0xb8, 0xa8, 0x70, 0xfb, 0x38, 0xd8, 0x64, 0xfa, 0x15, 0x77, 0x21, 0x1e, 0xd0, 0x2a, 0x0b, 0x32, 0xd4, 0xec, 0x90, 0xf6, 0x68, 0xa1, 0x96, 0xf2, 0x33, 0xe9, 0xe0, 0x17, 0x28, 0x37, 0x06, 0x8c, 0xb5, 0x47, 0x8a, 0x71, 0xfe, 0x69, 0x9d, 0x57, 0x57, 0xdd, 0xb3, 0x99, 0xc7, 0x75, 0x9f, 0x91, 0xea, 0x0d, 0xaa, 0x7b, 0xb1, 0x53, 0xec, 0xb0, 0x7c, 0x29, 0x45, 0x6c, 0x50, 0xc1, 0x32, 0x09, 0x48, 0x83, 0x23, 0x37, 0xae, 0xd5, 0x62, 0xe7, 0xbd, 0x66, 0xbf, 0xc5, 0x57, 0xf0, 0x05, 0xb2, 0xaf, 0x80, 0xc8, 0xe7, 0x1e, 0xcd, 0xf5, 0xa8, 0xe0, 0xdb, 0xb1, 0x2c, 0xd1, 0x46, 0x93, 0x60, 0x5f, 0x43, 0x56, 0x13, 0x5e, 0x23, 0x7b, }, { 0x30, 0x82, 0x04, 0x8a, 0x30, 0x82, 0x03, 0x72, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x12, 0x4d, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x30, 0x2b, 0x31, 0x29, 0x30, 0x27, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x20, 0x63, 0x61, 0x63, 0x6b, 0x6c, 0x69, 0x6e, 0x67, 0x20, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x67, 0x72, 0x61, 0x70, 0x68, 0x65, 0x72, 0x20, 0x66, 0x61, 0x6b, 0x65, 0x20, 0x52, 0x4f, 0x4f, 0x54, 0x30, 0x1e, 0x17, 0x0d, 0x31, 0x35, 0x31, 0x30, 0x32, 0x31, 0x32, 0x30, 0x31, 0x31, 0x35, 0x32, 0x5a, 0x17, 0x0d, 0x32, 0x30, 0x31, 0x30, 0x31, 0x39, 0x32, 0x30, 0x31, 0x31, 0x35, 0x32, 0x5a, 0x30, 0x1f, 0x31, 0x1d, 0x30, 0x1b, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x14, 0x68, 0x61, 0x70, 0x70, 0x79, 0x20, 0x68, 0x61, 0x63, 0x6b, 0x65, 0x72, 0x20, 0x66, 0x61, 0x6b, 0x65, 0x20, 0x43, 0x41, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xc2, 0x0a, 0x47, 0x79, 0x9a, 0x05, 0xc5, 0x12, 0xb2, 0x77, 0x17, 0x63, 0x34, 0x13, 0xd7, 0x70, 0xf9, 0x36, 0xbf, 0x99, 0xde, 0x62, 0xf1, 0x30, 0xc8, 0x77, 0x4d, 0x47, 0x6d, 0xea, 0xc0, 0x02, 0x9a, 0xa6, 0xc9, 0xd1, 0xbb, 0x51, 0x96, 0x05, 0xdf, 0x32, 0xd3, 0x4b, 0x33, 0x63, 0x94, 0xd4, 0x8e, 0x9a, 0xdc, 0x9b, 0xbe, 0xb4, 0x86, 0x52, 0x76, 0x7d, 0xaf, 0xdb, 0x52, 0x41, 0xc2, 0xfc, 0x54, 0xce, 0x96, 0x50, 0xe3, 0x3c, 0xb6, 0x72, 0x29, 0x88, 0x88, 0xc4, 0x03, 0x64, 0x24, 0x07, 0x27, 0x0c, 0xc2, 0xf4, 0x66, 0x67, 0xf0, 0x76, 0x96, 0xd3, 0xdd, 0x62, 0xcf, 0xd1, 0xf4, 0x1a, 0x8d, 0xc0, 0xed, 0x60, 0xd7, 0xc1, 0x83, 0x66, 0xb1, 0xd2, 0xcd, 0x46, 0x2d, 0x34, 0xa3, 0x5e, 0x14, 0x8e, 0x86, 0x95, 0xa9, 0xa3, 0xec, 0x62, 0xb6, 0x56, 0xbd, 0x12, 0x9a, 0x21, 0x1a, 0x9a, 0x53, 0x48, 0x47, 0x99, 0x2d, 0x00, 0x5b, 0x04, 0x12, 0xbc, 0xdf, 0xfd, 0xde, 0x23, 0x08, 0x5e, 0xec, 0xa2, 0xc3, 0x2c, 0x26, 0x93, 0x02, 0x9b, 0x5a, 0x79, 0xf1, 0x09, 0x0f, 0xe0, 0xb1, 0xcb, 0x4a, 0x15, 0x4b, 0x5c, 0x36, 0xbc, 0x04, 0xc7, 0xd5, 0xa0, 0x8f, 0xa2, 0xa5, 0x87, 0x00, 0xd3, 0xc8, 0x8d, 0x50, 0x59, 0x20, 0x5b, 0xc5, 0x56, 0x0d, 0xc9, 0x48, 0x0f, 0x17, 0x32, 0xb1, 0xad, 0x29, 0xb0, 0x30, 0xed, 0x32, 0x35, 0xf7, 0xfb, 0x86, 0x8f, 0x90, 0x4f, 0xdc, 0x79, 0xf9, 0x8f, 0xfb, 0x5c, 0x4e, 0x7d, 0x4b, 0x83, 0x1c, 0xe1, 0x95, 0xf1, 0x71, 0x72, 0x9e, 0xc3, 0xf8, 0x12, 0x94, 0xdf, 0x54, 0xe6, 0x6b, 0xd3, 0xf8, 0x3d, 0x81, 0x84, 0x3b, 0x64, 0x0a, 0xea, 0x5d, 0x7e, 0xc6, 0x4d, 0x09, 0x05, 0xa9, 0xdb, 0xb0, 0x3e, 0x6f, 0xf0, 0xe6, 0xac, 0x52, 0x3d, 0x36, 0xab, 0x02, 0x03, 0x01, 0x00, 0x01, 0xa3, 0x82, 0x01, 0xc2, 0x30, 0x82, 0x01, 0xbe, 0x30, 0x12, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, 0x08, 0x30, 0x06, 0x01, 0x01, 0xff, 0x02, 0x01, 0x00, 0x30, 0x43, 0x06, 0x03, 0x55, 0x1d, 0x1e, 0x04, 0x3c, 0x30, 0x3a, 0xa1, 0x38, 0x30, 0x06, 0x82, 0x04, 0x2e, 0x6d, 0x69, 0x6c, 0x30, 0x0a, 0x87, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x22, 0x87, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x1d, 0x0f, 0x01, 0x01, 0xff, 0x04, 0x04, 0x03, 0x02, 0x01, 0x86, 0x30, 0x7f, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x01, 0x01, 0x04, 0x73, 0x30, 0x71, 0x30, 0x32, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01, 0x86, 0x26, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x69, 0x73, 0x72, 0x67, 0x2e, 0x74, 0x72, 0x75, 0x73, 0x74, 0x69, 0x64, 0x2e, 0x6f, 0x63, 0x73, 0x70, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x72, 0x75, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x30, 0x3b, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x02, 0x86, 0x2f, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x61, 0x70, 0x70, 0x73, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x72, 0x75, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x72, 0x6f, 0x6f, 0x74, 0x73, 0x2f, 0x64, 0x73, 0x74, 0x72, 0x6f, 0x6f, 0x74, 0x63, 0x61, 0x78, 0x33, 0x2e, 0x70, 0x37, 0x63, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, 0x14, 0xe9, 0xa4, 0x3f, 0xee, 0x9e, 0xa5, 0xe6, 0xf2, 0xd5, 0xd7, 0x79, 0x60, 0x3c, 0x93, 0xa6, 0x2e, 0x24, 0x8e, 0x97, 0xaa, 0x30, 0x54, 0x06, 0x03, 0x55, 0x1d, 0x20, 0x04, 0x4d, 0x30, 0x4b, 0x30, 0x08, 0x06, 0x06, 0x67, 0x81, 0x0c, 0x01, 0x02, 0x01, 0x30, 0x3f, 0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xdf, 0x13, 0x01, 0x01, 0x01, 0x30, 0x30, 0x30, 0x2e, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x02, 0x01, 0x16, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x63, 0x70, 0x73, 0x2e, 0x72, 0x6f, 0x6f, 0x74, 0x2d, 0x78, 0x31, 0x2e, 0x6c, 0x65, 0x74, 0x73, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2e, 0x6f, 0x72, 0x67, 0x30, 0x3c, 0x06, 0x03, 0x55, 0x1d, 0x1f, 0x04, 0x35, 0x30, 0x33, 0x30, 0x31, 0xa0, 0x2f, 0xa0, 0x2d, 0x86, 0x2b, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x63, 0x72, 0x6c, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x72, 0x75, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x44, 0x53, 0x54, 0x52, 0x4f, 0x4f, 0x54, 0x43, 0x41, 0x58, 0x33, 0x43, 0x52, 0x4c, 0x2e, 0x63, 0x72, 0x6c, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, 0x16, 0x04, 0x14, 0xfb, 0x78, 0x4f, 0x12, 0xf9, 0x60, 0x15, 0x83, 0x2c, 0x9f, 0x17, 0x7f, 0x34, 0x19, 0xb3, 0x2e, 0x36, 0xea, 0x41, 0x89, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x0d, 0x18, 0x01, 0xe2, 0xd7, 0x3a, 0x49, 0x71, 0xe2, 0x18, 0x42, 0x8a, 0x45, 0x14, 0x97, 0xe0, 0x5d, 0x9c, 0x57, 0xe7, 0xd6, 0x0d, 0x16, 0xe4, 0x08, 0x90, 0x2d, 0x53, 0x48, 0x38, 0xbe, 0x8f, 0x9e, 0xa5, 0xee, 0xd3, 0x08, 0xe7, 0x84, 0xdc, 0xa1, 0xa9, 0xdc, 0x27, 0x7e, 0x16, 0x0c, 0x9e, 0x8c, 0xb8, 0x89, 0xd4, 0x61, 0xe2, 0x50, 0x7a, 0xa7, 0x3f, 0x06, 0x7f, 0xda, 0x67, 0x94, 0xb7, 0x7f, 0x0e, 0x75, 0x92, 0x57, 0x78, 0x12, 0xfb, 0xf2, 0x3f, 0x98, 0x09, 0x75, 0x74, 0x29, 0x0a, 0x6f, 0x2e, 0x87, 0xd9, 0x5e, 0xfd, 0xd1, 0xee, 0x88, 0x1e, 0x62, 0x0a, 0xd8, 0xa6, 0x26, 0xde, 0x39, 0xbe, 0x53, 0xef, 0x85, 0x21, 0xd1, 0xa9, 0x05, 0xdb, 0x10, 0x4d, 0x27, 0x66, 0x89, 0x7f, 0xeb, 0x03, 0x2b, 0xb1, 0x30, 0x33, 0x34, 0x69, 0xd3, 0x24, 0x34, 0x1c, 0x13, 0x7d, 0x09, 0x99, 0x40, 0x2c, 0xfa, 0x9c, 0x61, 0xf0, 0xd5, 0x07, 0x12, 0x3d, 0x93, 0x2f, 0x66, 0x9b, 0x02, 0xf1, 0x29, 0x2f, 0x7a, 0x4c, 0xf1, 0xb2, 0x31, 0x75, 0xa2, 0xd3, 0xab, 0x31, 0x43, 0xcd, 0x3d, 0x0b, 0xed, 0x4d, 0x6a, 0xd5, 0xc7, 0xc1, 0xa5, 0x47, 0x6a, 0x9f, 0xc7, 0xfe, 0x31, 0x6d, 0x06, 0xb5, 0xbf, 0x67, 0xeb, 0x36, 0xf1, 0x41, 0x0a, 0x63, 0xb9, 0xf6, 0x0a, 0x33, 0xfa, 0x39, 0xd6, 0xbd, 0xf4, 0xed, 0x8f, 0x63, 0x63, 0xc0, 0x36, 0x7b, 0xbe, 0x53, 0x01, 0xb0, 0xf9, 0xb5, 0xe9, 0xd6, 0xca, 0x60, 0x2f, 0x38, 0xd2, 0xeb, 0xd1, 0xa1, 0x98, 0xf1, 0xaa, 0xc7, 0x5c, 0xdc, 0x28, 0x42, 0x66, 0x3a, 0xa2, 0x98, 0x5a, 0xa5, 0x34, 0x6f, 0xf1, 0xa0, 0xf0, 0x3c, 0xf9, 0xd4, 0xe2, 0x71, 0x9c, 0xc2, 0x5c, 0x4f, 0x62, 0x8c, 0xf2, 0x1c, 0xbd, 0x6e, 0xb2, 0xd5, 0xa6, 0xb7, 0x6c, 0x6c, }, } func TestLoadCertificates(t *testing.T) { b, err := LoadCertificates([]byte(testCerts)) if err != nil { t.Fatalf("cannot load certificates") } if !reflect.DeepEqual(b, testCertsDER) { t.Fatalf("certificate load mismatch") } } const testKey = `-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAwpBqfaZxkAhaq7snn/qf4pDnbX5WWwkR7fXz2Esl/NNvGkPF qD8OImjbFyXiNl4QCSsVHglnGzr3eWH3ZidtJ9bBnQi+MRQ05gkaz0vn3AANO7vA SgzbEBZZJSHznOIrCls/8kTuLUPj4yWWX/ERx9vOlth2UYo0PvrAAbJqknijN1ev YGO2nisSIwaPI5+Bu6t7HbQXK/UDjRTB1op3Kmea0TJch0/7792PjJYZfeuzPI7V 6sbOwIieD0QOE+t4vdzu5lWdkmuuVQ9LuY8nM9+NRmEJYr0FCeIChIeJmIHSUAHZ NQ1TiBqoaCPhMO/Uo9qmo/Wyl1whYaoD7jSPvQIDAQABAoIBAH69WlE9Ui6T8pR5 lsdUmEAbSlX9/TzR/Lb5B332/ejixjXivefqI0fw6/75M5Fc1q9SPDBTybFoSPru AR2vQyC3eWnU3yfTVN4gQjGU7ZVXB0fI9uqF01F4bVuN8UZZ4dWeOVqU4l3kIpe9 zWX0ADQjkECdAv38vCnmZ5rzYQ7FmPoZse8fC7TqJIqvMN2lkBQd/7ecfSGiDsxS jdV/a2gE+AuS1d06yfLiu8UuRYe7le1VEx84NHBAQv71IG1kc+9bKWqOlXPXvS7o WQi976wETxbqEbZLtQlHhX7ND/Svof+O/IwbfHcfz1qhMGZ8wP4CeuLQGXpVqSpc PWTR/bUCgYEAy6zdOmvpj/Ss6o2oHWqXek6PKWVmnE13NZ45GrWaBT8tG9znJRoG qA3Cj5BedJSzCEfrVZ4Yi29PrRl7HbHYEcIiJZ1rwdmPmlRK/VWgk8RpBAzT/ilS 90jTXBPlA/WvzMgwtO0hJEbnpvH5EB2WviyNWgvRhwdzPWqJVEy/S3MCgYEA9IxW 2VU1Rnx1mcudl0Y68pcLU25pXA7VsA5IsyCk1a5rwecgRDF3dVJ1lJGp8+Xf4/Wo eDhqk1tdTAp0Uy+GfJcTkrAJZO1jeYoJj6Hy4QT50xPNMQZHZ0uOkIbLfHHcXJHa 9qcEIzKpcPiq5cD0PYqJQO3f3gw5V/aLKCTDTA8CgYAYQmi/UqcpLF2EOocxqcaN HNpUde145Ik0a7olgkfsJBA9Z1xowObWEX2BGBMIE8YXUGmpcwE9am61EXnmmY5A 3zyt+kgheMZv/WZJMm/D6fsQLm75sPZe2d/C8eOvSw47eATFKBFwfrmM2vltf/i6 ghf8B7hXOv3w7MWasuF+HQKBgEixQsu9uWVnb6m0zfZ+qN0dqv807dqwijKYPGIK zRS7kUqFQqEityjHxy3Pkt5uMXxYtKhv1LZSzUviO6vSj76PRgEvlMtMiWpCbw8k C8d5rC1jUHZHMIhy/EDX4LrwOZnGvXjeMqunzphHQr2i+rckbCJB02704ULVhY38 R5VfAoGBAISqeTh28iF9SirK18xpsW6c6j+x6qfjs8Rr6pPAnO1vovUCH6vlxV4I RLeWOCS9ROFhbef4WqWpa82vGd0OxoUBirPRWRFyt0pgH9qAmwqD2YN8KFCyPztD xtiaTxLKSHYuEVxbJZJmDNkjDEtGk7hVzOQ0+GO+Tvknkp/kkHsn -----END RSA PRIVATE KEY-----` const testKeyPKCS8 = `-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCkGp9pnGQCFqr uyef+p/ikOdtflZbCRHt9fPYSyX8028aQ8WoPw4iaNsXJeI2XhAJKxUeCWcbOvd5 YfdmJ20n1sGdCL4xFDTmCRrPS+fcAA07u8BKDNsQFlklIfOc4isKWz/yRO4tQ+Pj JZZf8RHH286W2HZRijQ++sABsmqSeKM3V69gY7aeKxIjBo8jn4G7q3sdtBcr9QON FMHWincqZ5rRMlyHT/vv3Y+Mlhl967M8jtXqxs7AiJ4PRA4T63i93O7mVZ2Sa65V D0u5jycz341GYQlivQUJ4gKEh4mYgdJQAdk1DVOIGqhoI+Ew79Sj2qaj9bKXXCFh qgPuNI+9AgMBAAECggEAfr1aUT1SLpPylHmWx1SYQBtKVf39PNH8tvkHffb96OLG NeK95+ojR/Dr/vkzkVzWr1I8MFPJsWhI+u4BHa9DILd5adTfJ9NU3iBCMZTtlVcH R8j26oXTUXhtW43xRlnh1Z45WpTiXeQil73NZfQANCOQQJ0C/fy8KeZnmvNhDsWY +hmx7x8LtOokiq8w3aWQFB3/t5x9IaIOzFKN1X9raAT4C5LV3TrJ8uK7xS5Fh7uV 7VUTHzg0cEBC/vUgbWRz71spao6Vc9e9LuhZCL3vrARPFuoRtku1CUeFfs0P9K+h /478jBt8dx/PWqEwZnzA/gJ64tAZelWpKlw9ZNH9tQKBgQDLrN06a+mP9Kzqjagd apd6To8pZWacTXc1njkatZoFPy0b3OclGgaoDcKPkF50lLMIR+tVnhiLb0+tGXsd sdgRwiIlnWvB2Y+aVEr9VaCTxGkEDNP+KVL3SNNcE+UD9a/MyDC07SEkRuem8fkQ HZa+LI1aC9GHB3M9aolUTL9LcwKBgQD0jFbZVTVGfHWZy52XRjrylwtTbmlcDtWw DkizIKTVrmvB5yBEMXd1UnWUkanz5d/j9ah4OGqTW11MCnRTL4Z8lxOSsAlk7WN5 igmPofLhBPnTE80xBkdnS46Qhst8cdxckdr2pwQjMqlw+KrlwPQ9iolA7d/eDDlX 9osoJMNMDwKBgBhCaL9SpyksXYQ6hzGpxo0c2lR17XjkiTRruiWCR+wkED1nXGjA 5tYRfYEYEwgTxhdQaalzAT1qbrUReeaZjkDfPK36SCF4xm/9Zkkyb8Pp+xAubvmw 9l7Z38Lx469LDjt4BMUoEXB+uYza+W1/+LqCF/wHuFc6/fDsxZqy4X4dAoGASLFC y725ZWdvqbTN9n6o3R2q/zTt2rCKMpg8YgrNFLuRSoVCoSK3KMfHLc+S3m4xfFi0 qG/UtlLNS+I7q9KPvo9GAS+Uy0yJakJvDyQLx3msLWNQdkcwiHL8QNfguvA5mca9 eN4yq6fOmEdCvaL6tyRsIkHTbvThQtWFjfxHlV8CgYEAhKp5OHbyIX1KKsrXzGmx bpzqP7Hqp+OzxGvqk8Cc7W+i9QIfq+XFXghEt5Y4JL1E4WFt5/hapalrza8Z3Q7G hQGKs9FZEXK3SmAf2oCbCoPZg3woULI/O0PG2JpPEspIdi4RXFslkmYM2SMMS0aT uFXM5DT4Y75O+SeSn+SQeyc= -----END PRIVATE KEY-----` var testKeyN, _ = big.NewInt(0).SetString("00c2906a7da67190085aabbb279ffa9fe290e76d7e565b0911edf5f3d84b25fcd36f1a43c5a83f0e2268db1725e2365e10092b151e09671b3af77961f766276d27d6c19d08be311434e6091acf4be7dc000d3bbbc04a0cdb1016592521f39ce22b0a5b3ff244ee2d43e3e325965ff111c7dbce96d876518a343efac001b26a9278a33757af6063b69e2b1223068f239f81bbab7b1db4172bf5038d14c1d68a772a679ad1325c874ffbefdd8f8c96197debb33c8ed5eac6cec0889e0f440e13eb78bddceee6559d926bae550f4bb98f2733df8d46610962bd0509e2028487899881d25001d9350d53881aa86823e130efd4a3daa6a3f5b2975c2161aa03ee348fbd", 16) var testKeyD, _ = big.NewInt(0).SetString("7ebd5a513d522e93f2947996c75498401b4a55fdfd3cd1fcb6f9077df6fde8e2c635e2bde7ea2347f0ebfef933915cd6af523c3053c9b16848faee011daf4320b77969d4df27d354de20423194ed95570747c8f6ea85d351786d5b8df14659e1d59e395a94e25de42297bdcd65f400342390409d02fdfcbc29e6679af3610ec598fa19b1ef1f0bb4ea248aaf30dda590141dffb79c7d21a20ecc528dd57f6b6804f80b92d5dd3ac9f2e2bbc52e4587bb95ed55131f3834704042fef5206d6473ef5b296a8e9573d7bd2ee85908bdefac044f16ea11b64bb50947857ecd0ff4afa1ff8efc8c1b7c771fcf5aa130667cc0fe027ae2d0197a55a92a5c3d64d1fdb5", 16) var testKeyPrime1, _ = big.NewInt(0).SetString("00cbacdd3a6be98ff4acea8da81d6a977a4e8f2965669c4d77359e391ab59a053f2d1bdce7251a06a80dc28f905e7494b30847eb559e188b6f4fad197b1db1d811c222259d6bc1d98f9a544afd55a093c469040cd3fe2952f748d35c13e503f5afccc830b4ed212446e7a6f1f9101d96be2c8d5a0bd18707733d6a89544cbf4b73", 16) var testKeyPrime2, _ = big.NewInt(0). SetString("00f48c56d95535467c7599cb9d97463af2970b536e695c0ed5b00e48b320a4d5ae6bc1e7204431777552759491a9f3e5dfe3f5a878386a935b5d4c0a74532f867c971392b00964ed63798a098fa1f2e104f9d313cd310647674b8e9086cb7c71dc5c91daf6a7042332a970f8aae5c0f43d8a8940eddfde0c3957f68b2824c34c0f", 16) var testKeyDp, _ = big.NewInt(0).SetString("184268bf52a7292c5d843a8731a9c68d1cda5475ed78e489346bba258247ec24103d675c68c0e6d6117d8118130813c6175069a973013d6a6eb51179e6998e40df3cadfa482178c66ffd6649326fc3e9fb102e6ef9b0f65ed9dfc2f1e3af4b0e3b7804c52811707eb98cdaf96d7ff8ba8217fc07b8573afdf0ecc59ab2e17e1d", 16) var testKeyDq, _ = big.NewInt(0).SetString("48b142cbbdb965676fa9b4cdf67ea8dd1daaff34eddab08a32983c620acd14bb914a8542a122b728c7c72dcf92de6e317c58b4a86fd4b652cd4be23babd28fbe8f46012f94cb4c896a426f0f240bc779ac2d63507647308872fc40d7e0baf03999c6bd78de32aba7ce984742bda2fab7246c2241d36ef4e142d5858dfc47955f", 16) var testKeyQinv, _ = big.NewInt(0).SetString("0084aa793876f2217d4a2acad7cc69b16e9cea3fb1eaa7e3b3c46bea93c09ced6fa2f5021fabe5c55e0844b7963824bd44e1616de7f85aa5a96bcdaf19dd0ec685018ab3d1591172b74a601fda809b0a83d9837c2850b23f3b43c6d89a4f12ca48762e115c5b2592660cd9230c4b4693b855cce434f863be4ef927929fe4907b27", 16) var testKeyValue = &rsa.PrivateKey{ PublicKey: rsa.PublicKey{ N: testKeyN, E: 0x10001, }, D: testKeyD, Primes: []*big.Int{ testKeyPrime1, testKeyPrime2, }, Precomputed: rsa.PrecomputedValues{ Dp: testKeyDp, Dq: testKeyDq, Qinv: testKeyQinv, CRTValues: []rsa.CRTValue{}, }, } const testECKey = `-----BEGIN EC PRIVATE KEY----- MHcCAQEEIABVf3JmY6Fs0MpQwXlUCw0T27BO+hjjr93v/nLT9PrZoAoGCCqGSM49 AwEHoUQDQgAEkDk1q0XGYa6flpAaBVHEI5UStWvdewAHpIbN+PpOlKNgeK1Xu0nw Pv7jwLK+95tYBYbVu1gUnQ+OjWpxyuITaA== -----END EC PRIVATE KEY-----` const testECKeyPKCS8 = `-----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgAFV/cmZjoWzQylDB eVQLDRPbsE76GOOv3e/+ctP0+tmhRANCAASQOTWrRcZhrp+WkBoFUcQjlRK1a917 AAekhs34+k6Uo2B4rVe7SfA+/uPAsr73m1gFhtW7WBSdD46NanHK4hNo -----END PRIVATE KEY-----` var testECKeyX, _ = big.NewInt(0).SetString("903935ab45c661ae9f96901a0551c4239512b56bdd7b0007a486cdf8fa4e94a3", 16) var testECKeyY, _ = big.NewInt(0).SetString("6078ad57bb49f03efee3c0b2bef79b580586d5bb58149d0f8e8d6a71cae21368", 16) var testECKeyD, _ = big.NewInt(0).SetString("557f726663a16cd0ca50c179540b0d13dbb04efa18e3afddeffe72d3f4fad9", 16) var testECKeyValue = &ecdsa.PrivateKey{ PublicKey: ecdsa.PublicKey{ Curve: elliptic.P256(), X: testECKeyX, Y: testECKeyY, }, D: testECKeyD, } func TestLoadKey(t *testing.T) { pk, err := LoadPrivateKey([]byte(testKey)) if err != nil { t.Fatalf("failed to load private key: %v", err) } if !reflect.DeepEqual(testKeyValue, pk) { t.Fatalf("key mismatch: %#v %#v", testKeyValue, pk) } pk2, err := LoadPrivateKey([]byte(testKeyPKCS8)) if err != nil { t.Fatalf("failed to load private key: %v", err) } if !reflect.DeepEqual(testKeyValue, pk2) { t.Fatalf("key mismatch: %#v %#v", testKeyValue, pk2) } epk, err := LoadPrivateKey([]byte(testECKey)) if err != nil { t.Fatalf("failed to load EC private key: %v", err) } if !reflect.DeepEqual(epk, testECKeyValue) { t.Fatalf("EC key mismatch: %#v %#v", epk, testECKeyValue) } epk2, err := LoadPrivateKey([]byte(testECKeyPKCS8)) if err != nil { t.Fatalf("failed to load EC private key: %v", err) } if !reflect.DeepEqual(epk2, testECKeyValue) { t.Fatalf("EC key mismatch: %#v %#v", epk2, testECKeyValue) } b := bytes.Buffer{} err = SavePrivateKey(&b, pk) if err != nil { t.Fatalf("%v", err) } pkc, err := LoadPrivateKey(b.Bytes()) if err != nil { t.Fatalf("%v", err) } if !reflect.DeepEqual(pk, pkc) { t.Fatalf("mismatch after save-load") } b = bytes.Buffer{} err = SavePrivateKey(&b, epk) if err != nil { t.Fatalf("%v", err) } epkc, err := LoadPrivateKey(b.Bytes()) if err != nil { t.Fatalf("%v", err) } if !reflect.DeepEqual(epk, epkc) { t.Fatalf("mismatch after save-load") } } const testCSR = `-----BEGIN CERTIFICATE REQUEST----- MIICWzCCAUMCAQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3 DQEBAQUAA4IBDwAwggEKAoIBAQDCkGp9pnGQCFqruyef+p/ikOdtflZbCRHt9fPY SyX8028aQ8WoPw4iaNsXJeI2XhAJKxUeCWcbOvd5YfdmJ20n1sGdCL4xFDTmCRrP S+fcAA07u8BKDNsQFlklIfOc4isKWz/yRO4tQ+PjJZZf8RHH286W2HZRijQ++sAB smqSeKM3V69gY7aeKxIjBo8jn4G7q3sdtBcr9QONFMHWincqZ5rRMlyHT/vv3Y+M lhl967M8jtXqxs7AiJ4PRA4T63i93O7mVZ2Sa65VD0u5jycz341GYQlivQUJ4gKE h4mYgdJQAdk1DVOIGqhoI+Ew79Sj2qaj9bKXXCFhqgPuNI+9AgMBAAGgADANBgkq hkiG9w0BAQsFAAOCAQEAGwrSUMmte+rXVUVsYS7cqN0xJFOc18vuMDUezAsAw8Ye UDhivw2wsPTlPBl3zpVavlGSM5ZPWgv4osZtDeS/VeL+ow/7wVShlc2rbyuXTwoq Lh4+Oe4svEEfGwvia5Ui4XA2eFFLSaTVM+FWGZNUXoB++bS831ro2fffoI3jrjDz 0edY1zhsjaV9Fej9k37O/1GhkRfTq0WMAnX/VP6L/2Gjs2ZwvAwCItUpCwPw1B3Q h+/TTT56DupPyRFI1ZmbpX6Rp/A0gL+ykqpjASJj1ai+jkBXiDz/I4LJ2oUhtOxJ vGQkX0wJF929X+IQlURO6rP5ET9tzmF4S9/F/RA1kw== -----END CERTIFICATE REQUEST-----` var testCSRDER = []byte{ 0x30, 0x82, 0x02, 0x5b, 0x30, 0x82, 0x01, 0x43, 0x02, 0x01, 0x00, 0x30, 0x16, 0x31, 0x14, 0x30, 0x12, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xc2, 0x90, 0x6a, 0x7d, 0xa6, 0x71, 0x90, 0x08, 0x5a, 0xab, 0xbb, 0x27, 0x9f, 0xfa, 0x9f, 0xe2, 0x90, 0xe7, 0x6d, 0x7e, 0x56, 0x5b, 0x09, 0x11, 0xed, 0xf5, 0xf3, 0xd8, 0x4b, 0x25, 0xfc, 0xd3, 0x6f, 0x1a, 0x43, 0xc5, 0xa8, 0x3f, 0x0e, 0x22, 0x68, 0xdb, 0x17, 0x25, 0xe2, 0x36, 0x5e, 0x10, 0x09, 0x2b, 0x15, 0x1e, 0x09, 0x67, 0x1b, 0x3a, 0xf7, 0x79, 0x61, 0xf7, 0x66, 0x27, 0x6d, 0x27, 0xd6, 0xc1, 0x9d, 0x08, 0xbe, 0x31, 0x14, 0x34, 0xe6, 0x09, 0x1a, 0xcf, 0x4b, 0xe7, 0xdc, 0x00, 0x0d, 0x3b, 0xbb, 0xc0, 0x4a, 0x0c, 0xdb, 0x10, 0x16, 0x59, 0x25, 0x21, 0xf3, 0x9c, 0xe2, 0x2b, 0x0a, 0x5b, 0x3f, 0xf2, 0x44, 0xee, 0x2d, 0x43, 0xe3, 0xe3, 0x25, 0x96, 0x5f, 0xf1, 0x11, 0xc7, 0xdb, 0xce, 0x96, 0xd8, 0x76, 0x51, 0x8a, 0x34, 0x3e, 0xfa, 0xc0, 0x01, 0xb2, 0x6a, 0x92, 0x78, 0xa3, 0x37, 0x57, 0xaf, 0x60, 0x63, 0xb6, 0x9e, 0x2b, 0x12, 0x23, 0x06, 0x8f, 0x23, 0x9f, 0x81, 0xbb, 0xab, 0x7b, 0x1d, 0xb4, 0x17, 0x2b, 0xf5, 0x03, 0x8d, 0x14, 0xc1, 0xd6, 0x8a, 0x77, 0x2a, 0x67, 0x9a, 0xd1, 0x32, 0x5c, 0x87, 0x4f, 0xfb, 0xef, 0xdd, 0x8f, 0x8c, 0x96, 0x19, 0x7d, 0xeb, 0xb3, 0x3c, 0x8e, 0xd5, 0xea, 0xc6, 0xce, 0xc0, 0x88, 0x9e, 0x0f, 0x44, 0x0e, 0x13, 0xeb, 0x78, 0xbd, 0xdc, 0xee, 0xe6, 0x55, 0x9d, 0x92, 0x6b, 0xae, 0x55, 0x0f, 0x4b, 0xb9, 0x8f, 0x27, 0x33, 0xdf, 0x8d, 0x46, 0x61, 0x09, 0x62, 0xbd, 0x05, 0x09, 0xe2, 0x02, 0x84, 0x87, 0x89, 0x98, 0x81, 0xd2, 0x50, 0x01, 0xd9, 0x35, 0x0d, 0x53, 0x88, 0x1a, 0xa8, 0x68, 0x23, 0xe1, 0x30, 0xef, 0xd4, 0xa3, 0xda, 0xa6, 0xa3, 0xf5, 0xb2, 0x97, 0x5c, 0x21, 0x61, 0xaa, 0x03, 0xee, 0x34, 0x8f, 0xbd, 0x02, 0x03, 0x01, 0x00, 0x01, 0xa0, 0x00, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x1b, 0x0a, 0xd2, 0x50, 0xc9, 0xad, 0x7b, 0xea, 0xd7, 0x55, 0x45, 0x6c, 0x61, 0x2e, 0xdc, 0xa8, 0xdd, 0x31, 0x24, 0x53, 0x9c, 0xd7, 0xcb, 0xee, 0x30, 0x35, 0x1e, 0xcc, 0x0b, 0x00, 0xc3, 0xc6, 0x1e, 0x50, 0x38, 0x62, 0xbf, 0x0d, 0xb0, 0xb0, 0xf4, 0xe5, 0x3c, 0x19, 0x77, 0xce, 0x95, 0x5a, 0xbe, 0x51, 0x92, 0x33, 0x96, 0x4f, 0x5a, 0x0b, 0xf8, 0xa2, 0xc6, 0x6d, 0x0d, 0xe4, 0xbf, 0x55, 0xe2, 0xfe, 0xa3, 0x0f, 0xfb, 0xc1, 0x54, 0xa1, 0x95, 0xcd, 0xab, 0x6f, 0x2b, 0x97, 0x4f, 0x0a, 0x2a, 0x2e, 0x1e, 0x3e, 0x39, 0xee, 0x2c, 0xbc, 0x41, 0x1f, 0x1b, 0x0b, 0xe2, 0x6b, 0x95, 0x22, 0xe1, 0x70, 0x36, 0x78, 0x51, 0x4b, 0x49, 0xa4, 0xd5, 0x33, 0xe1, 0x56, 0x19, 0x93, 0x54, 0x5e, 0x80, 0x7e, 0xf9, 0xb4, 0xbc, 0xdf, 0x5a, 0xe8, 0xd9, 0xf7, 0xdf, 0xa0, 0x8d, 0xe3, 0xae, 0x30, 0xf3, 0xd1, 0xe7, 0x58, 0xd7, 0x38, 0x6c, 0x8d, 0xa5, 0x7d, 0x15, 0xe8, 0xfd, 0x93, 0x7e, 0xce, 0xff, 0x51, 0xa1, 0x91, 0x17, 0xd3, 0xab, 0x45, 0x8c, 0x02, 0x75, 0xff, 0x54, 0xfe, 0x8b, 0xff, 0x61, 0xa3, 0xb3, 0x66, 0x70, 0xbc, 0x0c, 0x02, 0x22, 0xd5, 0x29, 0x0b, 0x03, 0xf0, 0xd4, 0x1d, 0xd0, 0x87, 0xef, 0xd3, 0x4d, 0x3e, 0x7a, 0x0e, 0xea, 0x4f, 0xc9, 0x11, 0x48, 0xd5, 0x99, 0x9b, 0xa5, 0x7e, 0x91, 0xa7, 0xf0, 0x34, 0x80, 0xbf, 0xb2, 0x92, 0xaa, 0x63, 0x01, 0x22, 0x63, 0xd5, 0xa8, 0xbe, 0x8e, 0x40, 0x57, 0x88, 0x3c, 0xff, 0x23, 0x82, 0xc9, 0xda, 0x85, 0x21, 0xb4, 0xec, 0x49, 0xbc, 0x64, 0x24, 0x5f, 0x4c, 0x09, 0x17, 0xdd, 0xbd, 0x5f, 0xe2, 0x10, 0x95, 0x44, 0x4e, 0xea, 0xb3, 0xf9, 0x11, 0x3f, 0x6d, 0xce, 0x61, 0x78, 0x4b, 0xdf, 0xc5, 0xfd, 0x10, 0x35, 0x93, } func TestLoadCSR(t *testing.T) { b, err := LoadCSR([]byte(testCSR)) if err != nil { t.Fatalf("load csr") } if !reflect.DeepEqual(b, testCSRDER) { t.Fatalf("bad csr") } } golang-gopkg-hlandau-acmeapi.v2-2.0.1/api-res.go000066400000000000000000000321741355172230600213250ustar00rootroot00000000000000package acmeapi import ( "context" "crypto" "encoding/json" "errors" "fmt" denet "github.com/hlandau/goutils/net" "gopkg.in/hlandau/acmeapi.v2/acmeutils" "io/ioutil" "mime" "net/http" "time" ) type postAccount struct { TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"` ContactURIs []string `json:"contact,omitempty"` Status AccountStatus `json:"status,omitempty"` OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"` } func (c *RealmClient) postAccount(ctx context.Context, acct *Account, onlyReturnExisting bool) error { postAcct := &postAccount{ ContactURIs: acct.ContactURIs, TermsOfServiceAgreed: acct.TermsOfServiceAgreed, OnlyReturnExisting: onlyReturnExisting, } if acct.Status == AccountDeactivated { postAcct.Status = acct.Status } endp := acct.URL expectCode := updateAccountCodes updating := true if endp == "" { di, err := c.getDirectory(ctx) if err != nil { return err } endp = di.NewAccount expectCode = newAccountCodes updating = false } acctU := acct if !updating { acctU = &noAccountNeeded } res, err := c.doReq(ctx, "POST", endp, acctU, acct.PrivateKey, postAcct, acct) if res == nil { return err } if !isStatusCode(res, expectCode) { if err != nil { return err } return fmt.Errorf("unexpected status code: %d: %q", res.StatusCode, endp) } loc := res.Header.Get("Location") if !updating { if !ValidURL(loc) { return fmt.Errorf("invalid URL: %q", loc) } acct.URL = loc } else { if loc != "" { return fmt.Errorf("unexpected Location header: %q", loc) } } return nil } func (c *RealmClient) registerAccount(ctx context.Context, acct *Account, onlyReturnExisting bool) error { if acct.URL != "" { return fmt.Errorf("cannot register account which already has an URL") } return c.postAccount(ctx, acct, onlyReturnExisting) } // Registers a new account. acct.URL must be empty and TermsOfServiceAgreed must // be true. // // The only fields of acct used for requests (that is, the only fields of an // account modifiable by the client) are the ContactURIs field, the // TermsOfServiceAgreed field and the Status field. The Status field is only sent // if it is set to AccountDeactivated ("deactivated"); no other transition can be // manually requested by the client. func (c *RealmClient) RegisterAccount(ctx context.Context, acct *Account) error { return c.registerAccount(ctx, acct, false) } // Tries to find an existing account by key if the URL is not yet known. // acct.URL must be empty. Fails if the account does not exist. func (c *RealmClient) LocateAccount(ctx context.Context, acct *Account) error { return c.registerAccount(ctx, acct, true) } // Updates an existing account. acct.URL must be set. func (c *RealmClient) UpdateAccount(ctx context.Context, acct *Account) error { if acct.URL == "" { return fmt.Errorf("cannot update account for which URL is unknown") } return c.postAccount(ctx, acct, false) } var newAccountCodes = []int{201 /* Created */, 200 /* OK */} var updateAccountCodes = []int{200 /* OK */} func isStatusCode(res *http.Response, codes []int) bool { for _, c := range codes { if c == res.StatusCode { return true } } return false } const defaultPollTime = 10 * time.Second // AUTHORIZATIONS // Load or reload the details of an authorization via its URI. // // You can load an authorization from only the URI by creating an Authorization // with the URI set and then calling this method. func (c *RealmClient) LoadAuthorization(ctx context.Context, acct *Account, az *Authorization) error { // POST-as-GET. res, err := c.doReq(ctx, "POST", az.URL, acct, nil, "", az) if err != nil { return err } err = az.validate() if err != nil { return err } az.retryAt = retryAtDefault(res.Header, defaultPollTime) return nil } // Like LoadAuthorization, but waits the retry time if this is not the first attempt // to load this authorization. To be used when polling. // // The retry delay will not work if you recreate the object; use the same // Authorization struct between calls. func (c *RealmClient) WaitLoadAuthorization(ctx context.Context, acct *Account, az *Authorization) error { err := waitUntil(ctx, az.retryAt) if err != nil { return err } return c.LoadAuthorization(ctx, acct, az) } func (az *Authorization) validate() error { if len(az.Challenges) == 0 { return errors.New("no challenges offered") } return nil } // Create a new authorization for the given hostname. // // IDN hostnames must be in punycoded form. // // Use of this method facilitates preauthentication, which is rarely necessary. // Consider simply using NewOrder instead. func (c *RealmClient) NewAuthorization(ctx context.Context, acct *Account, ident Identifier) (*Authorization, error) { di, err := c.getDirectory(ctx) if err != nil { return nil, err } az := &Authorization{ Identifier: ident, } res, err := c.doReq(ctx, "POST", di.NewAuthz, acct, nil, az, az) if err != nil { return nil, err } if res.StatusCode != 201 { return nil, fmt.Errorf("expected status code 201, got %v", res.StatusCode) } loc := res.Header.Get("Location") if !ValidURL(loc) { return nil, fmt.Errorf("expected valid location, got %q", loc) } az.URL = loc err = az.validate() if err != nil { return nil, err } return az, nil } type postOrder struct { Identifiers []Identifier `json:"identifiers,omitempty"` NotBefore *time.Time `json:"notBefore,omitempty"` NotAfter *time.Time `json:"notAfter,omitempty"` } // Creates a new order. You must set at least the Identifiers field of Order. // The NotBefore and NotAfter fields may also optionally be set. The other // fields, including URI, will be filled in when the method returns. func (c *RealmClient) NewOrder(ctx context.Context, acct *Account, order *Order) error { di, err := c.getDirectory(ctx) if err != nil { return err } po := &postOrder{ Identifiers: order.Identifiers, NotBefore: &order.NotBefore, NotAfter: &order.NotAfter, } if po.NotBefore.IsZero() { po.NotBefore = nil } if po.NotAfter.IsZero() { po.NotAfter = nil } res, err := c.doReq(ctx, "POST", di.NewOrder, acct, nil, po, order) if err != nil { return err } defer res.Body.Close() if res.StatusCode != 201 { return fmt.Errorf("expected status code 201, got %v", res.StatusCode) } loc := res.Header.Get("Location") if !ValidURL(loc) { return fmt.Errorf("invalid URI: %#v", loc) } order.URL = loc return nil } // Load or reload an order. // // You can load an order from its URI by creating an Order with the URI set and // then calling this. func (c *RealmClient) LoadOrder(ctx context.Context, acct *Account, order *Order) error { // POST-as-GET. res, err := c.doReq(ctx, "POST", order.URL, acct, nil, "", order) if err != nil { return err } err = order.validate() if err != nil { return err } order.retryAt = retryAtDefault(res.Header, defaultPollTime) return nil } // Like LoadOrder, but waits the retry time if this is not the first attempt to load // this certificate. To be used when polling. // // The retry delay will not work if you recreate the object; use the same Challenge // struct between calls. func (c *RealmClient) WaitLoadOrder(ctx context.Context, acct *Account, order *Order) error { err := waitUntil(ctx, order.retryAt) if err != nil { return err } return c.LoadOrder(ctx, acct, order) } // Wait for an order to finish processing. The order must be in the // "processing" state and the method returns once this ceases to be the case. // Only the URI is required to be set. func (c *RealmClient) WaitForOrder(ctx context.Context, acct *Account, order *Order) error { for { if order.Status != "" && order.Status != OrderProcessing { return nil } err := c.WaitLoadOrder(ctx, acct, order) if err != nil { return err } } } func (c *RealmClient) LoadCertificate(ctx context.Context, acct *Account, cert *Certificate) error { // Check input. if !ValidURL(cert.URL) { return fmt.Errorf("invalid request URL: %q", cert.URL) } // Make request. res, err := c.doReqAccept(ctx, "POST", cert.URL, "application/pem-certificate-chain", acct, nil, "", nil) if err != nil { return err } defer res.Body.Close() mimeType, params, err := mime.ParseMediaType(res.Header.Get("Content-Type")) if err != nil { return err } err = validateContentType(mimeType, params, "application/pem-certificate-chain") if err != nil { return err } b, err := ioutil.ReadAll(denet.LimitReader(res.Body, 512*1024)) if err != nil { return err } cert.CertificateChain, err = acmeutils.LoadCertificates(b) if err != nil { return err } return nil } // This is a rather kludgy method needed for backwards compatibility with // old-ACME URLs. If it is not known whether an URL is to a certificate or an // order, this method can be used to load the URL. Returns with isCertificate // == true if the URL appears to address a certificate (in which case the // passed cert structure is populated), and isCertificate == false if the URL // appears to address an order (in which case the passed order structure is // populated). If the URL does not appear to address either type of resource, // an error is returned. func (c *RealmClient) LoadOrderOrCertificate(ctx context.Context, url string, acct *Account, order *Order, cert *Certificate) (isCertificate bool, err error) { // Check input. if !ValidURL(url) { err = fmt.Errorf("invalid request URL: %q", url) return } // Make request. res, err := c.doReqAccept(ctx, "POST", url, "application/json, application/pem-certificate-chain", acct, nil, "", nil) if err != nil { return } defer res.Body.Close() mimeType, params, err := mime.ParseMediaType(res.Header.Get("Content-Type")) if err != nil { return } err = validateContentType(mimeType, params, mimeType) // check params only if err != nil { return } switch mimeType { case "application/json": order.URL = url err = json.NewDecoder(res.Body).Decode(order) if err != nil { return } err = order.validate() if err != nil { return } order.retryAt = retryAtDefault(res.Header, defaultPollTime) return case "application/pem-certificate-chain": var b []byte b, err = ioutil.ReadAll(denet.LimitReader(res.Body, 512*1024)) cert.URL = url cert.CertificateChain, err = acmeutils.LoadCertificates(b) isCertificate = true return } err = fmt.Errorf("response was not an order or certificate (unexpected content type %q)", mimeType) return } type finalizeReq struct { // Required. The CSR to be used for issuance. CSR denet.Base64up `json:"csr"` } // Finalize the order. This will only work if the order has the "ready" status. func (c *RealmClient) Finalize(ctx context.Context, acct *Account, order *Order, csr []byte) error { req := finalizeReq{ CSR: csr, } _, err := c.doReq(ctx, "POST", order.FinalizeURL, acct, nil, &req, order) if err != nil { return err } return nil } type revokeReq struct { Certificate []byte `json:"certificate"` Reason int `json:"reason,omitempty"` } // Requests revocation of a certificate. The certificate must be provided in // DER form. If revocationKey is non-nil, the revocation request is signed with // the given key; otherwise, the request is signed with the account key. // // In general, you should expect to be able to revoke any certificate if a // request to do so is signed using that certificate's key. You should also // expect to be able to revoke a certificate if the request is signed with the // account key of the account for which the certificate was issued, or where // the request is signed with the account key of an account for which presently // valid authorizations are held for all DNS names on the certificate. // // The reason is a CRL reason code, or 0 if no explicit reason code is to be // given. func (c *RealmClient) Revoke(ctx context.Context, acct *Account, certificateDER []byte, revocationKey crypto.PrivateKey, reason int) error { di, err := c.getDirectory(ctx) if err != nil { return err } if di.RevokeCert == "" { return fmt.Errorf("endpoint does not support revocation") } req := &revokeReq{ Certificate: certificateDER, Reason: reason, } res, err := c.doReq(ctx, "POST", di.RevokeCert, nil, revocationKey, req, nil) if err != nil { return err } defer res.Body.Close() return nil } func (ord *Order) validate() error { return nil } // Submit a challenge response. Only the challenge URL is required to be set in // the Challenge object. The account need only have the URL set. func (c *RealmClient) RespondToChallenge(ctx context.Context, acct *Account, ch *Challenge, response json.RawMessage) error { _, err := c.doReq(ctx, "POST", ch.URL, acct, nil, &response, ch) if err != nil { return err } return nil } // Submit a key change request. The acct specified is used to authorize the // change; the key for the account identified by acct.URL is changed from // acct.PrivateKey/acct.Key to the key specified by newKey. // // When this method returns nil error, the key has been successfully changed. // The acct object's Key and PrivateKey fields will also be changed to newKey. func (c *RealmClient) ChangeKey(ctx context.Context, acct *Account, newKey crypto.PrivateKey) error { panic("not yet implemented") } golang-gopkg-hlandau-acmeapi.v2-2.0.1/api.go000066400000000000000000000433171355172230600205370ustar00rootroot00000000000000// Package acmeapi provides an API for accessing ACME servers. // // See type RealmClient for introductory documentation. package acmeapi // import "gopkg.in/hlandau/acmeapi.v2" import ( "context" "crypto" "crypto/ecdsa" "crypto/rsa" "encoding/json" "errors" "fmt" gnet "github.com/hlandau/goutils/net" "github.com/hlandau/xlog" "github.com/peterhellberg/link" "golang.org/x/net/context/ctxhttp" "gopkg.in/square/go-jose.v2" "io" "mime" "net/http" "net/url" "runtime" "strings" "sync" "sync/atomic" "time" ) var log, Log = xlog.NewQuiet("acmeapi") // Sentinel value for doReq. var noAccountNeeded = Account{} // Internal use only. All ACME URLs must use "https" and not "http". However, // for testing purposes, if this is set, "http" URLs will be allowed. This is useful // for testing when a test ACME server doesn't have TLS configured. var TestingAllowHTTP = false // You should set this to a string identifying the code invoking this library. // Optional. // // You can alternatively set the user agent on a per-Client basis via // Client.UserAgent, but usually a user agent is set at program scope and it // makes more sense to set it here. var UserAgent string // Returns true if the URL given is (potentially) a valid ACME resource URL. // // The URL must be an HTTPS URL. func ValidURL(u string) bool { ur, err := url.Parse(u) return err == nil && (ur.Scheme == "https" || (TestingAllowHTTP && ur.Scheme == "http")) } // Configuration data used to instantiate a RealmClient. type RealmClientConfig struct { // Optional but usually necessary. The Directory URL for the ACME realm (ACME // server). This must be an HTTPS URL. This will usually be provided via // out-of-band means; it is the root from which all other ACME resources are // accessed. // // Specifying the directory URL is usually necessary, but it can be omitted // in some cases; see the documentation for RealmClient. DirectoryURL string // Optional. HTTP client used to make HTTP requests. If this is nil, the // default net/http Client is used, which will suffice in the vast majority // of cases. HTTPClient *http.Client // Optional. Custom User-Agent string. If not specified, uses the global // User-Agent string configured at acmeapi package level (UserAgent var). UserAgent string } // Client used to access and mutate resources provided by an ACME server. // // // REALM TERMINOLOGY // // A “realm” means an ACME server, including all resources provided by it (e.g. // accounts, orders, nonces). A nonce can be used to issue a signed request // against a resource in a given realm if and only if it that nonce was issued // by the same realm. (This term is specific to this client, and not a general // ACME term. It is coined here to aid clarity in understanding the scope of // ACME resources.) // // You instantiate a RealmClient to consume the services of a realm. If you // want to consume the services of multiple ACME servers — that is, multiple // realms — you must create one RealmClient for each such realm. For example, // if you wanted to use both the ExampleCA Live ACME server (which issues live // certificates) and the ExampleCA Staging ACME server (which issues non-live // certificates), you would need to create one RealmClient for each, and make // any calls to the right RealmClient. Calling a method on the wrong // RealmClient will fail under most circumstances. // // // INSTANTIATION // // Call NewRealmClient to create a new RealmClient. When you create a RealmClient, // you begin by passing the realm's (ACME server's) directory URL as part of // the client configuration. This is the entrypoint for the consumption of the // services provided by an ACME server realm. See RealmClientConfig for details. // // // DIRECTORY AUTO-DISCOVERY // // It is possible to instantiate a RealmClient without passing a directory URL. // If you do this, it is still possible to access some resources, where their // particular URL is explicitly known. Moreover, a RealmClient which has no // particular directory URL configured will automatically ascertain the // appropriate directory URL when it (if ever) first loads a resource where the // response from the server states the directory URL for the realm of which // that resource is a member. Once this occurs, that RealmClient is thereafter // specific to that realm, and must not be used for other purposes. // // This directory auto-discovery mechanic is useful when you have an URL for a // specific resource of an ACME realm but don't know the directory URL or the // identity of the realm or any other information about the realm. This allows // e.g. a certificate to be revoked knowing only its URL and private key. (The // revocation endpoint is discoverable from the directory resource, which is // itself discoverable by a link provided at the certificate resource, which is // addressed via the certificate URL.) // // // CONCURRENCY // // All methods of RealmClient are concurrency-safe. This means you can make // multiple in-flight requests. This is useful, for example, when you create a // new order and wish to retrieve all the authorizations created as part of it, // which are referenced by URL and not serialized inline as part of the order // object. // // // STANDARD METHOD ARGUMENTS // // All methods which involve network I/O, or which may involve network I/O, // take a context, to facilitate timeouts and cancellations. // // All methods which involve making signed requests take an *Account argument. // This is used to provide the URL and private key for the account; the other // fields of *Account arguments are only used by methods which work directly // with account resources. // // The URL and PrivateKey fields of a provided *Account are mandatory in most cases. // They are optional only in the following cases: // // When calling UpsertAccount, the account URL may be omitted. (If the URL of // an existing account is not known, this method may (and must) be used to // discover the URL of the account before methods requiring an URL may be // called.) // // When calling Revoke, the account URL and account private key may be omitted, // but only if the revocation request is being authorized on the basis of // possession of the certificate's corresponding private key. All other // revocation requests require an account URL and account private key. type RealmClient struct { cfg RealmClientConfig directoryURLMutex sync.RWMutex // Protects cfg.DirectoryURL. nonceSource nonceSource dir atomic.Value // *directoryInfo dirMutex sync.Mutex // Ensures single flight for directory requests. } // Directory resource structure. type directoryInfo struct { NewNonce string `json:"newNonce"` NewAccount string `json:"newAccount"` NewOrder string `json:"newOrder"` NewAuthz string `json:"newAuthz"` RevokeCert string `json:"revokeCert"` KeyChange string `json:"keyChange"` Meta RealmMeta `json:"meta"` } // Metadata for a realm, retrieved from the directory resource. type RealmMeta struct { // (Sent by server; optional.) If the CA requires agreement to certain terms of // service, this is set to an URL for the terms of service document. TermsOfServiceURL string `json:"termsOfService,omitempty"` // (Sent by server; optional.) A website pertaining to the CA. WebsiteURL string `json:"website,omitempty"` // (Sent by server; optional.) List of domain names which the CA recognises as // referring to itself for the purposes of CAA record validation. CAAIdentities []string `json:"caaIdentities,omitempty"` // (Sent by server; optional.) As per specification. ExternalAccountRequired bool `json:"externalAccountRequired,omitempty"` } // Instantiates a new RealmClient. func NewRealmClient(cfg RealmClientConfig) (*RealmClient, error) { rc := &RealmClient{ cfg: cfg, } if rc.cfg.DirectoryURL != "" && !ValidURL(rc.cfg.DirectoryURL) { return nil, fmt.Errorf("not a valid directory URL: %q", rc.cfg.DirectoryURL) } rc.nonceSource.GetNonceFunc = rc.obtainNewNonce return rc, nil } func (c *RealmClient) getDirectoryURL() string { c.directoryURLMutex.RLock() defer c.directoryURLMutex.RUnlock() return c.cfg.DirectoryURL } // Directory Retrieval // Returns the directory information for the realm accessed by the RealmClient. // // This may return instantly (if the directory information has already been // retrieved and cached), or may cause a request to be made to retrieve and // cache the information, hence the context argument. // // Multiple concurrent calls to getDirectory with no directory information // cached result only in a single request being made; all of the callers to // getDirectory wait for the single request. func (c *RealmClient) getDirectory(ctx context.Context) (*directoryInfo, error) { dir := c.getDirp() if dir != nil { return dir, nil } c.dirMutex.Lock() defer c.dirMutex.Unlock() if dir := c.getDirp(); dir != nil { return dir, nil } dir, err := c.getDirectoryActual(ctx) if err != nil { return nil, err } c.setDirp(dir) return dir, nil } func (c *RealmClient) getDirp() *directoryInfo { v, _ := c.dir.Load().(*directoryInfo) return v } func (c *RealmClient) setDirp(d *directoryInfo) { c.dir.Store(d) } // Error returned when directory URL was needed for an operation but it is unknown. var ErrUnknownDirectoryURL = errors.New("unable to retrieve directory because the directory URL is unknown") // Error returned if directory does not provide endpoints required by the specification. var ErrMissingEndpoints = errors.New("directory does not provide required endpoints") // Make actual request to retrieve directory. func (c *RealmClient) getDirectoryActual(ctx context.Context) (*directoryInfo, error) { directoryURL := c.getDirectoryURL() if directoryURL == "" { return nil, ErrUnknownDirectoryURL } var dir *directoryInfo _, err := c.doReq(ctx, "GET", directoryURL, nil, nil, nil, &dir) if err != nil { return nil, err } if !ValidURL(dir.NewNonce) || !ValidURL(dir.NewAccount) || !ValidURL(dir.NewOrder) { return nil, ErrMissingEndpoints } return dir, nil } // Returns the directory metadata for the realm. // // This method must be used to retrieve the realm's current Terms of Service // URI when calling UpsertAccount. func (c *RealmClient) GetMeta(ctx context.Context) (RealmMeta, error) { di, err := c.getDirectory(ctx) if err != nil { return RealmMeta{}, err } return di.Meta, nil } // This method is configured as the GetNewNonce function for the nonceSource // which constitutes part of the RealmClient. It is called if the nonceSource's // cache of nonces is empty, meaning that an HTTP request must be made to // retrieve a new nonce. func (c *RealmClient) obtainNewNonce(ctx context.Context) error { di, err := c.getDirectory(ctx) if err != nil { return err } // We don't need to cache the nonce explicitly; doReq automatically caches // any fresh nonces provided in a reply. res, err := c.doReq(ctx, "HEAD", di.NewNonce, nil, nil, nil, nil) if res != nil { res.Body.Close() } return err } // Request Methods // Makes an ACME request. // // method: HTTP method in uppercase. // // url: Absolute HTTPS URL. // // requestData: If non-nil, signed and sent as request body. // // responseData: If non-nil, response, if JSON, is unmarshalled into this. // // acct: Mandatory if requestData is non-nil. This is used to determine the // account URL, which is used for signing requests. If key is nil, acct.PrivateKey // is used to sign the request. If the request should be signed with an embedded JWK // rather than an URL account reference, pass the special sentinel value // &noAccountNeeded. // // key: Overrides the private key used; used instead of acct.PrivateKey if non-nil. // The HTTP response structure is returned; the state of the Body stream is undefined, // but need not be manually closed if err is non-nil or if responseData is non-nil. func (c *RealmClient) doReq(ctx context.Context, method, url string, acct *Account, key crypto.PrivateKey, requestData, responseData interface{}) (*http.Response, error) { return c.doReqAccept(ctx, method, url, "application/json", acct, key, requestData, responseData) } func (c *RealmClient) doReqAccept(ctx context.Context, method, url, accepts string, acct *Account, key crypto.PrivateKey, requestData, responseData interface{}) (*http.Response, error) { backoff := gnet.Backoff{ MaxTries: 20, InitialDelay: 100 * time.Millisecond, MaxDelay: 1 * time.Second, MaxDelayAfterTries: 4, Jitter: 0.10, } for { res, err := c.doReqOneTry(ctx, method, url, accepts, acct, key, requestData, responseData) if err == nil { return res, nil } // If the error is specifically a "bad nonce" error, we are supposed to // retry. if he, ok := err.(*HTTPError); ok && he.Problem != nil && he.Problem.Type == "urn:ietf:params:acme:error:badNonce" { if backoff.Sleep() { log.Debugf("retrying after bad nonce: %v\n", he) continue } } // Other error, return. return res, err } } func (c *RealmClient) doReqOneTry(ctx context.Context, method, url, accepts string, acct *Account, key crypto.PrivateKey, requestData, responseData interface{}) (*http.Response, error) { // Check input. if !ValidURL(url) { return nil, fmt.Errorf("invalid request URL: %q", url) } // Request marshalling and signing. var rdr io.Reader if requestData != nil { if acct == nil { return nil, fmt.Errorf("must provide account object when making signed requests") } if key == nil { key = acct.PrivateKey } if key == nil { return nil, fmt.Errorf("account key must be specified") } var b []byte var err error if s, ok := requestData.(string); ok && s == "" { b = []byte{} } else { b, err = json.Marshal(requestData) if err != nil { return nil, err } } kalg, err := algorithmFromKey(key) if err != nil { return nil, err } signKey := jose.SigningKey{ Algorithm: kalg, Key: key, } extraHeaders := map[jose.HeaderKey]interface{}{ "url": url, } useInlineKey := (acct == &noAccountNeeded) if !useInlineKey { accountURL := acct.URL if !ValidURL(accountURL) { return nil, fmt.Errorf("acct must have a valid URL, not %q", accountURL) } extraHeaders["kid"] = accountURL } signOptions := jose.SignerOptions{ NonceSource: c.nonceSource.WithContext(ctx), EmbedJWK: useInlineKey, ExtraHeaders: extraHeaders, } signer, err := jose.NewSigner(signKey, &signOptions) if err != nil { return nil, err } sig, err := signer.Sign(b) if err != nil { return nil, err } s := sig.FullSerialize() if err != nil { return nil, err } rdr = strings.NewReader(s) } // Make request. req, err := http.NewRequest(method, url, rdr) if err != nil { return nil, err } req.Header.Set("Accept", accepts) if method != "GET" && method != "HEAD" { req.Header.Set("Content-Type", "application/jose+json") } res, err := c.doReqServer(ctx, req) if err != nil { return res, err } // Otherwise, if we are expecting response data, unmarshal into the provided // struct. if responseData != nil { defer res.Body.Close() mimeType, params, err := mime.ParseMediaType(res.Header.Get("Content-Type")) if err != nil { return res, err } err = validateContentType(mimeType, params, "application/json") if err != nil { return res, err } err = json.NewDecoder(res.Body).Decode(responseData) if err != nil { return res, err } } // Done. return res, nil } func validateContentType(mimeType string, params map[string]string, expectedMimeType string) error { if mimeType != expectedMimeType { return fmt.Errorf("unexpected response content type: %q", mimeType) } if ch, ok := params["charset"]; ok && ch != "" && strings.ToLower(ch) != "utf-8" { return fmt.Errorf("content type charset is not UTF-8: %q, %q", mimeType, ch) } return nil } // Make an HTTP request to an ACME endpoint. func (c *RealmClient) doReqServer(ctx context.Context, req *http.Request) (*http.Response, error) { res, err := c.doReqActual(ctx, req) if err != nil { return nil, err } // If the response includes a nonce, add it to our cache of nonces. if n := res.Header.Get("Replay-Nonce"); n != "" { c.nonceSource.AddNonce(n) } // Autodiscover directory URL if it we didn't prevously know it and it's // specified in the response. if c.getDirectoryURL() == "" { func() { c.directoryURLMutex.Lock() defer c.directoryURLMutex.Unlock() if c.cfg.DirectoryURL != "" { return } if link := link.ParseResponse(res)["index"]; link != nil && ValidURL(link.URI) { c.cfg.DirectoryURL = link.URI } }() } // If the response was an error, parse the response body as an error and return. if res.StatusCode >= 400 && res.StatusCode < 600 { defer res.Body.Close() return res, newHTTPError(res) } return res, err } // Make an HTTP request. This is used by doReq and can also be used for // non-ACME requests (e.g. OCSP). func (c *RealmClient) doReqActual(ctx context.Context, req *http.Request) (*http.Response, error) { req.Header.Set("User-Agent", formUserAgent(c.cfg.UserAgent)) return ctxhttp.Do(ctx, c.cfg.HTTPClient, req) } func algorithmFromKey(key crypto.PrivateKey) (jose.SignatureAlgorithm, error) { switch v := key.(type) { case *rsa.PrivateKey: return jose.RS256, nil case *ecdsa.PrivateKey: name := v.Curve.Params().Name switch name { case "P-256": return jose.ES256, nil case "P-384": return jose.ES384, nil case "P-521": return jose.ES512, nil default: return "", fmt.Errorf("unsupported ECDSA curve: %s", name) } default: return "", fmt.Errorf("unsupported private key type: %T", key) } } func formUserAgent(userAgent string) string { if userAgent == "" { userAgent = UserAgent } if userAgent != "" { userAgent += " " } return fmt.Sprintf("%sacmeapi/2a Go-http-client/1.1 %s/%s", userAgent, runtime.GOOS, runtime.GOARCH) } golang-gopkg-hlandau-acmeapi.v2-2.0.1/api_test.go000066400000000000000000000066341355172230600215770ustar00rootroot00000000000000// +build integration package acmeapi import ( "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "encoding/json" "github.com/hlandau/acmeapi/pebbletest" "testing" ) func TestRealmClient(t *testing.T) { rc, err := NewRealmClient(RealmClientConfig{ DirectoryURL: "https://localhost:14000/dir", HTTPClient: pebbletest.HTTPClient, }) if err != nil { t.Fatalf("couldn't instantiate realm client: %v", err) } meta, err := rc.GetMeta(context.TODO()) if err != nil { t.Fatalf("couldn't get metadata: %v", err) } t.Logf("metadata: %#v", meta) privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatalf("couldn't generate key: %v", err) } acct := &Account{ PrivateKey: privKey, TermsOfServiceAgreed: true, } err = rc.LocateAccount(context.TODO(), acct) if err == nil { t.Fatalf("locate account did NOT fail: %v", err) } t.Logf("locate account failed as expected: %v", err) err = rc.RegisterAccount(context.TODO(), acct) if err != nil { t.Fatalf("error while registering account: %v", err) } acct2 := &Account{ PrivateKey: privKey, } err = rc.LocateAccount(context.TODO(), acct2) if err != nil { t.Fatalf("locate account failed: %v", err) } acct.ContactURIs = []string{"mailto:foo@example.com"} err = rc.UpdateAccount(context.TODO(), acct) if err != nil { t.Fatalf("update account failed: %v", err) } // CHECK: NewOrder order := &Order{ Identifiers: []Identifier{ {Type: IdentifierTypeDNS, Value: "example.com"}, }, } err = rc.NewOrder(context.TODO(), acct, order) if err != nil { t.Fatalf("error creating order: %v", err) } // CHECK: LoadAuthorization t.Logf("order: %#v", order) var authorizations []*Authorization for _, authURL := range order.AuthorizationURLs { authz := &Authorization{ URL: authURL, } err = rc.LoadAuthorization(context.TODO(), acct, authz) if err != nil { t.Fatalf("cannot load authorization: %v", err) } authorizations = append(authorizations, authz) } // CHECK: LoadOrder order2 := &Order{ URL: order.URL, } err = rc.LoadOrder(context.TODO(), order2) if err != nil { t.Fatalf("cannot load order: %v", err) } // CHECK: LoadOrderOrCertificate (order case) var order3 Order var dummyCert Certificate isCert, err := rc.LoadOrderOrCertificate(context.TODO(), order.URL, &order3, &dummyCert) if err != nil { t.Fatalf("cannot load order/certificate: %v", err) } if isCert || dummyCert.URL != "" || order3.URL == "" { t.Fatalf("unexpected certificate") } err = rc.RespondToChallenge(context.TODO(), acct, &authorizations[0].Challenges[0], json.RawMessage("{}")) if err != nil { t.Fatalf("failed to respond to challenge: %v", err) } err = rc.WaitLoadAuthorization(context.TODO(), acct, authorizations[0]) if err != nil { t.Fatalf("failed to wait for authorization: %v", err) } // We don't care if it succeeded or not, that's not our job. We're just // testing the client functionality. if !authorizations[0].Status.IsFinal() { t.Fatalf("authorization is not final") } // Test loading a resource where the directory URL is unknown. rc2, err := NewRealmClient(RealmClientConfig{ HTTPClient: pebbletest.HTTPClient, }) if err != nil { t.Fatalf("couldn't instantiate second realm client: %v", err) } order4 := &Order{ URL: order.URL, } err = rc2.LoadOrder(context.TODO(), order4) if err != nil { t.Fatalf("couldn't load order with second client: %v", err) } } golang-gopkg-hlandau-acmeapi.v2-2.0.1/nonce.go000066400000000000000000000045631355172230600210700ustar00rootroot00000000000000package acmeapi import ( "context" "errors" "sync" ) // Stores a pool of nonces used to make replay-proof requests. type nonceSource struct { // If set, called when the nonce store is exhausted and a nonce is requested. // This function should add nonces to the nonceSource by calling AddNonce one // or more times. If this is not set, or if the function returns an error or // does not call AddNonce when called, an error is returned when attempting // to retrieve a nonce. GetNonceFunc func(ctx context.Context) error initOnce sync.Once pool map[string]struct{} poolMutex sync.Mutex } func (ns *nonceSource) init() { ns.initOnce.Do(func() { ns.pool = map[string]struct{}{} }) } // Retrieves a new nonce. If no nonces remain in the pool, GetNonceFunc is used // if possible to retrieve a new one. This may result in network I/O, hence the // ctx parameter. func (ns *nonceSource) Nonce(ctx context.Context) (string, error) { ns.init() k := ns.tryPop() if k != "" { return k, nil } err := ns.obtainNonce(ctx) if err != nil { return "", err } k = ns.tryPop() if k != "" { return k, nil } return "", errors.New("failed to retrieve additional nonce") } func (ns *nonceSource) tryPop() string { ns.poolMutex.Lock() defer ns.poolMutex.Unlock() for k := range ns.pool { delete(ns.pool, k) return k } return "" } func (ns *nonceSource) obtainNonce(ctx context.Context) error { if ns.GetNonceFunc == nil { return errors.New("out of nonces - this should never happen") } return ns.GetNonceFunc(ctx) } // Add a nonce to the pool. This is a no-op if the nonce is already in the // pool. func (ns *nonceSource) AddNonce(nonce string) { ns.init() ns.poolMutex.Lock() defer ns.poolMutex.Unlock() ns.pool[nonce] = struct{}{} } // Returns a struct with a single method, Nonce() which can be called to obtain // a nonce. This adapts a nonceSource to an interface without a ctx parameter, // which some libraries may expect, while allowing calls made by that library // to still be made under a context. func (ns *nonceSource) WithContext(ctx context.Context) *nonceSourceWithCtx { return &nonceSourceWithCtx{ns, ctx} } type nonceSourceWithCtx struct { nonceSource *nonceSource ctx context.Context } // Obtain a nonce, using a context known by the object. func (nc *nonceSourceWithCtx) Nonce() (string, error) { return nc.nonceSource.Nonce(nc.ctx) } golang-gopkg-hlandau-acmeapi.v2-2.0.1/nonce_test.go000066400000000000000000000007641355172230600221260ustar00rootroot00000000000000package acmeapi import ( "context" "testing" ) func TestNonce(t *testing.T) { ns := nonceSource{} ns.AddNonce("my-nonce") nsc := ns.WithContext(context.TODO()) n, err := nsc.Nonce() if err != nil { t.Fatal() } if n != "my-nonce" { t.Fatal() } n, err = nsc.Nonce() if err == nil { t.Fatal() } ns.GetNonceFunc = func(ctx context.Context) error { ns.AddNonce("nonce2") return nil } n, err = nsc.Nonce() if err != nil { t.Fatal() } if n != "nonce2" { t.Fatal() } } golang-gopkg-hlandau-acmeapi.v2-2.0.1/ocsp.go000066400000000000000000000031221355172230600207200ustar00rootroot00000000000000package acmeapi import ( "context" "crypto/x509" "encoding/base64" "fmt" denet "github.com/hlandau/goutils/net" "golang.org/x/crypto/ocsp" "io/ioutil" "net/http" ) // Checks OCSP for a certificate. The immediate issuer must be specified. If // the certificate does not support OCSP, (nil, nil) is returned. Uses HTTP // GET rather than POST. The response is verified. The caller must check the // response status. The raw OCSP response is also returned, even if parsing // failed and err is non-nil. // // This method is realm-independent. func (c *RealmClient) CheckOCSP(ctx context.Context, crt, issuer *x509.Certificate) (parsedResponse *ocsp.Response, rawResponse []byte, err error) { if len(crt.OCSPServer) == 0 { return } b, err := ocsp.CreateRequest(crt, issuer, nil) if err != nil { return } b64 := base64.StdEncoding.EncodeToString(b) path := crt.OCSPServer[0] + "/" + b64 req, err := http.NewRequest("GET", path, nil) if err != nil { return } req.Header.Set("Accept", "application/ocsp-response") res, err := c.doReqActual(ctx, req) if err != nil { return } defer res.Body.Close() if res.StatusCode != 200 { err = fmt.Errorf("OCSP response has status %#v", res.Status) return } if res.Header.Get("Content-Type") != "application/ocsp-response" { err = fmt.Errorf("response to OCSP request had unexpected content type") return } // Read response, limiting response to 1MiB. rawResponse, err = ioutil.ReadAll(denet.LimitReader(res.Body, 1*1024*1024)) if err != nil { return } parsedResponse, err = ocsp.ParseResponse(rawResponse, issuer) return } golang-gopkg-hlandau-acmeapi.v2-2.0.1/ocsp_test.go000066400000000000000000000070601355172230600217640ustar00rootroot00000000000000package acmeapi import ( "context" "crypto/x509" "github.com/hlandau/acmeapi/acmeutils" "golang.org/x/crypto/ocsp" "testing" ) const testOCSPCerts = `-----BEGIN CERTIFICATE----- MIIE6DCCA9CgAwIBAgITAPr3OLUNFF72kSERFC+leb00HDANBgkqhkiG9w0BAQsF ADAfMR0wGwYDVQQDDBRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNjAxMTcxNjAx MDBaFw0xNjA0MTYxNjAxMDBaMB4xHDAaBgNVBAMTE2FxMS5saGguZGV2ZXZlci5u ZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVQT8bs4n6+3QLyehW GseFUI+xMMlAM0Mrkol0rB2ZbC4rWanxfqG9TE6i/ToEe+9dL7NxpBKXrRnD/4jK cpDxHbGy+hqx/XZefmpdLK2E7FtO53sE0rDcQVGZ2r4YweumfS6jNoNeNZsMzJ6/ aAeXoz+j+rPJG73NjgWz2BIWwum7AMquq2YeERp3eu5hXQDsZxk6dlNwJ3XVaho7 EZZojQENm2/BRkpr1oLzq5fMKVc+zRGzuoCJqeYH6yYzWG7oUypW+H477pKDfKLE RGwEoTAAx4SS4HwXYrCftFgfmWw6fFV9L8aqON8ypW9CZ5HCprymypcy+6/n/S7k ruH3AgMBAAGjggIcMIICGDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYB BQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFN/DSOGyPtfc 1X8rffIJtSocqMzbMB8GA1UdIwQYMBaAFPt4TxL5YBWDLJ8XfzQZsy426kGJMHgG CCsGAQUFBwEBBGwwajAzBggrBgEFBQcwAYYnaHR0cDovL29jc3Auc3RhZ2luZy14 MS5sZXRzZW5jcnlwdC5vcmcvMDMGCCsGAQUFBzAChidodHRwOi8vY2VydC5zdGFn aW5nLXgxLmxldHNlbmNyeXB0Lm9yZy8wHgYDVR0RBBcwFYITYXExLmxoaC5kZXZl dmVyLm5ldDCB/gYDVR0gBIH2MIHzMAgGBmeBDAECATCB5gYLKwYBBAGC3xMBAQEw gdYwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQub3JnMIGrBggr BgEFBQcCAjCBngyBm1RoaXMgQ2VydGlmaWNhdGUgbWF5IG9ubHkgYmUgcmVsaWVk IHVwb24gYnkgUmVseWluZyBQYXJ0aWVzIGFuZCBvbmx5IGluIGFjY29yZGFuY2Ug d2l0aCB0aGUgQ2VydGlmaWNhdGUgUG9saWN5IGZvdW5kIGF0IGh0dHBzOi8vbGV0 c2VuY3J5cHQub3JnL3JlcG9zaXRvcnkvMA0GCSqGSIb3DQEBCwUAA4IBAQAVkT8U oD2AJVjtHogCyt7BkPQ+j6zN1zaN9Bd9nI6a7tpAT6B+j6IqB4o2vCFYawiKaDwR ri06Yi9Ohf1QY50D7P21wzfsRoizHbsmHDPPnlDfFe/R1MzB7jYI1JV4LkjWLpuC OjTQZs3hIoEbTEBA/TIcwAfS9oMFgk+LgL5B4zQUZgqVp0+A4NNy3J1nBhYC2k2T 6qiE0CeU8bCfR2V2MZ6Az2X8nwWkWwovosDQR0oOWDcACDbDnS6OPMuHtZi7Wtqn UeMJ3YfZ7VBWzJTmDRPoDdbP92YI8FqRGbA6GO/XzyJvkOKSnc3CDfJ9Od0IeVeV aC0Q8qLjOhazFhj0 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDETCCAfmgAwIBAgIJAJzxkS6o1QkIMA0GCSqGSIb3DQEBCwUAMB8xHTAbBgNV BAMMFGhhcHB5IGhhY2tlciBmYWtlIENBMB4XDTE1MDQwNzIzNTAzOFoXDTI1MDQw NDIzNTAzOFowHzEdMBsGA1UEAwwUaGFwcHkgaGFja2VyIGZha2UgQ0EwggEiMA0G CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCCkd5mgXFErJ3F2M0E9dw+Ta/md5i 8TDId01HberAApqmydG7UZYF3zLTSzNjlNSOmtybvrSGUnZ9r9tSQcL8VM6WUOM8 tnIpiIjEA2QkBycMwvRmZ/B2ltPdYs/R9BqNwO1g18GDZrHSzUYtNKNeFI6Glamj 7GK2Vr0SmiEamlNIR5ktAFsEErzf/d4jCF7sosMsJpMCm1p58QkP4LHLShVLXDa8 BMfVoI+ipYcA08iNUFkgW8VWDclIDxcysa0psDDtMjX3+4aPkE/cefmP+1xOfUuD HOGV8XFynsP4EpTfVOZr0/g9gYQ7ZArqXX7GTQkFqduwPm/w5qxSPTarAgMBAAGj UDBOMB0GA1UdDgQWBBT7eE8S+WAVgyyfF380GbMuNupBiTAfBgNVHSMEGDAWgBT7 eE8S+WAVgyyfF380GbMuNupBiTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA A4IBAQAd9Da+Zv+TjMv7NTAmliqnWHY6d3UxEZN3hFEJ58IQVHbBZVZdW7zhRktB vR05Kweac0HJeK91TKmzvXl21IXLvh0gcNLU/uweD3no/snfdB4OoFompljThmgl zBqiqWoKBJQrLCA8w5UB+ReomRYd/EYXF/6TAfzm6hr//Xt5mPiUHPdvYt75lMAo vRxLSbF8TSQ6b7BYxISWjPgFASNNqJNHEItWsmQMtAjjwzb9cs01XH9pChVAWn9L oeMKa+SlHSYrWG93+EcrIH/dGU76uNOiaDzBSKvaehG53h25MHuO1anNICJvZovW rFo4Uv1EnkKJm3vJFe50eJGhEKlx -----END CERTIFICATE-----` func TestOCSP(t *testing.T) { b, err := acmeutils.LoadCertificates([]byte(testOCSPCerts)) if err != nil { t.Fatalf("cannot load certificates") } c0, err := x509.ParseCertificate(b[0]) if err != nil { t.Fatalf("cannot parse certificate") } c1, err := x509.ParseCertificate(b[1]) if err != nil { t.Fatalf("cannot parse certificate") } cl := RealmClient{} res, _, err := cl.CheckOCSP(context.TODO(), c0, c1) if err != nil { t.Fatalf("ocsp error: %v", err) } if res.Status != ocsp.Revoked { t.Fatalf("ocsp status should be revoked (1) but is %v", res.Status) } } golang-gopkg-hlandau-acmeapi.v2-2.0.1/pebbletest/000077500000000000000000000000001355172230600215605ustar00rootroot00000000000000golang-gopkg-hlandau-acmeapi.v2-2.0.1/pebbletest/pebbletest.go000066400000000000000000000010051355172230600242340ustar00rootroot00000000000000// Package pebbletest provides facilities for using Pebble during testing. package pebbletest import ( "crypto/tls" "net/http" ) // HTTP client which can be used to talk to Pebble. Disables certificate // checks, etc. as necessary. You must call Init() before using this. var HTTPClient *http.Client func init() { httpTransport := *http.DefaultTransport.(*http.Transport) httpTransport.TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, } HTTPClient = &http.Client{ Transport: &httpTransport, } } golang-gopkg-hlandau-acmeapi.v2-2.0.1/types.go000066400000000000000000000337731355172230600211370ustar00rootroot00000000000000package acmeapi import ( "crypto" "encoding/json" "fmt" "gopkg.in/square/go-jose.v2" "time" ) // RFC7807 problem document. These structures are used by an ACME endpoint to // return error information. type Problem struct { // URI representing the problem type. Typically an URN. Type string `json:"type,omitempty"` // One-line summary of the error. Title string `json:"title,omitempty"` // HTTP status code (optional). If present, this should match the actual HTTP // status code returned. Advisory use only. Status int `json:"status,omitempty"` // More detailed explanation of the error. Detail string `json:"detail,omitempty"` // Optional, potentially relative URI identifying the specific problem. // May refer to an object which relates to the problem, etc. Instance string `json:"instance,omitempty"` // ACME-specific. Optional. List of problems which constitute components of // this problem. Subproblem []*Problem `json:"subproblems,omitempty"` // ACME-specific. Optional. Identifier relating to this problem. Identifier *Identifier `json:"identifier,omitempty"` } func (p *Problem) Error() string { extra := "" for _, sp := range p.Subproblem { extra += "\n (subproblem " + sp.Error() + ")" } return fmt.Sprintf("(problem (type %q) (instance %q) (id %v) (title %q): (detail %q)%s)", p.Type, p.Instance, p.Identifier, p.Title, p.Detail, extra) } // Represents an identifier for a resource for which authorization is required. type Identifier struct { // The type of the identifier. Type IdentifierType `json:"type"` // The identifier string. The format is determined by Type. Value string `json:"value"` } func (id *Identifier) String() string { return fmt.Sprintf("Identifier(%q, %q)", id.Type, id.Value) } // A type of Identifier. Currently, the only supported value is "dns". type IdentifierType string const ( // Indicates that the identifier value is a DNS name. IdentifierTypeDNS IdentifierType = "dns" ) // --------------------------------------------------------------------------------------------------------- // Represents an account. type Account struct { // The URL of the account. URL string `json:"-"` // Private key used to authorize requests. This is never sent to any server, // but is used to sign requests when passed as an argument to RealmClient // methods. PrivateKey crypto.PrivateKey `json:"-"` // Account public key. // // Always sent by the server; cannot be modified directly by client (use the // ChangeKey method). Key *jose.JSONWebKey `json:"key,omitempty"` // Account status. // // Always sent by server. Cannot be directly modified by client in general, // except that a client may set this to AccountDeactivated ("deactivated") to // request deactivation. This is the only client-initiated change to this // field allowed. Status AccountStatus `json:"status,omitempty"` // Contact URIs which may be used to contact the accountholder. // // Most realms will accept e. mail addresses expressed as “mailto:” URIs // here. Some realms may accept “tel:” URIs. Acceptance of other URIs is in // practice unlikely, and may result in rejection by the server. // // Always sent by the server, and may be sent by client to modify the current // value. If this field is not specified when registering, the account will // have no contact URIs, and if the field is not specified when updating the // account, the current set of contact URIs will be left unchanged. ContactURIs []string `json:"contact,omitempty"` // Whether the client agrees/has agreed to the terms of service. // // Always sent by the server, and may be sent by client to indicate assent. TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"` // URL at which the orders attached to the account can be enumerated. // // Always sent by server, if enumeration is supported. Read only. OrdersURL string `json:"orders,omitempty"` } // Specifies a current account status. type AccountStatus string const ( // The account is usable. This is the initial state. AccountValid AccountStatus = "valid" // The account is no longer usable, by accountholder request. This is a final // state. This is the only state which can be explicitly requested by the // client by setting it as the Status field. AccountDeactivated = "deactivated" // The account is no longer usable, due to administrative action. // This is a final state. AccountRevoked = "revoked" ) // Returns true iff the account status is a recognised account status value. func (s AccountStatus) IsWellFormed() bool { switch s { case "valid", "deactivated", "revoked": return true default: return false } } // Returns true iff the account status is a final status. func (s AccountStatus) IsFinal() bool { switch s { case "deactivated", "revoked": return true default: return false } } // Implements encoding/json.Unmarshaler. func (s *AccountStatus) UnmarshalJSON(data []byte) error { var ss string err := json.Unmarshal(data, &ss) if err != nil { return err } if !AccountStatus(ss).IsWellFormed() { return fmt.Errorf("not a valid status: %#v", ss) } *s = AccountStatus(ss) return nil } // --------------------------------------------------------------------------------------------------------- // Represents a request for a certificate. type Order struct { // The URL of the order. URL string `json:"-"` // Order status. // // Always sent by server; read-only. Status OrderStatus `json:"status,omitempty"` // Time at which the order expires. // // Sent by server if status is "pending" or "valid". Read only. Expires time.Time `json:"expires,omitempty"` // RFC 3339 // The identifiers that the order pertains to. Identifiers []Identifier `json:"identifiers,omitempty"` // DER-encoded X.509 CSR. // // Must be sent by client at resource creation time. Always sent by server // and is immutable after resource creation. //CSR denet.Base64up `json:"csr,omitempty"` // Optionally sent by client at order creation time to constrain certificate // validity period. // // Always sent by server and is immutable after resource creation. NotBefore time.Time `json:"notBefore,omitempty"` // RFC 3339 NotAfter time.Time `json:"notAfter,omitempty"` // RFC 3339 // An error which occurred during the processing of the order, if any. Error *Problem `json:"error,omitempty"` // RFC7807 // List of URLs to authorization objects. All of the authorizations must be // completed to cause issuance. Issuance will commence as soon as all // authorizations are completed. Some authorizations may already be completed // when the order is created. // // Always sent by server. Read only. AuthorizationURLs []string `json:"authorizations,omitempty"` // An URL for submitting a CSR. FinalizeURL string `json:"finalize,omitempty"` // URL from which the certificate can be downloaded. // // Sent by server, but only when state is "valid". Read only. CertificateURL string `json:"certificate,omitempty"` retryAt time.Time } // Specifies an order status. type OrderStatus string const ( // The order is waiting for one or more client actions before issuance // occurs. This is the initial state. OrderPending OrderStatus = "pending" // All preconditions to order fulfilment have been completed, and now the // order is simply waiting for a client to invoke the finalize operation. The // order will then transition to "processing". OrderReady = "ready" // Issuance is underway, and the state will transition to "valid" or // "invalid" automatically. OrderProcessing = "processing" // The order is valid. The certificate has been issued and can be retrieved. // This is a final state. OrderValid = "valid" // The certificate issuance request has failed. // This is a final state. OrderInvalid = "invalid" ) // Returns true iff the order status is a recognised order status value. func (s OrderStatus) IsWellFormed() bool { switch s { case "pending", "ready", "processing", "valid", "invalid": return true default: return false } } // Returns true iff the order status is a final status. func (s OrderStatus) IsFinal() bool { switch s { case "valid", "invalid": // TODO return true default: return false } } // Implements encoding/json.Unmarshaler. func (s *OrderStatus) UnmarshalJSON(data []byte) error { var ss string err := json.Unmarshal(data, &ss) if err != nil { return err } if !OrderStatus(ss).IsWellFormed() { return fmt.Errorf("not a valid status: %#v", ss) } *s = OrderStatus(ss) return nil } // --------------------------------------------------------------------------------------------------------- // Represents an authorization which must be completed to enable certificate // issuance. type Authorization struct { // The URL of the authorization. URL string `json:"-"` // The identifier for which authorization is required. // // Sent by server. Read only. Identifier Identifier `json:"identifier,omitempty"` // The status of the authorization. // // Sent by server. Read only. Status AuthorizationStatus `json:"status,omitempty"` // The expiry time of the authorization. // // May be sent by server; always sent if status is "valid". Read only. Expires time.Time `json:"expires,omitempty"` // True if the authorization is for a wildcard domain name. Wildcard bool `json:"wildcard,omitempty"` // Array of Challenge objects. Any one challenge in the array must be // completed to complete the authorization. // // Always sent by server. Read only. Challenges []Challenge `json:"challenges,omitempty"` retryAt time.Time } // Specifies an authorization status. type AuthorizationStatus string const ( // The authorization is waiting for one or more client actions before // it becomes valid. This is the initial state. AuthorizationPending AuthorizationStatus = "pending" // The authorization is valid. // The only state transition possible is to "revoked". AuthorizationValid = "valid" // The authorization is invalid. // This is a final state. AuthorizationInvalid = "invalid" // The authorization is deactivated. // This is a final state. AuthorizationDeactivated = "deactivated" // The authorization has been revoked. // This is a final state. AuthorizationRevoked = "revoked" ) // Returns true iff the authorization status is a recognised authorization // status value. func (s AuthorizationStatus) IsWellFormed() bool { switch s { case "pending", "valid", "invalid", "deactivated", "revoked": return true default: return false } } // Returns true iff the authorization status is a final status. func (s AuthorizationStatus) IsFinal() bool { switch s { case "valid", "invalid", "deactivated", "revoked": return true default: return false } } // Implements encoding/json.Unmarshaler. func (s *AuthorizationStatus) UnmarshalJSON(data []byte) error { var ss string err := json.Unmarshal(data, &ss) if err != nil { return err } if !AuthorizationStatus(ss).IsWellFormed() { return fmt.Errorf("not a valid status: %#v", ss) } *s = AuthorizationStatus(ss) return nil } // --------------------------------------------------------------------------------------------------------- // Represents a challenge which may be completed to satisfy an authorization // requirement. type Challenge struct { // The URL of the challenge object. // // Always sent by server. Read only. URL string `json:"url,omitempty"` // The challenge type. // // Always sent by server. Read only. Type string `json:"type,omitempty"` // The challenge status. Always sent by the server. Status ChallengeStatus `json:"status,omitempty"` // The time at which the challenge was successfully validated. Optional // unless Status is ChallengeValid. Validated time.Time `json:"validated,omitempty"` // RFC 3339 // Error that occurred while the server was validating the challenge, if any. // Multiple errors are indicated using subproblems. This should (but is not // guaranteed to) be present if Status is StatusInvalid. Error *Problem `json:"error,omitempty"` // Sent for http-01, tls-sni-02, dns-01. Token string `json:"token,omitempty"` retryAt time.Time } // Specifies a challenge status. type ChallengeStatus string const ( // The challenge is waiting to be initiated by client action before // verification occurs. This is the initial state. ChallengePending ChallengeStatus = "pending" // The challenge moves from the pending state to the processing state once // the client has initiated the challenge. The status will autonomously // advance to ChallengeValid or ChallengeInvalid. ChallengeProcessing = "processing" // Verification has succeeded. This is a final state. ChallengeValid = "valid" // Verification has failed. This is a final state. ChallengeInvalid = "invalid" ) // Returns true iff the challenge status is a recognised challenge status // value. func (s ChallengeStatus) IsWellFormed() bool { switch s { case "pending", "processing", "valid", "invalid": return true default: return false } } // Returns true iff the challenge status is a final status. func (s ChallengeStatus) IsFinal() bool { switch s { case "valid", "invalid": return true default: return false } } // Implements encoding/json.Unmarshaler. func (s *ChallengeStatus) UnmarshalJSON(data []byte) error { var ss string err := json.Unmarshal(data, &ss) if err != nil { return err } if !ChallengeStatus(ss).IsWellFormed() { return fmt.Errorf("not a valid status: %#v", ss) } *s = ChallengeStatus(ss) return nil } // --------------------------------------------------------------------------------------------------------- // Represents a certificate which has been issued. type Certificate struct { // The URL of the certificate resource. URL string `json:"-"` // The chain of certificates which a TLS server should send to a client in // order to facilitate client verification. A slice of DER-encoded // certificates. The first certificate is the end-entity certificate, the // second certificate if any is the issuing intermediate certificate, etc. // // Does not generally include the root certificate. If you need it (e.g. // because you are using DANE) you must append it yourself. CertificateChain [][]byte `json:"-"` } golang-gopkg-hlandau-acmeapi.v2-2.0.1/types_test.go000066400000000000000000000005341355172230600221630ustar00rootroot00000000000000package acmeapi import ( "encoding/json" "testing" ) func TestStatus(t *testing.T) { var s OrderStatus err := json.Unmarshal([]byte(`"pending"`), &s) if err != nil { t.Fatalf("%v", err) } if s != "pending" || !s.IsWellFormed() || s.IsFinal() { t.Fatal() } err = json.Unmarshal([]byte(`"f9S0"`), &s) if err == nil { t.Fatal() } } golang-gopkg-hlandau-acmeapi.v2-2.0.1/util-errors.go000066400000000000000000000030521355172230600222450ustar00rootroot00000000000000package acmeapi import ( "encoding/json" "fmt" denet "github.com/hlandau/goutils/net" "io/ioutil" "mime" "net/http" ) // Error returned when an HTTP request results in a valid response, but which // has an unexpected failure status code. Used so that the response can still // be examined if desired. type HTTPError struct { // The HTTP response which was an error. Res.Body is no longer available // by the time the HTTPError is returned. Res *http.Response // If the response had an application/problem+json response body, this is // the parsed problem. nil if the problem document was unparseable. Problem *Problem // If the response had an application/problem+json response body, this is // that JSON data. ProblemRaw json.RawMessage } // Summarises the response status, headers, and the JSON problem body if available. func (he *HTTPError) Error() string { return fmt.Sprintf("HTTP error: %v\n%v", he.Res.Status, he.Problem) } func (he *HTTPError) Temporary() bool { switch he.Res.StatusCode { case 202, 408, 500, 502, 503, 504: return true default: return false } } func newHTTPError(res *http.Response) error { defer res.Body.Close() he := &HTTPError{ Res: res, } mimeType, params, err := mime.ParseMediaType(res.Header.Get("Content-Type")) if err == nil && validateContentType(mimeType, params, "application/problem+json") == nil { b, err := ioutil.ReadAll(denet.LimitReader(res.Body, 512*1024)) if err == nil { he.ProblemRaw = b json.Unmarshal([]byte(he.ProblemRaw), &he.Problem) // ignore errors } } return he } golang-gopkg-hlandau-acmeapi.v2-2.0.1/util-retry.go000066400000000000000000000025501355172230600221000ustar00rootroot00000000000000package acmeapi import ( "context" "net/http" "strconv" "time" "github.com/hlandau/goutils/clock" ) var defaultClock = clock.Real func parseRetryAfter(h http.Header) (t time.Time, ok bool) { v := h.Get("Retry-After") if v == "" { return time.Time{}, false } n, err := strconv.ParseUint(v, 10, 31) if err != nil { t, err = time.Parse(time.RFC1123, v) if err != nil { return time.Time{}, false } return t, true } return defaultClock.Now().Add(time.Duration(n) * time.Second), true } func retryAtDefault(h http.Header, d time.Duration) time.Time { t, ok := parseRetryAfter(h) if ok { return t } return defaultClock.Now().Add(d) } // Wait until time t. If t is before the current time, returns immediately. // Cancellable via ctx, in which case err is passed through. Otherwise returns // nil. func waitUntil(ctx context.Context, t time.Time) error { var ch <-chan time.Time ch = closedChannel now := defaultClock.Now() if t.After(now) { ch = defaultClock.After(t.Sub(now)) } // make sure ctx.Done() is checked here even when we are using closedChannel, // as select doesn't guarantee any particular priority. select { case <-ctx.Done(): return ctx.Err() default: select { case <-ctx.Done(): return ctx.Err() case <-ch: } } return nil } var closedChannel = make(chan time.Time) func init() { close(closedChannel) } golang-gopkg-hlandau-acmeapi.v2-2.0.1/util-retry_test.go000066400000000000000000000042671355172230600231460ustar00rootroot00000000000000package acmeapi import ( "context" "github.com/hlandau/goutils/clock" "net/http" "testing" "time" ) func withClock(cl clock.Clock, f func()) { origClock := defaultClock defer func() { defaultClock = origClock }() defaultClock = cl f() } var clk clock.Fake var slowClk clock.Fake func init() { refTime, _ := time.Parse(time.RFC3339, "2009-10-11T11:09:06Z") clk = clock.NewFastAt(refTime) slowClk = clock.NewSlowAt(refTime) } func TestRetryAfter(t *testing.T) { withClock(clk, func() { h := http.Header{} t1, ok := parseRetryAfter(h) if ok { t.Fatal() } h.Set("Retry-After", "Mon, 02 Jan 2006 15:04:05 UTC") t1 = retryAtDefault(h, 1*time.Second) tref, _ := time.Parse("Mon, 02 Jan 2006 15:04:05 UTC", "Mon, 02 Jan 2006 15:04:05 UTC") if t1 != tref || tref.IsZero() { t.Fatal() } t2, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") if t1 != t2 { t.Fatalf("%v %v", t1, t2) } h.Set("Retry-After", "20") t1, ok = parseRetryAfter(h) now := defaultClock.Now() if !ok { t.Fatal() } d := now.Add(20 * time.Second).Sub(t1) if d != 0 { t.Fatalf("%v", d) } h.Set("Retry-After", "Mon 02 Jan 2006 15:04:05 UTC") t1, ok = parseRetryAfter(h) if ok || !t1.IsZero() { t.Fatal() } }) } func TestRetryAfterDefault(t *testing.T) { withClock(clk, func() { h := http.Header{} t1 := retryAtDefault(h, 42*time.Second) now := defaultClock.Now() d := now.Add(42 * time.Second).Sub(t1) if d != 0 { t.Fatalf("%v", d) } }) } func TestWaitUntil(t *testing.T) { withClock(clk, func() { tgt := defaultClock.Now().Add(49828 * time.Millisecond) waitUntil(context.TODO(), tgt) if defaultClock.Now().Sub(tgt) != 0 { t.Fatalf("%v", defaultClock.Now().Sub(tgt)) } }) withClock(slowClk, func() { tgt := defaultClock.Now().Add(49828 * time.Millisecond) ctx, _ := context.WithTimeout(context.TODO(), 10*time.Millisecond) err := waitUntil(ctx, tgt) if err == nil { t.Fatal() } ctx, cancel := context.WithCancel(context.TODO()) cancel() err = waitUntil(ctx, tgt) if err == nil { t.Fatal() } slowClk.Advance(49829 * time.Millisecond) err = waitUntil(context.TODO(), tgt) if err != nil { t.Fatal() } }) }