Add serialization for JwtEcdsaPublicKey objects.

PiperOrigin-RevId: 530294696
Change-Id: I0e76efbae588240e4c9ea982d52dbe65b0577983
diff --git a/src/main/java/com/google/crypto/tink/jwt/BUILD.bazel b/src/main/java/com/google/crypto/tink/jwt/BUILD.bazel
index 27e63cb..22b7221 100644
--- a/src/main/java/com/google/crypto/tink/jwt/BUILD.bazel
+++ b/src/main/java/com/google/crypto/tink/jwt/BUILD.bazel
@@ -1075,15 +1075,22 @@
     srcs = ["JwtEcdsaProtoSerialization.java"],
     deps = [
         ":jwt_ecdsa_parameters-android",
+        ":jwt_ecdsa_public_key-android",
         "//proto:jwt_ecdsa_java_proto_lite",
         "//proto:tink_java_proto_lite",
         "//src/main/java/com/google/crypto/tink:accesses_partial_key-android",
+        "//src/main/java/com/google/crypto/tink:secret_key_access-android",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser-android",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer-android",
         "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry-android",
         "//src/main/java/com/google/crypto/tink/internal:parameters_parser-android",
         "//src/main/java/com/google/crypto/tink/internal:parameters_serializer-android",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization-android",
         "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization-android",
         "//src/main/java/com/google/crypto/tink/internal:util-android",
         "//src/main/java/com/google/crypto/tink/util:bytes-android",
+        "@maven//:com_google_code_findbugs_jsr305",
         "@maven//:com_google_protobuf_protobuf_javalite",
     ],
 )
@@ -1093,15 +1100,22 @@
     srcs = ["JwtEcdsaProtoSerialization.java"],
     deps = [
         ":jwt_ecdsa_parameters",
+        ":jwt_ecdsa_public_key",
         "//proto:jwt_ecdsa_java_proto",
         "//proto:tink_java_proto",
         "//src/main/java/com/google/crypto/tink:accesses_partial_key",
+        "//src/main/java/com/google/crypto/tink:secret_key_access",
+        "//src/main/java/com/google/crypto/tink/internal:big_integer_encoding",
+        "//src/main/java/com/google/crypto/tink/internal:key_parser",
+        "//src/main/java/com/google/crypto/tink/internal:key_serializer",
         "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
         "//src/main/java/com/google/crypto/tink/internal:parameters_parser",
         "//src/main/java/com/google/crypto/tink/internal:parameters_serializer",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
         "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
         "//src/main/java/com/google/crypto/tink/internal:util",
         "//src/main/java/com/google/crypto/tink/util:bytes",
+        "@maven//:com_google_code_findbugs_jsr305",
         "@maven//:com_google_protobuf_protobuf_java",
     ],
 )
diff --git a/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaProtoSerialization.java b/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaProtoSerialization.java
index a168fc3..cf95b77 100644
--- a/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaProtoSerialization.java
+++ b/src/main/java/com/google/crypto/tink/jwt/JwtEcdsaProtoSerialization.java
@@ -19,17 +19,26 @@
 import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
 
 import com.google.crypto.tink.AccessesPartialKey;
+import com.google.crypto.tink.SecretKeyAccess;
+import com.google.crypto.tink.internal.BigIntegerEncoding;
+import com.google.crypto.tink.internal.KeyParser;
+import com.google.crypto.tink.internal.KeySerializer;
 import com.google.crypto.tink.internal.MutableSerializationRegistry;
 import com.google.crypto.tink.internal.ParametersParser;
 import com.google.crypto.tink.internal.ParametersSerializer;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
 import com.google.crypto.tink.internal.ProtoParametersSerialization;
 import com.google.crypto.tink.proto.JwtEcdsaAlgorithm;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
 import com.google.crypto.tink.proto.KeyTemplate;
 import com.google.crypto.tink.proto.OutputPrefixType;
 import com.google.crypto.tink.util.Bytes;
+import com.google.protobuf.ByteString;
 import com.google.protobuf.ExtensionRegistryLite;
 import com.google.protobuf.InvalidProtocolBufferException;
 import java.security.GeneralSecurityException;
+import java.security.spec.ECPoint;
+import javax.annotation.Nullable;
 
 /**
  * Methods to serialize and parse {@link JwtEcdsaPrivateKey}, {@link JwtEcdsaPublicKey}, and {@link
@@ -42,6 +51,10 @@
       "type.googleapis.com/google.crypto.tink.JwtEcdsaPrivateKey";
   private static final Bytes TYPE_URL_BYTES = toBytesFromPrintableAscii(TYPE_URL);
 
+  private static final String PUBLIC_TYPE_URL =
+      "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey";
+  private static final Bytes PUBLIC_TYPE_URL_BYTES = toBytesFromPrintableAscii(PUBLIC_TYPE_URL);
+
   private static final ParametersSerializer<JwtEcdsaParameters, ProtoParametersSerialization>
       PARAMETERS_SERIALIZER =
           ParametersSerializer.create(
@@ -55,6 +68,19 @@
           TYPE_URL_BYTES,
           ProtoParametersSerialization.class);
 
+  private static final KeySerializer<JwtEcdsaPublicKey, ProtoKeySerialization>
+      PUBLIC_KEY_SERIALIZER =
+          KeySerializer.create(
+              JwtEcdsaProtoSerialization::serializePublicKey,
+              JwtEcdsaPublicKey.class,
+              ProtoKeySerialization.class);
+
+  private static final KeyParser<ProtoKeySerialization> PUBLIC_KEY_PARSER =
+      KeyParser.create(
+          JwtEcdsaProtoSerialization::parsePublicKey,
+          PUBLIC_TYPE_URL_BYTES,
+          ProtoKeySerialization.class);
+
   private static JwtEcdsaAlgorithm toProtoAlgorithm(JwtEcdsaParameters.Algorithm algorithm)
       throws GeneralSecurityException {
     if (JwtEcdsaParameters.Algorithm.ES256.equals(algorithm)) {
@@ -147,6 +173,115 @@
         .build();
   }
 
+  private static int getEncodingLength(JwtEcdsaParameters.Algorithm algorithm)
+      throws GeneralSecurityException {
+    // 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.
+    // We want to prevent bugs similar to b/264525021
+    if (algorithm.equals(JwtEcdsaParameters.Algorithm.ES256)) {
+      return 33;
+    }
+    if (algorithm.equals(JwtEcdsaParameters.Algorithm.ES384)) {
+      return 49;
+    }
+    if (algorithm.equals(JwtEcdsaParameters.Algorithm.ES512)) {
+      return 67;
+    }
+    throw new GeneralSecurityException("Unknown algorithm: " + algorithm);
+  }
+
+  private static OutputPrefixType toProtoOutputPrefixType(JwtEcdsaParameters parameters) {
+    if (parameters.getKidStrategy().equals(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)) {
+      return OutputPrefixType.TINK;
+    }
+    return OutputPrefixType.RAW;
+  }
+
+  private static com.google.crypto.tink.proto.JwtEcdsaPublicKey serializePublicKey(
+      JwtEcdsaPublicKey key) throws GeneralSecurityException {
+    int encLength = getEncodingLength(key.getParameters().getAlgorithm());
+    ECPoint publicPoint = key.getPublicPoint();
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey.Builder builder =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            .setAlgorithm(toProtoAlgorithm(key.getParameters().getAlgorithm()))
+            .setX(
+                ByteString.copyFrom(
+                    BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                        publicPoint.getAffineX(), encLength)))
+            .setY(
+                ByteString.copyFrom(
+                    BigIntegerEncoding.toBigEndianBytesOfFixedLength(
+                        publicPoint.getAffineY(), encLength)));
+    if (key.getParameters().getKidStrategy().equals(JwtEcdsaParameters.KidStrategy.CUSTOM)) {
+      builder.setCustomKid(
+          com.google.crypto.tink.proto.JwtEcdsaPublicKey.CustomKid.newBuilder()
+              .setValue(key.getKid().get())
+              .build());
+    }
+    return builder.build();
+  }
+
+  private static ProtoKeySerialization serializePublicKey(
+      JwtEcdsaPublicKey key, @Nullable SecretKeyAccess access) throws GeneralSecurityException {
+    return ProtoKeySerialization.create(
+        PUBLIC_TYPE_URL,
+        serializePublicKey(key).toByteString(),
+        KeyMaterialType.ASYMMETRIC_PUBLIC,
+        toProtoOutputPrefixType(key.getParameters()),
+        key.getIdRequirementOrNull());
+  }
+
+  @SuppressWarnings("UnusedException")
+  private static JwtEcdsaPublicKey parsePublicKey(
+      ProtoKeySerialization serialization, @Nullable SecretKeyAccess access)
+      throws GeneralSecurityException {
+    if (!serialization.getTypeUrl().equals(PUBLIC_TYPE_URL)) {
+      throw new IllegalArgumentException(
+          "Wrong type URL in call to EcdsaProtoSerialization.parsePublicKey: "
+              + serialization.getTypeUrl());
+    }
+    try {
+      com.google.crypto.tink.proto.JwtEcdsaPublicKey protoKey =
+          com.google.crypto.tink.proto.JwtEcdsaPublicKey.parseFrom(
+              serialization.getValue(), ExtensionRegistryLite.getEmptyRegistry());
+      if (protoKey.getVersion() != 0) {
+        throw new GeneralSecurityException("Only version 0 keys are accepted");
+      }
+      JwtEcdsaParameters.Builder parametersBuilder = JwtEcdsaParameters.builder();
+      JwtEcdsaPublicKey.Builder keyBuilder = JwtEcdsaPublicKey.builder();
+
+      if (serialization.getOutputPrefixType().equals(OutputPrefixType.TINK)) {
+        if (protoKey.hasCustomKid()) {
+          throw new GeneralSecurityException(
+              "Keys serialized with OutputPrefixType TINK should not have a custom kid");
+        }
+        @Nullable Integer idRequirement = serialization.getIdRequirementOrNull();
+        if (idRequirement == null) {
+          throw new GeneralSecurityException(
+              "Keys serialized with OutputPrefixType TINK need an ID Requirement");
+        }
+        parametersBuilder.setKidStrategy(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID);
+        keyBuilder.setIdRequirement(idRequirement);
+      } else if (serialization.getOutputPrefixType().equals(OutputPrefixType.RAW)) {
+        if (protoKey.hasCustomKid()) {
+          parametersBuilder.setKidStrategy(JwtEcdsaParameters.KidStrategy.CUSTOM);
+          keyBuilder.setCustomKid(protoKey.getCustomKid().getValue());
+        } else {
+          parametersBuilder.setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED);
+        }
+      }
+      parametersBuilder.setAlgorithm(toAlgorithm(protoKey.getAlgorithm()));
+      keyBuilder.setPublicPoint(
+          new ECPoint(
+              BigIntegerEncoding.fromUnsignedBigEndianBytes(protoKey.getX().toByteArray()),
+              BigIntegerEncoding.fromUnsignedBigEndianBytes(protoKey.getY().toByteArray())));
+      return keyBuilder.setParameters(parametersBuilder.build()).build();
+    } catch (InvalidProtocolBufferException | IllegalArgumentException e) {
+      throw new GeneralSecurityException("Parsing EcdsaPublicKey failed");
+    }
+  }
+
   public static void register() throws GeneralSecurityException {
     register(MutableSerializationRegistry.globalInstance());
   }
@@ -155,6 +290,8 @@
       throws GeneralSecurityException {
     registry.registerParametersSerializer(PARAMETERS_SERIALIZER);
     registry.registerParametersParser(PARAMETERS_PARSER);
+    registry.registerKeySerializer(PUBLIC_KEY_SERIALIZER);
+    registry.registerKeyParser(PUBLIC_KEY_PARSER);
   }
 
   private JwtEcdsaProtoSerialization() {}
diff --git a/src/test/java/com/google/crypto/tink/jwt/BUILD.bazel b/src/test/java/com/google/crypto/tink/jwt/BUILD.bazel
index 1e953aa..60ca94e 100644
--- a/src/test/java/com/google/crypto/tink/jwt/BUILD.bazel
+++ b/src/test/java/com/google/crypto/tink/jwt/BUILD.bazel
@@ -473,12 +473,18 @@
     deps = [
         "//proto:jwt_ecdsa_java_proto",
         "//proto:tink_java_proto",
+        "//src/main/java/com/google/crypto/tink:insecure_secret_key_access",
+        "//src/main/java/com/google/crypto/tink:key",
         "//src/main/java/com/google/crypto/tink:parameters",
         "//src/main/java/com/google/crypto/tink/internal:mutable_serialization_registry",
+        "//src/main/java/com/google/crypto/tink/internal:proto_key_serialization",
         "//src/main/java/com/google/crypto/tink/internal:proto_parameters_serialization",
         "//src/main/java/com/google/crypto/tink/internal/testing:asserts",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_parameters",
         "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_proto_serialization",
+        "//src/main/java/com/google/crypto/tink/jwt:jwt_ecdsa_public_key",
+        "//src/main/java/com/google/crypto/tink/subtle:hex",
+        "@maven//:com_google_protobuf_protobuf_java",
         "@maven//:com_google_truth_truth",
         "@maven//:junit_junit",
     ],
diff --git a/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaProtoSerializationTest.java b/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaProtoSerializationTest.java
index 0278eaf..5c76034 100644
--- a/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaProtoSerializationTest.java
+++ b/src/test/java/com/google/crypto/tink/jwt/JwtEcdsaProtoSerializationTest.java
@@ -20,12 +20,21 @@
 import static com.google.crypto.tink.internal.testing.Asserts.assertEqualWhenValueParsed;
 import static org.junit.Assert.assertThrows;
 
+import com.google.crypto.tink.InsecureSecretKeyAccess;
+import com.google.crypto.tink.Key;
 import com.google.crypto.tink.Parameters;
 import com.google.crypto.tink.internal.MutableSerializationRegistry;
+import com.google.crypto.tink.internal.ProtoKeySerialization;
 import com.google.crypto.tink.internal.ProtoParametersSerialization;
 import com.google.crypto.tink.proto.JwtEcdsaAlgorithm;
+import com.google.crypto.tink.proto.JwtEcdsaPublicKey.CustomKid;
+import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
 import com.google.crypto.tink.proto.OutputPrefixType;
+import com.google.crypto.tink.subtle.Hex;
+import com.google.protobuf.ByteString;
+import java.math.BigInteger;
 import java.security.GeneralSecurityException;
+import java.security.spec.ECPoint;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -169,4 +178,348 @@
                 .build());
     assertThrows(GeneralSecurityException.class, () -> registry.parseParameters(serialization));
   }
+
+  // PUBLIC KEY PARSING ========================================================= PUBLIC KEY PARSING
+  @Test
+  public void serializeParsePublicKey_es256_kidIgnored_equal() throws Exception {
+    // a valid P256 point. Each coordinate is encoded in 32 bytes.
+    String hexX = "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287";
+    String hexY = "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac";
+
+    JwtEcdsaPublicKey key =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(
+                JwtEcdsaParameters.builder()
+                    .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+                    .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+                    .build())
+            .setPublicPoint(new ECPoint(new BigInteger(hexX, 16), new BigInteger(hexY, 16)))
+            .build();
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES256)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    Key parsed = registry.parseKey(serialization, /* access= */ null);
+    assertThat(parsed.equalsKey(key)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, /* access= */ null);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void serializeParsePublicKey_es384_kidIgnored_equal() throws Exception {
+    // a valid P384 point. Each coordinate is encoded in 48 bytes.
+    String hexX =
+        "EC3A4E415B4E19A4568618029F427FA5DA9A8BC4AE92E02E06AAE5286B300C64"
+            + "DEF8F0EA9055866064A254515480BC13";
+    String hexY =
+        "8015D9B72D7D57244EA8EF9AC0C621896708A59367F9DFB9F54CA84B3F1C9DB1"
+            + "288B231C3AE0D4FE7344FD2533264720";
+
+    JwtEcdsaPublicKey key =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(
+                JwtEcdsaParameters.builder()
+                    .setAlgorithm(JwtEcdsaParameters.Algorithm.ES384)
+                    .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+                    .build())
+            .setPublicPoint(new ECPoint(new BigInteger(hexX, 16), new BigInteger(hexY, 16)))
+            .build();
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES384)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    Key parsed = registry.parseKey(serialization, /* access= */ null);
+    assertThat(parsed.equalsKey(key)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, /* access= */ null);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void serializeParsePublicKey_es521_kidIgnored_equal() throws Exception {
+    // a valid P521 point, but encoded with leading zeros or truncated zeros.
+    String hexXTruncated =
+        "685a48e86c79f0f0875f7bc18d25eb5fc8c0b07e5da4f4370f3a949034085433"
+            + "4b1e1b87fa395464c60626124a4e70d0f785601d37c09870ebf176666877a2046d";
+    String hexYWithLeadingZeros =
+        "0000000000000001ba52c56fc8776d9e8f5db4f0cc27636d0b741bbe05400697942e80b739884a83"
+            + "bde99e0f6716939e632bc8986fa18dccd443a348b6c3e522497955a4f3c302f676";
+
+    JwtEcdsaPublicKey key =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(
+                JwtEcdsaParameters.builder()
+                    .setAlgorithm(JwtEcdsaParameters.Algorithm.ES512)
+                    .setKidStrategy(JwtEcdsaParameters.KidStrategy.IGNORED)
+                    .build())
+            .setPublicPoint(
+                new ECPoint(
+                    new BigInteger(hexXTruncated, 16), new BigInteger(hexYWithLeadingZeros, 16)))
+            .build();
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            .setX(ByteString.copyFrom(Hex.decode(hexXTruncated)))
+            .setY(ByteString.copyFrom(Hex.decode(hexYWithLeadingZeros)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES512)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    Key parsed = registry.parseKey(serialization, /* access= */ null);
+    assertThat(parsed.equalsKey(key)).isTrue();
+
+    // X and Y are currently serialized with an extra zero at the beginning. So we expect X and Y to
+    // always be encoded in 67 bytes.
+    String expectedHexX =
+        "00"
+            + "00685a48e86c79f0f0875f7bc18d25eb5fc8c0b07e5da4f4370f3a949034085433"
+            + "4b1e1b87fa395464c60626124a4e70d0f785601d37c09870ebf176666877a2046d";
+    String expectedHexY =
+        "00"
+            + "01ba52c56fc8776d9e8f5db4f0cc27636d0b741bbe05400697942e80b739884a83"
+            + "bde99e0f6716939e632bc8986fa18dccd443a348b6c3e522497955a4f3c302f676";
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey expectedProtoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            .setX(ByteString.copyFrom(Hex.decode(expectedHexX)))
+            .setY(ByteString.copyFrom(Hex.decode(expectedHexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES512)
+            .build();
+    ProtoKeySerialization expectedSerialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            expectedProtoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, /* access= */ null);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.parser(), serialized, expectedSerialization);
+  }
+
+  @Test
+  public void serializeParsePublicKey_es256_kidCustom_equal() throws Exception {
+    // a valid P256 point. Each coordinate is encoded in 32 bytes.
+    String hexX = "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287";
+    String hexY = "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac";
+
+    JwtEcdsaPublicKey key =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(
+                JwtEcdsaParameters.builder()
+                    .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+                    .setKidStrategy(JwtEcdsaParameters.KidStrategy.CUSTOM)
+                    .build())
+            .setPublicPoint(new ECPoint(new BigInteger(hexX, 16), new BigInteger(hexY, 16)))
+            .setCustomKid("weirdCustomKid")
+            .build();
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES256)
+            .setCustomKid(CustomKid.newBuilder().setValue("weirdCustomKid").build())
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.RAW,
+            /* idRequirement= */ null);
+
+    Key parsed = registry.parseKey(serialization, /* access= */ null);
+    assertThat(parsed.equalsKey(key)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, /* access= */ null);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.parser(), serialized, serialization);
+  }
+
+  @Test
+  public void serializeParsePublicKey_es256_base64Kid_equal() throws Exception {
+    // a valid P256 point. Each coordinate is encoded in 32 bytes.
+    String hexX = "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287";
+    String hexY = "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac";
+
+    JwtEcdsaPublicKey key =
+        JwtEcdsaPublicKey.builder()
+            .setParameters(
+                JwtEcdsaParameters.builder()
+                    .setAlgorithm(JwtEcdsaParameters.Algorithm.ES256)
+                    .setKidStrategy(JwtEcdsaParameters.KidStrategy.BASE64_ENCODED_KEY_ID)
+                    .build())
+            .setPublicPoint(new ECPoint(new BigInteger(hexX, 16), new BigInteger(hexY, 16)))
+            .setIdRequirement(12345)
+            .build();
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES256)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 12345);
+
+    Key parsed = registry.parseKey(serialization, /* access= */ null);
+    assertThat(parsed.equalsKey(key)).isTrue();
+
+    ProtoKeySerialization serialized =
+        registry.serializeKey(key, ProtoKeySerialization.class, /* access= */ null);
+    assertEqualWhenValueParsed(
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.parser(), serialized, serialization);
+  }
+
+  // INVALID PUBLIC KEY SERIALIZATIONS =========================== INVALID PUBLIC KEY SERIALIZATIONS
+  @Test
+  public void parsePublicKey_crunchy_cannotBeParsed_throws() throws Exception {
+    String hexX = "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287";
+    String hexY = "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac";
+
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES256)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.CRUNCHY,
+            /* idRequirement= */ 12345);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+
+  @Test
+  public void parsePublicKey_tinkAndCustomKeyId_throws() throws Exception {
+    String hexX = "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287";
+    String hexY = "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac";
+
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES256)
+            .setCustomKid(CustomKid.newBuilder().setValue("WillNotParseWithRAW").build())
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 12345);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+
+  @Test
+  public void parsePublicKey_wrongVersion_throws() throws Exception {
+    String hexX = "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287";
+    String hexY = "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac";
+
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(1)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES256)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 12345);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
+
+  @Test
+  public void parsePublicKey_unknownAlgorithm_throws() throws Exception {
+    String hexX = "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287";
+    String hexY = "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac";
+
+    com.google.crypto.tink.proto.JwtEcdsaPublicKey protoPublicKey =
+        com.google.crypto.tink.proto.JwtEcdsaPublicKey.newBuilder()
+            .setVersion(0)
+            // X and Y are currently serialized with an extra zero at the beginning.
+            .setX(ByteString.copyFrom(Hex.decode("00" + hexX)))
+            .setY(ByteString.copyFrom(Hex.decode("00" + hexY)))
+            .setAlgorithm(JwtEcdsaAlgorithm.ES_UNKNOWN)
+            .build();
+    ProtoKeySerialization serialization =
+        ProtoKeySerialization.create(
+            "type.googleapis.com/google.crypto.tink.JwtEcdsaPublicKey",
+            protoPublicKey.toByteString(),
+            KeyMaterialType.ASYMMETRIC_PUBLIC,
+            OutputPrefixType.TINK,
+            /* idRequirement= */ 12345);
+
+    assertThrows(
+        GeneralSecurityException.class,
+        () -> registry.parseKey(serialization, InsecureSecretKeyAccess.get()));
+  }
 }