{'type': 'urn:ietf:params:acme:error:malformed', 'detail': 'JWS verification error', 'status': 400} with python

I enconter this issue when creating my account with acme api

My domain is:

I'm creating an account and I haven't add a domain.

I ran this command:

import base64
import json
import requests
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec, utils
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat

# Function to base64url encode data
def base64url_encode(data):
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode('utf-8')

# Function to get a new nonce from Let's Encrypt
def get_nonce(new_nonce_url):
    response = requests.head(new_nonce_url)
    return response.headers['Replay-Nonce']

# Generate a new ECDSA key pair
private_key = ec.generate_private_key(ec.SECP256R1())

# Let's Encrypt API URLs
new_account_url = "https://acme-v02.api.letsencrypt.org/acme/new-acct"
new_nonce_url = "https://acme-v02.api.letsencrypt.org/acme/new-nonce"

# Get a new nonce from Let's Encrypt
nonce = get_nonce(new_nonce_url)

# Construct protected header
protected_header = {
    "alg": "ES256",
    "nonce": nonce,
    "url": new_account_url,
}

# Serialize the public key to DER format
public_key_der = private_key.public_key().public_bytes(
    encoding=Encoding.DER,
    format=PublicFormat.SubjectPublicKeyInfo
)

# Extract x and y coordinates from the DER encoded public key
x_coordinate = public_key_der[27:59]   # Adjust indices as per the key format
y_coordinate = public_key_der[59:]     # Adjust indices as per the key format

# Base64url encode the coordinates
x_base64 = base64.urlsafe_b64encode(x_coordinate).rstrip(b'=').decode('utf-8')
y_base64 = base64.urlsafe_b64encode(y_coordinate).rstrip(b'=').decode('utf-8')

# Construct the JWK (JSON Web Key)
jwk = {
    "kty": "EC",
    "crv": "P-256",  # Adjust as per the curve used (e.g., P-384 for SECP384R1)
    "x": x_base64,
    "y": y_base64
}

# Add the JWK to the protected header
protected_header["jwk"] = jwk

# Base64url encode the protected header
protected_header_base64 = base64url_encode(json.dumps(protected_header).encode('utf-8'))

# Construct payload
payload = {
    "termsOfServiceAgreed": True,
    "contact": ["mailto:my email"],  # Replace with your email address
}
payload_base64 = base64url_encode(json.dumps(payload).encode('utf-8'))

# Combine protected header and payload
message = f"{protected_header_base64}.{payload_base64}".encode('utf-8')

# Sign the message
signature = private_key.sign(
    message,
    ec.ECDSA(hashes.SHA256())
)

# Encode the signature in DER format
r, s = utils.decode_dss_signature(signature)
signature_der = utils.encode_dss_signature(r, s)
signature_base64 = base64url_encode(signature_der)

# Construct the final JWS (JSON Web Signature) payload
jws_payload = {
    "protected": protected_header_base64,
    "payload": payload_base64,
    "signature": signature_base64
}

# Send the registration request to Let's Encrypt
headers = {
    "Content-Type": "application/jose+json"
}
response = requests.post(new_account_url, headers=headers, json=jws_payload)

# Print the response from Let's Encrypt
print(response.json())

It produced this output:

{'type': 'urn:ietf:params:acme:error:malformed', 'detail': 'JWS verification error', 'status': 400}

My web server is (include version):

I ran this locally with python 3.12.1

The operating system my web server runs on is (include version):

Microsoft Windows 10.0.19045.4529

My hosting provider, if applicable, is:

I'm running this locally

I can login to a root shell on my machine (yes or no, or I don't know):

Yes, but I'm not going to install my ssl cert on my computer, I don't have root access on my remote server.

I'm using a control panel to manage my site (no, or provide the name and version of the control panel):

NO

The version of my client is (e.g. output of certbot --version or certbot-auto --version if you're using Certbot):

python 3.12.1

This code is given by chatgpt but I don't understand how was theJWK generated.

Hi @xinjianzhanghao,

Let’s Encrypt offers Domain Validation (DV) certificates.

So without a domain name Let’s Encrypt will not be able to issue a certificate.

2 Likes

You should use a well-known and tested ACME library.

The above appears to be untested random snippets of code, probably emitted from an AI search engine, and run against production level endpoints.

If you insist on starting from scratch, you should develop your client against a local test server like Pebble and have it pass a full local test suite before testing against the staging, and then running against production, endpoints.

4 Likes

Can you please elaborate what you're trying to do exactly? Are developing your own ACME client in Python? With what end goal? Just for the fun of it? Or perhaps to learn more about the ACME protocol? Or Python? Or are you actually trying to develop an ACME client for "daily use" in your environment/company?

3 Likes

I could be wrong but I think when you construct your protected header it may need to be in the exact order of:

"protected": base64url({
       "alg": "ES256",
       "jwk": {...},
       "nonce": "6S8IqOGY7eL2lsGoTZYifg",
       "url": "https://example.com/acme/new-account"
     })

I think otherwise you could perhaps run into problems with the message signature not matching?

3 Likes

There is a nuance here, so you are both right and wrong about this.

The spec will take a "Key/Value Pair" structure (hash, dict, mapping, object, etc), for both the protected and payload values, serialize them to UTF-8 encoded JSON strings, concat them into a single string, compute a signature based on the concatenated string, and create a "signed payload" that contains the protected, payload, and signature elements.

This means "the order does not matter" because the "inner payload" is serialized to JSON, and the "outer payload" contains both the serialized JSON and a signature built from the serialized JSON.

This however also means "the order DOES matter" in the sense that the "outer payload" must contain an (encoded and) serialized JSON object with the same order (and data) as what the signature was built on.

This can be pretty simple and straightforward. My code for generating it is below, and follows a pattern that most clients do - first compute the b64 serializations, then construct the signature and outer payload.

A potential cause of error though, is if the following conditions are met:

  • The function does not re-use the same computed string used for signature on the outer payload and protected keys, and
  • The serialization function is not deterministic (will always produce the same output/order from identical input.) JSON encoding is not deterministic in many programming environments, some will require an extra command to ensure ordered keys (or similar).

TLDR; the spec does not require a specific order of elements in the outer and inner payloads, but it does require the order of elements to be consistent across both outer and inner payloads.

    protected64 = b64_payload(protected)
    payload64 = b64_payload(payload)
    protected_input = "{0}.{1}".format(protected64, payload64).encode("utf8")
    signature = cert_utils.account_key__sign(
        protected_input,
        key_pem=accountKeyData.key_pem,
        key_pem_filepath=accountKeyData.key_pem_filepath,
    )
    _signed_payload = json.dumps(
        {
            "protected": protected64,
            "payload": payload64,
            "signature": cert_utils.jose_b64(signature),
        }
    )
4 Likes

Hi There :grinning:,thanks for replying,I'm currently creating an account via https://acme-v02.api.letsencrypt.org/acme/new-acct when facing this error. I think an account is needed to create an account?

1 Like

I'm just learning about the ACME protocol and I won't use this code in production due to its quality and my server can only excute php scripts. I'm just learning it for now. :wink: