How to sign JWT with PKCS#8 PrivateKeyInfo

1.2k views Asked by At

I can't figure out how to sign JWT with PKCS#8 key. The key is similar to this one:

-----BEGIN PRIVATE KEY-----
MIGTAgEAMBNGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgtbN7M/7webqa1i3k
3UiwERpWUIoRj6nebM7yRyFphVWgCgYIKoHihj0DAQehRANCAAQl6Z+2bWXLgxJC
J2It6UNYSuxios4A1A6/7/7hNs0y3Yus53q6RD1snvMU5yTBewrRALyDz/8MNADm
eN7dRD41
-----END PRIVATE KEY-----

The key is explained in this SO answer: https://stackoverflow.com/a/54981397/1051180

I need to use the com.nimbusds library. I think it should be doable but couldn't find the way. The closest I found is this SO answer: https://stackoverflow.com/a/57437626/1051180

I managed to sign it with the io.jsonwebtoken library:

String token = Jwts.builder().signWith(getPrivateKey(), SignatureAlgorithm.ES256).compact();

private static PrivateKey getPrivateKey() {
    PrivateKey key = null;
    try (var pemParser = new PEMParser(privateKeyReader)) {
        var keyInfo = (PrivateKeyInfo) pemParser.readObject();
        key = new JcaPEMKeyConverter().getPrivateKey(keyInfo);      
    }
    return key;
}

Background: I obtained the key in an .p8 file. I use it to sign JWT that is used to authenticate against Apple server during Sign In with Apple.

1

There are 1 answers

2
Sergei On BEST ANSWER

Since I did not have an Apple-provided private key at hand I tried to generate one myself using this command:

openssl ecparam -name prime256v1 -genkey -noout -out key.p8

Here's the code that can use such PEM file to sign a token:

// load PEM as String from "key.p8" resource
String p8key;
ClassLoader cl = Thread.currentThread().getContextClassLoader();
try (InputStream is = cl.getResourceAsStream("key2.p8");
     InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
     BufferedReader r = new BufferedReader(isr)) {
   p8key = r.lines().collect(Collectors.joining("\n"));
}

// Load ECKey from the PEM
ECKey ecJWK = (ECKey) ECKey.parseFromPEMEncodedObjects(p8key);

// Create the EC signer
JWSSigner signer = new ECDSASigner(ecJWK);

// Create the JWS object with a payload
JWSObject jwsObject = new JWSObject(
        new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(ecJWK.getKeyID()).build(),
        new Payload("Elliptic cure"));

// Compute the EC signature
jwsObject.sign(signer);

// Serialize the JWS to compact form
String jwtToken = jwsObject.serialize();

// print the generated token
System.out.println(jwtToken);

Apparently, the PKCS#8 file provided by Apple does not have a public key included in it. Hence, the above method to create ECDSASigner fails with "Missing PEM-encoded public key to construct JWK" exception. The code below loads the private key from such PEM file and creates an instance of ECDSASigner which can be used to sign the token.

JWSSigner signer = new ECDSASigner(loadECPrivateKey("key.p8"));

private static final Provider BC = new BouncyCastleProvider();

private ECPrivateKey loadECPrivateKey(String resource) throws Exception {
  ClassLoader cl = Thread.currentThread().getContextClassLoader();
  try (InputStream is = cl.getResourceAsStream(resource);
       InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
       BufferedReader r = new BufferedReader(isr)) {

    // strip header and footer, decode from base64, then load the key
    String text = r.lines()
        .filter(s -> s.length() > 0 && !s.startsWith("---"))
        .collect(Collectors.joining());
    byte[] bytes = Base64.getDecoder().decode(text);

    PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
    KeyFactory factory = KeyFactory.getInstance("EC", BC);
    return (ECPrivateKey)factory.generatePrivate(spec);
  }
}