JSON Web Token (JWT) with ES256K (secp256k1) signature

Support for EC DSA signatures on the secp256k1 curve, which is used in Bitcoin and Ethereum, was added in version 5.9 of the Nimbus JOSE+JWT library.

  • JWS algorithm: ES256K
  • JWK curve name: secp256k1

The ES256K algorithm for JOSE is specified in COSE and JOSE Registrations for WebAuthn Algorithms.

Key generation

How to generate an EC key pair on the secp256k1 curve?

import java.security.*;
import java.security.interfaces.*;
import com.nimbusds.jose.jwk.*;
import com.nimbusds.jose.jwk.gen.*;

// Generate EC key pair on the secp256k1 curve
ECKey ecJWK = new ECKeyGenerator(Curve.SECP256K1)
    .keyUse(KeyUSE.SIGNATURE)
    .keyID("123")
    .generate();

// Get the public EC key, for recipients to validate the signatures
ECKey ecPublicJWK = ecJWK.toPublicJWK();

With Java 17 which no longer has built-in support for secp256k1 use the alternative BouncyCastle JCA provider.

Signing

To sign a JWT with ES256K on the secp256k1 curve:

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jwt.*;


// Sample JWT claims
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
    .subject("alice")
    .build();

// Create JWT for ES256K alg
SignedJWT jwt = new SignedJWT(
    new JWSHeader.Builder(JWSAlgorithm.ES256K)
        .keyID(ecJWK.getKeyID())
        .build(),
    claimsSet);

// Sign with private EC key
jwt.sign(new ECDSASigner(ecJWK));

// Output the JWT
System.out.println(jwt.serialize());

// With line break for clarity:
// eyJraWQiOiIxMjMiLCJhbGciOiJFUzI1NksifQ.eyJzdWIiOiJhbGljZSJ9
// .zRQyjdmePW97V5JYbPxwOrrtL0MdDPuz7w9O0CWvF-U40g195qBuZ8fXH2
// XZi_-U4RdMr4JvbiTKXH1ClofZgw

If you application code is running on Java 17 here you'll also need to import and use the alternative BouncyCastle JCA provider:

import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton;

var signer = new ECDSASigner(ecJWK);
signer.getJCAContext().setProvider(BouncyCastleProviderSingleton.getInstance());

Signature verification

To verify the ES256K signature:

import java.security.interfaces.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jwt.*;

// Parse the signed JWT
String jwtString =
"eyJraWQiOiIxMjMiLCJhbGciOiJFUzI1NksifQ.eyJzdWIiOiJhbGljZSJ9" +
".zRQyjdmePW97V5JYbPxwOrrtL0MdDPuz7w9O0CWvF-U40g195qBuZ8fXH2" +
"XZi_-U4RdMr4JvbiTKXH1ClofZgw";

SignedJWT jwt = SignedJWT.parse(jwtString);

// Verify the ES256K signature with the public EC key
assertTrue(jwt.verify(new ECDSAVerifier(ecPublicJWK)));

// Output the JWT claims: {"sub":"alice"}
System.out.println(jwt.getJWTClaimsSet().toJSONObject());

To set the alternative BouncyCastle JCA provider the approach is the same as for the JWSSigner above:

import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton;

var verifier = new ECDSAVerifier(ecPublicJWK);
verifier.getJCAContext().setProvider(BouncyCastleProviderSingleton.getInstance());