Hi!
Iâm writing a custom ACME client and Iâm having difficulty reconciling the format of a JSON Web Signature in RFC 7515 https://tools.ietf.org/html/rfc7515#section-3 and the format of the Registration request in the ACME spec https://ietf-wg-acme.github.io/acme/#rfc.section.6.3
What exactly is the mapping between these two? Is the example registration object in the request in 6.3 of the ACME request intended to be the âJWS payloadâ?
Seeing an example POST request produced by the existing client might help.
I noticed that the ACME standard says that JWS Flattened JSON Serialization is used, so my best guess is that a request would look like:
POST /route HTTP/1.1
Host: example.com
{
âpayloadâ: â<base64url of {âresourceâ:ânew-regâ,âŚetc}>â,
âprotectedâ:"?",
âheaderâ:"?",
âsignatureâ:""
}
Hi here is an Post body example. The header is the Key Information you are using,
the protected is the base64 encoded request signature is the base64 signature of the protected.
Code:
final Map<String, Object> payload = new TreeMap<>();
payload.put(âresourceâ, ânew-regâ);
if (!empty(this.account.contacts )) { payload.put(âcontactâ , this.account.contacts); }
if (!empty(this.ca.acceptedTermsOfService)) { payload.put(âagreementâ, this.ca.acceptedTermsOfService); }
final String body = JWK.signWith(this.ca.SIG_ALG, this.account.accountKeyPair(), null, getNextNonce(this.ca.newReg), payload);
I hope this example will help.
{âheaderâ:
{âalgâ: âRS256â, âjwkâ: {âeâ: âAQABâ, âktyâ: âRSAâ, ânâ: âsZFtFY1cKjOWâŚVkâ}}, âprotectedâ: âeyJub25jZSI6ICJEY2xQekJHcW8tY0hDSVlSQW9QMTliZXg0bVI2cXRTR0hIbExaMWh1X0JNIn0â, âpayloadâ: âeyJpZGVudGlmaWVyIjogeyJ0eXBlIjogImRucyIsICJ2YWx1ZSI6ICJpcC0zNy0yNC0zMC0yOC5oc2kxNC51bml0eW1lZGlhZ3JvdXAuZGUifSwgInJlc291cmNlIjogIm5ldy1hdXRoeiJ9â, âsignatureâ: âIHPVUyK7ldJIQof7JHJ6cB3fM3EPTVRlTLvxwmcSXskCADLNfsZNaQNmp0WNy_Eaf9h9CnRSloRm5SEPgMGnKbfHN4ElQ8NX9tc_WUge2MvdghpcHiHG4L3bggoZxoq8QPIq41Xg0xwk_OfRe6WwfVvq9Va6eYUUt4Tb6VsMV9y1W0Po9Qev7T3iXTyPkTggfrHu__lpgDjDPbqNkCDR08bSicePvnN3VWUzRaPMf7NqbEHW-xKkfanaIFsK7iYr0i5fmJMlcNX8vFOIPk5SlYrbnykG9iA2ANvpdkHSMVP-8OGP54RFvx_tq4FJyLS0Ipfdm0ly_oQCZ19L7wqMAm9cR28mHHrOBUVDaDgP0cpxd-_lg9lc7n37owAE-zrbsWwOYFTY8xd2YgRJ3o6bMZpy6_LsIhZâ5ZntvFtzW8Je2_BYMCPo_Sf-HVRlGsM3Q_xvfu_G5-r_Vy2qmEVba-u634zxWZxKcndBE8mCmAuQurd2WWX3nJu4Rt3m_GYuuQ0l3zHmiy-pDvM2GiycLNe1JuGqp0llmy7W4Zjfdo7bmMn_XPStxCXroFSc50Jkyu50_J0Ac3t5rspyco_w9HdzgX1J2Tvq-ljm6x-jQg0nkygr4o8wC2uLwo1wZnYvWVRavQBMefKgNtkSsV5kSZIIiJmVdROIob_8rJNYd4â}
1 Like
Thanks, tlussnig! That helps clarify things.
Iâll implement this soon and let you know if I have any more detailed questions about your example and/or code.
In your example protected contains a nonce, the value of which I notice is also base64 encoded. What is actually signed though? My best guess is that the payload and protected values are concatenated and that object is what is signed to produce the contents of the signature field. I notice that the signature field is not valid base64 as it contains -
.
Also that example appears to be an ânew-authzâ example. Is the ânew-regâ request of the same format?
Here is an full example:
HEAD https://acme-staging.api.letsencrypt.org/acme/new-authz HTTP-405 Method Not Allowed
{"header": {"alg":"ES256","jwk":{"crv":"P-256","kty":"EC","x":"mZ6rH9ov6ZTAw78Y20gbkpMuFz2C8app2bwWuX3AVNo","y":"lYLPOcBaJL-7zqxcnaiXt1LvCK9QuEgiIyxv7CwHBPM"}}, "protected": "eyJub25jZSI6ICIzdmxWSDNkMHhvbDJIV2xKeUR3VEhUTGdmUmhPaTBjdGNKa0hJQ1lzaThZIn0", "payload": "eyJpZGVudGlmaWVyIjogeyJ0eXBlIjogImRucyIsICJ2YWx1ZSI6ICJpcC0zNy0yNC0zMC0yOC5oc2kxNC51bml0eW1lZGlhZ3JvdXAuZGUifSwgInJlc291cmNlIjogIm5ldy1hdXRoeiJ9", "signature": "eMeas1QBBqkB95IVT5vP4GpXtG_BDoBEQy_WjZj2Ue0WHPSgCzV039xMVZMBmk4JImz3AiKbFAQVxbTtufb3CQ"}
POST https://acme-staging.api.letsencrypt.org/acme/new-authz HTTP-201 Created {"identifier":{"type":"dns","value":"ip-37-24-30-28.hsi14.unitymediagroup.de "},"status":"pending","expires":"2016-02-03T21:00:44.968493419Z","challenges":[{"type":"tls-sni-01","status":"pending","uri":"https://acme-staging.api.letsencrypt.org/acme/challenge/IfXH0zEY3ibXj_qG5r_Zg8ajPD3k_8dPogsa2YM_iuY/1090298","token":"7hG_hhOkcH7MRyf46EXZG3N3bC0Mmj6ZoUDd2_OyT-8"},{"type":"http-01","status":"pending","uri":"https://acme-staging.api.letsencrypt.org/acme/challenge/IfXH0zEY3ibXj_qG5r_Zg8ajPD3k_8dPogsa2YM_iuY/1090299","token":"n-eyDsPVCLfD_nMubTmUzroJzESsa9Qh-O83W4sndbQ"},{"type":"dns-01","status":"pending","uri":"https://acme-staging.api.letsencrypt.org/acme/challenge/IfXH0zEY3ibXj_qG5r_Zg8ajPD3k_8dPogsa2YM_iuY/1090300","token":"NVPtlUXnH2Be267kMBhA5nDb7Bx7F2ZO4oAi7qAoDCo"}],"combinations ":[[0],[2],[1]]}
{"header": {"alg":"ES256","jwk":{"crv":"P-256","kty":"EC","x":"mZ6rH9ov6ZTAw78Y20gbkpMuFz2C8app2bwWuX3AVNo","y":"lYLPOcBaJL-7zqxcnaiXt1LvCK9QuEgiIyxv7CwHBPM"}}, "protected": "eyJub25jZSI6ICJHQmRsMUJvQUNiT2FGN2QyZ0o3NGRodVJmYXdaOU96NXllbjdEMjhWOENZIn0", "payload": "eyJrZXlBdXRob3JpemF0aW9uIjogIjdoR19oaE9rY0g3TVJ5ZjQ2RVhaRzNOM2JDME1tajZab1VEZDJfT3lULTguSGx0UU9ENk4zMENkcnlzY3VIdU1tUV9MQ1dPaWtYb3dpRng0MF93TVBYSSIsICJyZXNvdXJjZSI6ICJjaGFsbGVuZ2UiLCAidHlwZSI6ICJ0bHMtc25pLTAxIn0", "signature": "UsyQbDq4B_Bn1JFkmjQm3TOwhhtp-CIodmDXWDt2YyRD77Jr9Q2xY8N15wRHQibflnsuftjdKwbZrT-mDCUm4g"}
POST https://acme-staging.api.letsencrypt.org/acme/challenge/IfXH0zEY3ibXj_qG5r_Zg8ajPD3k_8dPogsa2YM_iuY/1090298 HTTP-202 Accepted {"type":"tls-sni-01","status":"pending","uri":"https://acme-staging.api.letsencrypt.org/acme/challenge/IfXH0zEY3ibXj_qG5r_Zg8ajPD3k_8dPogsa2YM_iuY/1090298","token":"7hG_hhOkcH7MRyf46EXZG3N3bC0Mmj6ZoUDd2_OyT-8","keyAuthorization":"7hG_hhOkcH7MRyf46EXZG3N3bC0Mmj6ZoUDd2_OyT-8.HltQOD6N30CdryscuHuMmQ_LCWOikXowiFx40_wMPXI "}
GET https://acme-staging.api.letsencrypt.org/acme/challenge/IfXH0zEY3ibXj_qG5r_Zg8ajPD3k_8dPogsa2YM_iuY/1090298 HTTP-202 Accepted {"type":"tls-sni-01","status":"valid","uri":"https://acme-staging.api.letsencrypt.org/acme/challenge/IfXH0zEY3ibXj_qG5r_Zg8ajPD3k_8dPogsa2YM_iuY/1090298","token":"7hG_hhOkcH7MRyf46EXZG3N3bC0Mmj6ZoUDd2_OyT-8","keyAuthorization":"7hG_hhOkcH7MRyf46EXZG3N3bC0Mmj6ZoUDd2_OyT-8.HltQOD6N30CdryscuHuMmQ_LCWOikXowiFx40_wMPXI","validationRecord":[{"hostname":"ip-37-24-30-28.hsi14.unitymediagroup.de","port":"443","addressesResolved":["37.24.30.28"],"addressUsed":"37.24.30.28 "}]}
{"header": {"alg":"ES256","jwk":{"crv":"P-256","kty":"EC","x":"mZ6rH9ov6ZTAw78Y20gbkpMuFz2C8app2bwWuX3AVNo","y":"lYLPOcBaJL-7zqxcnaiXt1LvCK9QuEgiIyxv7CwHBPM"}}, "protected": "eyJub25jZSI6ICJST0dlQXN4NE9XLUpPRG14TGs2T1d3MWRVZ0ctWWI2SGFFU0Rydkx0WERRIn0", "payload": "eyJyZXNvdXJjZSI6ICJuZXctY2VydCIsICJjc3IiOiAiTUlJQk16Q0IyUUlCQURBeU1UQXdMZ1lEVlFRREV5ZHBjQzB6TnkweU5DMHpNQzB5T0M1b2Mya3hOQzUxYm1sMGVXMWxaR2xoWjNKdmRYQXVaR1V3V1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVNkOVlxMFRpVmRaU00zbkRyeDJvSG9YVkhBd01hWFV5bkJjYmM2U3N3aHZyWFg4UGMwbFMzOUtTLXdNbDF5U09FSFVjR2E0SkVmY2ZsclBPLWNmR3Azb0VVd1F3WUpLb1pJaHZjTkFRa09NVFl3TkRBeUJnTlZIUkVFS3pBcGdpZHBjQzB6TnkweU5DMHpNQzB5T0M1b2Mya3hOQzUxYm1sMGVXMWxaR2xoWjNKdmRYQXVaR1V3REFZSUtvWkl6ajBFQXdJRkFBTkhBREJFQWlCa0o4cnlGNVotUFZSLXlsR0ZqMDg2dVVQQjBlN0F3NGxYZk1pU0QzSXpfQUlnUlBNSDBHSmVsY0FwVGNDMkRMUFQybU9IbXN5djl4MUhfUnR1S01vZE1BOCJ9", "signature": "CD-gNsZJ5JbYTMG385PYolqBlAgFr9hUuwjLaJ27SPY2Fi65WDdMa0TQcCMnwE_01Rt51UC4gQVUnDRpADPJUQ"}
POST https://acme-staging.api.letsencrypt.org/acme/new-cert HTTP-201 Created
Oh I see. I figured url safe base64 would be a subset of the default base64 encoding.
I see now that base64url is just an alternative character set for base64 encoding. I thought it was something more complicated.
Thanks tlussnig. Thereâs still not a ânew-regâ in there. Do I need a ânew-regâ?
I get the base64url encoding now but my big question is precisely what data am I actually supposed to be signing with my key to product the signature. Iâm writing that part myself.
For everybodyâs reference Iâm working in Go, but in this thread Iâm only trying to pin down the specification. Once thatâs clear the code is easy.
Here is the newReg sample
{"header": {"alg":"ES256","jwk":{"crv":"P-256","kty":"EC","x":"YNb5oD9_XiksR23sPjGlRvgjSa2FhaCiY1Ni7fcn7lE","y":"EDsKVBf4RWMw8T3mla_laURmDI6jG52CC4Sqx7Sc6jI"}}, "protected": "eyJub25jZSI6ICJFTXpiQjUtX1d6RDFEc29lbHhGdzFFQWVHUW5HSFc1VG5NSTNaa2NqS013In0", "payload": "eyJhZ3JlZW1lbnQiOiAiaHR0cHM6Ly9sZXRzZW5jcnlwdC5vcmcvZG9jdW1lbnRzL0xFLVNBLXYxLjAuMS1KdWx5LTI3LTIwMTUucGRmIiwgImNvbnRhY3QiOiBbIm1haWx0bzp0ZXN0MUBzdWNoZS5vcmciXSwgInJlc291cmNlIjogIm5ldy1yZWcifQ", "signature": "hZeY-NiMQvb1Ur8gjjl4y3OfwpqSTQvg59AbA-smHc4pTHbTfIS6xntzjOvkY8dTcEXcMp7f-h9JlDT9xFNqVg"}
POST https://acme-staging.api.letsencrypt.org/acme/new-reg HTTP-201 Created {"id":115335,"key":{"kty":"EC","crv":"P-256","x":"YNb5oD9_XiksR23sPjGlRvgjSa2FhaCiY1Ni7fcn7lE","y":"EDsKVBf4RWMw8T3mla_laURmDI6jG52CC4Sqx7Sc6jI"},"contact":["mailto:test1@suche.org "],"agreement":"https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf","initialIp":"37.24.30.28","createdAt":"2016-01-27T22:42:10.34151518Z "}
private static String signWith(final String hash, final KeyPair kp, final byte[] protected_,final byte[] payload_) {
final String jwkSigAlg = jwkSigAlg(hash, kp.getPublic());
final String jdkSigAlg = jdkSigAlg(hash, kp.getPublic());
final String headerJson = "{\"alg\":\""+jwkSigAlg+"\",\"jwk\":"+toJWK(kp.getPublic())+"}";
final String prot = base64UrlEncode(protected_);
final String payload = base64UrlEncode(payload_ );
try {
final Signature sig = Signature.getInstance(jdkSigAlg);
sig.initSign(kp.getPrivate());
sig.update(prot.getBytes());
sig.update(".".getBytes());
sig.update(payload.getBytes());
byte[] signatureBytes = sig.sign();
if(kp.getPublic() instanceof ECKey) signatureBytes = convertDerToConcatenated(signatureBytes, signatureBytes.length-6);
System.out.println("signWith("+jwkSigAlg+" , "+jdkSigAlg+")=["+signatureBytes.length+"]");
final String signature = base64UrlEncode(signatureBytes);
return "{\"header\": "+headerJson+", \"protected\": \""+prot+"\", \"payload\": \""+payload+"\", \"signature\": \""+signature+"\"}";
} catch(final Throwable t) {
throw new IllegalStateException("signWith("+jwkSigAlg+" , <"+kp.getPublic().getAlgorithm()+"> , ...) SigAlg="+jdkSigAlg+" => "+t.getMessage());
}
}
Cool, thanks a bunch! I gather from your code that the signature is a signature of the byte representation of:
eyJub25jZSI6ICJFTXpiQjUtX1d6RDFEc29lbHhGdzFFQWVHUW5HSFc1VG5NSTNaa2NqS013In0.eyJhZ3JlZW1lbnQiOiAiaHR0cHM6Ly9sZXRzZW5jcnlwdC5vcmcvZG9jdW1lbnRzL0xFLVNBLXYxLjAuMS1KdWx5LTI3LTIwMTUucGRmIiwgImNvbnRhY3QiOiBbIm1haWx0bzp0ZXN0MUBzdWNoZS5vcmciXSwgInJlc291cmNlIjogIm5ldy1yZWcifQ
I.E. the base64url encoded protected header followed by a â.â followed by the base64url encoded payload.
All the nonces in the protected headers above are 43 bytes. Are they always 43 bytes? I donât remember reading anything about that in the ACME or JWS specs. 43 seems like an interesting choice.
Think about base64 for binary you need /4*3
Sorry I got that wrong. The encoded nonces are 43 characters long. Base64 encoding converts three octets into four encoded characters. A base64 encoded string should have a length in characters that is divisible by 4. An encoded nonce with 43 characters is odd, no?
The protected field seems to be 75 characters, which is also odd.
I feel like Iâm missing something really obvious here.
janitor
January 29, 2016, 12:39am
14
Only when the binary octets' length was divisible by 3 in the first place. Barring that, it's only divisible by 4 if the base64 includes '=' padding at the end, which in this context it never will: "all trailing '=' characters omitted" . 32 octets encode to 43 characters.
1 Like
Okay no worries that clarifies things. Thanks!
My issue now is slightly different. Iâm getting the following.
Sending:
{"header":{"alg":"RS256","jwk":{"kty":"RSA","n":25160054269361828693404896131727942008415274134217289889172744710049896590612155882977291547908736001626219466717269115930048793228237648745405511843590501552456157810013173059671173372263096019494711238129060263827058408351359522594717679930450948522279043282343908838366172975556990550026488862614366277364242651504207023428435101044731741721101197864037816501991270683741497954238046212091368064951982861611613113389964327701600021542684872344511064803710455955966205980994242282540407974452019282118787772107896346890443732265381662542311325749504938277803307381101953283410384954253577335322625433693923328684823,"e":65537}},"protected":"eyJub25jZSI6ImkxcktEb1J3QU0wZkpnQ3BUdEZYUFZaT3o1Y24tLUU1ZGJaQ1pkTlVGQ3cifQ","payload":"eyJyZXNvdXJjZSI6Im5ldy1yZWciLCJjb250YWN0IjpbIm1haWx0bzp2b3V0YXNhdXJ1c0BnbWFpbC5jb20iXSwiYWdyZWVtZW50IjoiaHR0cHM6Ly9sZXRzZW5jcnlwdC5vcmcvZG9jdW1lbnRzL0xFLVNBLXYxLjAuMS1KdWx5LTI3LTIwMTUucGRmIn0","signature":"p9ZIpkwVTS8XJ0gJkM-sc0DS8SW89DRYyF2huDoyI8cDlcf3KRtbcVVD80WktBS4_A93u_y9mkNEVTV_kvVEhY6uJc3PLljypT70g0EPnSRRN_0PBii36z7VbolwpL-sxXtgRmDRHNB4CTSCgNPhGmkRvBrp5FdBSByccpMdOhpRmMAtT2Cch1ho4ZqUWnmQVe8UDmSZrc4plgWwD77PkBOxYbTHdVaFEfdyQ0Q9jMDd8EqvENfMdy3yaqQ-B3ZwnRx-hY2sAIc0uzSTuBzdKbWfpHEcxlBnhCzkz_hJO2oEMA6sdeuQbfjoENWR6KodTNQinY99U7VapDCySA8plg"}
Iâm getting back:
{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}
Iâm thinking this may have something to do with this issue though: https://github.com/letsencrypt/boulder/issues/1279
I think that Iâm marshaling the JWS correctly and signing it correctly.
janitor
January 29, 2016, 7:40am
17
^ What they said. (edit, Well not exactly what they said, not hexadecimal...)
Just like in tlussnig's earlier post.
And the RFC.
Base64urlUInt
The representation of a positive or zero integer value as the
base64url encoding of the value's unsigned big-endian
representation as an octet sequence. The octet sequence MUST
utilize the minimum number of octets needed to represent the
value. Zero is represented as BASE64URL(single zero-valued
octet), which is "AA".
It should read "e":"AQAB", not 65537, and encode "n" the same way.
1 Like
Thanks, both of you.
That does look like base64 as opposed to hex though.
Thanks for the explanation of Base64urlUInt. Without that it wasnât super clear to me what to do with big.Int so but I think I can figure it out now.
Using the following:
{"header":{"alg":"RS256","jwk":{"kty":"RSA","n":"0TVKj-2WveXzMFxrRb77jPkmPJoME4bphlqmB9Tge7OmK_SmRZ4vcj7q666e_B4WNDGgZXwPvKs7g-U1Tn_ObNNJEs9uxijBEaXyL23Kp4iHJoFiLpxvBIXkzTiioulAre6dMLzD4kTJMAKMikHBkxt_FL-r3tezmPXAFK2BYRDgqPqMmslMDx14ekzTrKjqTr1GpVMOA6_8F2f8kJCOsX50kfAevcnqmL_5kl1VQj4HdVb8_DDVmOXi_aWLpbYHYQizwwaJZLqtOhf9A_y2JvTCLzByIRulouZE0nRl6Q-kQL0QHrM7bVRRRGWJKrstMmiLUkNQIFLWpj-hcXRR5Q","e":"AQAB"}},"protected":"eyJub25jZSI6IjFKbkhQdGdRam85R29kM3F4MWx0SFE3ekN2UkpxaFFBRFpKWEM0SVdtSlEifQ","payload":"eyJyZXNvdXJjZSI6Im5ldy1yZWciLCJjb250YWN0IjpbIm1haWx0bzp2b3V0YXNhdXJ1c0BnbWFpbC5jb20iXSwiYWdyZWVtZW50IjoiaHR0cHM6Ly9sZXRzZW5jcnlwdC5vcmcvZG9jdW1lbnRzL0xFLVNBLXYxLjAuMS1KdWx5LTI3LTIwMTUucGRmIn0","signature":"bSa4PBUguHLKo_8Vb5VFp2zlPisoe3ZZV7AO9PdGzGrH_NnXejhoWqh8CQiiCfY-Rpv96eusLzhJopDoXFXPMg-Bxc-FdfVhVdgCFnnXzbqNYGZH5uUz_PmZKPUENwQfJ2uoIJ15IQ5UESyWe58djECoY32BK0OXuZWi-nuMyL35fUhYHJgYPKE8FPrN7DPiTSFzP4t0cuPd9psP13j-mTVjrOmbOjveGXN_qOIvVOvBrNwMG--V1E-MP3BebRTBUavvfe9-oMnoyFyvarEZbAGgXU3UaOof64qshOFzKo1ttPKfd4lFtSO5A9ue6tospryv5pp8ScsaM3qeLP11pA"}
I get a different error! Hooray!
{"type":"urn:acme:error:malformed","detail":"JWS verification error","status":400}
I will investigate this sometime soon but I thought Iâd update on my progress.
janitor
January 31, 2016, 12:06pm
20
Thatâs not a valid signature, so you could be signing the wrong thing.
JWS Signing Input
The input to the digital signature or MAC computation. Its value
is ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' ||
BASE64URL(JWS Payload)).
So the actual string thatâs signed should be
eyJub25jZSI6IjFKbkhQdGdRam85R29kM3F4MWx0SFE3ekN2UkpxaFFBRFpKWEM0SVdtSlEifQ.eyJyZXNvdXJjZSI6Im5ldy1yZWciLCJjb250YWN0IjpbIm1haWx0bzp2b3V0YXNhdXJ1c0BnbWFpbC5jb20iXSwiYWdyZWVtZW50IjoiaHR0cHM6Ly9sZXRzZW5jcnlwdC5vcmcvZG9jdW1lbnRzL0xFLVNBLXYxLjAuMS1KdWx5LTI3LTIwMTUucGRmIn0
jsha
February 1, 2016, 1:42am
21
voutasaurus:
I'm working in Go
Given that you're working in Go, I'd strongly recommend using the existing go-jose package from Square, rather than implementing one from scratch.