JSON web signature format for registration object/request

Hi! :slightly_smiling:

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. :slightly_smiling:

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?

I thought JWS used base64url, the URL-safe encoding that uses hyphens and underscores.

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. :slightly_smiling:

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.

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.

^ 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. :slightly_smiling:

That does look like base64 as opposed to hex though. :stuck_out_tongue:

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! :slightly_smiling:

{"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. :slightly_smiling:

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

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.