// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package com.google.crypto.tink.jwt;
import com.google.crypto.tink.KeyStatus;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.internal.BigIntegerEncoding;
import com.google.crypto.tink.internal.MutableSerializationRegistry;
import com.google.crypto.tink.internal.ProtoKeySerialization;
import com.google.crypto.tink.proto.JwtEcdsaAlgorithm;
import com.google.crypto.tink.proto.JwtEcdsaPublicKey;
import com.google.crypto.tink.proto.JwtRsaSsaPkcs1Algorithm;
import com.google.crypto.tink.proto.JwtRsaSsaPkcs1PublicKey;
import com.google.crypto.tink.proto.JwtRsaSsaPssAlgorithm;
import com.google.crypto.tink.proto.JwtRsaSsaPssPublicKey;
import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
import com.google.crypto.tink.proto.OutputPrefixType;
import com.google.crypto.tink.subtle.Base64;
import com.google.crypto.tink.tinkkey.KeyAccess;
import com.google.errorprone.annotations.InlineMe;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.internal.Streams;
import com.google.gson.stream.JsonReader;
import com.google.protobuf.ByteString;
import com.google.protobuf.ExtensionRegistryLite;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.IOException;
import java.io.StringReader;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.util.Optional;
import javax.annotation.Nullable;
/**
* Provides functions to import and export public Json Web Key (JWK) sets.
*
*
The currently supported algorithms are ES256, ES384, ES512, RS256, RS384, RS512, PS256, PS384
* and PS512.
*/
public final class JwkSetConverter {
/**
* Converts a Tink KeysetHandle with JWT public keys into a Json Web Key (JWK) set.
*
*
The currently supported algorithms are ES256, ES384, ES512, RS256, RS384, RS512, PS256,
* PS384 and PS512. JWK is defined in https://www.rfc-editor.org/rfc/rfc7517.txt.
*/
public static String fromPublicKeysetHandle(KeysetHandle handle)
throws IOException, GeneralSecurityException {
// Check validity of the keyset handle before calling "getAt".
// See comments in {@link KeysetHandle#Entry#getAt}.
handle = KeysetHandle.newBuilder(handle).build();
// We never throw a IOException anymore, but keep it in the interface for compatibility.
JsonArray keys = new JsonArray();
for (int i = 0; i < handle.size(); i++) {
KeysetHandle.Entry entry = handle.getAt(i);
if (entry.getStatus() != KeyStatus.ENABLED) {
continue;
}
ProtoKeySerialization protoKeySerialization =
MutableSerializationRegistry.globalInstance()
.serializeKey(entry.getKey(), ProtoKeySerialization.class, /* access= */ null);
if ((protoKeySerialization.getOutputPrefixType() != OutputPrefixType.RAW)
&& (protoKeySerialization.getOutputPrefixType() != OutputPrefixType.TINK)) {
throw new GeneralSecurityException("only OutputPrefixType RAW and TINK are supported");
}
if (protoKeySerialization.getKeyMaterialType() != KeyMaterialType.ASYMMETRIC_PUBLIC) {
throw new GeneralSecurityException("only public keys can be converted");
}
switch (protoKeySerialization.getTypeUrl()) {
case JWT_ECDSA_PUBLIC_KEY_URL:
keys.add(convertJwtEcdsaKey(protoKeySerialization));
break;
case JWT_RSA_SSA_PKCS1_PUBLIC_KEY_URL:
keys.add(convertJwtRsaSsaPkcs1(protoKeySerialization));
break;
case JWT_RSA_SSA_PSS_PUBLIC_KEY_URL:
keys.add(convertJwtRsaSsaPss(protoKeySerialization));
break;
default:
throw new GeneralSecurityException(
String.format("key type %s is not supported", protoKeySerialization.getTypeUrl()));
}
}
JsonObject jwkSet = new JsonObject();
jwkSet.add("keys", keys);
return jwkSet.toString();
}
/**
* Converts a Json Web Key (JWK) set with public keys into a Tink KeysetHandle.
*
*
It requires that all keys in the set have the "alg" field set. The currently supported
* algorithms are ES256, ES384, ES512, RS256, RS384, RS512, PS256, PS384 and PS512. JWK is defined
* in https://www.rfc-editor.org/rfc/rfc7517.txt.
*/
public static KeysetHandle toPublicKeysetHandle(String jwkSet)
throws IOException, GeneralSecurityException {
// We never throw a IOException anymore, but keep it in the interface for compatibility.
JsonObject jsonKeyset;
try {
JsonReader jsonReader = new JsonReader(new StringReader(jwkSet));
jsonReader.setLenient(false);
jsonKeyset = Streams.parse(jsonReader).getAsJsonObject();
} catch (IllegalStateException | JsonParseException | StackOverflowError ex) {
throw new GeneralSecurityException("JWK set is invalid JSON", ex);
}
KeysetHandle.Builder builder = KeysetHandle.newBuilder();
JsonArray jsonKeys = jsonKeyset.get("keys").getAsJsonArray();
for (JsonElement element : jsonKeys) {
JsonObject jsonKey = element.getAsJsonObject();
String algPrefix = getStringItem(jsonKey, "alg").substring(0, 2);
ProtoKeySerialization keySerialization;
switch (algPrefix) {
case "RS":
keySerialization = convertToRsaSsaPkcs1Key(jsonKey);
break;
case "PS":
keySerialization = convertToRsaSsaPssKey(jsonKey);
break;
case "ES":
keySerialization = convertToEcdsaKey(jsonKey);
break;
default:
throw new GeneralSecurityException(
"unexpected alg value: " + getStringItem(jsonKey, "alg"));
}
builder.addEntry(
KeysetHandle.importKey(
MutableSerializationRegistry.globalInstance()
.parseKeyWithLegacyFallback(keySerialization, null))
.withRandomId());
}
if (builder.size() <= 0) {
throw new GeneralSecurityException("empty keyset");
}
builder.getAt(0).makePrimary();
return builder.build();
}
private static final String JWT_ECDSA_PUBLIC_KEY_URL =
"type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey";
private static final String JWT_RSA_SSA_PKCS1_PUBLIC_KEY_URL =
"type.googleapis.com/google.crypto.tink.JwtRsaSsaPkcs1PublicKey";
private static final String JWT_RSA_SSA_PSS_PUBLIC_KEY_URL =
"type.googleapis.com/google.crypto.tink.JwtRsaSsaPssPublicKey";
private static Optional getKid(@Nullable Integer idRequirement) {
if (idRequirement == null) {
return Optional.empty();
}
byte[] bigEndianKeyId = ByteBuffer.allocate(4).putInt(idRequirement).array();
return Optional.of(Base64.urlSafeEncode(bigEndianKeyId));
}
private static JsonObject convertJwtEcdsaKey(ProtoKeySerialization protoKeySerialization)
throws GeneralSecurityException {
JwtEcdsaPublicKey jwtEcdsaPublicKey;
try {
jwtEcdsaPublicKey =
JwtEcdsaPublicKey.parseFrom(
protoKeySerialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
} catch (InvalidProtocolBufferException e) {
throw new GeneralSecurityException("failed to parse value as JwtEcdsaPublicKey proto", e);
}
String alg;
String crv;
// We currently encode with one extra 0 byte at the beginning, to make sure
// that parsing is correct even if passing of a two's complement encoding is used.
// See also b/264525021.
int encLength;
switch (jwtEcdsaPublicKey.getAlgorithm()) {
case ES256:
alg = "ES256";
crv = "P-256";
encLength = 33;
break;
case ES384:
alg = "ES384";
crv = "P-384";
encLength = 49;
break;
case ES512:
alg = "ES512";
crv = "P-521";
encLength = 67;
break;
default:
throw new GeneralSecurityException("unknown algorithm");
}
JsonObject jsonKey = new JsonObject();
jsonKey.addProperty("kty", "EC");
jsonKey.addProperty("crv", crv);
BigInteger x =
BigIntegerEncoding.fromUnsignedBigEndianBytes(jwtEcdsaPublicKey.getX().toByteArray());
BigInteger y =
BigIntegerEncoding.fromUnsignedBigEndianBytes(jwtEcdsaPublicKey.getY().toByteArray());
jsonKey.addProperty(
"x", Base64.urlSafeEncode(BigIntegerEncoding.toBigEndianBytesOfFixedLength(x, encLength)));
jsonKey.addProperty(
"y", Base64.urlSafeEncode(BigIntegerEncoding.toBigEndianBytesOfFixedLength(y, encLength)));
jsonKey.addProperty("use", "sig");
jsonKey.addProperty("alg", alg);
JsonArray keyOps = new JsonArray();
keyOps.add("verify");
jsonKey.add("key_ops", keyOps);
Optional kid = getKid(protoKeySerialization.getIdRequirementOrNull());
if (kid.isPresent()) {
jsonKey.addProperty("kid", kid.get());
} else if (jwtEcdsaPublicKey.hasCustomKid()) {
jsonKey.addProperty("kid", jwtEcdsaPublicKey.getCustomKid().getValue());
}
return jsonKey;
}
private static JsonObject convertJwtRsaSsaPkcs1(ProtoKeySerialization protoKeySerialization)
throws GeneralSecurityException {
JwtRsaSsaPkcs1PublicKey jwtRsaSsaPkcs1PublicKey;
try {
jwtRsaSsaPkcs1PublicKey =
JwtRsaSsaPkcs1PublicKey.parseFrom(
protoKeySerialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
} catch (InvalidProtocolBufferException e) {
throw new GeneralSecurityException(
"failed to parse value as JwtRsaSsaPkcs1PublicKey proto", e);
}
String alg;
switch (jwtRsaSsaPkcs1PublicKey.getAlgorithm()) {
case RS256:
alg = "RS256";
break;
case RS384:
alg = "RS384";
break;
case RS512:
alg = "RS512";
break;
default:
throw new GeneralSecurityException("unknown algorithm");
}
JsonObject jsonKey = new JsonObject();
jsonKey.addProperty("kty", "RSA");
jsonKey.addProperty("n", Base64.urlSafeEncode(jwtRsaSsaPkcs1PublicKey.getN().toByteArray()));
jsonKey.addProperty("e", Base64.urlSafeEncode(jwtRsaSsaPkcs1PublicKey.getE().toByteArray()));
jsonKey.addProperty("use", "sig");
jsonKey.addProperty("alg", alg);
JsonArray keyOps = new JsonArray();
keyOps.add("verify");
jsonKey.add("key_ops", keyOps);
Optional kid = getKid(protoKeySerialization.getIdRequirementOrNull());
if (kid.isPresent()) {
jsonKey.addProperty("kid", kid.get());
} else if (jwtRsaSsaPkcs1PublicKey.hasCustomKid()) {
jsonKey.addProperty("kid", jwtRsaSsaPkcs1PublicKey.getCustomKid().getValue());
}
return jsonKey;
}
private static JsonObject convertJwtRsaSsaPss(ProtoKeySerialization protoKeySerialization)
throws GeneralSecurityException {
JwtRsaSsaPssPublicKey jwtRsaSsaPssPublicKey;
try {
jwtRsaSsaPssPublicKey =
JwtRsaSsaPssPublicKey.parseFrom(
protoKeySerialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
} catch (InvalidProtocolBufferException e) {
throw new GeneralSecurityException("failed to parse value as JwtRsaSsaPssPublicKey proto", e);
}
String alg;
switch (jwtRsaSsaPssPublicKey.getAlgorithm()) {
case PS256:
alg = "PS256";
break;
case PS384:
alg = "PS384";
break;
case PS512:
alg = "PS512";
break;
default:
throw new GeneralSecurityException("unknown algorithm");
}
JsonObject jsonKey = new JsonObject();
jsonKey.addProperty("kty", "RSA");
jsonKey.addProperty("n", Base64.urlSafeEncode(jwtRsaSsaPssPublicKey.getN().toByteArray()));
jsonKey.addProperty("e", Base64.urlSafeEncode(jwtRsaSsaPssPublicKey.getE().toByteArray()));
jsonKey.addProperty("use", "sig");
jsonKey.addProperty("alg", alg);
JsonArray keyOps = new JsonArray();
keyOps.add("verify");
jsonKey.add("key_ops", keyOps);
Optional kid = getKid(protoKeySerialization.getIdRequirementOrNull());
if (kid.isPresent()) {
jsonKey.addProperty("kid", kid.get());
} else if (jwtRsaSsaPssPublicKey.hasCustomKid()) {
jsonKey.addProperty("kid", jwtRsaSsaPssPublicKey.getCustomKid().getValue());
}
return jsonKey;
}
private static String getStringItem(JsonObject obj, String name) throws GeneralSecurityException {
if (!obj.has(name)) {
throw new GeneralSecurityException(name + " not found");
}
if (!obj.get(name).isJsonPrimitive() || !obj.get(name).getAsJsonPrimitive().isString()) {
throw new GeneralSecurityException(name + " is not a string");
}
return obj.get(name).getAsString();
}
private static void expectStringItem(JsonObject obj, String name, String expectedValue)
throws GeneralSecurityException {
String value = getStringItem(obj, name);
if (!value.equals(expectedValue)) {
throw new GeneralSecurityException("unexpected " + name + " value: " + value);
}
}
private static void validateUseIsSig(JsonObject jsonKey) throws GeneralSecurityException {
if (!jsonKey.has("use")) {
return;
}
expectStringItem(jsonKey, "use", "sig");
}
private static void validateKeyOpsIsVerify(JsonObject jsonKey) throws GeneralSecurityException {
if (!jsonKey.has("key_ops")) {
return;
}
if (!jsonKey.get("key_ops").isJsonArray()) {
throw new GeneralSecurityException("key_ops is not an array");
}
JsonArray keyOps = jsonKey.get("key_ops").getAsJsonArray();
if (keyOps.size() != 1) {
throw new GeneralSecurityException("key_ops must contain exactly one element");
}
if (!keyOps.get(0).isJsonPrimitive() || !keyOps.get(0).getAsJsonPrimitive().isString()) {
throw new GeneralSecurityException("key_ops is not a string");
}
if (!keyOps.get(0).getAsString().equals("verify")) {
throw new GeneralSecurityException("unexpected keyOps value: " + keyOps.get(0).getAsString());
}
}
private static ProtoKeySerialization convertToRsaSsaPkcs1Key(JsonObject jsonKey)
throws GeneralSecurityException {
JwtRsaSsaPkcs1Algorithm algorithm;
switch (getStringItem(jsonKey, "alg")) {
case "RS256":
algorithm = JwtRsaSsaPkcs1Algorithm.RS256;
break;
case "RS384":
algorithm = JwtRsaSsaPkcs1Algorithm.RS384;
break;
case "RS512":
algorithm = JwtRsaSsaPkcs1Algorithm.RS512;
break;
default:
throw new GeneralSecurityException(
"Unknown Rsa Algorithm: " + getStringItem(jsonKey, "alg"));
}
if (jsonKey.has("p")
|| jsonKey.has("q")
|| jsonKey.has("dp")
|| jsonKey.has("dq")
|| jsonKey.has("d")
|| jsonKey.has("qi")) {
throw new UnsupportedOperationException("importing RSA private keys is not implemented");
}
expectStringItem(jsonKey, "kty", "RSA");
validateUseIsSig(jsonKey);
validateKeyOpsIsVerify(jsonKey);
JwtRsaSsaPkcs1PublicKey.Builder pkcs1PubKeyBuilder =
JwtRsaSsaPkcs1PublicKey.newBuilder()
.setVersion(0)
.setAlgorithm(algorithm)
.setE(ByteString.copyFrom(Base64.urlSafeDecode(getStringItem(jsonKey, "e"))))
.setN(ByteString.copyFrom(Base64.urlSafeDecode(getStringItem(jsonKey, "n"))));
if (jsonKey.has("kid")) {
pkcs1PubKeyBuilder.setCustomKid(
JwtRsaSsaPkcs1PublicKey.CustomKid.newBuilder()
.setValue(getStringItem(jsonKey, "kid"))
.build());
}
return ProtoKeySerialization.create(
JWT_RSA_SSA_PKCS1_PUBLIC_KEY_URL,
pkcs1PubKeyBuilder.build().toByteString(),
KeyMaterialType.ASYMMETRIC_PUBLIC,
OutputPrefixType.RAW,
null);
}
private static ProtoKeySerialization convertToRsaSsaPssKey(JsonObject jsonKey)
throws GeneralSecurityException {
JwtRsaSsaPssAlgorithm algorithm;
switch (getStringItem(jsonKey, "alg")) {
case "PS256":
algorithm = JwtRsaSsaPssAlgorithm.PS256;
break;
case "PS384":
algorithm = JwtRsaSsaPssAlgorithm.PS384;
break;
case "PS512":
algorithm = JwtRsaSsaPssAlgorithm.PS512;
break;
default:
throw new GeneralSecurityException(
"Unknown Rsa Algorithm: " + getStringItem(jsonKey, "alg"));
}
if (jsonKey.has("p")
|| jsonKey.has("q")
|| jsonKey.has("dq")
|| jsonKey.has("dq")
|| jsonKey.has("d")
|| jsonKey.has("qi")) {
throw new UnsupportedOperationException("importing RSA private keys is not implemented");
}
expectStringItem(jsonKey, "kty", "RSA");
validateUseIsSig(jsonKey);
validateKeyOpsIsVerify(jsonKey);
JwtRsaSsaPssPublicKey.Builder pssPubKeyBuilder =
JwtRsaSsaPssPublicKey.newBuilder()
.setVersion(0)
.setAlgorithm(algorithm)
.setE(ByteString.copyFrom(Base64.urlSafeDecode(getStringItem(jsonKey, "e"))))
.setN(ByteString.copyFrom(Base64.urlSafeDecode(getStringItem(jsonKey, "n"))));
if (jsonKey.has("kid")) {
pssPubKeyBuilder.setCustomKid(
JwtRsaSsaPssPublicKey.CustomKid.newBuilder()
.setValue(getStringItem(jsonKey, "kid"))
.build());
}
return ProtoKeySerialization.create(
JWT_RSA_SSA_PSS_PUBLIC_KEY_URL,
pssPubKeyBuilder.build().toByteString(),
KeyMaterialType.ASYMMETRIC_PUBLIC,
OutputPrefixType.RAW,
null);
}
private static ProtoKeySerialization convertToEcdsaKey(JsonObject jsonKey)
throws GeneralSecurityException {
JwtEcdsaAlgorithm algorithm;
switch (getStringItem(jsonKey, "alg")) {
case "ES256":
expectStringItem(jsonKey, "crv", "P-256");
algorithm = JwtEcdsaAlgorithm.ES256;
break;
case "ES384":
expectStringItem(jsonKey, "crv", "P-384");
algorithm = JwtEcdsaAlgorithm.ES384;
break;
case "ES512":
expectStringItem(jsonKey, "crv", "P-521");
algorithm = JwtEcdsaAlgorithm.ES512;
break;
default:
throw new GeneralSecurityException(
"Unknown Ecdsa Algorithm: " + getStringItem(jsonKey, "alg"));
}
if (jsonKey.has("d")) {
throw new UnsupportedOperationException("importing ECDSA private keys is not implemented");
}
expectStringItem(jsonKey, "kty", "EC");
validateUseIsSig(jsonKey);
validateKeyOpsIsVerify(jsonKey);
JwtEcdsaPublicKey.Builder ecdsaPubKeyBuilder =
JwtEcdsaPublicKey.newBuilder()
.setVersion(0)
.setAlgorithm(algorithm)
.setX(ByteString.copyFrom(Base64.urlSafeDecode(getStringItem(jsonKey, "x"))))
.setY(ByteString.copyFrom(Base64.urlSafeDecode(getStringItem(jsonKey, "y"))));
if (jsonKey.has("kid")) {
ecdsaPubKeyBuilder.setCustomKid(
JwtEcdsaPublicKey.CustomKid.newBuilder().setValue(getStringItem(jsonKey, "kid")).build());
}
return ProtoKeySerialization.create(
JWT_ECDSA_PUBLIC_KEY_URL,
ecdsaPubKeyBuilder.build().toByteString(),
KeyMaterialType.ASYMMETRIC_PUBLIC,
OutputPrefixType.RAW,
null);
}
/**
* @deprecated Use JwkSetConverter.fromPublicKeysetHandle(handle) instead.
*/
@InlineMe(
replacement = "JwkSetConverter.fromPublicKeysetHandle(handle)",
imports = "com.google.crypto.tink.jwt.JwkSetConverter")
@Deprecated
public static String fromKeysetHandle(KeysetHandle handle, KeyAccess keyAccess)
throws IOException, GeneralSecurityException {
return fromPublicKeysetHandle(handle);
}
/**
* @deprecated Use JwkSetConverter.toPublicKeysetHandle(jwkSet) instead.
*/
@InlineMe(
replacement = "JwkSetConverter.toPublicKeysetHandle(jwkSet)",
imports = "com.google.crypto.tink.jwt.JwkSetConverter")
@Deprecated
public static KeysetHandle toKeysetHandle(String jwkSet, KeyAccess keyAccess)
throws IOException, GeneralSecurityException {
return toPublicKeysetHandle(jwkSet);
}
private JwkSetConverter() {}
}