blob: 60f8fe87cf508cd9bdbca644391f5bcefbfd4d5b [file] [log] [blame]
// 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 static java.nio.charset.StandardCharsets.UTF_8;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.KeysetManager;
import com.google.crypto.tink.KeysetWriter;
import com.google.crypto.tink.proto.EncryptedKeyset;
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;
import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
import com.google.crypto.tink.proto.KeyStatusType;
import com.google.crypto.tink.proto.Keyset;
import com.google.crypto.tink.proto.KeysetInfo;
import com.google.crypto.tink.proto.OutputPrefixType;
import com.google.crypto.tink.subtle.Base64;
import com.google.crypto.tink.tinkkey.KeyAccess;
import com.google.crypto.tink.tinkkey.KeyHandle;
import com.google.crypto.tink.tinkkey.internal.ProtoKey;
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 java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.security.GeneralSecurityException;
import java.util.Optional;
/**
* Provides functions to import and export public Json Web Key (JWK) sets.
*
* <p>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.
*
* <p>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 {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
handle.writeNoSecret(new JwkSetWriter(outputStream));
return outputStream.toString();
}
/**
* Converts a Json Web Key (JWK) set with public keys into a Tink KeysetHandle.
*
* <p>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 {
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 IOException("JWK set is invalid JSON", ex);
}
KeysetManager manager = KeysetManager.withEmptyKeyset();
JsonArray jsonKeys = jsonKeyset.get("keys").getAsJsonArray();
for (JsonElement element : jsonKeys) {
JsonObject jsonKey = element.getAsJsonObject();
String algPrefix = getStringItem(jsonKey, "alg").substring(0, 2);
KeyData keyData;
switch (algPrefix) {
case "RS":
keyData = convertToRsaSsaPkcs1Key(jsonKey);
break;
case "PS":
keyData = convertToRsaSsaPssKey(jsonKey);
break;
case "ES":
keyData = convertToEcdsaKey(jsonKey);
break;
default:
throw new IOException("unexpected alg value: " + getStringItem(jsonKey, "alg"));
}
manager.add(
KeyHandle.createFromKey(
new ProtoKey(keyData, com.google.crypto.tink.KeyTemplate.OutputPrefixType.RAW),
KeyAccess.publicAccess()));
}
KeysetInfo info = manager.getKeysetHandle().getKeysetInfo();
if (info.getKeyInfoCount() <= 0) {
throw new IOException("empty keyset");
}
manager.setPrimary(info.getKeyInfo(0).getKeyId());
return manager.getKeysetHandle();
}
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 final class JwkSetWriter implements KeysetWriter {
private final OutputStream outputStream;
private JwkSetWriter(OutputStream outputStream) {
this.outputStream = outputStream;
}
@Override
public void write(Keyset keyset) throws IOException {
JsonObject jwkSet;
try {
jwkSet = convertKeyset(keyset);
} catch (GeneralSecurityException exception) {
throw new IOException(exception);
}
outputStream.write(jwkSet.toString().getBytes(UTF_8));
}
@Override
public void write(EncryptedKeyset keyset) {
throw new UnsupportedOperationException("EncryptedKeyset are not implemented");
}
private static JsonObject convertKeyset(Keyset keyset)
throws IOException, GeneralSecurityException {
JsonArray keys = new JsonArray();
for (Keyset.Key key : keyset.getKeyList()) {
if (key.getStatus() != KeyStatusType.ENABLED) {
continue;
}
if (key.getKeyData().getKeyMaterialType() != KeyMaterialType.ASYMMETRIC_PUBLIC) {
throw new GeneralSecurityException("only public keys can be converted");
}
if ((key.getOutputPrefixType() != OutputPrefixType.RAW)
&& (key.getOutputPrefixType() != OutputPrefixType.TINK)) {
throw new GeneralSecurityException("only OutputPrefixType RAW and TINK are supported");
}
switch (key.getKeyData().getTypeUrl()) {
case JWT_ECDSA_PUBLIC_KEY_URL:
keys.add(convertJwtEcdsaKey(key));
break;
case JWT_RSA_SSA_PKCS1_PUBLIC_KEY_URL:
keys.add(convertJwtRsaSsaPkcs1(key));
break;
case JWT_RSA_SSA_PSS_PUBLIC_KEY_URL:
keys.add(convertJwtRsaSsaPss(key));
break;
default:
throw new GeneralSecurityException(
String.format("key type %s is not supported", key.getKeyData().getTypeUrl()));
}
}
JsonObject jwkSet = new JsonObject();
jwkSet.add("keys", keys);
return jwkSet;
}
private static JsonObject convertJwtEcdsaKey(Keyset.Key key)
throws IOException, GeneralSecurityException {
JwtEcdsaPublicKey jwtEcdsaPublicKey =
JwtEcdsaPublicKey.parseFrom(
key.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
String alg;
String crv;
switch (jwtEcdsaPublicKey.getAlgorithm()) {
case ES256:
alg = "ES256";
crv = "P-256";
break;
case ES384:
alg = "ES384";
crv = "P-384";
break;
case ES512:
alg = "ES512";
crv = "P-521";
break;
default:
throw new GeneralSecurityException("unknown algorithm");
}
JsonObject jsonKey = new JsonObject();
jsonKey.addProperty("kty", "EC");
jsonKey.addProperty("crv", crv);
jsonKey.addProperty("x", Base64.urlSafeEncode(jwtEcdsaPublicKey.getX().toByteArray()));
jsonKey.addProperty("y", Base64.urlSafeEncode(jwtEcdsaPublicKey.getY().toByteArray()));
jsonKey.addProperty("use", "sig");
jsonKey.addProperty("alg", alg);
JsonArray keyOps = new JsonArray();
keyOps.add("verify");
jsonKey.add("key_ops", keyOps);
Optional<String> kid = JwtFormat.getKid(key.getKeyId(), key.getOutputPrefixType());
if (kid.isPresent()) {
jsonKey.addProperty("kid", kid.get());
} else if (jwtEcdsaPublicKey.hasCustomKid()) {
jsonKey.addProperty("kid", jwtEcdsaPublicKey.getCustomKid().getValue());
}
return jsonKey;
}
private static JsonObject convertJwtRsaSsaPkcs1(Keyset.Key key)
throws IOException, GeneralSecurityException {
JwtRsaSsaPkcs1PublicKey jwtRsaSsaPkcs1PublicKey =
JwtRsaSsaPkcs1PublicKey.parseFrom(
key.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
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<String> kid = JwtFormat.getKid(key.getKeyId(), key.getOutputPrefixType());
if (kid.isPresent()) {
jsonKey.addProperty("kid", kid.get());
} else if (jwtRsaSsaPkcs1PublicKey.hasCustomKid()) {
jsonKey.addProperty("kid", jwtRsaSsaPkcs1PublicKey.getCustomKid().getValue());
}
return jsonKey;
}
private static JsonObject convertJwtRsaSsaPss(Keyset.Key key)
throws IOException, GeneralSecurityException {
JwtRsaSsaPssPublicKey jwtRsaSsaPssPublicKey =
JwtRsaSsaPssPublicKey.parseFrom(
key.getKeyData().getValue(), ExtensionRegistryLite.getEmptyRegistry());
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<String> kid = JwtFormat.getKid(key.getKeyId(), key.getOutputPrefixType());
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 IOException {
if (!obj.has(name)) {
throw new IOException(name + " not found");
}
if (!obj.get(name).isJsonPrimitive() || !obj.get(name).getAsJsonPrimitive().isString()) {
throw new IOException(name + " is not a string");
}
return obj.get(name).getAsString();
}
private static void expectStringItem(JsonObject obj, String name, String expectedValue)
throws IOException {
String value = getStringItem(obj, name);
if (!value.equals(expectedValue)) {
throw new IOException("unexpected " + name + " value: " + value);
}
}
private static void validateUseIsSig(JsonObject jsonKey) throws IOException {
if (!jsonKey.has("use")) {
return;
}
expectStringItem(jsonKey, "use", "sig");
}
private static void validateKeyOpsIsVerify(JsonObject jsonKey) throws IOException {
if (!jsonKey.has("key_ops")) {
return;
}
if (!jsonKey.get("key_ops").isJsonArray()) {
throw new IOException("key_ops is not an array");
}
JsonArray keyOps = jsonKey.get("key_ops").getAsJsonArray();
if (keyOps.size() != 1) {
throw new IOException("key_ops must contain exactly one element");
}
if (!keyOps.get(0).isJsonPrimitive() || !keyOps.get(0).getAsJsonPrimitive().isString()) {
throw new IOException("key_ops is not a string");
}
if (!keyOps.get(0).getAsString().equals("verify")) {
throw new IOException("unexpected keyOps value: " + keyOps.get(0).getAsString());
}
}
private static KeyData convertToRsaSsaPkcs1Key(JsonObject jsonKey) throws IOException {
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 IOException("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 KeyData.newBuilder()
.setTypeUrl(JWT_RSA_SSA_PKCS1_PUBLIC_KEY_URL)
.setValue(pkcs1PubKeyBuilder.build().toByteString())
.setKeyMaterialType(KeyMaterialType.ASYMMETRIC_PUBLIC)
.build();
}
private static KeyData convertToRsaSsaPssKey(JsonObject jsonKey) throws IOException {
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 IOException("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 KeyData.newBuilder()
.setTypeUrl(JWT_RSA_SSA_PSS_PUBLIC_KEY_URL)
.setValue(pssPubKeyBuilder.build().toByteString())
.setKeyMaterialType(KeyMaterialType.ASYMMETRIC_PUBLIC)
.build();
}
private static KeyData convertToEcdsaKey(JsonObject jsonKey) throws IOException {
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 IOException("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 KeyData.newBuilder()
.setTypeUrl(JWT_ECDSA_PUBLIC_KEY_URL)
.setValue(ecdsaPubKeyBuilder.build().toByteString())
.setKeyMaterialType(KeyMaterialType.ASYMMETRIC_PUBLIC)
.build();
}
/** @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() {}
}