Help with error message from server

Hi,
before realizing that there already alternative clients I started writing my own in PHP, based on the JavaScript-source from gethttpsforfree.com. Unfortunately I’m currently stuck when sending the new-reg request to the letsencrypt server. This is what I send (using mimetype application/json): {"header":{"alg":"RS256","jwk":{"e":"AQAB","kty":"RSA","n":"xlCo3ljLZUOmUN8-Fwd1wSLo3ZDZhHlJuiRF8Z8bTt_Y3cud42gH532fFXNzQrtxLbgdy0M_PPZBU8voQQJIeGJdHOrbvGtxf_b-BNoM49K-SE7x4aTk4pgA8RuFTScqU_2w4NpfB4aQcUngJNJzqbCq87kbYn9n3yVY97xnqxifnAbZnEZI-Hn0XkKSeS4dfxckfEHdETDgadOVOrmHkU6CG5ZI1G8H5IufaG21j1XzL0TJFdH7bTgrf_BadDXs74qDG93PsN5EW74nvb2Zz4AihFQtSsDEGXOAjfxCf1Pw-ADXUgmUoi4Pa0arAe4sTrf9hFrzIt4PmUn7i-ONbt52_aExJ2q2d1o_Xj9v3Y6v_K_g7-Ee5baaE_2b-uQRT9Pigt7Xxe5UidlDxPYreK5f7d-jnOQZRWVSunhrTj4c2KrcOQByCgG4EO0zfIA6akWZYzWsucu7w1OWExHaPett6GrI13R7WfZmLSjl232wxrEqjm71q3R8xMUIAKDpYFqDSaRsMuPEkRfm83M1wR-0NF4Gd8PEKsq8Qm-CRvvQBS8PWVwVtsfVWU5OyCGLP7M9YHhRz9ghTUzA3I3VDnekN9J3Ek9SDt2i0pvuldH_XYOEpebC7BdZPEQqfLJjI1Jl9KqcfoT5gquYQlc51oxQ-H8ogYEQJhQ_FtDfTms"}},"protected":"eyJub25jZSI6IkV5dzJwU3hXUVMxSk85QnVzVXNhOTk0ZlU2V05FOWF1NGNGOHdCSUpSMGcifQ","payload":"eyJyZXNvdXJjZSI6Im5ldy1yZWciLCJjb250YWN0IjoibWFpbHRvOndlYm1hc3RlckBleGFtcGxlLmNvbSIsImFncmVlbWVudCI6Imh0dHBzOlwvXC9sZXRzZW5jcnlwdC5vcmdcL2RvY3VtZW50c1wvTEUtU0EtdjEuMC4xLUp1bHktMjctMjAxNS5wZGYifQ","signature":"NFgQn55_OAQFR3h-xclQ9XQOvACDW9T4h5mQEoDvoyQTI6nBup26AVKKZr2ZzBpZJTJXGY13TQ-bf0jg7g8-cJcL9vyYOiQlaAtmYJxmjyZNZ21W1k080IYD2AMQK8EoFbz8tIRaE3C5esfnXM3A8DN7CUBod-EwcyD-7tOSpIBhtYR50bIvAFiplUJzU-ZFTOZ1RpFeDFUMpWsIbF4kNrsJ1f_pydC6WvVtSVVuP8NUIse7aGbTffPjTaPpsbDhd5QalOxm93AyXAtBONZZDZ-74-GDAAjFIsp5aYAR_-9AJ4YEZJIEBI8nJFpH2vURhzmH-z7kI30LXKK5ic-ZD3nfJyznXv8Xx9_WaJJblPzRSzm3d98XeoqTVcXc0mUwEOLxejEEzVDxvG5EwXx_Cj7236B668f95JeQ2v3CalFBMNX38MT3IvKIg-uDh6sfOEWOg6PSHrgPJmiiu52KRK2KPSRA72I7KJ8zJZeMepnKb07lSaeQefAAz__ql8CliWX-r6FoB-cz11jTLFxLoeHphR3kkzyZHsMTjC-5cW5TOvWlQb1N-mce7iI3gWUrN_66BdsY0opPF4MFhvbSfCKFwqGcxjkrtiB78f1dP1PM532VXBLzPsq3-Gv47ALkc5seo1YhbHQgtpMVJI29-Tqltaw15i-IWQ21-jSdnzM"}
The answer is:{"type":"urn:acme:error:malformed","detail":"Error unmarshaling JSON","status":400}What exactly is the problem? The signature is a SHA-256 signature of protected.payload (with the key in jwk).
I’m testing with the staging server using test keys created just for that purpose.
Best regards
Stefan

The JSON seems to parse fine, both the outer as well the base64 encoded objects. I’m afraid your signature is just not valid: Edit: Sorry, I forgot the urlsafe Base64 decoding.

The signature is indeed valid:

pry(main)> req = JSON.parse('{"header":{"alg":"RS256","jwk":{"e":"AQAB","kty":"RSA","n":"xlCo3ljLZUOmUN8-Fwd1wSLo3ZDZhHlJuiRF8Z8bTt_Y3cud42gH532fFXNzQrtxLbgdy0M_PPZBU8voQQJIeGJdHOrbvGtxf_b-BNoM49K-SE7x4aTk4pgA8RuFTScqU_2w4NpfB4aQcUngJNJzqbCq87kbYn9n3yVY97xnqxifnAbZnEZI-Hn0XkKSeS4dfxckfEHdETDgadOVOrmHkU6CG5ZI1G8H5IufaG21j1XzL0TJFdH7bTgrf_BadDXs74qDG93PsN5EW74nvb2Zz4AihFQtSsDEGXOAjfxCf1Pw-ADXUgmUoi4Pa0arAe4sTrf9hFrzIt4PmUn7i-ONbt52_aExJ2q2d1o_Xj9v3Y6v_K_g7-Ee5baaE_2b-uQRT9Pigt7Xxe5UidlDxPYreK5f7d-jnOQZRWVSunhrTj4c2KrcOQByCgG4EO0zfIA6akWZYzWsucu7w1OWExHaPett6GrI13R7WfZmLSjl232wxrEqjm71q3R8xMUIAKDpYFqDSaRsMuPEkRfm83M1wR-0NF4Gd8PEKsq8Qm-CRvvQBS8PWVwVtsfVWU5OyCGLP7M9YHhRz9ghTUzA3I3VDnekN9J3Ek9SDt2i0pvuldH_XYOEpebC7BdZPEQqfLJjI1Jl9KqcfoT5gquYQlc51oxQ-H8ogYEQJhQ_FtDfTms"}},"protected":"eyJub25jZSI6IkV5dzJwU3hXUVMxSk85QnVzVXNhOTk0ZlU2V05FOWF1NGNGOHdCSUpSMGcifQ","payload":"eyJyZXNvdXJjZSI6Im5ldy1yZWciLCJjb250YWN0IjoibWFpbHRvOndlYm1hc3RlckBleGFtcGxlLmNvbSIsImFncmVlbWVudCI6Imh0dHBzOlwvXC9sZXRzZW5jcnlwdC5vcmdcL2RvY3VtZW50c1wvTEUtU0EtdjEuMC4xLUp1bHktMjctMjAxNS5wZGYifQ","signature":"NFgQn55_OAQFR3h-xclQ9XQOvACDW9T4h5mQEoDvoyQTI6nBup26AVKKZr2ZzBpZJTJXGY13TQ-bf0jg7g8-cJcL9vyYOiQlaAtmYJxmjyZNZ21W1k080IYD2AMQK8EoFbz8tIRaE3C5esfnXM3A8DN7CUBod-EwcyD-7tOSpIBhtYR50bIvAFiplUJzU-ZFTOZ1RpFeDFUMpWsIbF4kNrsJ1f_pydC6WvVtSVVuP8NUIse7aGbTffPjTaPpsbDhd5QalOxm93AyXAtBONZZDZ-74-GDAAjFIsp5aYAR_-9AJ4YEZJIEBI8nJFpH2vURhzmH-z7kI30LXKK5ic-ZD3nfJyznXv8Xx9_WaJJblPzRSzm3d98XeoqTVcXc0mUwEOLxejEEzVDxvG5EwXx_Cj7236B668f95JeQ2v3CalFBMNX38MT3IvKIg-uDh6sfOEWOg6PSHrgPJmiiu52KRK2KPSRA72I7KJ8zJZeMepnKb07lSaeQefAAz__ql8CliWX-r6FoB-cz11jTLFxLoeHphR3kkzyZHsMTjC-5cW5TOvWlQb1N-mce7iI3gWUrN_66BdsY0opPF4MFhvbSfCKFwqGcxjkrtiB78f1dP1PM532VXBLzPsq3-Gv47ALkc5seo1YhbHQgtpMVJI29-Tqltaw15i-IWQ21-jSdnzM"}')
pry(main)> JSON::JWK.new(req["header"]["jwk"]).to_key.public_key.verify OpenSSL::Digest::SHA256.new, UrlSafeBase64.decode64(req["signature"]), "#{req["protected"]}.#{req["payload"]}"
=> true

The protected header seems okay (side note, you can include everything into the protected header too and spare the unprotected header):

pry(main)> JSON.parse UrlSafeBase64.decode64(req["protected"])
=> {"nonce"=>"Eyw2pSxWQS1JO9BusUsa994fU6WNE9au4cF8wBIJR0g"}

On to the payload:

pry(main)> JSON.parse UrlSafeBase64.decode64(req["payload"])
=> {"resource"=>"new-reg", "contact"=>"mailto:webmaster@example.com", "agreement"=>"https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"}

The spec says contact should be an array and I’m not sure if boulder implements or even accepts agreement yet.

Thanks, after changing contact to array it works! Actually I use agreement because gethttpsforfree does that (and I successfully created a certificate using that page). I don’t know Python, so I can barely read the code of the official client and the specification is somewhat complicated (is there a simple spec?)

I haven't seen anything, so if it really is too complicated for you your best bet is to go of other existing third party clients and libraries.

I think the reason it’s so complicated for me is that english is not my native language and it not only contains a lot of text, it seems to contain a lot of stuff that is not implemented anywhere yet.

Got it working! I just got my first certificate out of my client. All in just one PHP file, no call to external programs and no external libs (but you need to have a webserver running). It’s currently quite messy, after cleaning it up I will publish it (probably after christmas).
The idea is to run it as a script (#!/usr/bin/php) on the webhost, not as a PHP script on the webserver run from the browser. So it can be run as cron-job once a day, it will check how long the certificate is still valid and if that is under a certain time (like less than 48 hours) it will renew it.
Are there special commands to renew or is it just a new cert with the same CSR? As long as I use the same key (same CSR) for the certificate the signature checked in HPKP remains the same, correct?
I noticed that the server after once accepting my public key with return code 201 it now always answers with 409, do I have to to that only once and never ever again? Or is just for a short time in the cache?

Renewal is just the same as initial issuance for now, that includes having to solve a challenge again. The official client generates a completely new key even.

I think so, so reusing simplifies rollover but might be harmful in case your key gets compromised without you noticing.

Which key? The account key? I've read somewhere it expires after 10 months, but I'm not sure that's true and if it's true I hope it's only 10 months after last usage. So yes, for the account key you only need to register it once.

[quote=“jhass, post:7, topic:6918”]
I think so, so reusing simplifies rollover but might be harmful in case your key gets compromised without you noticing.
[/quote]Yes, but on the other hand if the key got compromised unless someone found a way to break RSA the chance is high that the entire host is compromised (or at least the webserver), and in that case a compromised key is a minor problem compared to everything else (assuming you have PFS). On my host the key has never left it, it is owned by the webserver-user and no other user/group has access. Changing the key everytime makes HPKP quite useless. In case some malicious CA signed a fake certificate for your server the user cannot know anymore if it’s just a new key or hijacking

[quote=“jhass, post:7, topic:6918”]
Which key? The account key? I’ve read somewhere it expires after 10 months, but I’m not sure that’s true and if it’s true I hope it’s only 10 months after last usage. So yes, for the account key you only need to register it once.
[/quote]Yes, the account key. Does it really matter after what time it expires? Is there any reason why you shouldn’t always try to register? Either the return code is 201 or 409, for both you know it’s ok and you can continue.

I just wouldn't consider it a clean implementation when you can know whether you registered it already or not. I don't know if there are any plans to rate limit failed registration attempts too.

[quote=“jhass, post:9, topic:6918”]
I just wouldn’t consider it a clean implementation when you can know whether you registered it already or not. I don’t know if there are any plans to rate limit failed registration attempts too.
[/quote]You could also do it the other way round: First try to get a challenge, if you get a error message, register again. I don’t see any other way how to find out if you already registered (of course you could configure the client). Also in the return message of the registration there is no expire date.

Other question: I read in another thread that there is a 5 certificates per domain per 7 days rate limit, does that apply for the staging server as well? To solve the challenge I have to use a real domain and cannot use example.com. Or is there some other server without limitations to test client implementations, even though at the end it will just return something like “If you would connect to a real server this would be a certificate” instead of a certificate.

The staging server has much higher rate limits. It verifies the same way as production but uses a self signed untrusted CA to issue certificates.

Unfortunately my script does not work anymore: In the final step new-cert I get this answer now instead of a certificate:

{"type":"urn:acme:error:badNonce","detail":"Unable to read/verify body :: JWS has invalid anti-replay nonce","status":400}Has something changed at the server? Do I have to use a special nonce from a certain answer? The same script was working just a few days ago.
Sorry, my fault, I accidentally commented one line out and did not see that