Thank you everyone, for proposing clients that could help me checking ACME account keys! I could not try all of them, so I picked the go program proposed by @_az. I hope the other proposals can be useful for someone else in the future.
With that go program, I was able to confirmed that one of the suspected private keys was indeed a Let's Encrypt account key. Both production and staging accepted it. In order to test that, I've done a few modifications to the code:
import (
"crypto"
"crypto/x509"
"encoding/pem"
"errors"
"io/ioutil"
"log"
"os"
"github.com/eggsampler/acme/v3"
)
func main() {
if len(os.Args) < 2 || len(os.Args) >= 4 {
log.Fatalf("Usage: %s [pem private key] {acme url}", os.Args[0])
}
keyFile := os.Args[1]
acmeUrl := "https://acme-v02.api.letsencrypt.org/directory"
if len(os.Args) >= 3 {
acmeUrl = os.Args[2]
}
buf, err := ioutil.ReadFile(keyFile)
if err != nil {
log.Fatalf("Couldn't read PEM file: %v", err)
}
asPEM, _ := pem.Decode(buf)
var privateKey interface{}
switch asPEM.Type {
case "PRIVATE KEY":
privateKey, err = x509.ParsePKCS8PrivateKey(asPEM.Bytes)
case "RSA PRIVATE KEY":
privateKey, err = x509.ParsePKCS1PrivateKey(asPEM.Bytes)
case "EC PRIVATE KEY":
privateKey, err = x509.ParseECPrivateKey(asPEM.Bytes)
default:
log.Fatalf("Unsupported PEM key type: %s", asPEM.Type)
}
if err != nil {
log.Fatalf("Couldn't parse PEM private key: %v", err)
}
// Query the ACME server with this key
client, err := acme.NewClient(acmeUrl)
if err != nil {
log.Fatalf("Error loading ACME directory: %v", err)
}
var acmeError acme.Problem
_, err = client.NewAccount(privateKey.(crypto.Signer), true, true)
if err != nil {
errors.As(err, &acmeError)
if acmeError.Type == "urn:ietf:params:acme:error:accountDoesNotExist" {
log.Fatalf("Account does not exist for key %s! ACME server said:\n%v", keyFile, err)
} else if acmeError.Type == "urn:ietf:params:acme:error:unauthorized" {
log.Fatalf("Account is deactivated for key %s! ACME server said:\n%v", keyFile, err)
} else {
log.Fatalf("Failed to query account for unknown reason! ACME server said:\v%v", err)
}
} else {
log.Printf("Ding ding! Account exists for key %s", keyFile)
}
}
My changes are:
- Allow to pass an ACME server URL as optional CLI argument. That helped me testing with Let's Encrypt staging and could work against other ACME servers, too.
- Improve handling of ACME responses. Turns out that Let's Encrypt uses different errors for unknown accounts vs deactivated accounts. The program takes that into account now, but also prints the exact error message from the server.
It's the first time I've tinkered with go code, other than a simple hello world. So please excuse the code quality!
One more remark:
The program accepts key files in PEM format. This happened to be the format that I've encountered in our git repo. Certbot stores the account key in JWK format instead (which is also used in some ACME request bodies). In order to test with a new account created with certbot, I used an online converter. This was fine, because that account had not issued certs yet, didn't have any authorizations, and I unregistered it shortly after. In other situations, please do not post private keys to random websites!