Add External Account Binding support. (#516)

* Second draft of External Account Binding support with xenolf's proposed
changes included.
* Require --eab if the ACME directory says it requires External Account
Binding.
* Inner EAB JWS should not contain any nonce. Ref: https://ietf-wg-acme.github.io/acme/draft-ietf-acme-acme.html#rfc.section.7.3.5
This commit is contained in:
Robert Kästel 2018-05-30 02:05:57 +02:00 committed by Ludovic Fernandez
parent 8a990209a9
commit 5115a955b2
5 changed files with 128 additions and 7 deletions

View file

@ -149,6 +149,11 @@ func (c *Client) GetToSURL() string {
return c.directory.Meta.TermsOfService return c.directory.Meta.TermsOfService
} }
// GetExternalAccountRequired returns the External Account Binding requirement of the Directory
func (c *Client) GetExternalAccountRequired() bool {
return c.directory.Meta.ExternalAccountRequired
}
// Register the current account to the ACME server. // Register the current account to the ACME server.
func (c *Client) Register(tosAgreed bool) (*RegistrationResource, error) { func (c *Client) Register(tosAgreed bool) (*RegistrationResource, error) {
if c == nil || c.user == nil { if c == nil || c.user == nil {
@ -183,6 +188,55 @@ func (c *Client) Register(tosAgreed bool) (*RegistrationResource, error) {
return reg, nil return reg, nil
} }
// Register the current account to the ACME server.
func (c *Client) RegisterWithExternalAccountBinding(tosAgreed bool, kid string, hmacEncoded string) (*RegistrationResource, error) {
if c == nil || c.user == nil {
return nil, errors.New("acme: cannot register a nil client or user")
}
logf("[INFO] acme: Registering account (EAB) for %s", c.user.GetEmail())
accMsg := accountMessage{}
if c.user.GetEmail() != "" {
accMsg.Contact = []string{"mailto:" + c.user.GetEmail()}
} else {
accMsg.Contact = []string{}
}
accMsg.TermsOfServiceAgreed = tosAgreed
hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded)
if err != nil {
return nil, fmt.Errorf("acme: could not decode hmac key: %s", err.Error())
}
eabJWS, err := c.jws.signEABContent(c.directory.NewAccountURL, kid, hmac)
if err != nil {
return nil, fmt.Errorf("acme: error signing eab content: %s", err.Error())
}
eabPayload := eabJWS.FullSerialize()
accMsg.ExternalAccountBinding = []byte(eabPayload)
var serverReg accountMessage
hdr, err := postJSON(c.jws, c.directory.NewAccountURL, accMsg, &serverReg)
if err != nil {
remoteErr, ok := err.(RemoteError)
if ok && remoteErr.StatusCode == 409 {
} else {
return nil, err
}
}
reg := &RegistrationResource{
URI: hdr.Get("Location"),
Body: serverReg,
}
c.jws.kid = reg.URI
return reg, nil
}
// ResolveAccountByKey will attempt to look up an account using the given account key // ResolveAccountByKey will attempt to look up an account using the given account key
// and return its registration resource. // and return its registration resource.
func (c *Client) ResolveAccountByKey() (*RegistrationResource, error) { func (c *Client) ResolveAccountByKey() (*RegistrationResource, error) {

View file

@ -87,6 +87,35 @@ func (j *jws) signContent(url string, content []byte) (*jose.JSONWebSignature, e
return signed, nil return signed, nil
} }
func (j *jws) signEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) {
jwk := jose.JSONWebKey{Key: j.privKey}
jwkJSON, err := jwk.Public().MarshalJSON()
if err != nil {
return nil, fmt.Errorf("acme: error encoding eab jwk key: %s", err.Error())
}
signer, err := jose.NewSigner(
jose.SigningKey{Algorithm: jose.HS256, Key: hmac},
&jose.SignerOptions{
EmbedJWK: false,
ExtraHeaders: map[jose.HeaderKey]interface{}{
"kid": kid,
"url": url,
},
},
)
if err != nil {
return nil, fmt.Errorf("Failed to create External Account Binding jose signer -> %s", err.Error())
}
signed, err := signer.Sign(jwkJSON)
if err != nil {
return nil, fmt.Errorf("Failed to External Account Binding sign content -> %s", err.Error())
}
return signed, nil
}
func (j *jws) Nonce() (string, error) { func (j *jws) Nonce() (string, error) {
if nonce, ok := j.nonces.Pop(); ok { if nonce, ok := j.nonces.Pop(); ok {
return nonce, nil return nonce, nil

View file

@ -2,6 +2,7 @@ package acme
import ( import (
"time" "time"
"encoding/json"
) )
// RegistrationResource represents all important informations about a registration // RegistrationResource represents all important informations about a registration
@ -26,11 +27,12 @@ type directory struct {
} }
type accountMessage struct { type accountMessage struct {
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
Contact []string `json:"contact,omitempty"` Contact []string `json:"contact,omitempty"`
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"` TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"`
Orders string `json:"orders,omitempty"` Orders string `json:"orders,omitempty"`
OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"` OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"`
ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"`
} }
type orderResource struct { type orderResource struct {

12
cli.go
View file

@ -128,6 +128,18 @@ func main() {
Name: "accept-tos, a", Name: "accept-tos, a",
Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.", Usage: "By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.",
}, },
cli.BoolFlag{
Name: "eab",
Usage: "Use External Account Binding for account registration. Requires --kid and --hmac.",
},
cli.StringFlag{
Name: "kid",
Usage: "Key identifier from External CA. Used for External Account Binding.",
},
cli.StringFlag{
Name: "hmac",
Usage: "MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.",
},
cli.StringFlag{ cli.StringFlag{
Name: "key-type, k", Name: "key-type, k",
Value: "rsa2048", Value: "rsa2048",

View file

@ -122,6 +122,10 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01}) client.ExcludeChallenges([]acme.Challenge{acme.HTTP01})
} }
if client.GetExternalAccountRequired() && !c.GlobalIsSet("eab") {
logger().Fatal("Server requires External Account Binding. Use --eab with --kid and --hmac.")
}
return conf, acc, client return conf, acc, client
} }
@ -241,6 +245,8 @@ func readCSRFile(filename string) (*x509.CertificateRequest, error) {
} }
func run(c *cli.Context) error { func run(c *cli.Context) error {
var err error
conf, acc, client := setup(c) conf, acc, client := setup(c)
if acc.Registration == nil { if acc.Registration == nil {
accepted := handleTOS(c, client) accepted := handleTOS(c, client)
@ -248,7 +254,25 @@ func run(c *cli.Context) error {
logger().Fatal("You did not accept the TOS. Unable to proceed.") logger().Fatal("You did not accept the TOS. Unable to proceed.")
} }
reg, err := client.Register(accepted) var reg *acme.RegistrationResource
if c.GlobalBool("eab") {
kid := c.GlobalString("kid")
hmacEncoded := c.GlobalString("hmac")
if kid == "" || hmacEncoded == "" {
logger().Fatalf("Requires arguments --kid and --hmac.")
}
reg, err = client.RegisterWithExternalAccountBinding(
accepted,
kid,
hmacEncoded,
)
} else {
reg, err = client.Register(accepted)
}
if err != nil { if err != nil {
logger().Fatalf("Could not complete registration\n\t%s", err.Error()) logger().Fatalf("Could not complete registration\n\t%s", err.Error())
} }
@ -301,7 +325,7 @@ func run(c *cli.Context) error {
os.Exit(1) os.Exit(1)
} }
if err := checkFolder(conf.CertPath()); err != nil { if err = checkFolder(conf.CertPath()); err != nil {
logger().Fatalf("Could not check/create path: %s", err.Error()) logger().Fatalf("Could not check/create path: %s", err.Error())
} }