3

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

Topaco
  • 40,594
  • 4
  • 35
  • 62
  • 1
    Your key is given in SEC1 format, which contains the raw private key `d`, i.e. your SEC1 key is not equal to `d`. Either you extract `d` from your SEC1 key or you use another import function from [`KEYUTIL`](https://kjur.github.io/jsrsasign/api/symbols/KEYUTIL.html). I am not sure if SEC1 is supported, you may have to convert the key. For example PKCS8 is supported with `KEYUTIL.getKeyFromPlainPrivatePKCS8PEM()`. – Topaco Mar 01 '22 at 11:45
  • Dear Topaco, Converting SEC1 to PKCS8 was the solution to my problem. SEC1 does not seem te be supported. I have converted the key with openssl and now it works like a charm! Thank you for the hint! openssl pkcs8 -topk8 -nocrypt -in sec1.pem -out pkcs8.pem – Jack Kwakman Mar 01 '22 at 12:23
  • If you submit the comment as answer I can mark it as resolved Topaco. – Jack Kwakman Mar 01 '22 at 12:35
  • You are welcome. In the meantime I've figured out how to directly import a SEC1 key, so I will describe that as well. – Topaco Mar 01 '22 at 15:02

1 Answers1

1

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.

Topaco
  • 40,594
  • 4
  • 35
  • 62