Browse Source

Remove square/go-jose dependency

master
Isaac 4 years ago
parent
commit
7108fcc89b
  1. 2
      README.md
  2. 35
      THIRD-PARTY
  3. 51
      account.go
  4. 3
      account_test.go
  5. 114
      acme.go
  6. 3
      acme_test.go
  7. 3
      certificate.go
  8. 6
      example/certbot.go
  9. 158
      jws.go
  10. 322
      jws_test.go
  11. 3
      nonce.go
  12. 9
      nonce_test.go
  13. 30
      options.go
  14. 31
      options_test.go
  15. 5
      order_test.go
  16. 14
      types.go

2
README.md

@ -15,4 +15,4 @@ This code demonstrates account registation, new order submission, fulfilling cha
## Tests
The tests are designed to be run against a local instance of [boulder](https://github.com/letsencrypt/boulder) running the `config-next` configuration.
The tests are designed to be run against a local instance of [boulder](https://github.com/letsencrypt/boulder) running the `config-next` configuration along with the FAKE_DNS variable set.

35
THIRD-PARTY

@ -0,0 +1,35 @@
This document contains Third Party Software Notices and/or Additional Terms and Conditions for licensed third party software components included within this product.
==
https://github.com/golang/crypto/blob/master/acme/jws.go
https://github.com/golang/crypto/blob/master/acme/jws_test.go
(with modifications)
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

51
account.go

@ -6,30 +6,12 @@ import (
"fmt"
"crypto"
"encoding/base64"
"encoding/json"
"crypto/ecdsa"
"crypto/rsa"
"gopkg.in/square/go-jose.v2"
)
// Helper function to make an account "thumbprint" used as part of authorization challenges
// More details: https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-8.1
func makeThumbprint(privateKey interface{}) (string, error) {
jwk := jose.JSONWebKey{Key: privateKey}
bThumbprint, err := jwk.Thumbprint(crypto.SHA256)
if err != nil {
return "", fmt.Errorf("acme: error making account key thumbprint: %v", err)
}
return base64.RawURLEncoding.EncodeToString(bThumbprint), nil
}
// NewAccount registers a new account with the acme service
// More details: https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.3
func (c AcmeClient) NewAccount(privateKey interface{}, onlyReturnExisting, termsOfServiceAgreed bool, contact ...string) (AcmeAccount, error) {
func (c AcmeClient) NewAccount(privateKey crypto.Signer, onlyReturnExisting, termsOfServiceAgreed bool, contact ...string) (AcmeAccount, error) {
newAccountReq := struct {
OnlyReturnExisting bool `json:"onlyReturnExisting"`
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
@ -50,7 +32,7 @@ func (c AcmeClient) NewAccount(privateKey interface{}, onlyReturnExisting, terms
account.PrivateKey = privateKey
if account.Thumbprint == "" {
account.Thumbprint, err = makeThumbprint(account.PrivateKey)
account.Thumbprint, err = JWKThumbprint(account.PrivateKey.Public())
if err != nil {
return account, err
}
@ -82,7 +64,7 @@ func (c AcmeClient) UpdateAccount(account AcmeAccount, termsOfServiceAgreed bool
}
if account.Thumbprint == "" {
account.Thumbprint, err = makeThumbprint(account.PrivateKey)
account.Thumbprint, err = JWKThumbprint(account.PrivateKey.Public())
if err != nil {
return account, err
}
@ -93,34 +75,31 @@ func (c AcmeClient) UpdateAccount(account AcmeAccount, termsOfServiceAgreed bool
// AccountKeyChange rolls over an account to a new key.
// More details: https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.3.6
func (c AcmeClient) AccountKeyChange(account AcmeAccount, newPrivateKey interface{}) (AcmeAccount, error) {
var newJwKeyPub jose.JSONWebKey
switch k := newPrivateKey.(type) {
case *rsa.PrivateKey:
newJwKeyPub = jose.JSONWebKey{Algorithm: "RSA", Key: k.Public()}
case *ecdsa.PrivateKey:
newJwKeyPub = jose.JSONWebKey{Algorithm: "ECDSA", Key: k.Public()}
default:
return account, fmt.Errorf("acme: unsupported private key type: %+v", k)
func (c AcmeClient) AccountKeyChange(account AcmeAccount, newPrivateKey crypto.Signer) (AcmeAccount, error) {
newJwkKeyPub, err := jwkEncode(newPrivateKey.Public())
if err != nil {
return account, fmt.Errorf("acme: error encoding new private key: %v", err)
}
keyChangeReq := struct {
Account string `json:"account"`
NewKey jose.JSONWebKey `json:"newKey"`
NewKey json.RawMessage `json:"newKey"`
}{
Account: account.Url,
NewKey: newJwKeyPub,
NewKey: []byte(newJwkKeyPub),
}
innerJws, err := encapsulateJws(nil, c.Directory.KeyChange, "", newPrivateKey, keyChangeReq)
nonce, err := c.nonces.Nonce()
if err != nil {
return account, err
}
var b json.RawMessage
b = []byte(innerJws.FullSerialize())
innerJws, err := jwsEncodeJSON(keyChangeReq, newPrivateKey, c.Directory.KeyChange, "", nonce)
if err != nil {
return account, fmt.Errorf("acme: error encoding inner jws: %v", err)
}
if _, err := c.post(c.Directory.KeyChange, account.Url, account.PrivateKey, b, nil, http.StatusOK); err != nil {
if _, err := c.post(c.Directory.KeyChange, account.Url, account.PrivateKey, json.RawMessage(innerJws), nil, http.StatusOK); err != nil {
return account, err
}

3
account_test.go

@ -6,10 +6,11 @@ import (
"crypto/rand"
"testing"
"crypto"
"reflect"
)
func makePrivateKey(t *testing.T) interface{} {
func makePrivateKey(t *testing.T) crypto.Signer {
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("error creating account private key: %v", err)

114
acme.go

@ -11,12 +11,8 @@ import (
"strings"
"crypto/ecdsa"
"crypto/rsa"
"crypto/tls"
"gopkg.in/square/go-jose.v2"
"bytes"
"crypto"
)
const (
@ -25,32 +21,32 @@ const (
// NewClient creates a new acme client given a valid directory url.
// More details: https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.1.1
func NewClient(directoryURL string) (AcmeClient, error) {
ns := &nonceStack{}
client := AcmeClient{
httpClient: &http.Client{
Timeout: time.Second * 30,
},
nonces: ns,
func NewClient(directoryURL string, options ...AcmeOptionFunc) (AcmeClient, error) {
httpClient := http.DefaultClient
ns := &nonceStack{
client: httpClient,
}
if Debug {
client.httpClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
acmeClient := AcmeClient{
httpClient: httpClient,
nonces: ns,
}
for _, opt := range options {
if err := opt(acmeClient); err != nil {
return acmeClient, fmt.Errorf("acme: error setting option: %v", err)
}
}
if _, err := client.get(directoryURL, &client.Directory, http.StatusOK); err != nil {
return client, err
if _, err := acmeClient.get(directoryURL, &acmeClient.Directory, http.StatusOK); err != nil {
return acmeClient, err
}
client.Directory.Url = directoryURL
ns.newNonceURL = client.Directory.NewNonce
acmeClient.Directory.Url = directoryURL
ns.newNonceURL = acmeClient.Directory.NewNonce
return client, nil
return acmeClient, nil
}
// Helper function to get the poll interval and poll timeout, defaulting if 0
@ -124,72 +120,20 @@ func (c AcmeClient) get(url string, out interface{}, expectedStatus ...int) (*ht
return resp, nil
}
// Encapsulates a payload into a JSON Web Signature
// More details: https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-6.2
func encapsulateJws(nonceSource jose.NonceSource, requestURL, keyID string, privateKey interface{}, payload interface{}) (*jose.JSONWebSignature, error) {
var keyAlgo jose.SignatureAlgorithm
switch k := privateKey.(type) {
case *rsa.PrivateKey:
keyAlgo = jose.RS256
case *ecdsa.PrivateKey:
switch k.Params().Name {
case "P-256":
keyAlgo = jose.ES256
case "P-384":
keyAlgo = jose.ES384
case "P-521":
keyAlgo = jose.ES512
default:
return nil, fmt.Errorf("acme: unsupported private key ecdsa params: %s", k.Params().Name)
}
default:
return nil, fmt.Errorf("acme: unsupported private key type: %v", k)
}
rawPayload, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("acme: error marshalling payload: %v", err)
}
opts := jose.SignerOptions{}
if nonceSource != nil {
opts.NonceSource = nonceSource
}
opts.WithHeader("url", requestURL)
// jwk and kid fields are mutually exclusive
if keyID != "" {
opts.WithHeader("kid", keyID)
} else {
opts.EmbedJWK = true
}
sig := jose.SigningKey{
Key: privateKey,
Algorithm: keyAlgo,
}
signer, err := jose.NewSigner(sig, &opts)
if err != nil {
return nil, fmt.Errorf("acme: error creating new signer: %v", err)
}
object, err := signer.Sign(rawPayload)
if err != nil {
return object, fmt.Errorf("acme: error signing payload: %v", err)
}
return object, nil
}
// Helper function to perform an http post request and read the body.
// Will attempt to retry if error is badNonce
func (c AcmeClient) postRaw(isRetry bool, requestURL, keyID string, privateKey interface{}, payload interface{}, out interface{}, expectedStatus []int) (*http.Response, []byte, error) {
object, err := encapsulateJws(c.nonces, requestURL, keyID, privateKey, payload)
func (c AcmeClient) postRaw(isRetry bool, requestURL, keyID string, privateKey crypto.Signer, payload interface{}, out interface{}, expectedStatus []int) (*http.Response, []byte, error) {
nonce, err := c.nonces.Nonce()
if err != nil {
return nil, nil, err
}
req, err := http.NewRequest("POST", requestURL, strings.NewReader(object.FullSerialize()))
data, err := jwsEncodeJSON(payload, privateKey, requestURL, keyID, nonce)
if err != nil {
return nil, nil, fmt.Errorf("acme: error encoding json payload: %v", err)
}
req, err := http.NewRequest("POST", requestURL, bytes.NewReader(data))
if err != nil {
return nil, nil, fmt.Errorf("acme: error creating request: %v", err)
}
@ -228,7 +172,7 @@ func (c AcmeClient) postRaw(isRetry bool, requestURL, keyID string, privateKey i
}
// Helper function for performing a http post to an acme resource.
func (c AcmeClient) post(requestURL, keyID string, privateKey interface{}, payload interface{}, out interface{}, expectedStatus ...int) (*http.Response, error) {
func (c AcmeClient) post(requestURL, keyID string, privateKey crypto.Signer, payload interface{}, out interface{}, expectedStatus ...int) (*http.Response, error) {
resp, body, err := c.postRaw(false, requestURL, keyID, privateKey, payload, out, expectedStatus)
if err != nil {
return resp, err

3
acme_test.go

@ -12,8 +12,6 @@ import (
const (
testDirectoryUrl = "http://localhost:4001/directory" // boulder
// testDirectoryUrl = "https://localhost:14000/dir" // pebble
// testDirectoryUrl = "https://acme-staging-v02.api.letsencrypt.org/directory" // lets encrypt acme v2 staging
)
var testClient AcmeClient
@ -22,7 +20,6 @@ var challengeMap sync.Map
func init() {
rand.Seed(time.Now().UnixNano())
var err error
Debug = true
testClient, err = NewClient(testDirectoryUrl)
if err != nil {
panic("error connecting to acme server: " + err.Error())

3
certificate.go

@ -1,6 +1,7 @@
package acme
import (
"crypto"
"crypto/x509"
"encoding/base64"
"encoding/pem"
@ -46,7 +47,7 @@ func (c AcmeClient) FetchCertificates(certificateURL string) ([]*x509.Certificat
// RevokeCertificate revokes a given certificate given the certificate key or account key, and a reason.
// More details: https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.6
func (c AcmeClient) RevokeCertificate(account AcmeAccount, cert *x509.Certificate, key interface{}, reason int) error {
func (c AcmeClient) RevokeCertificate(account AcmeAccount, cert *x509.Certificate, key crypto.Signer, reason int) error {
revokeReq := struct {
Certificate string `json:"certificate"`
Reason int `json:"reason"`

6
example/certbot.go

@ -36,8 +36,8 @@ var (
)
type acmeAccountFile struct {
PrivateKey interface{} `json:"privateKey"`
Url string `json:"url"`
PrivateKey *ecdsa.PrivateKey `json:"privateKey"`
Url string `json:"url"`
}
func main() {
@ -240,7 +240,7 @@ func createAccount(client acme.AcmeClient) (acme.AcmeAccount, error) {
if err != nil {
return acme.AcmeAccount{}, fmt.Errorf("error creating new account: %v", err)
}
raw, err := json.Marshal(acmeAccountFile{PrivateKey: account.PrivateKey, Url: account.Url})
raw, err := json.Marshal(acmeAccountFile{PrivateKey: privKey, Url: account.Url})
if err != nil {
return acme.AcmeAccount{}, fmt.Errorf("error parsing new account: %v", err)
}

158
jws.go

@ -0,0 +1,158 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the THIRD-PARTY file.
package acme
import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
_ "crypto/sha512" // need for EC keys
"encoding/base64"
"encoding/json"
"fmt"
"math/big"
)
// jwsEncodeJSON signs claimset using provided key and a nonce.
// The result is serialized in JSON format.
// See https://tools.ietf.org/html/rfc7515#section-7.
func jwsEncodeJSON(claimset interface{}, key crypto.Signer, requestURL, keyID, nonce string) ([]byte, error) {
jwk, err := jwkEncode(key.Public())
if err != nil {
return nil, err
}
alg, sha := jwsHasher(key)
if alg == "" || !sha.Available() {
return nil, ErrUnsupportedKey
}
var phead string
if keyID != "" {
phead = fmt.Sprintf(`{"alg":%q,"kid":%q,"nonce":%q,"url":%q}`, alg, keyID, nonce, requestURL)
} else {
phead = fmt.Sprintf(`{"alg":%q,"jwk":%s,"nonce":%q,"url":%q}`, alg, jwk, nonce, requestURL)
}
phead = base64.RawURLEncoding.EncodeToString([]byte(phead))
cs, err := json.Marshal(claimset)
if err != nil {
return nil, err
}
payload := base64.RawURLEncoding.EncodeToString(cs)
hash := sha.New()
hash.Write([]byte(phead + "." + payload))
sig, err := jwsSign(key, sha, hash.Sum(nil))
if err != nil {
return nil, err
}
enc := struct {
Protected string `json:"protected"`
Payload string `json:"payload"`
Sig string `json:"signature"`
}{
Protected: phead,
Payload: payload,
Sig: base64.RawURLEncoding.EncodeToString(sig),
}
return json.Marshal(&enc)
}
// jwkEncode encodes public part of an RSA or ECDSA key into a JWK.
// The result is also suitable for creating a JWK thumbprint.
// https://tools.ietf.org/html/rfc7517
func jwkEncode(pub crypto.PublicKey) (string, error) {
switch pub := pub.(type) {
case *rsa.PublicKey:
// https://tools.ietf.org/html/rfc7518#section-6.3.1
n := pub.N
e := big.NewInt(int64(pub.E))
// Field order is important.
// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`,
base64.RawURLEncoding.EncodeToString(e.Bytes()),
base64.RawURLEncoding.EncodeToString(n.Bytes()),
), nil
case *ecdsa.PublicKey:
// https://tools.ietf.org/html/rfc7518#section-6.2.1
p := pub.Curve.Params()
n := p.BitSize / 8
if p.BitSize%8 != 0 {
n++
}
x := pub.X.Bytes()
if n > len(x) {
x = append(make([]byte, n-len(x)), x...)
}
y := pub.Y.Bytes()
if n > len(y) {
y = append(make([]byte, n-len(y)), y...)
}
// Field order is important.
// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`,
p.Name,
base64.RawURLEncoding.EncodeToString(x),
base64.RawURLEncoding.EncodeToString(y),
), nil
}
return "", ErrUnsupportedKey
}
// jwsSign signs the digest using the given key.
// It returns ErrUnsupportedKey if the key type is unknown.
// The hash is used only for RSA keys.
func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) {
switch key := key.(type) {
case *rsa.PrivateKey:
return key.Sign(rand.Reader, digest, hash)
case *ecdsa.PrivateKey:
r, s, err := ecdsa.Sign(rand.Reader, key, digest)
if err != nil {
return nil, err
}
rb, sb := r.Bytes(), s.Bytes()
size := key.Params().BitSize / 8
if size%8 > 0 {
size++
}
sig := make([]byte, size*2)
copy(sig[size-len(rb):], rb)
copy(sig[size*2-len(sb):], sb)
return sig, nil
}
return nil, ErrUnsupportedKey
}
// jwsHasher indicates suitable JWS algorithm name and a hash function
// to use for signing a digest with the provided key.
// It returns ("", 0) if the key is not supported.
func jwsHasher(key crypto.Signer) (string, crypto.Hash) {
switch key := key.(type) {
case *rsa.PrivateKey:
return "RS256", crypto.SHA256
case *ecdsa.PrivateKey:
switch key.Params().Name {
case "P-256":
return "ES256", crypto.SHA256
case "P-384":
return "ES384", crypto.SHA384
case "P-521":
return "ES512", crypto.SHA512
}
}
return "", 0
}
// JWKThumbprint creates a JWK thumbprint out of pub
// as specified in https://tools.ietf.org/html/rfc7638.
func JWKThumbprint(pub crypto.PublicKey) (string, error) {
jwk, err := jwkEncode(pub)
if err != nil {
return "", err
}
b := sha256.Sum256([]byte(jwk))
return base64.RawURLEncoding.EncodeToString(b[:]), nil
}

322
jws_test.go

@ -0,0 +1,322 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the THIRD-PARTY file.
package acme
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"math/big"
"testing"
)
const (
testKeyPEM = `
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA4xgZ3eRPkwoRvy7qeRUbmMDe0V+xH9eWLdu0iheeLlrmD2mq
WXfP9IeSKApbn34g8TuAS9g5zhq8ELQ3kmjr+KV86GAMgI6VAcGlq3QrzpTCf/30
Ab7+zawrfRaFONa1HwEzPY1KHnGVkxJc85gNkwYI9SY2RHXtvln3zs5wITNrdosq
EXeaIkVYBEhbhNu54pp3kxo6TuWLi9e6pXeWetEwmlBwtWZlPoib2j3TxLBksKZf
oyFyek380mHgJAumQ/I2fjj98/97mk3ihOY4AgVdCDj1z/GCoZkG5Rq7nbCGyosy
KWyDX00Zs+nNqVhoLeIvXC4nnWdJMZ6rogxyQQIDAQABAoIBACIEZTOI1Kao9nmV
9IeIsuaR1Y61b9neOF/MLmIVIZu+AAJFCMB4Iw11FV6sFodwpEyeZhx2WkpWVN+H
r19eGiLX3zsL0DOdqBJoSIHDWCCMxgnYJ6nvS0nRxX3qVrBp8R2g12Ub+gNPbmFm
ecf/eeERIVxfifd9VsyRu34eDEvcmKFuLYbElFcPh62xE3x12UZvV/sN7gXbawpP
G+w255vbE5MoaKdnnO83cTFlcHvhn24M/78qP7Te5OAeelr1R89kYxQLpuGe4fbS
zc6E3ym5Td6urDetGGrSY1Eu10/8sMusX+KNWkm+RsBRbkyKq72ks/qKpOxOa+c6
9gm+Y8ECgYEA/iNUyg1ubRdH11p82l8KHtFC1DPE0V1gSZsX29TpM5jS4qv46K+s
8Ym1zmrORM8x+cynfPx1VQZQ34EYeCMIX212ryJ+zDATl4NE0I4muMvSiH9vx6Xc
7FmhNnaYzPsBL5Tm9nmtQuP09YEn8poiOJFiDs/4olnD5ogA5O4THGkCgYEA5MIL
qWYBUuqbEWLRtMruUtpASclrBqNNsJEsMGbeqBJmoMxdHeSZckbLOrqm7GlMyNRJ
Ne/5uWRGSzaMYuGmwsPpERzqEvYFnSrpjW5YtXZ+JtxFXNVfm9Z1gLLgvGpOUCIU
RbpoDckDe1vgUuk3y5+DjZihs+rqIJ45XzXTzBkCgYBWuf3segruJZy5rEKhTv+o
JqeUvRn0jNYYKFpLBeyTVBrbie6GkbUGNIWbrK05pC+c3K9nosvzuRUOQQL1tJbd
4gA3oiD9U4bMFNr+BRTHyZ7OQBcIXdz3t1qhuHVKtnngIAN1p25uPlbRFUNpshnt
jgeVoHlsBhApcs5DUc+pyQKBgDzeHPg/+g4z+nrPznjKnktRY1W+0El93kgi+J0Q
YiJacxBKEGTJ1MKBb8X6sDurcRDm22wMpGfd9I5Cv2v4GsUsF7HD/cx5xdih+G73
c4clNj/k0Ff5Nm1izPUno4C+0IOl7br39IPmfpSuR6wH/h6iHQDqIeybjxyKvT1G
N0rRAoGBAKGD+4ZI/E1MoJ5CXB8cDDMHagbE3cq/DtmYzE2v1DFpQYu5I4PCm5c7
EQeIP6dZtv8IMgtGIb91QX9pXvP0aznzQKwYIA8nZgoENCPfiMTPiEDT9e/0lObO
9XWsXpbSTsRPj0sv1rB+UzBJ0PgjK4q2zOF0sNo7b1+6nlM3BWPx
-----END RSA PRIVATE KEY-----
`
// This thumbprint is for the testKey defined above.
testKeyThumbprint = "6nicxzh6WETQlrvdchkz-U3e3DOQZ4heJKU63rfqMqQ"
// openssl ecparam -name secp256k1 -genkey -noout
testKeyECPEM = `
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIK07hGLr0RwyUdYJ8wbIiBS55CjnkMD23DWr+ccnypWLoAoGCCqGSM49
AwEHoUQDQgAE5lhEug5xK4xBDZ2nAbaxLtaLiv85bxJ7ePd1dkO23HThqIrvawF5
QAaS/RNouybCiRhRjI3EaxLkQwgrCw0gqQ==
-----END EC PRIVATE KEY-----
`
// openssl ecparam -name secp384r1 -genkey -noout
testKeyEC384PEM = `
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDAQ4lNtXRORWr1bgKR1CGysr9AJ9SyEk4jiVnlUWWUChmSNL+i9SLSD
Oe/naPqXJ6CgBwYFK4EEACKhZANiAAQzKtj+Ms0vHoTX5dzv3/L5YMXOWuI5UKRj
JigpahYCqXD2BA1j0E/2xt5vlPf+gm0PL+UHSQsCokGnIGuaHCsJAp3ry0gHQEke
WYXapUUFdvaK1R2/2hn5O+eiQM8YzCg=
-----END EC PRIVATE KEY-----
`
// openssl ecparam -name secp521r1 -genkey -noout
testKeyEC512PEM = `
-----BEGIN EC PRIVATE KEY-----
MIHcAgEBBEIBSNZKFcWzXzB/aJClAb305ibalKgtDA7+70eEkdPt28/3LZMM935Z
KqYHh/COcxuu3Kt8azRAUz3gyr4zZKhlKUSgBwYFK4EEACOhgYkDgYYABAHUNKbx
7JwC7H6pa2sV0tERWhHhB3JmW+OP6SUgMWryvIKajlx73eS24dy4QPGrWO9/ABsD
FqcRSkNVTXnIv6+0mAF25knqIBIg5Q8M9BnOu9GGAchcwt3O7RDHmqewnJJDrbjd
GGnm6rb+NnWR9DIopM0nKNkToWoF/hzopxu4Ae/GsQ==
-----END EC PRIVATE KEY-----
`
// 1. openssl ec -in key.pem -noout -text
// 2. remove first byte, 04 (the header); the rest is X and Y
// 3. convert each with: echo <val> | xxd -r -p | base64 -w 100 | tr -d '=' | tr '/+' '_-'
testKeyECPubX = "5lhEug5xK4xBDZ2nAbaxLtaLiv85bxJ7ePd1dkO23HQ"
testKeyECPubY = "4aiK72sBeUAGkv0TaLsmwokYUYyNxGsS5EMIKwsNIKk"
testKeyEC384PubX = "MyrY_jLNLx6E1-Xc79_y-WDFzlriOVCkYyYoKWoWAqlw9gQNY9BP9sbeb5T3_oJt"
testKeyEC384PubY = "Dy_lB0kLAqJBpyBrmhwrCQKd68tIB0BJHlmF2qVFBXb2itUdv9oZ-TvnokDPGMwo"
testKeyEC512PubX = "AdQ0pvHsnALsfqlraxXS0RFaEeEHcmZb44_pJSAxavK8gpqOXHvd5Lbh3LhA8atY738AGwMWpxFKQ1VNeci_r7SY"
testKeyEC512PubY = "AXbmSeogEiDlDwz0Gc670YYByFzC3c7tEMeap7CckkOtuN0Yaebqtv42dZH0MiikzSco2ROhagX-HOinG7gB78ax"
// echo -n '{"crv":"P-256","kty":"EC","x":"<testKeyECPubX>","y":"<testKeyECPubY>"}' | \
// openssl dgst -binary -sha256 | base64 | tr -d '=' | tr '/+' '_-'
testKeyECThumbprint = "zedj-Bd1Zshp8KLePv2MB-lJ_Hagp7wAwdkA0NUTniU"
)
var (
testKey *rsa.PrivateKey
testKeyEC *ecdsa.PrivateKey
testKeyEC384 *ecdsa.PrivateKey
testKeyEC512 *ecdsa.PrivateKey
)
func init() {
testKey = parseRSA(testKeyPEM, "testKeyPEM")
testKeyEC = parseEC(testKeyECPEM, "testKeyECPEM")
testKeyEC384 = parseEC(testKeyEC384PEM, "testKeyEC384PEM")
testKeyEC512 = parseEC(testKeyEC512PEM, "testKeyEC512PEM")
}
func decodePEM(s, name string) []byte {
d, _ := pem.Decode([]byte(s))
if d == nil {
panic("no block found in " + name)
}
return d.Bytes
}
func parseRSA(s, name string) *rsa.PrivateKey {
b := decodePEM(s, name)
k, err := x509.ParsePKCS1PrivateKey(b)
if err != nil {
panic(fmt.Sprintf("%s: %v", name, err))
}
return k
}
func parseEC(s, name string) *ecdsa.PrivateKey {
b := decodePEM(s, name)
k, err := x509.ParseECPrivateKey(b)
if err != nil {
panic(fmt.Sprintf("%s: %v", name, err))
}
return k
}
func TestJWSEncodeJSON(t *testing.T) {
tests := []struct {
Claim interface{}
RequestURL string
KeyID string
Nonce string
Protected string
Payload string
Signature string
}{
{
Claim: struct{ Msg string }{Msg: "Hello JWS"},
RequestURL: "",
KeyID: "",
Nonce: "nonce",
Protected: "eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6IlJTQSIsIm4iOiI0eGdaM2VSUGt3b1J2eT" +
"dxZVJVYm1NRGUwVi14SDllV0xkdTBpaGVlTGxybUQybXFXWGZQOUllU0tBcGJuMzRnOFR1QVM5ZzV6aHE4RUxRM2ttanIt" +
"S1Y4NkdBTWdJNlZBY0dscTNRcnpwVENmXzMwQWI3LXphd3JmUmFGT05hMUh3RXpQWTFLSG5HVmt4SmM4NWdOa3dZSTlTWT" +
"JSSFh0dmxuM3pzNXdJVE5yZG9zcUVYZWFJa1ZZQkVoYmhOdTU0cHAza3hvNlR1V0xpOWU2cFhlV2V0RXdtbEJ3dFdabFBv" +
"aWIyajNUeExCa3NLWmZveUZ5ZWszODBtSGdKQXVtUV9JMmZqajk4Xzk3bWszaWhPWTRBZ1ZkQ0RqMXpfR0NvWmtHNVJxN2" +
"5iQ0d5b3N5S1d5RFgwMFpzLW5OcVZob0xlSXZYQzRubldkSk1aNnJvZ3h5UVEifSwibm9uY2UiOiJub25jZSIsInVybCI6IiJ9",
Payload: "eyJNc2ciOiJIZWxsbyBKV1MifQ",
Signature: "XnfumFVmQrm6mmewxMWbh6k1DQMhzCoo63grIo6RZS6Cfsr2ntAUHmHARvwoTp_tVQp6PbMWYRFjwmUMgLbpau" +
"YxrwVMwAQ2cf9T3SJnBcv8zS8lVeYLZ11oS1JAIAVRx2plEktdg5iqckEyn3WipglNeq-WEM7V0GzUYmUzWRCtCVfJw_2_" +
"dLzfKrEPx-y_AbOPF95eonII9YCPquawD5RAPxHrsrqbIch3g55DKG0MYwBFmcvZk_22-SaqNOZbIooxjP0xXWxaP6t1sh" +
"YLh0K0cFS1fFC96sSvf8lS-1FrDOML994jyGjusFo9yArn4GaxIAy4N8bhO9LezWXZng",
},
}
for _, currentTest := range tests {
b, err := jwsEncodeJSON(currentTest.Claim, testKey, currentTest.RequestURL, currentTest.KeyID, currentTest.Nonce)
if err != nil {
t.Fatal(err)
}
var jws struct{ Protected, Payload, Signature string }
if err := json.Unmarshal(b, &jws); err != nil {
t.Fatal(err)
}
if jws.Protected != currentTest.Protected {
t.Errorf("protected:\n%s\nwant:\n%s", jws.Protected, currentTest.Protected)
}
if jws.Payload != currentTest.Payload {
t.Errorf("payload:\n%s\nwant:\n%s", jws.Payload, currentTest.Payload)
}
if jws.Signature != currentTest.Signature {
t.Errorf("signature:\n%s\nwant:\n%s", jws.Signature, currentTest.Signature)
}
}
}
func TestJWSEncodeJSONEC(t *testing.T) {
tt := []struct {
key *ecdsa.PrivateKey
x, y string
alg, crv string
}{
{testKeyEC, testKeyECPubX, testKeyECPubY, "ES256", "P-256"},
{testKeyEC384, testKeyEC384PubX, testKeyEC384PubY, "ES384", "P-384"},
{testKeyEC512, testKeyEC512PubX, testKeyEC512PubY, "ES512", "P-521"},
}
for i, test := range tt {
claims := struct{ Msg string }{"Hello JWS"}
b, err := jwsEncodeJSON(claims, test.key, "", "", "nonce")
if err != nil {
t.Errorf("%d: %v", i, err)
continue
}
var jws struct{ Protected, Payload, Signature string }
if err := json.Unmarshal(b, &jws); err != nil {
t.Errorf("%d: %v", i, err)
continue
}
b, err = base64.RawURLEncoding.DecodeString(jws.Protected)
if err != nil {
t.Errorf("%d: jws.Protected: %v", i, err)
}
var head struct {
Alg string
Nonce string
JWK struct {
Crv string
Kty string
X string
Y string
} `json:"jwk"`
}
if err := json.Unmarshal(b, &head); err != nil {
t.Errorf("%d: jws.Protected: %v", i, err)
}
if head.Alg != test.alg {
t.Errorf("%d: head.Alg = %q; want %q", i, head.Alg, test.alg)
}
if head.Nonce != "nonce" {
t.Errorf("%d: head.Nonce = %q; want nonce", i, head.Nonce)
}
if head.JWK.Crv != test.crv {
t.Errorf("%d: head.JWK.Crv = %q; want %q", i, head.JWK.Crv, test.crv)
}
if head.JWK.Kty != "EC" {
t.Errorf("%d: head.JWK.Kty = %q; want EC", i, head.JWK.Kty)
}
if head.JWK.X != test.x {
t.Errorf("%d: head.JWK.X = %q; want %q", i, head.JWK.X, test.x)
}
if head.JWK.Y != test.y {
t.Errorf("%d: head.JWK.Y = %q; want %q", i, head.JWK.Y, test.y)
}
}
}
func TestJWKThumbprintRSA(t *testing.T) {
// Key example from RFC 7638
const base64N = "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt" +
"VT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn6" +
"4tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FD" +
"W2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n9" +
"1CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINH" +
"aQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw"
const base64E = "AQAB"
const expected = "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
b, err := base64.RawURLEncoding.DecodeString(base64N)
if err != nil {
t.Fatalf("Error parsing example key N: %v", err)
}
n := new(big.Int).SetBytes(b)
b, err = base64.RawURLEncoding.DecodeString(base64E)
if err != nil {
t.Fatalf("Error parsing example key E: %v", err)
}
e := new(big.Int).SetBytes(b)
pub := &rsa.PublicKey{N: n, E: int(e.Uint64())}
th, err := JWKThumbprint(pub)
if err != nil {
t.Error(err)
}
if th != expected {
t.Errorf("thumbprint = %q; want %q", th, expected)
}
}
func TestJWKThumbprintEC(t *testing.T) {
// Key example from RFC 7520
// expected was computed with
// echo -n '{"crv":"P-521","kty":"EC","x":"<base64X>","y":"<base64Y>"}' | \
// openssl dgst -binary -sha256 | \
// base64 | \
// tr -d '=' | tr '/+' '_-'
const (
base64X = "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkT" +
"KqjqvjyekWF-7ytDyRXYgCF5cj0Kt"
base64Y = "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUda" +
"QkAgDPrwQrJmbnX9cwlGfP-HqHZR1"
expected = "dHri3SADZkrush5HU_50AoRhcKFryN-PI6jPBtPL55M"
)
b, err := base64.RawURLEncoding.DecodeString(base64X)
if err != nil {
t.Fatalf("Error parsing example key X: %v", err)
}
x := new(big.Int).SetBytes(b)
b, err = base64.RawURLEncoding.DecodeString(base64Y)
if err != nil {
t.Fatalf("Error parsing example key Y: %v", err)
}
y := new(big.Int).SetBytes(b)
pub := &ecdsa.PublicKey{Curve: elliptic.P521(), X: x, Y: y}
th, err := JWKThumbprint(pub)
if err != nil {
t.Error(err)
}
if th != expected {
t.Errorf("thumbprint = %q; want %q", th, expected)
}
}
func TestJWKThumbprintErrUnsupportedKey(t *testing.T) {
_, err := JWKThumbprint(struct{}{})
if err != ErrUnsupportedKey {
t.Errorf("err = %q; want %q", err, ErrUnsupportedKey)
}
}

3
nonce.go

@ -12,7 +12,7 @@ type nonceStack struct {
lock sync.Mutex
stack []string
client http.Client
client *http.Client
newNonceURL string
}
@ -50,7 +50,6 @@ func (ns *nonceStack) pop() string {
return v
}
// NonceSource in gopkg.in/square/go-jose.v2/signing.go
// Used to insert a nonce field into a jws header.
func (ns *nonceStack) Nonce() (string, error) {
nonce := ns.pop()

9
nonce_test.go

@ -1,9 +1,14 @@
package acme
import "testing"
import (
"net/http"
"testing"
)
func TestNonceStack_Nonce(t *testing.T) {
ns := nonceStack{}
ns := nonceStack{
client: http.DefaultClient,
}
ns.push("test")
if len(ns.stack) != 1 {

30
options.go

@ -0,0 +1,30 @@
package acme
import (
"crypto/tls"
"net/http"
"time"
)
// Function prototype for passing options to NewClient
type AcmeOptionFunc func(client AcmeClient) error
// Option function which sets a timeout on the http client used by the AcmeClient
func WithHttpTimeout(duration time.Duration) AcmeOptionFunc {
return func(client AcmeClient) error {
client.httpClient.Timeout = duration
return nil
}
}
// Option function which sets InsecureSkipVerify on the http client transport tls client config used by the AcmeClient
func WithInsecureSkipVerify() AcmeOptionFunc {
return func(client AcmeClient) error {
client.httpClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
return nil
}
}

31
options_test.go

@ -0,0 +1,31 @@
package acme
import (
"net/http"
"testing"
"time"
)
func TestWithHttpTimeout(t *testing.T) {
acmeClient := AcmeClient{httpClient: http.DefaultClient}
timeout := 30 * time.Second
opt := WithHttpTimeout(timeout)
if err := opt(acmeClient); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if timeout != acmeClient.httpClient.Timeout {
t.Fatalf("timeout not set, expected %v, got %v", timeout, acmeClient.httpClient.Timeout)
}
}
func TestWithInsecureSkipVerify(t *testing.T) {
acmeClient := AcmeClient{httpClient: http.DefaultClient}
opt := WithInsecureSkipVerify()
if err := opt(acmeClient); err != nil {
t.Fatalf("unexpected error: %v", err)
}
tr := acmeClient.httpClient.Transport.(*http.Transport)
if tr.TLSClientConfig.InsecureSkipVerify != true {
t.Fatalf("InsecureSkipVerify not set")
}
}

5
order_test.go

@ -3,6 +3,7 @@ package acme
import (
"testing"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
@ -78,7 +79,7 @@ func TestAcmeClient_FetchOrder(t *testing.T) {
}
}
func newCSR(t *testing.T, domains []string) (*x509.CertificateRequest, interface{}) {
func newCSR(t *testing.T, domains []string) (*x509.CertificateRequest, crypto.Signer) {
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("error generating private key: %v", err)
@ -109,7 +110,7 @@ func newCSR(t *testing.T, domains []string) (*x509.CertificateRequest, interface
return csr, privKey
}
func makeOrderFinal(t *testing.T, domains []string) (AcmeAccount, AcmeOrder, interface{}) {
func makeOrderFinal(t *testing.T, domains []string) (AcmeAccount, AcmeOrder, crypto.Signer) {
csr, privKey := newCSR(t, domains)
var identifiers []AcmeIdentifier

14
types.go

@ -1,20 +1,20 @@
package acme
import (
"crypto"
"errors"
"net/http"
"time"
)
// Turns on debug mode. Currently, debug mode is only for disabling TLS checks for the http client.
var Debug = false
// Different possible challenge types provided by an ACME server.
var (
AcmeChallengeTypeDns01 = "dns-01"
AcmeChallengeTypeHttp01 = "http-01"
AcmeChallengeTypeTlsSni02 = "tls-sni-02"
AcmeChallengeTypeDns01 = "dns-01"
AcmeChallengeTypeHttp01 = "http-01"
)
var ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported")
// Constants used for certificate revocation, used for RevokeCertificate
// More details: https://tools.ietf.org/html/rfc5280#section-5.3.1
const (
@ -85,7 +85,7 @@ type AcmeAccount struct {
// The private key used to create or fetch the account.
// Not fetched from server.
PrivateKey interface{} `json:"-"`
PrivateKey crypto.Signer `json:"-"`
// SHA-256 digest JWK_Thumbprint of the account key.
// Used in updating challenges, see: https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-8.1

Loading…
Cancel
Save