diff --git a/acmev2/client.go b/acmev2/client.go index 38e78da8..a329769a 100644 --- a/acmev2/client.go +++ b/acmev2/client.go @@ -149,6 +149,11 @@ func (c *Client) GetToSURL() string { 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. func (c *Client) Register(tosAgreed bool) (*RegistrationResource, error) { if c == nil || c.user == nil { @@ -183,6 +188,55 @@ func (c *Client) Register(tosAgreed bool) (*RegistrationResource, error) { 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 // and return its registration resource. func (c *Client) ResolveAccountByKey() (*RegistrationResource, error) { diff --git a/acmev2/jws.go b/acmev2/jws.go index 9b87e437..26fcb20d 100644 --- a/acmev2/jws.go +++ b/acmev2/jws.go @@ -87,6 +87,35 @@ func (j *jws) signContent(url string, content []byte) (*jose.JSONWebSignature, e 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) { if nonce, ok := j.nonces.Pop(); ok { return nonce, nil diff --git a/acmev2/messages.go b/acmev2/messages.go index 0b734437..696ffeb3 100644 --- a/acmev2/messages.go +++ b/acmev2/messages.go @@ -2,6 +2,7 @@ package acme import ( "time" + "encoding/json" ) // RegistrationResource represents all important informations about a registration @@ -26,11 +27,12 @@ type directory struct { } type accountMessage struct { - Status string `json:"status,omitempty"` - Contact []string `json:"contact,omitempty"` - TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"` - Orders string `json:"orders,omitempty"` - OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"` + Status string `json:"status,omitempty"` + Contact []string `json:"contact,omitempty"` + TermsOfServiceAgreed bool `json:"termsOfServiceAgreed,omitempty"` + Orders string `json:"orders,omitempty"` + OnlyReturnExisting bool `json:"onlyReturnExisting,omitempty"` + ExternalAccountBinding json.RawMessage `json:"externalAccountBinding,omitempty"` } type orderResource struct { diff --git a/cli.go b/cli.go index ff3800a9..f8d85f58 100644 --- a/cli.go +++ b/cli.go @@ -128,6 +128,18 @@ func main() { Name: "accept-tos, a", 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{ Name: "key-type, k", Value: "rsa2048", diff --git a/cli_handlers.go b/cli_handlers.go index 4c939c86..dd71c9ed 100644 --- a/cli_handlers.go +++ b/cli_handlers.go @@ -122,6 +122,10 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) { 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 } @@ -241,6 +245,8 @@ func readCSRFile(filename string) (*x509.CertificateRequest, error) { } func run(c *cli.Context) error { + var err error + conf, acc, client := setup(c) if acc.Registration == nil { 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.") } - 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 { logger().Fatalf("Could not complete registration\n\t%s", err.Error()) } @@ -301,7 +325,7 @@ func run(c *cli.Context) error { 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()) }