Search code examples
javascriptjwtcryptographyverifyecdsa

KJUR jws jsrsasign: Cannot validate ES256 token on JWT.io


We are trying to make a JWT token for Apple Search Ads using the KJUR jws library. We are using the API documents from Apple:

https://developer.apple.com/documentation/apple_search_ads/implementing_oauth_for_the_apple_search_ads_api

We are generating a private key (prime256v1 curve):

openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem

Next we are generating a public key from the private key:

openssl ec -in private-key.pem -pubout -out public-key.pem

Next we setup the header and payload:

var tNow = KJUR.jws.IntDate.get('now');
var tEnd = KJUR.jws.IntDate.get('now + 1day');
var teamId = 'SEARCHADS.xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
var keyId = 'xxxxxx-xxxx-xxxx-xxxxxxxxxxx';
var privateKey = `-----BEGIN EC PRIVATE KEY-----
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END EC PRIVATE KEY-----`;
  
var oHeader = {
  "alg": "ES256",
  "kid": keyId
}
  
var oPayload = {
  "iss": teamId,
  "iat": tNow,
  "exp": tEnd,
  "aud": "https://appleid.apple.com",
  "sub": clientId
}
   
var sHeader = JSON.stringify(oHeader);
var sPayload = JSON.stringify(oPayload);
  
var sKey = KEYUTIL.getKey({d: privateKey, curve: 'prime256v1'});  
var sResult = KJUR.jws.JWS.sign('ES256', sHeader, sPayload, sKey);

Next we try to validate the JWT token (it has generated a token) on jwt.io but cannot be verified. Apple search ads also throws a invalid_client message. What am i missing? Does anybody have a clue what I am doing wrong here?

Kind regards,

Jack Kwakman


Solution

  • The issue is caused by an incorrect import of the key.

    The posted key is a PEM encoded private key in SEC1 format. In getKey() the key is passed in JWK format, specifying the raw private key d. The PEM encoded SEC1 key is used as the value for d. This is incorrect because the raw private key is not identical to the SEC1 key, but is merely contained within it.

    To fix the problem, the key must be imported correctly. jsrsasign also supports the import of a PEM encoded key in SEC1 format, but then it also needs the EC parameters, s. e.g. here. For prime256v1 aka secp256r1 this is:

    -----BEGIN EC PARAMETERS-----
    BggqhkjOPQMBBw==
    -----END EC PARAMETERS-----
    

    These can be created e.g. with OpenSSL as part of the key generation process:

    openssl ecparam -name secp256r1 -genkey
    

    With this, the fixed JavaScript code is:

    var tNow = KJUR.jws.IntDate.get('now');
    var tEnd = KJUR.jws.IntDate.get('now + 1day');
    var teamId = 'SEARCHADS.xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
    var keyId = 'xxxxxx-xxxx-xxxx-xxxxxxxxxxx';
    var privateKey = `-----BEGIN EC PARAMETERS-----
    BggqhkjOPQMBBw==
    -----END EC PARAMETERS-----
    -----BEGIN EC PRIVATE KEY-----
    MHcCAQEEIK1vV4iLOPym9KvJJU5hd6CMEp+DTt8QI7NPBdJSf+VDoAoGCCqGSM49
    AwEHoUQDQgAEMpHT+HNKM7zjhx0jZDHyzQlkbLV0xk0H/TFo6gfT23ish58blPNh
    YrFI51Q/czvkAwCtLZz/6s1n/M8aA9L1Vg==
    -----END EC PRIVATE KEY-----`;
      
    var oHeader = {
      "alg": "ES256",
      "kid": keyId
    }
      
    var oPayload = {
      "iss": teamId,
      "iat": tNow,
      "exp": tEnd,
      "aud": "https://appleid.apple.com",
      "sub": "clientId"
    }
       
    var sHeader = JSON.stringify(oHeader);
    var sPayload = JSON.stringify(oPayload);
      
    var sKey = KEYUTIL.getKey(privateKey);  
    var sResult = KJUR.jws.JWS.sign('ES256', sHeader, sPayload, sKey);
    
    document.getElementById("jwt").innerHTML = sResult;
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jsrsasign/10.4.0/jsrsasign-all-min.js"></script>
    <p style="font-family:'Courier New', monospace;" id="jwt"></p>

    A JWT generated with this code can be successfully verified on https://jwt.io/ using the following public key (associated with the private key above):

    -----BEGIN PUBLIC KEY-----
    MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMpHT+HNKM7zjhx0jZDHyzQlkbLV0
    xk0H/TFo6gfT23ish58blPNhYrFI51Q/czvkAwCtLZz/6s1n/M8aA9L1Vg==
    -----END PUBLIC KEY-----
    

    Of course, as mentioned in the comment, the private key can also be converted to the PKCS#8 format (e.g. with OpenSSL). The import is likewise possible with getKey() (or alternatively KEYUTIL.getKeyFromPlainPrivatePKCS8PEM()):

    var tNow = KJUR.jws.IntDate.get('now');
    var tEnd = KJUR.jws.IntDate.get('now + 1day');
    var teamId = 'SEARCHADS.xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
    var keyId = 'xxxxxx-xxxx-xxxx-xxxxxxxxxxx';
    var privateKey = `-----BEGIN PRIVATE KEY-----
    MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgrW9XiIs4/Kb0q8kl
    TmF3oIwSn4NO3xAjs08F0lJ/5UOhRANCAAQykdP4c0ozvOOHHSNkMfLNCWRstXTG
    TQf9MWjqB9PbeKyHnxuU82FisUjnVD9zO+QDAK0tnP/qzWf8zxoD0vVW
    -----END PRIVATE KEY-----`;
      
    var oHeader = {
      "alg": "ES256",
      "kid": keyId
    }
      
    var oPayload = {
      "iss": teamId,
      "iat": tNow,
      "exp": tEnd,
      "aud": "https://appleid.apple.com",
      "sub": "clientId"
    }
       
    var sHeader = JSON.stringify(oHeader);
    var sPayload = JSON.stringify(oPayload);
      
    var sKey = KEYUTIL.getKey(privateKey);  
    //var sKey = KEYUTIL.getKeyFromPlainPrivatePKCS8PEM(privateKey); // works also
    var sResult = KJUR.jws.JWS.sign('ES256', sHeader, sPayload, sKey);
    
    document.getElementById("jwt").innerHTML = sResult;
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jsrsasign/10.4.0/jsrsasign-all-min.js"></script>
    <p style="font-family:'Courier New', monospace;" id="jwt"></p>

    If the key is imported as JWK, the x and y coordinates of the raw public key must be specified in addition to the raw private key d. These values are most easily determined using an ASN.1 parser such as https://lapo.it/asn1js/. Furthermore, the key type (kty) must be specified and the keyword for the curve identifier is crv:

    var tNow = KJUR.jws.IntDate.get('now');
    var tEnd = KJUR.jws.IntDate.get('now + 1day');
    var teamId = 'SEARCHADS.xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
    var keyId = 'xxxxxx-xxxx-xxxx-xxxxxxxxxxx';
    var privateKey = `rW9XiIs4_Kb0q8klTmF3oIwSn4NO3xAjs08F0lJ_5UM`;
    var publicKeyX = `MpHT-HNKM7zjhx0jZDHyzQlkbLV0xk0H_TFo6gfT23g`;
    var publicKeyY = `rIefG5TzYWKxSOdUP3M75AMArS2c_-rNZ_zPGgPS9VY`;
      
    var oHeader = {
      "alg": "ES256",
      "kid": keyId
    }
      
    var oPayload = {
      "iss": teamId,
      "iat": tNow,
      "exp": tEnd,
      "aud": "https://appleid.apple.com",
      "sub": "clientId"
    }
       
    var sHeader = JSON.stringify(oHeader);
    var sPayload = JSON.stringify(oPayload);
      
    var sKey = KEYUTIL.getKey({kty: "EC", d: privateKey, x: publicKeyX, y: publicKeyY, crv: 'prime256v1'});  
    var sResult = KJUR.jws.JWS.sign('ES256', sHeader, sPayload, sKey);
    
    document.getElementById("jwt").innerHTML = sResult;
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jsrsasign/10.4.0/jsrsasign-all-min.js"></script>
    <p style="font-family:'Courier New', monospace;" id="jwt"></p>

    The JWTs generated by these codes can be successfully verified on https://jwt.io/ using the public key above.