Account Key Roll-over : Parse error reading JWS

I am writing a client for Account Key Roll-over. I followed this docs https://ietf-wg-acme.github.io/acme/draft-ietf-acme-acme.html#rfc.section.7.3.6 and this one https://tools.ietf.org/html/draft-ietf-acme-acme-06#section-7.3.3

I am posting the following data to /acme/key-change

{"header":{"alg":"RS256","kid":"https:\/\/acme-v01.api.letsencrypt.org\/acme\/reg\/21941027","jwk":{"kty":"RSA","n":"smmXFfmi_e2vb1QKC9LTFEsYwbKT2csLPENgqTREEqGpQ90117TJyYFA3n6qWwl9PDfG6y4AYhs38PA4DE5dKXiwmiyWua6a7r9-iOz9F1EvckuNSU_OFYcSJvT9WTwDfWydoJBYKOhDl0ev_Jr9UWKGfWdUN_sy1sKV6skymq0DO1_6sIcE7uPiH06PPt28_5YPumTYhK8LX9_pMTJy5gkfgYa3Lty8AecZSBY2hJ4TbmQ5hRq7WZnm5xEnukemGHPDSC_E4zrDB0hZDVNP2okxxWGLlD1nwoO3opL3IxLF7EUyqRO4bwJr5wmkHdiswZBIoJxEKXujIlcNScjjmJLPgc8Z8tC3d9SJ1i_VJssXK3yqQYK1Wfnka5yLm0R-N5l-hgrvES_ZfAvUi4jvj4Q-AUJb0r-pepzw6hRZdVldIUT5NpobfBkuw6jgtlPNSK4JpXckrVpZXVrs4zNFg_wn7MdECtimDB2Kk6681swSoPuNx76D_Jx8q86h6CbwVvESTslB0eHTEMlG9MyGbrdjHHD69pAAVoRMwHEtDlj3tta_u0-rYt_BgNsM6fM_oNcfEx-RufF83ZeTe9cvF5yXZr1Egx7srH0SOSzgb6A1lTyEAJuhQBRnbWHw1xbzloRFZmkxbCra2o4XYkl0RaOX4I_4G80_TarBUBEh_3k","e":"AQAB"}},"protected":"eyJhbGciOiJSUzI1NiIsImtpZCI6Imh0dHBzOlwvXC9hY21lLXYwMS5hcGkubGV0c2VuY3J5cHQub3JnXC9hY21lXC9yZWdcLzIxOTQxMDI3IiwiandrIjp7Imt0eSI6IlJTQSIsIm4iOiJzbW1YRmZtaV9lMnZiMVFLQzlMVEZFc1l3YktUMmNzTFBFTmdxVFJFRXFHcFE5MDExN1RKeVlGQTNuNnFXd2w5UERmRzZ5NEFZaHMzOFBBNERFNWRLWGl3bWl5V3VhNmE3cjktaU96OUYxRXZja3VOU1VfT0ZZY1NKdlQ5V1R3RGZXeWRvSkJZS09oRGwwZXZfSnI5VVdLR2ZXZFVOX3N5MXNLVjZza3ltcTBETzFfNnNJY0U3dVBpSDA2UFB0MjhfNVlQdW1UWWhLOExYOV9wTVRKeTVna2ZnWWEzTHR5OEFlY1pTQlkyaEo0VGJtUTVoUnE3V1pubTV4RW51a2VtR0hQRFNDX0U0enJEQjBoWkRWTlAyb2t4eFdHTGxEMW53b08zb3BMM0l4TEY3RVV5cVJPNGJ3SnI1d21rSGRpc3daQklvSnhFS1h1aklsY05TY2pqbUpMUGdjOFo4dEMzZDlTSjFpX1ZKc3NYSzN5cVFZSzFXZm5rYTV5TG0wUi1ONWwtaGdydkVTX1pmQXZVaTRqdmo0US1BVUpiMHItcGVwenc2aFJaZFZsZElVVDVOcG9iZkJrdXc2amd0bFBOU0s0SnBYY2tyVnBaWFZyczR6TkZnX3duN01kRUN0aW1EQjJLazY2ODFzd1NvUHVOeDc2RF9KeDhxODZoNkNid1Z2RVNUc2xCMGVIVEVNbEc5TXlHYnJkakhIRDY5cEFBVm9STXdIRXREbGozdHRhX3UwLXJZdF9CZ05zTTZmTV9vTmNmRXgtUnVmRjgzWmVUZTljdkY1eVhacjFFZ3g3c3JIMFNPU3pnYjZBMWxUeUVBSnVoUUJSbmJXSHcxeGJ6bG9SRlpta3hiQ3JhMm80WFlrbDBSYU9YNElfNEc4MF9UYXJCVUJFaF8zayIsImUiOiJBUUFCIn0sIm5vbmNlIjoiTGl2S2tFTjBHWHRPZFU5QnpwN0VKVVlYbnRhUFVlbWN4Z1dUeVduTUhjdyIsInVybCI6Imh0dHBzOlwvXC9hY21lLXYwMS5hcGkubGV0c2VuY3J5cHQub3JnXC9hY21lXC9rZXktY2hhbmdlIn0","payload":"eyJyZXNvdXJjZSI6ImtleS1jaGFuZ2UiLCJwcm90ZWN0ZWQiOiJleUpoYkdjaU9pSlNVekkxTmlJc0ltcDNheUk2ZXlKcmRIa2lPaUpTVTBFaUxDSnVJam9pZFdWVmNYaHNVRzFoWDAxaE5HcGlOMFpETTFwdVZETmxhRXR0TjB0eU4zZFNZV3BmV2poQ2FrOUxjVVJET0ZBMVFWVkdaRU5TVkhOVlVqUk5hbE5PYWs1UmRVMVZhazh3U0hOd1JVSm9lRzh4YWpOSFdGOVdTbnBUY1cxMWF6Qkpka1YyTkcxbVNtazJjVlZUWWw5VmFEbHdUbmx3Y25JMlluZHFVMGh2TTFKWWQwczJkVVZmVUc5UU56STBSa1kxZW5oNFMySkZRbTlRZEdwclNGZEpXblJmWDBsVVpUTlpOR2d5UWpCTmNIUlZPRmN5VVZkZlJtZHBOM0F5VVhSME5YcEVlbVZIVHpKSWN6VnNaMFJIYkUxVWJESnZTamhhUlZWVmRIZFlZMU5YV1RoV1YySnpTVEY2WjI1bE9XNWphbTB3UXpOdmNqTnhOa1oyZUROUWRGQTJTMXBEUW5SUVVVcFRTMmRpWDBnd01tTmtNREF5VDBwaWVURldRV1IxT1VWTE9HOVhkbUV3WW5OeFdrZERMVkEwWTBKMVVIZENSbWhvVjA1bGVrNXVTbmRqVUhKcVdGTXlNWFJ4TFZBdFJFWjFiREpOVldnMVoyeDJUV1J2WkVjMlJHaExhbUZEVFRWT1pUTmFaREJvUWpGaFJIbHBhelV4UVY5VldrODBjbFJPVVRKVkxUTXpaM0UyVlU5TGVraDBVRFZNVjBWS00wWnJaMVJVZDJWR2JGTjVVVjkyTVhkcVUwZEZaRnBUTWs1S1R6SndUakkyWTAwd2VVWTNkbWhaVUU0M2EzaE5kRE4zUjNCQk1tbEVSa1pGYW1KRmNXOUhZMmxNTUY5NVFrbzNOa1pXUm5oNmNuQmxZbGREWkZaamRVeGlNVGhTYlZSdWNUWlNlbXB2ZFRSMmJIUm5VbVZCVUdwRVdqZzBTRTFIU1V4VFlrSlhkbFphZW5FMGR5MVpRa0pFWjFOd1NFeEJWMGxWT0ZobFdEbHpRMmxwT1c5Q2QwNUhObWhhV2s1c2QwWm9iR2xHWlVOMWVtdHpVemhGTTFwTlNHOHlSbGRFVlRWVmFsTjFSWEZrUTJ0S1ltcERMVWcyUVhkVk9IUXpka2d4TUhKc2MzZE5Wa1ZhZVRRNGNIY3pTMHc0TkRnNGFHWnZNVk5DWTNoT1pscHhZWGxGVFRObk0wY3hSa0Y0YlRVNWVEazJhbFVpTENKbElqb2lRVkZCUWlKOUxDSjFjbXdpT2lKb2RIUndjenBjTDF3dllXTnRaUzEyTURFdVlYQnBMbXhsZEhObGJtTnllWEIwTG05eVoxd3ZZV050WlZ3dmEyVjVMV05vWVc1blpTSjkiLCJwYXlsb2FkIjoiZXlKaFkyTnZkVzUwSWpvaWFIUjBjSE02WEM5Y0wyRmpiV1V0ZGpBeExtRndhUzVzWlhSelpXNWpjbmx3ZEM1dmNtZGNMMkZqYldWY0wzSmxaMXd2TWpFNU5ERXdNamNpTENKdVpYZExaWGtpT25zaWEzUjVJam9pVWxOQklpd2liaUk2SW5WbFZYRjRiRkJ0WVY5TllUUnFZamRHUXpOYWJsUXpaV2hMYlRkTGNqZDNVbUZxWDFvNFFtcFBTM0ZFUXpoUU5VRlZSbVJEVWxSelZWSTBUV3BUVG1wT1VYVk5WV3BQTUVoemNFVkNhSGh2TVdvelIxaGZWa3A2VTNGdGRXc3dTWFpGZGpSdFprcHBObkZWVTJKZlZXZzVjRTU1Y0hKeU5tSjNhbE5JYnpOU1dIZExOblZGWDFCdlVEY3lORVpHTlhwNGVFdGlSVUp2VUhScWEwaFhTVnAwWDE5SlZHVXpXVFJvTWtJd1RYQjBWVGhYTWxGWFgwWm5hVGR3TWxGMGREVjZSSHBsUjA4eVNITTFiR2RFUjJ4TlZHd3liMG80V2tWVlZYUjNXR05UVjFrNFZsZGljMGt4ZW1kdVpUbHVZMnB0TUVNemIzSXpjVFpHZG5nelVIUlFOa3RhUTBKMFVGRktVMHRuWWw5SU1ESmpaREF3TWs5S1lua3hWa0ZrZFRsRlN6aHZWM1poTUdKemNWcEhReTFRTkdOQ2RWQjNRa1pvYUZkT1pYcE9ia3AzWTFCeWFsaFRNakYwY1MxUUxVUkdkV3d5VFZWb05XZHNkazFrYjJSSE5rUm9TMnBoUTAwMVRtVXpXbVF3YUVJeFlVUjVhV3MxTVVGZlZWcFBOSEpVVGxFeVZTMHpNMmR4TmxWUFMzcElkRkExVEZkRlNqTkdhMmRVVkhkbFJteFRlVkZmZGpGM2FsTkhSV1JhVXpKT1NrOHljRTR5Tm1OTk1IbEdOM1pvV1ZCT04ydDRUWFF6ZDBkd1FUSnBSRVpHUldwaVJYRnZSMk5wVERCZmVVSktOelpHVmtaNGVuSndaV0pYUTJSV1kzVk1ZakU0VW0xVWJuRTJVbnBxYjNVMGRteDBaMUpsUVZCcVJGbzRORWhOUjBsTVUySkNWM1pXV25weE5IY3RXVUpDUkdkVGNFaE1RVmRKVlRoWVpWZzVjME5wYVRsdlFuZE9SelpvV2xwT2JIZEdhR3hwUm1WRGRYcHJjMU00UlROYVRVaHZNa1pYUkZVMVZXcFRkVVZ4WkVOclNtSnFReTFJTmtGM1ZUaDBNM1pJTVRCeWJITjNUVlpGV25rME9IQjNNMHRNT0RRNE9HaG1iekZUUW1ONFRtWmFjV0Y1UlUwelp6TkhNVVpCZUcwMU9YZzVObXBWSWl3aVpTSTZJa0ZSUVVJaWZYMCIsInNpZ25hdHVyZXMiOiJqeVlWSTR5MWcxekExbGZqMDN3UkRoa1llYWp2LUpsMXUydnFzSk8yenFEamlxRVdBNVNLRGQtOHJvc0prYktWNUI3SDBPMjBDVlYxUnFjMmJ0Z2RkWVZaQ3hjcExTVFBMUlhOWTFGbWNEellZclpCSWUyVjlseVpIVjJSYU9CU2kxNmIycWVVT015Q3g3YzRpb0hUZGlPX1AxcVlYUlVPcnZBM1FzeEtRTml3aWFvemxJa0xjcjhuM0NZbU10eUxOWU9hTFJUS1ZMTlhmRFBxSDBhWElaNl90aUdGU3VkNzVRXzBNXzVaZzJuclR2aXR4VzVHSEh4Ml9WTkVRSExlRzFuSjVGWWZNblltOGhDejhkYnBXZGFrUGZoV0ttNXlGUWswbHFORjZoLUo3NENjVk5DNzg5ZDBlNFU3WG14UHBSbHpIWnFtZlBoeGVBcmpUV2w5UWJtb18wSEJuLS10THkzWUk5T0N6cW4yWjQxSUtDeDZyc1hGcndVZGpDWDdFamlPT1U5bHRyY20zbnM0RVJxZWd2TXpMRFFOZldkaDhfNGtDZ0x2Wl9GejAxOTZBM2VkdGV3MzN5SV9mSEpzM3pZcERsREZ3d0NzN2hoenN5OVJSLUIyS1VqLV9HQVZNaXF3UVM3VGpIMzNKZFVLcWhZWXBidkRPS0w3NGFUbGVMR1hVTXI1dXhqVmNUMHBuQVJKdWtHX2lJLW9jYTRONmxnQkhnckRmMk4tTl9VdjNiOS1mLWVXUWhRQ3ZQWEI2b1o5Z1lvM2pLY0dHSm9VUmpyZFM2YnBVaHlJZUNwaU1mSjFMMjhNUmQ2aTZQRS15dHpDOGtITlVWbExKbk4wclRzLTRYYUhDbVdBZmhMbUQ4Zkt5SllrT0dxYThYWUJGcG5HTFMxdUNxNCJ9","signature":"CAFtV9fs2Wk74iJ5ZHqm55mS8eP0ri8JeRMH8CxqaapMEUAX0ayTWEesWtaoA1QMVqs7qzyYhaNSc-OBrblFXClAOuct6BcCH2am597ugEoHdnXSy1aLERdw9afpPDfMXeV5ZQR2VmiWFBC_ECcS7Zt3k0FqcSL3gQFwQDFXVcrsyN7CQiUyHloCrGfXcTmYMN_fUcwebGUeg8etKTTjF5C2SO0N5t6XXQtTLedqOS3lK3UiHxKnWdWfsBzKnVja5zARuxxXO-rxGcnu7cKGScR3UTmCYivJYsX9bV0XK9HdGt5uLzcnogei-pTTDMNLk1N-PhYaX1jIHDgPfpyN2KLH8smlY_PHmMbeuPGrZoOt19D8bQ9Lu6nBqi2xQ_H5nN9tM2rq7nhbDZN7t74lgYPvgk6FJEBJiSDS-KCMDPRD0LKiWbuU9zvvMaz9KIbrm6cNdNUsZz3YkRj_fQbqj1t7kSbYjJFnpqZTE-cKfBfWneiYNq-qRBjInPtfothEPkC7lbrUd7Knj5AVMbU5be-gQrX13Ds21ld0s1GrglWx2LTv3fEV8ObNr1IICb0fKhUGfB8ELUwWLx4ukehciFKxTFc6z-7P5Hvq1-BcgCwhZiI1ICyJZHbeJO2eWgUS3gUhndv8luP9fjUqepzWHF0xG-gzLE0OQi9k5qPFgcU"}

And getting following responce:

    Array
    (
        [type] => urn:acme:error:malformed
        [detail] => Parse error reading JWS
        [status] => 400
    )

I am providing exact same value (JWK representation of new private key) for “newKey” and “jwk” of the inner JWS. The inner JWS is signed by new key and outer JWS is signed by the existing (or if we call it old) key.

Can’t find out where I am doing wrong. Please help.
Thank you.

i would suggest you install certbot on a test machine and run the change process there.

The logs will show you the messages that certbot submits (including the JWS packages)

You can then compare your signature and see if there are any differences

I would also suggest that you use the staging environment for your testing as well

Andrei

Hi @anindya,

Since you're developing against the Let's Encrypt service & Boulder make sure you also take into consideration our list of ACME divergences. Boulder doesn't strictly implement any exact draft RFC at this time since it evolved alongside of them.

In particular our key change implementation differs somewhat from spec but is closest to draft-05.

Is the code you're using to create the JWS available anywhere?

This would be a good idea normally but unfortunately Certbot doesn't implement ACME account key change.

Hello @cpu,

Thank you for help. However, I am getting the same error. I am posting the following data structure.

{
     "protected": base64url({
       "alg": "ES256",
       "jwk": /* old key */,
       "nonce": "K60BWPrMQG9SDxBDS_xtSw"
     }),
     "payload": base64url({
       "resource": "key-change",
       "protected": base64url({
         "alg": "ES256",
         "jwk": /* new key */
       }),
       "payload": base64url({
         "account": "https://example.com/acme/acct/1",
         "newKey": /* new key */
       }),
       "signature": "Xe8B94RD30Azj2ea...8BmZIRtcSKPSd8gU"
     }),
     "signature": "5TWiqIYQfIDfALQv...x9C2mg8JGPxl5bI4"
   }

I am using this script and written the following code based on it.

    <?php 
           // generate and save new key pair for the account       
            
           $new_key_path = dirname($this->accountKeyPath)."/new";
            
           //Generate new key if not exist
           if (!is_file($new_key_path."/private.pem") && !is_file($new_key_path."/public.pem")) {
               
               $this->generateKey($new_key_path);
               
           }
           
           $privateKey_New = $this->readPrivateKey($new_key_path."/private.pem");
            
           $details_New = openssl_pkey_get_details($privateKey_New);
           
           $jwk_New = array(
               "e" => Base64UrlSafeEncoder::encode($details_New["rsa"]["e"]),
               "kty" => "RSA",
               "n" => Base64UrlSafeEncoder::encode($details_New["rsa"]["n"])
           );
                   
           $protected_inner = array(
               "alg" => "RS256",
               "jwk" => $jwk_New
           );        
           
           $protected_inner64 = Base64UrlSafeEncoder::encode(json_encode($protected_inner));
           
          /**The URL for account being modified. The content of 
          * this field the exact string provided in the Location header field
          * in response to the new-account request that created the account.
          */
           $account = "https://acme-v01.api.letsencrypt.org/acme/reg/21941027";
            
           $payload_inner = array(
               "account" => $account,
               "newKey" => $jwk_New
           );
                   
           $payload_inner64 = Base64UrlSafeEncoder::encode(str_replace('\\/', '/', json_encode($payload_inner)));
            
           openssl_sign($protected_inner64.'.'.$payload_inner64, $signature_inner_New, $privateKey_New, "SHA256");
            
           $signature_inner_New64 = Base64UrlSafeEncoder::encode($signature_inner_New);
            
           $payload_outer = array(
               'resource' => 'key-change',
               'protected' => $protected_inner64,
               'payload' => $payload_inner64,
               'signatures' => $signature_inner_New64
           );
            
           // request certificates creation
           $result = $this->signedRequest("/acme/key-change", $payload_outer);
                     
           if ($this->client->getLastCode() == 200) {
               
               $this->log('Account Key Roll-over successful.');
              
               /**
                * Code to delete old key and rename new account key
                */
                          
           }
           else{           
               throw new \RuntimeException("Invalid response code: " . $this->client->getLastCode() . ", " . json_encode($result));                
           }
?>

Hi @anindya,

I haven’t forgotten about this thread - I’ll try to circle back around to this question in a little while when I’ve finished some other things I’ve fallen behind on.

Hi @cpu,

Thank you so much! Looking forward for your reply.

1 Like

Hi @anindya,

Apologies for the long delay getting back to you! I hope you didn't lose faith :slight_smile:

I think I understand what's wrong with the example key roll-over request you submitted. I wrote a small test program that uses the same JOSE library as Boulder to see more detail about the error. To save you some trouble I put the full output of the program at the top of its sourcecode so you don't have to run it yourself.

The outer JWS was fine - it verifies as expected :+1: - the parse error happens on trying to read the outer JWS' payload as an inner JWS:

Err parsing outer JWS payload as an inner JWS: json: cannot unmarshal string into Go value of type jose.rawSignatureInfo

Looking at the raw outer JWS JSON payload we can see that it looks like:

{
  "resource": "key-change",
  "protected": "<snipped base64url_raw data>",
  "payload": "<snipped base64url_rawl data>",
  "signatures": "jyYVI4y1g1zA1lfj03wRDhkYeajv-Jl1u2vqsJO2zqDjiqEWA5SKDd-8rosJkbKV5B7H0O20CVV1Rqc2btgddYVZCxcpLSTPLRXNY1FmcDzYYrZBIe2V9lyZHV2RaOBSi16b2qeUOMyCx7c4ioHTdiO_P1qYXRUOrvA3QsxKQNiwiaozlIkLcr8n3CYmMtyLNYOaLRTKVLNXfDPqH0aXIZ6_tiGFSud75Q_0M_5Zg2nrTvitxW5GHHx2_VNEQHLeG1nJ5FYfMnYm8hCz8dbpWdakPfhWKm5yFQk0lqNF6h-J74CcVNC789d0e4U7XmxPpRlzHZqmfPhxeArjTWl9Qbmo_0HBn--tLy3YI9OCzqn2Z41IKCx6rsXFrwUdjCX7EjiOOU9ltrcm3ns4ERqegvMzLDQNfWdh8_4kCgLvZ_Fz0196A3edtew33yI_fHJs3zYpDlDFwwCs7hhzsy9RR-B2KUj-_GAVMiqwQS7TjH33JdUKqhYYpbvDOKL74aTleLGXUMr5uxjVcT0pnARJukG_iI-oca4N6lgBHgrDf2N-N_Uv3b9-f-eWQhQCvPXB6oZ9gYo3jKcGGJoURjrdS6bpUhyIeCpiMfJ1L28MRd6i6PE-ytzC8kHNUVlLJnN0rTs-4XaHCmWAfhLmD8fKyJYkOGqa8XYBFpnGLS1uCq4"
}

The crux of the problem is that you are using JWS multi-signature support by sending a "signatures" key that would normally have an array as a value. In your case the value is a single string. You should instead send an inner JWS that has a signature (singular, note the lack of s on the end) key with a single string value.

The struct go-jose.v2 uses to represent a JWS expects to marshal the signatures field to an array of rawSignatureInfo complex types while the signature field is just a byte string. So when you send a simple string value for the signatures field it produces an unmarshal error trying to make a string into a []jose.rawSignatureInfo.

I'm not familiar with PHP or your JOSE library to offer more concrete help but I think this will be enough for you to troubleshoot further on your end. I replied with a suggestion based on the PHP code after another read.

Thanks again for being patient waiting for my response :slight_smile:

2 Likes

Looking at your example PHP code again I think you might be able to change:

            
           $payload_outer = array(
               'resource' => 'key-change',
               'protected' => $protected_inner64,
               'payload' => $payload_inner64,
               'signatures' => $signature_inner_New64
           );
            

to:

            
           $payload_outer = array(
               'resource' => 'key-change',
               'protected' => $protected_inner64,
               'payload' => $payload_inner64,
               'signature' => $signature_inner_New64
           );
            
2 Likes

@cpu
Thanks a million for your help. It fixed the issue. Account Key Roll-over is successful now.

Yes, I didn’t lose faith. :slight_smile:

Thanks again.

1 Like

:tada::tada: woohoo! Glad to hear it worked :tada::tada:

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.