jwtpostmanelliptic-curve

Verification of JWT created in Postman using ES512 is unstable


I am using the following approach to create and sign JWT (ES512) in Postman (pre-script):

var CryptoJS  = require("crypto-js");
var navigator = {};
var window    = {};

eval(pm.globals.get("jsrsasign-js"));

let issuerId   = pm.environment.get("issuerId");
let audienceId = pm.environment.get("audienceId");
let privateKey = pm.environment.get("privateKey");

let body     = pm.request.body.raw;    
let bodyHash = calculateHash(body);

const pl     = payload(issuerId, audienceId, bodyHash)
const jwt    = signJwt(pl, privateKey)

pm.collectionVariables.set("bearerToken", jwt);

function calculateHash(message) {
  const hash = CryptoJS.SHA256(message??'');
  return hash.toString(CryptoJS.enc.Base64);
}

function payload(iss, aud, bodyHash) {
  return {
    "iss": iss,
    "aud": aud,
    "sub": "api-request",
    "rbh": bodyHash,
    "exp": Math.round(Date.now() / 1000) + 59,
  };
}

function signJwt(payload, privateKey) {
  const alg      = 'ES512';
  const header   = {"alg":alg};
  const sHeader  = JSON.stringify(header);
  const sPayload = JSON.stringify(payload);
  return KJUR.jws.JWS.sign('ES512', sHeader, sPayload, privateKey);
}

Private key is generated:

openssl ecparam -name secp521r1 -genkey -noout -out private.pem

Public key:

openssl ec -in private.pem -pubin -outform PEM -out public.pem

Server uses: https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt/9.40 to verify JWT:

ECDSAVerifier(parseFromPEMEncodedObjects(publicKey).toECKey())

Jwt verification is quite unstable. Sometimes it succeeds (80%), sometimes fails (20%).
The reason is that sometimes jws signature length is 130 bytes (not OK), should be 132, sometimes 132.

OK JWT: eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhZTA3YmE1My0xOTYwLTRiMDYtYTYyZi01ZjdkYjRkYWM3MGYiLCJhdWQiOiJkMF9ha0IyNWJZaDNtI1ByekVmPHVLMXZRTGp7Q3hweUdIUHRMWkwpIiwic3ViIjoib3BlbmFwaS1yZXF1ZXN0IiwicmJoIjoiY2FyZ3VqY3lINHJiSDl0U3BxVWo4dFpkZmM0eHUyL1dvOGZJbzJYek1Nbz0iLCJleHAiOjE3MjI3NDc4NTV9.AJTc6e_4ymPhtQPSx6XoeYyOScFIYf5axTrSTXz2rtXuH9KZAFZBuoTMD35siR7-MIpRpIk2QslSNvU4A7KQSUVFAeH0Mb7yfwC2bN5ncJhWxg6j5dh6sJfPwLi3buGuw9i_TViF6mnLs8WClzXw8NqFBoNSnHexQPkWba8J-trAxoKo

Not OK JWT: eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhZTA3YmE1My0xOTYwLTRiMDYtYTYyZi01ZjdkYjRkYWM3MGYiLCJhdWQiOiJkMF9ha0IyNWJZaDNtI1ByekVmPHVLMXZRTGp7Q3hweUdIUHRMWkwpIiwic3ViIjoib3BlbmFwaS1yZXF1ZXN0IiwicmJoIjoiY2FyZ3VqY3lINHJiSDl0U3BxVWo4dFpkZmM0eHUyL1dvOGZJbzJYek1Nbz0iLCJleHAiOjE3MjI3NDc5MTZ9.gcJl3McLyAML3nfbb-ZXXtvfP-TRm0rjpEYX7iiagiFGSNoMi-8yu7jre_QTB0SW2_POrp4OcGg0QY2EeRCf6JnuApCmFTThF3qiwQ82HLS4OjNcHJkKRjxsDVC3aWhcyd7N9N30pWK5vzPCnaxAy1_3u0mHbByY_HMROuixXF07hw

public key (PEM format):

-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBm3GiQ0HmPdCyeuy7yWnxjwlQnjn9
dytNIqD/nsuhX1SVuDFH2iD12SM+csYrqLAwZKP4Y3BNIH+5QUFgpbCBwEEBu3w6
FeqM/5rJvxjG2/YJU2p1aUGed8Br/bZVEFVN6ohWgFGrC/hXxYuxVVwIwuzXnd8N
bXCVGZZeVKM5YrbcZQM=
-----END PUBLIC KEY-----

If I use java client (nimbus-jose-jwt) to create and sign JWT, verification is stable.

Do I use jsrsasign library incorrectly?

There is another JavaScript library: https://github.com/panva/jose.
Unfortunately, I can't import it to Postman.


Solution

  • As already suspected in the comment, the jsrsasign library seems to have a bug when converting the ECDSA signature to P1363 format, at least for curve P-521. In the P1363 format, the r and s values are represented as unsigned, big-endian and concatenated. For P-521, r and s are each 66 bytes in size. If a value is shorter, it is brought to 66 bytes with leading 0x00 values, so that the signature has a constant length of 132 bytes. This is necessary so that the r and s values are clearly identifiable.

    For the posted 130 bytes signature, the padding is missing for both the r and the s value, i.e. r-length/s-length = 65bytes/65bytes, as can easily be checked if the invalid signature (spaces only for display purposes):

    Base64url: -GJ6N27KEqRKHgmdyRIApgN6861cFbAXtksTPv3GVDVV-80Jjm266tiQqh3ORfLt_ucijNoKlvdZ8b90H1nb7SQSj3Mo0JZT8-Ht6fP9DA13LsWdTQuX7TTIUEqPegAI9lCmgZMFpbNeGqVarsOfgSIrpFvp7rFCdo7aFM7OaShGKw
    hex:       f8627a376eca12a44a1e099dc91200a6037af3ad5c15b017b64b133efdc6543555fbcd098e6dbaead890aa1dce45f2edfee7228cda0a96f759f1bf741f59dbed24 128f7328d09653f3e1ede9f3fd0c0d772ec59d4d0b97ed34c8504a8f7a0008f650a6819305a5b35e1aa55aaec39f81222ba45be9eeb142768eda14cece6928462b
    

    is manually fixed:

    hex:       00f8627a376eca12a44a1e099dc91200a6037af3ad5c15b017b64b133efdc6543555fbcd098e6dbaead890aa1dce45f2edfee7228cda0a96f759f1bf741f59dbed24 00128f7328d09653f3e1ede9f3fd0c0d772ec59d4d0b97ed34c8504a8f7a0008f650a6819305a5b35e1aa55aaec39f81222ba45be9eeb142768eda14cece6928462b
    Base64url: APhiejduyhKkSh4JnckSAKYDevOtXBWwF7ZLEz79xlQ1VfvNCY5tuurYkKodzkXy7f7nIozaCpb3WfG_dB9Z2-0kABKPcyjQllPz4e3p8_0MDXcuxZ1NC5ftNMhQSo96AAj2UKaBkwWls14apVquw5-BIiukW-nusUJ2jtoUzs5pKEYr
    

    The fixed token is then:

    eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhZTA3YmE1My0xOTYwLTRiMDYtYTYyZi01ZjdkYjRkYWM3MGYiLCJhdWQiOiJkMF9ha0IyNWJZaDNtI1ByekVmPHVLMXZRTGp7Q3hweUdIUHRMWkwpIiwic3ViIjoib3BlbmFwaS1yZXF1ZXN0IiwicmJoIjoiY2FyZ3VqY3lINHJiSDl0U3BxVWo4dFpkZmM0eHUyL1dvOGZJbzJYek1Nbz0iLCJleHAiOjE3MjI4NTAxMzJ9.APhiejduyhKkSh4JnckSAKYDevOtXBWwF7ZLEz79xlQ1VfvNCY5tuurYkKodzkXy7f7nIozaCpb3WfG_dB9Z2-0kABKPcyjQllPz4e3p8_0MDXcuxZ1NC5ftNMhQSo96AAj2UKaBkwWls14apVquw5-BIiukW-nusUJ2jtoUzs5pKEYr
    

    This token is now valid and will be verified as valid by any JWS compliant library, see e.g. here online at jwt.io.


    However, this fix is not 100% reliable. Theoretically, r and s values of different lengths are also possible (e.g. r-length/s-length = 66bytes/64bytes). Due to the lack of unambiguity, it is not possible to determine a priori which case applies, so this must be tried out. However, the assumption of an r-length/s-length = 65bytes/65bytes signature is plausible, since the probability of a shorter signature is smaller (as the following estimate makes plausible: probability of a leading 0x00 value: 1/256, two leading 0x00 values: 1/256^2 ...).
    Apart from this, other (invalid) signature sizes, such as 131 bytes, are also theoretically conceivable. Here you can only try out which of the parts has to be padded.

    Whether these theoretically possible cases actually occur, however, ultimately depends on the bug and cannot be answered without further code analysis. In my tests, I only ever found the lengths 130 and 132 bytes in 1000 generated signatures. However, I sporadically received an error message unknown ECDSA sig s length error when signing, which indicates a greater complexity of the bug.

    In my opinion, the most reasonable solution is to switch to another library (at least for P-521). If you cannot find one for your environment, the manual fix described above is an alternative.