From e3ef24b323193056426684bdf186ad48aefda566 Mon Sep 17 00:00:00 2001 From: Aaron Lew <64337293+aaronlew02@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:44:58 -0400 Subject: [PATCH] refactor: create dedicated TufGsonSupplier Signed-off-by: Aaron Lew <64337293+aaronlew02@users.noreply.github.com> --- .../sigstore/json/GsonByteArrayAdapter.java | 2 +- .../java/dev/sigstore/json/GsonChecked.java | 2 +- .../java/dev/sigstore/json/GsonSupplier.java | 15 ----- .../dev/sigstore/tuf/FileSystemTufStore.java | 6 +- .../java/dev/sigstore/tuf/MetaFetcher.java | 4 +- .../main/java/dev/sigstore/tuf/Updater.java | 4 +- .../sigstore/tuf/json/TufGsonSupplier.java | 66 ++++++++++++++++++ .../dev/sigstore/tuf/model/SignedTufMeta.java | 6 +- .../tuf/PassthroughCacheMetaStoreTest.java | 7 +- .../java/dev/sigstore/tuf/UpdaterTest.java | 5 +- .../tuf/json/TufGsonSupplierTest.java | 67 +++++++++++++++++++ .../tuf/model/TestTufJsonLoading.java | 17 +++-- .../sigstore/testkit/tuf/TestResources.java | 4 +- 13 files changed, 164 insertions(+), 41 deletions(-) create mode 100644 sigstore-java/src/main/java/dev/sigstore/tuf/json/TufGsonSupplier.java create mode 100644 sigstore-java/src/test/java/dev/sigstore/tuf/json/TufGsonSupplierTest.java diff --git a/sigstore-java/src/main/java/dev/sigstore/json/GsonByteArrayAdapter.java b/sigstore-java/src/main/java/dev/sigstore/json/GsonByteArrayAdapter.java index a131cb07b..ec75ee5aa 100644 --- a/sigstore-java/src/main/java/dev/sigstore/json/GsonByteArrayAdapter.java +++ b/sigstore-java/src/main/java/dev/sigstore/json/GsonByteArrayAdapter.java @@ -24,7 +24,7 @@ import java.util.Base64; /** Converts byte arrays to base64, not url safe */ -class GsonByteArrayAdapter extends TypeAdapter { +public class GsonByteArrayAdapter extends TypeAdapter { @Override public void write(JsonWriter out, byte[] value) throws IOException { out.value(new String(Base64.getEncoder().encodeToString(value))); diff --git a/sigstore-java/src/main/java/dev/sigstore/json/GsonChecked.java b/sigstore-java/src/main/java/dev/sigstore/json/GsonChecked.java index b6cc0fc4e..43568b4ed 100644 --- a/sigstore-java/src/main/java/dev/sigstore/json/GsonChecked.java +++ b/sigstore-java/src/main/java/dev/sigstore/json/GsonChecked.java @@ -25,7 +25,7 @@ public final class GsonChecked { Gson gson; - GsonChecked(Gson gson) { + public GsonChecked(Gson gson) { this.gson = gson; } diff --git a/sigstore-java/src/main/java/dev/sigstore/json/GsonSupplier.java b/sigstore-java/src/main/java/dev/sigstore/json/GsonSupplier.java index 5f3138a63..e7e476f05 100644 --- a/sigstore-java/src/main/java/dev/sigstore/json/GsonSupplier.java +++ b/sigstore-java/src/main/java/dev/sigstore/json/GsonSupplier.java @@ -20,7 +20,6 @@ import dev.sigstore.forbidden.SuppressForbidden; import dev.sigstore.rekor.client.GsonAdaptersRekorEntry; import dev.sigstore.rekor.client.GsonAdaptersRekorEntryBody; -import dev.sigstore.tuf.model.*; import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.util.function.Supplier; @@ -47,22 +46,8 @@ public enum GsonSupplier implements Supplier { ZonedDateTime.parse(json.getAsJsonPrimitive().getAsString()) .toLocalDateTime()) // Immutables generated GSON Adapters in alphabetical order - .registerTypeAdapterFactory(new GsonAdaptersDelegations()) - .registerTypeAdapterFactory(new GsonAdaptersDelegationRole()) - .registerTypeAdapterFactory(new GsonAdaptersHashes()) - .registerTypeAdapterFactory(new GsonAdaptersKey()) .registerTypeAdapterFactory(new GsonAdaptersRekorEntry()) .registerTypeAdapterFactory(new GsonAdaptersRekorEntryBody()) - .registerTypeAdapterFactory(new GsonAdaptersRoot()) - .registerTypeAdapterFactory(new GsonAdaptersRootMeta()) - .registerTypeAdapterFactory(new GsonAdaptersRootRole()) - .registerTypeAdapterFactory(new GsonAdaptersSignature()) - .registerTypeAdapterFactory(new GsonAdaptersSnapshot()) - .registerTypeAdapterFactory(new GsonAdaptersSnapshotMeta()) - .registerTypeAdapterFactory(new GsonAdaptersTargets()) - .registerTypeAdapterFactory(new GsonAdaptersTargetMeta()) - .registerTypeAdapterFactory(new GsonAdaptersTimestamp()) - .registerTypeAdapterFactory(new GsonAdaptersTimestampMeta()) .registerTypeAdapterFactory(new GsonAdaptersInTotoPayload()) .disableHtmlEscaping() .create()); diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/FileSystemTufStore.java b/sigstore-java/src/main/java/dev/sigstore/tuf/FileSystemTufStore.java index d4cff80b8..b6f6111a9 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/FileSystemTufStore.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/FileSystemTufStore.java @@ -15,7 +15,7 @@ */ package dev.sigstore.tuf; -import static dev.sigstore.json.GsonSupplier.GSON; +import static dev.sigstore.tuf.json.TufGsonSupplier.TUF_GSON; import com.google.common.annotations.VisibleForTesting; import dev.sigstore.json.JsonParseException; @@ -103,14 +103,14 @@ public > Optional readMeta( if (!roleFile.toFile().exists()) { return Optional.empty(); } - return Optional.of(GSON.get().fromJson(Files.readString(roleFile), tClass)); + return Optional.of(TUF_GSON.get().fromJson(Files.readString(roleFile), tClass)); } > void storeRole(String roleName, T role) throws IOException { try (BufferedWriter fileWriter = Files.newBufferedWriter(repoBaseDir.resolve(roleName + ".json"))) { - GSON.get().toJson(role, fileWriter); + TUF_GSON.get().toJson(role, fileWriter); } } diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/MetaFetcher.java b/sigstore-java/src/main/java/dev/sigstore/tuf/MetaFetcher.java index 372806351..220782f4c 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/MetaFetcher.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/MetaFetcher.java @@ -15,7 +15,7 @@ */ package dev.sigstore.tuf; -import static dev.sigstore.json.GsonSupplier.GSON; +import static dev.sigstore.tuf.json.TufGsonSupplier.TUF_GSON; import com.google.common.base.Preconditions; import dev.sigstore.json.JsonParseException; @@ -79,7 +79,7 @@ > Optional> getMet } var result = new MetaFetchResult( - roleBytes, GSON.get().fromJson(new String(roleBytes, StandardCharsets.UTF_8), t)); + roleBytes, TUF_GSON.get().fromJson(new String(roleBytes, StandardCharsets.UTF_8), t)); return Optional.of(result); } } diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java b/sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java index f6ed6efd2..36b6121df 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java @@ -15,7 +15,7 @@ */ package dev.sigstore.tuf; -import static dev.sigstore.json.GsonSupplier.GSON; +import static dev.sigstore.tuf.json.TufGsonSupplier.TUF_GSON; import com.google.common.annotations.VisibleForTesting; import com.google.common.hash.Hashing; @@ -164,7 +164,7 @@ void updateRoot() if (localRoot.isPresent()) { trustedRoot = localRoot.get(); } else { - trustedRoot = GSON.get().fromJson(trustedRootPath.get(), Root.class); + trustedRoot = TUF_GSON.get().fromJson(trustedRootPath.get(), Root.class); trustedMetaStore.setRoot(trustedRoot); } // verify root that we're bootstrapping this update with is good to go diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/json/TufGsonSupplier.java b/sigstore-java/src/main/java/dev/sigstore/tuf/json/TufGsonSupplier.java new file mode 100644 index 000000000..9bbf52a09 --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/json/TufGsonSupplier.java @@ -0,0 +1,66 @@ +/* + * Copyright 2026 The Sigstore Authors. + * + * 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 dev.sigstore.tuf.json; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializer; +import dev.sigstore.forbidden.SuppressForbidden; +import dev.sigstore.json.GsonByteArrayAdapter; +import dev.sigstore.json.GsonChecked; +import dev.sigstore.tuf.model.*; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.function.Supplier; + +/** Supplies a Gson with custom byte to base64 serialization, configured for TUF models. */ +@SuppressForbidden(reason = "GsonBuilder") +public enum TufGsonSupplier implements Supplier { + TUF_GSON; + + @SuppressWarnings("ImmutableEnumChecker") + private final GsonChecked gson = + new GsonChecked( + new GsonBuilder() + .registerTypeAdapter(byte[].class, new GsonByteArrayAdapter()) + .registerTypeAdapter( + LocalDateTime.class, + (JsonDeserializer) + (json, type, jsonDeserializationContext) -> + ZonedDateTime.parse(json.getAsJsonPrimitive().getAsString()) + .toLocalDateTime()) + // Immutables generated GSON Adapters in alphabetical order + .registerTypeAdapterFactory(new GsonAdaptersDelegations()) + .registerTypeAdapterFactory(new GsonAdaptersDelegationRole()) + .registerTypeAdapterFactory(new GsonAdaptersHashes()) + .registerTypeAdapterFactory(new GsonAdaptersKey()) + .registerTypeAdapterFactory(new GsonAdaptersRoot()) + .registerTypeAdapterFactory(new GsonAdaptersRootMeta()) + .registerTypeAdapterFactory(new GsonAdaptersRootRole()) + .registerTypeAdapterFactory(new GsonAdaptersSignature()) + .registerTypeAdapterFactory(new GsonAdaptersSnapshot()) + .registerTypeAdapterFactory(new GsonAdaptersSnapshotMeta()) + .registerTypeAdapterFactory(new GsonAdaptersTargets()) + .registerTypeAdapterFactory(new GsonAdaptersTargetMeta()) + .registerTypeAdapterFactory(new GsonAdaptersTimestamp()) + .registerTypeAdapterFactory(new GsonAdaptersTimestampMeta()) + .disableHtmlEscaping() + .create()); + + @Override + public GsonChecked get() { + return gson; + } +} diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/model/SignedTufMeta.java b/sigstore-java/src/main/java/dev/sigstore/tuf/model/SignedTufMeta.java index 84ca553bf..654114b28 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/model/SignedTufMeta.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/model/SignedTufMeta.java @@ -16,9 +16,9 @@ package dev.sigstore.tuf.model; import com.google.gson.JsonElement; -import dev.sigstore.json.GsonSupplier; import dev.sigstore.json.JsonParseException; import dev.sigstore.json.canonicalizer.JsonCanonicalizer; +import dev.sigstore.tuf.json.TufGsonSupplier; import java.io.IOException; import java.util.List; import org.immutables.gson.Gson; @@ -40,7 +40,7 @@ public interface SignedTufMeta { @Lazy @Gson.Ignore default T getSignedMeta(Class type) throws JsonParseException { - return GsonSupplier.GSON.get().fromJson(getRawSignedMeta(), type); + return TufGsonSupplier.TUF_GSON.get().fromJson(getRawSignedMeta(), type); } /** The raw signed json, just verify signature over this to prevent loss of unknown fields */ @@ -49,7 +49,7 @@ default T getSignedMeta(Class type) throws JsonParseException { @Lazy default byte[] getCanonicalSignedBytes() throws IOException { - return new JsonCanonicalizer(GsonSupplier.GSON.get().toJson(getRawSignedMeta())) + return new JsonCanonicalizer(TufGsonSupplier.TUF_GSON.get().toJson(getRawSignedMeta())) .getEncodedUTF8(); } } diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/PassthroughCacheMetaStoreTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/PassthroughCacheMetaStoreTest.java index 9809ba5e2..ddfcb4447 100644 --- a/sigstore-java/src/test/java/dev/sigstore/tuf/PassthroughCacheMetaStoreTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/PassthroughCacheMetaStoreTest.java @@ -15,7 +15,7 @@ */ package dev.sigstore.tuf; -import static dev.sigstore.json.GsonSupplier.GSON; +import static dev.sigstore.tuf.json.TufGsonSupplier.TUF_GSON; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -45,7 +45,8 @@ public static void readAllMeta() throws Exception { Path.of( Resources.getResource("dev/sigstore/tuf/synthetic/test/repository/timestamp.json") .getPath()); - timestamp = GSON.get().fromJson(Files.newBufferedReader(timestampResource), Timestamp.class); + timestamp = + TUF_GSON.get().fromJson(Files.newBufferedReader(timestampResource), Timestamp.class); } @BeforeEach @@ -80,7 +81,7 @@ public void readMeta_canInitFromDisk() throws Exception { try (BufferedWriter fileWriter = Files.newBufferedWriter(localStore.resolve("timestamp.json"))) { - GSON.get().toJson(timestamp, fileWriter); + TUF_GSON.get().toJson(timestamp, fileWriter); } assertEquals(timestamp, fileSystemTufStore.readMeta(RootRole.TIMESTAMP, Timestamp.class).get()); diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/UpdaterTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/UpdaterTest.java index 00da50c7d..579477276 100644 --- a/sigstore-java/src/test/java/dev/sigstore/tuf/UpdaterTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/UpdaterTest.java @@ -15,8 +15,8 @@ */ package dev.sigstore.tuf; -import static dev.sigstore.json.GsonSupplier.GSON; import static dev.sigstore.testkit.tuf.TestResources.UPDATER_SYNTHETIC_TRUSTED_ROOT; +import static dev.sigstore.tuf.json.TufGsonSupplier.TUF_GSON; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -459,7 +459,8 @@ public void testTargetsUpdate_success() throws Exception { var localTargets = updater.getMetaStore().getTargets(); assertNotNull(localTargets); var remoteTargets = - GSON.get() + TUF_GSON + .get() .fromJson( Files.newBufferedReader(localMirrorPath.resolve("3.targets.json")), Targets.class); assertEquals(localTargets.getSignedMeta(), remoteTargets.getSignedMeta()); diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/json/TufGsonSupplierTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/json/TufGsonSupplierTest.java new file mode 100644 index 000000000..33ffda46a --- /dev/null +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/json/TufGsonSupplierTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2026 The Sigstore Authors. + * + * 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 dev.sigstore.tuf.json; + +import static dev.sigstore.tuf.json.TufGsonSupplier.TUF_GSON; + +import dev.sigstore.json.GsonChecked; +import dev.sigstore.json.JsonParseException; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class TufGsonSupplierTest { + private final GsonChecked gson = TUF_GSON.get(); + + @Test + public void testWrite() { + Assertions.assertEquals("\"YWJjZA==\"", gson.toJson("abcd".getBytes(StandardCharsets.UTF_8))); + } + + @Test + public void testRead() throws Exception { + Assertions.assertArrayEquals( + "abcd".getBytes(StandardCharsets.UTF_8), gson.fromJson("\"YWJjZA==\"", byte[].class)); + Assertions.assertArrayEquals(new byte[] {}, gson.fromJson("\"\"", byte[].class)); + Assertions.assertArrayEquals(new byte[] {}, gson.fromJson("null", byte[].class)); + } + + @Test + public void testReadException() { + try { + gson.fromJson("%", byte[].class); + Assertions.fail("Expected JsonParseException but got nothing"); + } catch (JsonParseException e) { + // pass + } + } + + @Test + public void testLocalDateTime_read() throws Exception { + var expected = LocalDateTime.of(2023, 1, 12, 18, 22, 2); + Assertions.assertEquals( + expected, gson.fromJson("\"2023-01-12T18:22:02Z\"", LocalDateTime.class)); + Assertions.assertEquals( + expected, gson.fromJson("\"2023-01-12T18:22:02+00:00\"", LocalDateTime.class)); + } + + @Test + public void testLocalDateTime_readException() { + Assertions.assertThrows( + JsonParseException.class, () -> gson.fromJson("\"not-a-date\"", LocalDateTime.class)); + } +} diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/model/TestTufJsonLoading.java b/sigstore-java/src/test/java/dev/sigstore/tuf/model/TestTufJsonLoading.java index a304f9b84..21b34ca47 100644 --- a/sigstore-java/src/test/java/dev/sigstore/tuf/model/TestTufJsonLoading.java +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/model/TestTufJsonLoading.java @@ -15,7 +15,7 @@ */ package dev.sigstore.tuf.model; -import static dev.sigstore.json.GsonSupplier.GSON; +import static dev.sigstore.tuf.json.TufGsonSupplier.TUF_GSON; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -39,7 +39,7 @@ public void loadRootJson() throws Exception { Resources.asCharSource( Resources.getResource("dev/sigstore/tuf/model/root.json"), Charset.defaultCharset()) .openStream(); ) { - trustRoot = GSON.get().fromJson(reader, Root.class); + trustRoot = TUF_GSON.get().fromJson(reader, Root.class); } assertNotNull(trustRoot); assertEquals(5, trustRoot.getSignatures().size()); @@ -76,7 +76,7 @@ public void loadSnapshotJson() throws Exception { Resources.getResource("dev/sigstore/tuf/model/snapshot.json"), Charset.defaultCharset()) .openStream(); ) { - snapshot = GSON.get().fromJson(reader, Snapshot.class); + snapshot = TUF_GSON.get().fromJson(reader, Snapshot.class); } assertNotNull(snapshot); assertEquals(1, snapshot.getSignatures().size()); @@ -114,7 +114,7 @@ public void loadTargetsJson() throws Exception { Resources.getResource("dev/sigstore/tuf/model/targets.json"), Charset.defaultCharset()) .openStream(); ) { - targets = GSON.get().fromJson(reader, Targets.class); + targets = TUF_GSON.get().fromJson(reader, Targets.class); } assertNotNull(targets); assertEquals(5, targets.getSignatures().size()); @@ -172,13 +172,15 @@ public void loadTargetsJson() throws Exception { public void loadTargetData_oneHash() { Assertions.assertDoesNotThrow( () -> - GSON.get() + TUF_GSON + .get() .fromJson( "{\"custom\":{\"sigstore\":{\"status\":\"Active\",\"usage\":\"CTFE\"}},\"hashes\":{\"sha256\": \"7fcb94a5d0ed541260473b990b99a6c39864c1fb16f3f3e594a5a3cebbfe138a\"},\"length\":177}", TargetData.class)); Assertions.assertDoesNotThrow( () -> - GSON.get() + TUF_GSON + .get() .fromJson( "{\"custom\":{\"sigstore\":{\"status\":\"Active\",\"usage\":\"CTFE\"}},\"hashes\":{\"sha512\": \"4b20747d1afe2544238ad38cc0cc3010921b177d60ac743767e0ef675b915489bd01a36606c0ff83c06448622d7160f0d866c83d20f0c0f44653dcc3f9aa0bd4\"},\"length\":177}", TargetData.class)); @@ -190,7 +192,8 @@ public void loadTargetData_failNoHashes() { Assertions.assertThrows( JsonParseException.class, () -> - GSON.get() + TUF_GSON + .get() .fromJson( "{\"custom\":{\"sigstore\":{\"status\":\"Active\",\"usage\":\"CTFE\"}},\"hashes\":{},\"length\":177}", TargetData.class)); diff --git a/sigstore-testkit/src/main/java/dev/sigstore/testkit/tuf/TestResources.java b/sigstore-testkit/src/main/java/dev/sigstore/testkit/tuf/TestResources.java index 742d01722..20638a262 100644 --- a/sigstore-testkit/src/main/java/dev/sigstore/testkit/tuf/TestResources.java +++ b/sigstore-testkit/src/main/java/dev/sigstore/testkit/tuf/TestResources.java @@ -16,8 +16,8 @@ package dev.sigstore.testkit.tuf; import com.google.common.io.Resources; -import dev.sigstore.json.GsonSupplier; import dev.sigstore.json.JsonParseException; +import dev.sigstore.tuf.json.TufGsonSupplier; import dev.sigstore.tuf.model.Root; import java.io.IOException; import java.nio.file.Files; @@ -46,6 +46,6 @@ public static void setupRepoFiles(String repoName, Path destinationDir, String.. } public static Root loadRoot(Path rootPath) throws IOException, JsonParseException { - return GsonSupplier.GSON.get().fromJson(Files.readString(rootPath), Root.class); + return TufGsonSupplier.TUF_GSON.get().fromJson(Files.readString(rootPath), Root.class); } }