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:
parent
8a990209a9
commit
5115a955b2
5 changed files with 128 additions and 7 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
12
cli.go
|
@ -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",
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue