Trouble with external account HMAC

1. Caddy version (caddy version):

v2.0.0 h1:pQSaIJGFluFvu8KDGDODV8u4/QRED/OPyIR+MWYYse8=

2. How I run Caddy:

still trying

a. System environment:

centos7

b. Command:

caddy run --config caddy.json

c. Service/unit/compose file:

d. My complete Caddyfile or JSON config:

{
  "admin": {
    "listen": ":2019"
  },
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "listen": [
            ":443"
          ],
          "routes": [
            {
              "handle": [
                {
                  "handler": "subroute",
                  "routes": [
                    {
                      "handle": [
                        {
                          "handler": "file_server",
                          "hide": [
                            "Caddyfile"
                          ]
                        }
                      ]
                    }
                  ]
                }
              ],
              "match": [
                {
                  "host": [
                    "emrb-appdev01.icts.uiowa.edu"
                  ]
                }
              ],
              "terminal": true
            }
          ],
          "tls_connection_policies": [
            {
              "default_sni": "emrb-appdev01.icts.uiowa.edu"
            }
          ]
        }
      }
    },
    "tls": {
      "automation": {
        "on_demand": {
          "rate_limit": {
            "burst": 1,
            "interval": 1800000000000
          }
        },
        "policies": [
          {
            "issuer": {
              "ca": "https://acme.sectigo.com/v2/InCommonRSAOV",
              "email": "redacted@uni.edu",
              "external_account": {
                "hmac": "redacted",
                "key_id": "redacted"
              },
              "module": "acme"
            }
          }
        ]
      }
    }
  },
  "logging": {
    "logs": {
      "default": {
        "level": "DEBUG"
      }
    }
  }
}

3. The problem I’m having:

Trying to get the external account binding to work. But I can’t get it to accept the HMAC

I have tried several things including putting the same string I put into the json config into this go program

package main

import (
        "fmt"
        "encoding/base64"
)

func main() {

  str := "redacted"
  x, err := base64.StdEncoding.DecodeString(str)

  
  if err != nil {
    fmt.Println(err)
  }
  fmt.Println(string(x))
}

This program is able to decode.
In addition, in the json value I have tried adding a \n at the end (there is an equal sign present for padding)

4. Error messages and/or full log output:

5. What I already tried:

2020/06/09 09:29:15 [ERROR] Making new ACME client: acme: could not decode hmac key: illegal base64 data at input byte 115 (attempt 1/2)

6. Links to relevant resources:

The error is pretty clear:

could not decode hmac key: illegal base64 data at input byte 115

So look at byte 115 and make sure it’s valid base64.

It is or else I would expected my program to give the same error?

I’m trying to trace it down, but learning go in the process so slow. There seems to be a discrepancy between lego and certmagic

Lego is using RawURLEncoding

certmagic is using StdEncoding

If I make this change

diff --git a/acmeclient.go b/acmeclient.go
index 33803de..7b7f362 100644
--- a/acmeclient.go
+++ b/acmeclient.go
@@ -168,7 +168,7 @@ func (am *ACMEManager) newACMEClient(useTestCA, interactive bool) (*acmeClient,
 			reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
 				TermsOfServiceAgreed: am.Agreed,
 				Kid:                  am.ExternalAccount.KeyID,
-				HmacEncoded:          base64.StdEncoding.EncodeToString(am.ExternalAccount.HMAC),
+				HmacEncoded:          base64.RawURLEncoding.EncodeToString(am.ExternalAccount.HMAC),
 			})
 		} else {
 			reg, err = client.Registration.Register(registration.RegisterOptions{

to certmagic and then retry I get past the base64 issue and get a different error:

2020/06/09 11:38:36 [ERROR] attempt 1: [emrb-appdev01.icts.uiowa.edu] Obtain: acme: error: 0 :: POST :: https://acme.sectigo.com/v2/InCommonRSAOV/newAccount :: urn:ietf:params:acme:error:malformed :: [External Account Binding] Invalid MAC on JWS request, url:  - retrying in 1m0s (3.497480004s/720h0m0s elapsed)...

I am not sure if that is because I am running this form my development machine or if something else is wrong too

1 Like

Thanks for finding that! I’ll try to fixup certmagic with that soon.

As for the second error, Invalid MAC on JWS request, I am not sure. It does not originate from the ACME library (lego) or CertMagic…

@matt hold up on the change … I am starting to think that the value should be passed straight through without any extra encoding / decoding. Trying to test it out

That’s possible, if your provider already gave you the base64 encoded HMAC.

I have successfully gotten caddy to provision with sectigo. Forgive the length, I don’t think I understand everything well enough to be brief.

I’m pretty sure we don’t want certmagic to do any encoding of the value. Lego ultimately wants the bytes[] and will RawURL decode the value it is given.

diff --git a/acmeclient.go b/acmeclient.go
index 33803de..75c99e0 100644
--- a/acmeclient.go
+++ b/acmeclient.go
@@ -17,7 +17,6 @@ package certmagic
 import (
 	"context"
 	"crypto/tls"
-	"encoding/base64"
 	"fmt"
 	"log"
 	weakrand "math/rand"
@@ -168,7 +167,7 @@ func (am *ACMEManager) newACMEClient(useTestCA, interactive bool) (*acmeClient,
 			reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
 				TermsOfServiceAgreed: am.Agreed,
 				Kid:                  am.ExternalAccount.KeyID,
-				HmacEncoded:          base64.StdEncoding.EncodeToString(am.ExternalAccount.HMAC),
+				HmacEncoded:          string(am.ExternalAccount.HMAC),
 			})
 		} else {
 			reg, err = client.Registration.Register(registration.RegisterOptions{

In sectigo’s case (I can’t speak to any other ACME server) the hmac key it gives you is already RawURL encoded (meaning go’s definition of RawURL which means without padding)
I suspect this will be true in many cases though because whatever the implementation is it has to give it to the user somehow and that probably means showing it to them on a screen which means encoding it.

A possible future enhancement for Caddy might be to relax the decoding it does to the hmac, so that it can handle it with or without the padding. As it is right now, what I have to do is copy the value out of sectigo then run it through this

valueFromUI := []byte("REDACTED")
str1 := base64.StdEncoding.EncodeToString(valueFromUI)
fmt.Println("Encoding for config")
fmt.Println(str1)

And take that value and put it into my caddy.json.

Specific to sectigo (and possible how our org is configured):

  1. Once an external account binding has been ?registered? it cannot be reused.
  2. Must use rsa2048 or rsa4096 key type or you get a completely unhelpful error
  3. No challenge is ever performed for the request. I ask for a domain that has been associated with the external account and I get a cert.

Last question, I would love it if I could put the external account into the caddy file. I suspect this won’t be terribly complex to implement? Would you take a PR and be willing to provide some guidance? Given that it is only valid for one use, maybe it is best to only expose it via the config api?

1 Like

Ah, thanks for looking into it more! I’ve been trying to get free accounts with Sectigo and other CAs to test with for months now but no luck so far. :slightly_frowning_face: This is very helpful, thank you.

I think it’s reasonable if CertMagic is changed to not do any decoding or manipulation of the HMAC value, i.e. just accept the user’s input directly, as your patch suggests. How’s that sound? Then you don’t need to go through any encoding rigamarole.

Absolutely – it’s not hard. Would be happy to review a PR for this.

Sounds good.

I think it looks like there’s an options.go that registers the global options, does that seem like a reasonable place to add the parameter logic?

Just to be clear, there is still rigamarole because Caddy itself is doing its own decoding of the value when it reads it from the json

RawUrl == no padding
Std == padding
base64 package - encoding/base64 - pkg.go.dev

  1. sectigo → output hmac[RawUrl base64]
  2. caddy.json ← input hmac[Std base64]
  3. caddy module ← decode hmac from caddy.json so Std base64 (now bytes)
  4. caddy → certmagic string(bytes)
  5. certmagic ← decode hmac [RawUrl base64]

I just don’t know if changing caddy would cause problems for people so I didn’t suggest it, but maybe it could look at the end of the string to decide which encoding to use or could do a try/catch/try sort of thing?

It’s just a string – so the only decoding that’s done is JSON string decoding.

The field name in the JSON is just "hmac" so we can leave it as-is, and change the comment and the Go field name from EncodedHMAC to HMAC, especially since we’re fixing what is probably a bug. I guess it makes sense if CAs give you an HMAC that is already encoded the way they want.

So all we have to do is ensure that neither Caddy nor CertMagic are encoding or decoding the value any further than that of a JSON string: no extra special handling.

Yeah, maybe as a subdirective of the acme_ca option.

@chrisortman I have pushed a commit on the eab-fix branch that I hope will resolve the issue. Together with this patch in CertMagic, we don’t do any more encoding or decoding of the HMAC – we just use it as-is given by the user and we treat it as a string.

Can you build Caddy from the eab-fix branch and see if that works for you? Hopefully you can just paste in the HMAC as your provider gave it to you.

Just tested and it works great. 100% less rigamarole too!

1 Like

Excellent, thanks for confirming!

This topic was automatically closed after 30 days. New replies are no longer allowed.