Search code examples
javacryptographyjwtmapkitcxf

Generate a valid ES256 signature in Java


I'm trying to integrate Apple Map Web Snapshot which needs a signature query parameter in the URL. I able to successfully generate and validate ES256 signature in JWA package from NPM but not in Java. Please help me on finding equivalent lib to generate valid signature, I have tried few JWA libs in Java.

// Required modules.
const { readFileSync } = require("fs");
const { sign } = require("jwa")("ES256");

/* Read your private key from the file system. (Never add your private key
 * in code or in source control. Always keep it secure.)
 */ 
const privateKey = readFileSync("[file_system_path]");
// Replace the team ID and key ID values with your actual values.
const teamId = "[team ID]";
const keyId = "[key ID]";

// Creates the signature string and returns the full Snapshot request URL including the signature.
function sign(params) {
    const snapshotPath = `/api/v1/snapshot?${params}`;
    const completePath = `${snapshotPath}&teamId=${teamId}&keyId=${keyId}`;
    const signature = sign(completePath, privateKey);
    // In this example, the jwa module returns the signature as a Base64 URL-encoded string.

    // Append the signature to the end of the request URL, and return.
    return `${completePath}&signature=${signature}`;
}

// Call the sign function with a simple map request.
sign("center=apple+park") 

// The return value expected is: "/api/v1/snapshot?center=apple+park&teamId=[team ID]&keyId=[key ID]&signature=[base64_url_encoded_signature]"

Apache CXF - This lib generates similar to JWA module in node but failed to authenticate.

      String teamId = [Team Id];
       String keyId = [Key id];
       String privateKey = [private key path];

       String privateKeyContent = getKeyFileContent(privateKey);

       String API_VERSION_PATH = "/api/v1/snapshot?";

       String param = [QueryParam];

       //example -> param = "center=[city,country or lat,lang]&size=90x90&lang=en&radius=2";

       String params = param + "&teamId="+ teamId + "&keyId=" + keyId;

       String payload = API_VERSION_PATH + params;

       PrivateKey key = KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(
               Base64.decodeBase64(privateKeyContent)));

       JwsCompactProducer compactProducer = new JwsCompactProducer(payload);
       compactProducer.getJwsHeaders().setSignatureAlgorithm(SignatureAlgorithm.ES256);
       //compactProducer.getJwsHeaders().setKeyId(keyId);


       compactProducer.signWith(key);

       String signed = compactProducer.getEncodedSignature();

       String encodedSignature = new String(Base64.encodeBase64URLSafe(compactProducer.getEncodedSignature().getBytes()));

       System.out.println(SNAPSHOT_API_PATH + payload + "&signature=" + signed);

JJWT - This lib generates big signature then the signature generated in node module.

String signed = new String(Base64.encodeBase64URLSafe(Jwts.builder().setPayload(payload)
                .signWith(io.jsonwebtoken.SignatureAlgorithm.ES256, key).compact().getBytes()));

        System.out.println(SNAPSHOT_API_PATH + payload + "&signature=" + signed);

sample output signature

compactProducer.getEncodedSignature() signed --> qQ5G9_lwGJ9w158FVSmtPx_iH43xlg2_gx9BlHEJbER73xpAeIHtDRnT8wnveH_UEPxNe7Zgv4csJ48Oiq-ZIQ

Base64.encodeBase64URLSafe(signature) --> cVE1RzlfbHdHSjl3MTU4RlZTbXRQeF9pSDQzeGxnMl9neDlCbEhFSmJFUjczeHBBZUlIdERSblQ4d252ZUhfVUVQeE5lN1pndjRjc0o0OE9pcS1aSVE

JJWT signed -> ZXlKaGJHY2lPaUpGVXpJMU5pSjkuTDJGd2FTOTJNUzl6Ym1Gd2MyaHZkRDlqWlc1MFpYSTlRM1Z3WlhKMGFXNXZMRlZUUVNaMFpXRnRTV1E5V0ZaWU5GWlhSbEZUTXlaclpYbEpaRDFWUVRWTlNGWlhWMWhMLlExUEtoeGwzSjFoVWVUWGtmeXRLckliYm5zeDdZem5lZVpxTVc4WkJOVU9uLVlYeFhyTExVU05ZVTZCSG5Xc3FheFd3YVB5dlF0Yml4TVBSZGdjamJ3


Solution

  • The signature in the NodeJS code is generated by the jwa('ES256')#sign method, which has the following functionality:

    1. ES256: ECDSA using P-256 curve and SHA-256 hash algorithm [1].
    2. The signature will be the pair (r, s), where r and s are 256-bit unsigned integers [2].
    3. The signature is base64url-encoded [3].

    Ad 1: A corresponding implementation for ES256 is possible in Java using on-board means (SunEC provider, Java 1.7 or higher), [4]:

    Signature ecdsa = Signature.getInstance("SHA256withECDSA");
    ecdsa.initSign(privateKey);     
    String payload = "The quick brown fox jumps over the lazy dog";
    ecdsa.update(payload.getBytes(StandardCharsets.UTF_8));
    byte[] signatureDER = ecdsa.sign();
    

    Here privateKey is the private key of type java.security.PrivateKey, analogous to key in the CXF code.

    Ad 2: The Java code returns the signature in the ASN.1 DER format and must therefore be converted into the (r,s) format [5]. Either a user-defined method can be implemented or a method from a supporting library can be used, e.g. the method com.nimbusds.jose.crypto.impl.ECDSA.transcodeSignatureToConcat of the Nimbus JOSE + JWT library [6][7][8]:

    byte[] signature = transcodeSignatureToConcat(signatureDER, 64);
    

    Ad 3: Base64url encoding is possible in Java with on-board means [9]:

    String signatureBase64url = Base64.getUrlEncoder().withoutPadding().encodeToString(signature);
    

    Since a different signature is generated each time, a direct comparison of the signatures generated in both codes isn't possible. However, compatibility with the jwa-npm library can be tested by verifying the signature generated in the Java code with the jwa-npm library:

    const jwa = require("jwa");
    const ecdsa = jwa('ES256');
    
    var message = "The quick brown fox jumps over the lazy dog";
    var verify = ecdsa.verify(message, signatureBase64url, publicKey);
    

    Here, signatureBase64url is the signature generated with the Java code. publicKey is the corresponding public key in X.509 PEM format (-----BEGIN PUBLIC KEY-----...) [10].


    The functionality of the jwa('ES256')#sign method is different from that of the posted JJWT or Apache CXF code: The last two generate a JWT [11]. The header is the base64url encoding of {"alg": "ES256"}. Accordingly the signature is that for the Base64url encoded header and the Base64url encoded payload, both separated by a dot:

    String payload = "The quick brown fox jumps over the lazy dog";
    
    //JJWT
    String jwtJJWT = Jwts.builder().setPayload(payload).signWith(io.jsonwebtoken.SignatureAlgorithm.ES256, privateKey).compact();
    
    //CXF
    JwsCompactProducer compactProducer = new JwsCompactProducer(payload);
    compactProducer.getJwsHeaders().setSignatureAlgorithm(SignatureAlgorithm.ES256);
    String jwtCXF = compactProducer.signWith(privateKey);
    String signatureCXF = compactProducer.getEncodedSignature(); // signature, 3. portion of JWT
    

    Example for a JWT generated by this:

    eyJhbGciOiJFUzI1NiJ9.VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw.rcrzqr3ovu7SH9ci-S6deLn6BuiQkNv9CmeOnUPwva30pfK9BOX0YOgjZP5T08wjxCBTTHV3aex0M76toL8qpw