From 0a91cbb9c899d6b43c637b0c02fb6fa269b8812d Mon Sep 17 00:00:00 2001 From: Appu Goundan Date: Tue, 10 Feb 2026 18:21:02 +0000 Subject: [PATCH] Delegated targets support - No support for succinct delegations - Removed eager download of all targets - SigstoreTufClient must now download the targets it wants explicitly - Rename update() to refresh() to match naming of other clients - Correctly handle url escaping in names - Delegated target meta is cached per session NOTE: this was produced with the help of AI coding tools Signed-off-by: Appu Goundan --- .../dev/sigstore/tuf/FileSystemTufStore.java | 22 +- .../java/dev/sigstore/tuf/MetaFetcher.java | 5 +- .../dev/sigstore/tuf/SigstoreTufClient.java | 13 +- .../dev/sigstore/tuf/TrustedMetaStore.java | 8 + .../main/java/dev/sigstore/tuf/TufNames.java | 30 ++ .../main/java/dev/sigstore/tuf/Updater.java | 332 +++++++++++++-- .../sigstore/tuf/model/DelegationRole.java | 7 + .../sigstore/tuf/SigstoreTufClientTest.java | 4 +- .../java/dev/sigstore/tuf/UpdaterTest.java | 246 +++++++++-- .../dev/sigstore/tuf/synthetic/README.md | 391 ++++++++++++++++++ .../synthetic/delegation-basic/1.release.json | 22 + .../synthetic/delegation-basic/1.root.json | 71 ++++ .../delegation-basic/1.snapshot.json | 22 + .../synthetic/delegation-basic/1.targets.json | 39 ++ .../tuf/synthetic/delegation-basic/root.json | 71 ++++ ...4e04ee160d9dd3b98e72fc2954f98.artifact.txt | 1 + .../synthetic/delegation-basic/timestamp.json | 19 + .../1.production.json | 22 + .../delegation-non-terminating/1.root.json | 71 ++++ .../1.snapshot.json | 25 ++ .../delegation-non-terminating/1.staging.json | 15 + .../delegation-non-terminating/1.targets.json | 57 +++ .../delegation-non-terminating/root.json | 71 ++++ ...1331140f63e02550ab2381aa0e2ee9e5.found.txt | 1 + .../delegation-non-terminating/timestamp.json | 19 + .../delegation-terminating/1.fallback.json | 22 + .../delegation-terminating/1.release.json | 15 + .../delegation-terminating/1.root.json | 71 ++++ .../delegation-terminating/1.snapshot.json | 25 ++ .../delegation-terminating/1.targets.json | 57 +++ .../delegation-terminating/root.json | 71 ++++ ...2a03e7cbd6ff07b04b2b64855e1ce2.missing.txt | 1 + .../delegation-terminating/timestamp.json | 19 + .../synthetic/delegation-trusted-root.json | 71 ++++ tuf-cli/build.gradle.kts | 3 +- .../java/dev/sigstore/tuf/cli/Download.java | 6 +- .../java/dev/sigstore/tuf/cli/Refresh.java | 2 +- tuf-cli/tuf-cli-server.xfails | 21 - tuf-cli/tuf-cli.xfails | 22 - 39 files changed, 1850 insertions(+), 140 deletions(-) create mode 100644 sigstore-java/src/main/java/dev/sigstore/tuf/TufNames.java create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/README.md create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/1.release.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/1.root.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/1.snapshot.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/1.targets.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/root.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/targets/release/42bd420cc2f99e68e60005fa7c28fc2f60e4e04ee160d9dd3b98e72fc2954f98.artifact.txt create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/timestamp.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.production.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.root.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.snapshot.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.staging.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.targets.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/root.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/targets/ce355849c5722b5468f50a99dab3c6031331140f63e02550ab2381aa0e2ee9e5.found.txt create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/timestamp.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.fallback.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.release.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.root.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.snapshot.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.targets.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/root.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/targets/release/6c2814d5403ee7ccbd04f096f62cd1da362a03e7cbd6ff07b04b2b64855e1ce2.missing.txt create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/timestamp.json create mode 100644 sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-trusted-root.json delete mode 100644 tuf-cli/tuf-cli.xfails 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 d2db8799b..ae4efae50 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/FileSystemTufStore.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/FileSystemTufStore.java @@ -23,8 +23,6 @@ import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; @@ -69,26 +67,22 @@ public String getIdentifier() { @Override public void writeTarget(String targetName, byte[] targetContents) throws IOException { - var encoded = URLEncoder.encode(targetName, StandardCharsets.UTF_8); - Files.write(targetsDir.resolve(encoded), targetContents); + Files.write(targetsDir.resolve(TufNames.encode(targetName)), targetContents); } @Override public byte[] readTarget(String targetName) throws IOException { - var encoded = URLEncoder.encode(targetName, StandardCharsets.UTF_8); - return Files.readAllBytes(targetsDir.resolve(encoded)); + return Files.readAllBytes(targetsDir.resolve(TufNames.encode(targetName))); } @Override public InputStream getTargetInputSteam(String targetName) throws IOException { - var encoded = URLEncoder.encode(targetName, StandardCharsets.UTF_8); - return Files.newInputStream(targetsDir.resolve(encoded)); + return Files.newInputStream(targetsDir.resolve(TufNames.encode(targetName))); } @Override public boolean hasTarget(String targetName) throws IOException { - var encoded = URLEncoder.encode(targetName, StandardCharsets.UTF_8); - return Files.isRegularFile(targetsDir.resolve(encoded)); + return Files.isRegularFile(targetsDir.resolve(TufNames.encode(targetName))); } @Override @@ -99,7 +93,7 @@ public void writeMeta(String roleName, SignedTufMeta meta) throws IOException @Override public > Optional readMeta(String roleName, Class tClass) throws IOException, JsonParseException { - Path roleFile = repoBaseDir.resolve(roleName + ".json"); + Path roleFile = repoBaseDir.resolve(TufNames.encode(roleName) + ".json"); if (!roleFile.toFile().exists()) { return Optional.empty(); } @@ -107,15 +101,15 @@ public > Optional readMeta(String roleName, Class< } > void storeRole(String roleName, T role) throws IOException { - try (BufferedWriter fileWriter = - Files.newBufferedWriter(repoBaseDir.resolve(roleName + ".json"))) { + Path roleFile = repoBaseDir.resolve(TufNames.encode(roleName) + ".json"); + try (BufferedWriter fileWriter = Files.newBufferedWriter(roleFile)) { GSON.get().toJson(role, fileWriter); } } @Override public void clearMeta(String role) throws IOException { - Path metaFile = repoBaseDir.resolve(role + ".json"); + Path metaFile = repoBaseDir.resolve(TufNames.encode(role) + ".json"); if (Files.isRegularFile(metaFile)) { Files.delete(metaFile); } 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..84f0ca448 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/MetaFetcher.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/MetaFetcher.java @@ -65,9 +65,10 @@ public > Optional> } private static String getFileName(String role, @Nullable Integer version) { + String encodedRole = TufNames.encode(role); return version == null - ? role + ".json" - : String.format(Locale.ROOT, "%d.%s.json", version, role); + ? encodedRole + ".json" + : String.format(Locale.ROOT, "%d.%s.json", version, encodedRole); } > Optional> getMeta( diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/SigstoreTufClient.java b/sigstore-java/src/main/java/dev/sigstore/tuf/SigstoreTufClient.java index 37d8bc01f..f88e7c8f7 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/SigstoreTufClient.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/SigstoreTufClient.java @@ -26,9 +26,6 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; import java.time.Duration; import java.time.Instant; @@ -149,12 +146,10 @@ public void update() throws SigstoreConfigurationException { /** Force an update, ignoring any cache validity. */ public void forceUpdate() throws SigstoreConfigurationException { try { - updater.update(); - } catch (IOException - | NoSuchAlgorithmException - | InvalidKeySpecException - | InvalidKeyException - | JsonParseException ex) { + updater.refresh(); + updater.downloadTarget(TRUST_ROOT_FILENAME); + updater.downloadTarget(SIGNING_CONFIG_FILENAME); + } catch (IOException | JsonParseException ex) { throw new SigstoreConfigurationException("TUF repo failed to update", ex); } lastUpdate = Instant.now(); diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMetaStore.java b/sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMetaStore.java index 3f032ca63..66fee0337 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMetaStore.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/TrustedMetaStore.java @@ -110,6 +110,10 @@ public void setTargets(Targets targets) throws IOException { metaStore.writeMeta(RootRole.TARGETS, targets); } + public void setTargets(String roleName, Targets targets) throws IOException { + metaStore.writeMeta(roleName, targets); + } + public Targets getTargets() throws IOException, JsonParseException { return getMeta(RootRole.TARGETS, Targets.class); } @@ -118,6 +122,10 @@ public Optional findTargets() throws IOException, JsonParseException { return metaStore.readMeta(RootRole.TARGETS, Targets.class); } + public Optional findTargets(String roleName) throws IOException, JsonParseException { + return metaStore.readMeta(roleName, Targets.class); + } + public void clearMetaDueToKeyRotation() throws IOException { metaStore.clearMeta(RootRole.TIMESTAMP); metaStore.clearMeta(RootRole.SNAPSHOT); diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/TufNames.java b/sigstore-java/src/main/java/dev/sigstore/tuf/TufNames.java new file mode 100644 index 000000000..61fcedf60 --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/TufNames.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022 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; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** URL-encodes TUF role and target names for safe use in file paths and URLs. */ +public class TufNames { + + private TufNames() {} + + /** URL-encode a name, using %20 for spaces (not +). */ + public static String encode(String name) { + return URLEncoder.encode(name, StandardCharsets.UTF_8).replace("+", "%20"); + } +} 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..f16151003 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java @@ -27,14 +27,16 @@ import dev.sigstore.tuf.model.Timestamp; import dev.sigstore.tuf.model.TufMeta; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SignatureException; -import java.security.spec.InvalidKeySpecException; import java.time.Clock; import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -59,6 +61,7 @@ public class Updater { // Limit the update loop to retrieve a max of 1024 subsequent versions as expressed in 5.3.3 of // spec. private static final int MAX_UPDATES = 1024; + private static final int MAX_DELEGATIONS = 32; private static final Logger log = Logger.getLogger(Updater.class.getName()); @@ -73,6 +76,8 @@ public class Updater { // Mutable State private ZonedDateTime updateStartTime; + // In-session cache of verified delegated targets, cleared on each refresh(). + private final Map trustedDelegations = new HashMap<>(); Updater( Clock clock, @@ -95,19 +100,9 @@ public static Builder builder() { return new Builder(); } - /** Update metadata and download all targets. */ - public void update() - throws IOException, - NoSuchAlgorithmException, - InvalidKeySpecException, - InvalidKeyException, - JsonParseException { - updateMeta(); - downloadTargets(trustedMetaStore.getTargets()); - } - - /** Update just metadata but do not download targets. */ - public void updateMeta() throws IOException, JsonParseException { + /** Refresh metadata (root → timestamp → snapshot → targets) without downloading targets. */ + public void refresh() throws IOException, JsonParseException { + trustedDelegations.clear(); updateRoot(); var oldTimestamp = trustedMetaStore.findTimestamp(); updateTimestamp(); @@ -123,13 +118,14 @@ public void updateMeta() throws IOException, JsonParseException { /** * Download a single target defined in targets. Will not re-download a target that is already - * cached locally. Does not handle delegated targets. + * cached locally. Supports delegated targets. */ public void downloadTarget(String targetName) throws IOException, JsonParseException { - var targetData = trustedMetaStore.getTargets().getSignedMeta().getTargets().get(targetName); - if (targetData == null) { + var targetDataMaybe = findTargetData(targetName, trustedMetaStore.getTargets()); + if (targetDataMaybe.isEmpty()) { throw new TargetMetadataMissingException(targetName); } + TargetData targetData = targetDataMaybe.get(); if (targetStore.hasTarget(targetName)) { byte[] target = targetStore.readTarget(targetName); // TODO: Using exceptions for control flow here, we should have something that returns a true @@ -507,28 +503,6 @@ void updateTargets() trustedMetaStore.setTargets(targetsResult.getMetaResource()); } - void downloadTargets(Targets targets) - throws IOException, - TargetMetadataMissingException, - FileNotFoundException, - JsonParseException { - // Skip #7 and go straight to downloading targets. It looks like delegations were removed from - // sigstore TUF data. - // {@see https://github.com/sigstore/sigstore/issues/562} - for (Map.Entry entry : - targets.getSignedMeta().getTargets().entrySet()) { - String targetName = entry.getKey(); - // 8) If target is missing metadata fail. - // Note: This can't actually happen due to the way GSON is setup the targets.json would fail - // to parse. Leaving this code in in-case we eventually allow it in de-serialization. - if (entry.getValue() == null) { - throw new TargetMetadataMissingException(targetName); - } - TargetMeta.TargetData targetData = entry.getValue(); - downloadTarget(targetName, targetData); - } - } - void downloadTarget(String targetName, TargetData targetData) throws IOException { var calculatedName = targetName; var calculatedPath = ""; @@ -562,6 +536,286 @@ void downloadTarget(String targetName, TargetData targetData) throws IOException targetStore.writeTarget(targetName, targetBytes); } + /** + * Check whether a target name falls within the scope of a delegation role. Per the TUF spec, + * roles use either {@code paths} (glob patterns) or {@code path_hash_prefixes} (hex prefix match + * on SHA-256 of target name), but not both. + */ + @VisibleForTesting + boolean isTargetInRole(DelegationRole role, String targetName) { + List paths = role.getPaths(); + List prefixes = role.getPathHashPrefixes(); + + boolean hasPaths = !paths.isEmpty(); + boolean hasPrefixes = !prefixes.isEmpty(); + + if (!hasPaths && !hasPrefixes) { + return false; + } + + // Per TUF spec, paths and path_hash_prefixes are mutually exclusive. + // We check whichever is present; if both are present (invalid metadata), + // we require both to match as a conservative choice. + if (hasPaths) { + boolean pathMatched = false; + for (String pattern : paths) { + if (matches(targetName, pattern)) { + pathMatched = true; + break; + } + } + if (!pathMatched) { + return false; + } + } + + if (hasPrefixes) { + String targetHash = + Hashing.sha256().hashString(targetName, StandardCharsets.UTF_8).toString(); + boolean hashMatched = false; + for (String prefix : prefixes) { + if (targetHash.startsWith(prefix.toLowerCase(Locale.ROOT))) { + hashMatched = true; + break; + } + } + if (!hashMatched) { + return false; + } + } + + return true; + } + + @VisibleForTesting + static boolean matches(String targetName, String pattern) { + // Convert TUF glob to regex + // * -> [^/]* + // ? -> [^/] + // everything else escaped + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < pattern.length(); i++) { + char c = pattern.charAt(i); + if (c == '*') { + sb.append("[^/]*"); + } else if (c == '?') { + sb.append("[^/]"); + } else if (".[]{}()\\^$+|".indexOf(c) != -1) { + sb.append('\\').append(c); + } else { + sb.append(c); + } + } + return targetName.matches(sb.toString()); + } + + private static class PendingDelegation { + final DelegationRole role; + final Targets parent; + + PendingDelegation(DelegationRole role, Targets parent) { + this.role = role; + this.parent = parent; + } + } + + /** + * Iterative preorder depth-first walk of the delegation tree, matching Python TUF's + * _preorder_depth_first_walk. Limits total delegations visited (not just depth) and tracks + * visited roles to prevent cycles. Metadata is loaded lazily when a role is popped from the + * stack. + */ + private Optional findTargetData(String targetName, Targets topLevelTargets) + throws IOException, JsonParseException { + var visitedRoleNames = new HashSet(); + var delegationsToVisit = new ArrayList(); + + // Check top-level targets first, then seed the stack with its delegations + TargetData topData = topLevelTargets.getSignedMeta().getTargets().get(targetName); + if (topData != null) { + return Optional.of(topData); + } + visitedRoleNames.add(RootRole.TARGETS); + pushChildDelegations(targetName, topLevelTargets, delegationsToVisit); + + while (visitedRoleNames.size() <= MAX_DELEGATIONS && !delegationsToVisit.isEmpty()) { + var current = delegationsToVisit.remove(delegationsToVisit.size() - 1); + String roleName = current.role.getName(); + + // Skip visited roles to prevent cycles + if (visitedRoleNames.contains(roleName)) { + continue; + } + + Targets currentTargets; + try { + currentTargets = updateDelegatedTargets(current.role, current.parent); + } catch (SnapshotTargetMissingException | FileNotFoundException e) { + log.log( + Level.FINE, + "TUF: Delegated targets metadata for role {0} not found, skipping: {1}", + new Object[] {roleName, e.getMessage()}); + continue; + } catch (SignatureVerificationException | RoleExpiredException e) { + log.log( + Level.FINE, + "TUF: Delegated targets metadata for role {0} is invalid, skipping: {1}", + new Object[] {roleName, e.getMessage()}); + continue; + } + + // Check if target is in current role's targets + TargetData data = currentTargets.getSignedMeta().getTargets().get(targetName); + if (data != null) { + return Optional.of(data); + } + + // Mark as visited after checking targets (matches Python behavior) + visitedRoleNames.add(roleName); + pushChildDelegations(targetName, currentTargets, delegationsToVisit); + } + + if (!delegationsToVisit.isEmpty()) { + log.log( + Level.WARNING, + "TUF: {0} roles left to visit but max delegations ({1}) reached while searching for {2}", + new Object[] {delegationsToVisit.size(), MAX_DELEGATIONS, targetName}); + } + + return Optional.empty(); + } + + /** + * Pushes child delegations from {@code targets} onto {@code delegationsToVisit} for roles that + * match {@code targetName}. Pushes in reverse order so the first matching role is on top of the + * stack. Clears the stack if a terminating role is encountered. + */ + private void pushChildDelegations( + String targetName, Targets targets, List delegationsToVisit) + throws JsonParseException { + var delegationsMaybe = targets.getSignedMeta().getDelegations(); + if (delegationsMaybe.isEmpty()) { + return; + } + var children = new ArrayList(); + for (DelegationRole role : delegationsMaybe.get().getRoles()) { + if (!isTargetInRole(role, targetName)) { + continue; + } + children.add(new PendingDelegation(role, targets)); + if (role.isTerminating()) { + delegationsToVisit.clear(); + break; + } + } + // Push in reverse so first child is on top of stack (popped first) + Collections.reverse(children); + delegationsToVisit.addAll(children); + } + + private Targets updateDelegatedTargets(DelegationRole role, Targets parent) + throws IOException, JsonParseException { + String roleName = role.getName(); + + // Return immediately if already verified in this session + if (trustedDelegations.containsKey(roleName)) { + return trustedDelegations.get(roleName); + } + + SnapshotMeta.SnapshotTarget snapshotTarget = + trustedMetaStore.getSnapshot().getSignedMeta().getMeta().get(roleName + ".json"); + if (snapshotTarget == null) { + throw new SnapshotTargetMissingException(roleName + ".json"); + } + + Optional localTargets = trustedMetaStore.findTargets(roleName); + if (localTargets.isPresent()) { + Targets local = localTargets.get(); + int localVersion = local.getSignedMeta().getVersion(); + if (localVersion == snapshotTarget.getVersion()) { + // Re-verify signatures and expiry before trusting cached metadata, consistent with go-tuf + // and python-tuf. Hash re-verification requires raw bytes not available from the + // deserialized cache; hashes were verified on first download. + try { + Delegations parentDelegations = + parent + .getSignedMeta() + .getDelegations() + .orElseThrow( + () -> + new IllegalStateException( + "Parent targets metadata has no delegations for role: " + + role.getName())); + verifyDelegate( + local.getSignatures(), + parentDelegations.getKeys(), + role, + local.getCanonicalSignedBytes()); + throwIfExpired(local.getSignedMeta().getExpiresAsDate()); + trustedDelegations.put(roleName, local); + return local; + } catch (SignatureVerificationException | RoleExpiredException e) { + log.log( + Level.FINE, + "TUF: Cached delegated targets for role {0} failed verification, re-fetching: {1}", + new Object[] {roleName, e.getMessage()}); + // fall through to fetch from remote + } + } + } + + // Fetch from remote + var targetsResultMaybe = + metaFetcher.getMeta( + roleName, + snapshotTarget.getVersion(), + Targets.class, + snapshotTarget.getLengthOrDefault()); + + if (targetsResultMaybe.isEmpty()) { + throw new FileNotFoundException(roleName + ".json", metaFetcher.getSource()); + } + var targetsResult = targetsResultMaybe.get(); + + // Verify hash + if (snapshotTarget.getHashes().isPresent()) { + verifyHashes( + roleName + ".json", targetsResult.getRawBytes(), snapshotTarget.getHashes().get()); + } + + // Verify against parent's delegation keys/threshold + Delegations parentDelegations = + parent + .getSignedMeta() + .getDelegations() + .orElseThrow( + () -> + new IllegalStateException( + "Parent targets metadata has no delegations for role: " + role.getName())); + verifyDelegate( + targetsResult.getMetaResource().getSignatures(), + parentDelegations.getKeys(), + role, + targetsResult.getMetaResource().getCanonicalSignedBytes()); + + // Check version matches snapshot + if (targetsResult.getMetaResource().getSignedMeta().getVersion() + != snapshotTarget.getVersion()) { + throw new SnapshotVersionMismatchException( + snapshotTarget.getVersion(), + targetsResult.getMetaResource().getSignedMeta().getVersion()); + } + + // Check expiration + throwIfExpired(targetsResult.getMetaResource().getSignedMeta().getExpiresAsDate()); + + // Persist and add to trusted set + trustedMetaStore.setTargets(roleName, targetsResult.getMetaResource()); + trustedDelegations.put(roleName, targetsResult.getMetaResource()); + + return targetsResult.getMetaResource(); + } + @VisibleForTesting TargetStore getTargetStore() { return targetStore; diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/model/DelegationRole.java b/sigstore-java/src/main/java/dev/sigstore/tuf/model/DelegationRole.java index 5ab77fa07..139f097af 100644 --- a/sigstore-java/src/main/java/dev/sigstore/tuf/model/DelegationRole.java +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/model/DelegationRole.java @@ -63,4 +63,11 @@ public interface DelegationRole extends Role { /** A boolean indicating whether subsequent delegations should be considered. */ @Gson.Named("terminating") boolean isTerminating(); + + /** + * A list of strings, where each string is a hex-encoded hash prefix. Clients MUST check that the + * SHA-256 hash of the target's name starts with one of these prefixes. + */ + @Gson.Named("path_hash_prefixes") + List getPathHashPrefixes(); } diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/SigstoreTufClientTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/SigstoreTufClientTest.java index 6da4cf161..4bc01104a 100644 --- a/sigstore-java/src/test/java/dev/sigstore/tuf/SigstoreTufClientTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/SigstoreTufClientTest.java @@ -110,7 +110,7 @@ public void testUpdate_updateWhenCacheInvalid() throws Exception { client.update(); Thread.sleep(3000); client.update(); - Mockito.verify(mockUpdater, Mockito.times(2)).update(); + Mockito.verify(mockUpdater, Mockito.times(2)).refresh(); } @Test @@ -120,7 +120,7 @@ public void testUpdate_noUpdateWhenCacheValid() throws Exception { client.update(); client.update(); - Mockito.verify(mockUpdater, Mockito.times(1)).update(); + Mockito.verify(mockUpdater, Mockito.times(1)).refresh(); } private static Updater mockUpdater() throws IOException { 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..1c1c6f9da 100644 --- a/sigstore-java/src/test/java/dev/sigstore/tuf/UpdaterTest.java +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/UpdaterTest.java @@ -19,6 +19,7 @@ import static dev.sigstore.testkit.tuf.TestResources.UPDATER_SYNTHETIC_TRUSTED_ROOT; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -26,13 +27,16 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.hash.Hashing; import com.google.common.io.Resources; import dev.sigstore.http.URIFormat; import dev.sigstore.json.JsonParseException; import dev.sigstore.testkit.tuf.TestResources; import dev.sigstore.tuf.encryption.Verifier; import dev.sigstore.tuf.encryption.Verifiers; +import dev.sigstore.tuf.model.DelegationRole; import dev.sigstore.tuf.model.Hashes; +import dev.sigstore.tuf.model.ImmutableDelegationRole; import dev.sigstore.tuf.model.ImmutableKey; import dev.sigstore.tuf.model.ImmutableRootRole; import dev.sigstore.tuf.model.ImmutableSignature; @@ -211,7 +215,7 @@ public void testRootUpdate_metaFileTooBig() throws Exception { public void testTimestampUpdate_throwMetaNotFoundException() throws Exception { setupMirror("synthetic/test-template", "2.root.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - var ex = assertThrows(FileNotFoundException.class, updater::update); + var ex = assertThrows(FileNotFoundException.class, updater::refresh); MatcherAssert.assertThat( ex.getMessage(), CoreMatchers.startsWith("file (timestamp.json) was not found at source")); } @@ -223,7 +227,7 @@ public void testTimestampUpdate_throwsSignatureVerificationException() throws Ex var ex = assertThrows( SignatureVerificationException.class, - updater::update, + updater::refresh, "The timestamp was not signed so should have thown a SignatureVerificationException."); assertEquals(0, ex.getVerifiedSignatures(), "verified signature threshold did not match"); assertEquals(1, ex.getRequiredSignatures(), "required signatures found did not match"); @@ -237,7 +241,7 @@ public void testTimestampUpdate_throwsRollbackVersionException() throws Exceptio var ex = assertThrows( RollbackVersionException.class, - updater::update, + updater::refresh, "The repo in this test provides an older signed timestamp version that should have caused a RoleVersionException."); assertEquals(3, ex.getCurrentVersion(), "expected timestamp version did not match"); assertEquals(1, ex.getFoundVersion(), "found timestamp version did not match"); @@ -253,7 +257,7 @@ public void testTimestampUpdate_throwsRoleExpiredException() throws Exception { assertThrows( RoleExpiredException.class, - updater::update, + updater::refresh, "Expects a RoleExpiredException as the repo timestamp.json should be expired."); } @@ -308,7 +312,7 @@ public void testSnapshotUpdate_invalidHash() throws Exception { var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); assertThrows( InvalidHashesException.class, - updater::update, + updater::refresh, "snapshot.json edited and should fail hash test."); } @@ -319,7 +323,7 @@ public void testSnapshotUpdate_timestampSnapshotVersionMismatch() throws Excepti var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); assertThrows( SnapshotVersionMismatchException.class, - updater::update, + updater::refresh, "snapshot version should not match the timestamp metadata."); } @@ -336,7 +340,7 @@ public void testSnapshotUpdate_snapshotTargetMissing() throws Exception { var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); assertThrows( SnapshotTargetMissingException.class, - updater::update, + updater::refresh, "All targets from previous versions of snapshot should be contained in future versions of snapshot."); } @@ -356,7 +360,7 @@ public void testSnapshotUpdate_snapshotTargetVersionRollback() throws Exception var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); assertThrows( SnapshotTargetVersionException.class, - updater::update, + updater::refresh, "The new snapshot.json has a targets.json version that is lower than the current target and so we expect a SnapshotTargetVersionException."); } @@ -380,7 +384,7 @@ public void testSnapshotUpdate_expired() throws Exception { "2022-11-20T18:07:27Z"); // one day after assertThrows( RoleExpiredException.class, - updater::update, + updater::refresh, "Expects a RoleExpiredException as the repo snapshot.json should be expired."); } @@ -390,7 +394,7 @@ public void testTargetsUpdate_targetMetaMissing() throws Exception { var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); assertThrows( FileNotFoundException.class, - updater::update, + updater::refresh, "Expected remote with no target.json to throw FileNotFoundException."); } @@ -405,7 +409,7 @@ public void testTargetsUpdate_invalidHash() throws Exception { var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); assertThrows( InvalidHashesException.class, - updater::update, + updater::refresh, "targets.json has been modified to have an invalid hash."); } @@ -420,7 +424,7 @@ public void testTargetsUpdate_snapshotVersionMismatch() throws Exception { var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); assertThrows( SnapshotVersionMismatchException.class, - updater::update, + updater::refresh, "targets version should not match the snapshot targets metadata."); } @@ -440,7 +444,7 @@ public void testTargetsUpdate_targetExpired() throws Exception { "2022-11-20T18:07:27Z"); // one day after assertThrows( RoleExpiredException.class, - updater::update, + updater::refresh, "targets are out of date and should cause RoleExpiredException."); } @@ -455,7 +459,7 @@ public void testTargetsUpdate_success() throws Exception { var updater = createTimeStaticUpdater( localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT, "2022-11-20T18:07:27Z"); - updater.updateMeta(); + updater.refresh(); var localTargets = updater.getMetaStore().getTargets(); assertNotNull(localTargets); var remoteTargets = @@ -477,8 +481,8 @@ public void testTargetsDownload_targetMissingTargetMetadata() throws Exception { var ex = assertThrows( JsonParseException.class, - updater::update, - "targets.json data should be causing a gson error due to missing TargetData. If at some point we support nullable TargetData this test should be updated to expect TargetMetadataMissingException while calling downloadTargets()."); + updater::refresh, + "targets.json data should be causing a gson error due to missing TargetData. If at some point we support nullable TargetData this test should be updated to expect TargetMetadataMissingException while calling downloadTarget()."); MatcherAssert.assertThat( ex.getMessage(), CoreMatchers.endsWith( @@ -494,10 +498,10 @@ public void testTargetsDownload_targetFileNotFound() throws Exception { "3.snapshot.json", "3.targets.json"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - updater.updateMeta(); + updater.refresh(); assertThrows( FileNotFoundException.class, - () -> updater.downloadTargets(updater.getMetaStore().getTargets()), + () -> updater.downloadTarget("test.txt"), "the target file for download should be missing from the repo and cause an exception."); } @@ -511,10 +515,10 @@ public void testTargetsDownload_targetInvalidLength() throws Exception { "3.targets.json", "targets/860de8f9a858eea7190fcfa1b53fe55914d3c38f17f8f542273012d19cc9509bb423f37b7c13c577a56339ad7f45273b479b1d0df837cb6e20a550c27cce0885.test.txt"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - updater.updateMeta(); + updater.refresh(); assertThrows( FileExceedsMaxLengthException.class, - () -> updater.downloadTargets(updater.getMetaStore().getTargets()), + () -> updater.downloadTarget("test.txt"), "The target file is expected to not match the length specified in targets.json target data."); } @@ -528,10 +532,10 @@ public void testTargetsDownload_targetFileInvalidHash() throws Exception { "3.targets.json", "targets/860de8f9a858eea7190fcfa1b53fe55914d3c38f17f8f542273012d19cc9509bb423f37b7c13c577a56339ad7f45273b479b1d0df837cb6e20a550c27cce0885.test.txt"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - updater.updateMeta(); + updater.refresh(); assertThrows( InvalidHashesException.class, - () -> updater.downloadTargets(updater.getMetaStore().getTargets()), + () -> updater.downloadTarget("test.txt"), "The target file has been modified and should not match the expected hash"); } @@ -547,7 +551,10 @@ public void testTargetsDownload_success() throws Exception { "targets/32005f02eac21b4cf161a02495330b6c14b548622b5f7e19d59ecfa622de650603ecceea39ed86cc322749a813503a72ad14ce5462c822b511eaf2f2cd2ad8f2.test.txt.v2", "targets/53904bc6216230bf8da0ec42d34004a3f36764de698638641870e37d270e4fd13e1079285f8bca73c2857a279f6f7fbc82038274c3eb48ec5bb2da9b2e30491a.test2.txt"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - updater.update(); + updater.refresh(); + updater.downloadTarget("test.txt"); + updater.downloadTarget("test.txt.v2"); + updater.downloadTarget("test2.txt"); assertNotNull(updater.getTargetStore().readTarget("test.txt")); assertNotNull(updater.getTargetStore().readTarget("test.txt.v2")); assertNotNull(updater.getTargetStore().readTarget("test2.txt")); @@ -570,7 +577,12 @@ public void testTargetsDownload_sha256Only() throws Exception { Resources.getResource("dev/sigstore/tuf/synthetic/targets-sha256-or-sha512/root.json") .getPath()); var updater = createTimeStaticUpdater(localStorePath, UPDATER_ROOT); - assertDoesNotThrow(updater::update); + assertDoesNotThrow( + () -> { + updater.refresh(); + updater.downloadTarget("test.txt"); + updater.downloadTarget("test2.txt"); + }); } @Test @@ -585,7 +597,7 @@ public void testDownloadTarget_singleTarget() throws Exception { "targets/32005f02eac21b4cf161a02495330b6c14b548622b5f7e19d59ecfa622de650603ecceea39ed86cc322749a813503a72ad14ce5462c822b511eaf2f2cd2ad8f2.test.txt.v2", "targets/53904bc6216230bf8da0ec42d34004a3f36764de698638641870e37d270e4fd13e1079285f8bca73c2857a279f6f7fbc82038274c3eb48ec5bb2da9b2e30491a.test2.txt"); var updater = createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); - updater.updateMeta(); + updater.refresh(); updater.downloadTarget("test.txt"); Assertions.assertEquals(1, countFilesInTargetsDir(updater)); updater.downloadTarget("test2.txt"); @@ -606,11 +618,121 @@ public void testDownloadTarget_inSubDirectory() throws Exception { "1.targets.json", "targets/subdir/860de8f9a858eea7190fcfa1b53fe55914d3c38f17f8f542273012d19cc9509bb423f37b7c13c577a56339ad7f45273b479b1d0df837cb6e20a550c27cce0885.test.txt"); var updater = createTimeStaticUpdater(localStorePath, root); - updater.updateMeta(); + updater.refresh(); updater.downloadTarget("subdir/test.txt"); Assertions.assertEquals(1, countFilesInTargetsDir(updater)); } + private static final Path DELEGATION_TRUSTED_ROOT = + Path.of( + Resources.getResource("dev/sigstore/tuf/synthetic/delegation-trusted-root.json") + .getPath()); + + @Test + public void testMatches() { + assertTrue(Updater.matches("foo.txt", "foo.txt")); + assertTrue(Updater.matches("foo.txt", "*.txt")); + assertTrue(Updater.matches("dir/foo.txt", "dir/*.txt")); + assertFalse(Updater.matches("dir/foo.txt", "*.txt")); // TUF globs don't match across separators + assertTrue(Updater.matches("foo-1.txt", "foo-?.txt")); + assertFalse(Updater.matches("foo-11.txt", "foo-?.txt")); + assertTrue(Updater.matches("targets/foo.tgz", "targets/*.tgz")); + assertFalse(Updater.matches("targets/foo.txt", "targets/*.tgz")); + + // Substring matching prevention + assertFalse(Updater.matches("foo.txt", "foo")); + assertFalse(Updater.matches("foo.txt", "txt")); + assertFalse(Updater.matches("prefix-foo.txt", "foo.txt")); + + // ? should not cross directory boundaries + assertTrue(Updater.matches("foo1bar.txt", "foo?bar.txt")); + assertFalse(Updater.matches("foo/bar.txt", "foo?bar.txt")); + + // Multiple wildcards + assertTrue(Updater.matches("a-b-c.txt", "a-*-*.txt")); + assertFalse(Updater.matches("a/b/c.txt", "a-*-*.txt")); + assertTrue( + Updater.matches( + "dir/foo.txt", "**/*.txt")); // ** doesn't cross separators, but matches "dir" + assertFalse(Updater.matches("a/b/c.txt", "**/*.txt")); + assertFalse(Updater.matches("dir/foo.txt", "dir/**/*.txt")); + + // Regex character escaping + assertTrue(Updater.matches("foo[a].txt", "foo[a].txt")); + assertFalse(Updater.matches("fooa.txt", "foo[a].txt")); + assertTrue(Updater.matches("foo+bar$baz.txt", "foo+bar$baz.txt")); + assertFalse(Updater.matches("fooobar.txt", "foo+bar.txt")); + } + + @Test + public void testDelegationResolution() throws Exception { + setupMirror( + "synthetic/delegation-basic", + "timestamp.json", + "1.snapshot.json", + "1.targets.json", + "1.release.json", + "targets/release/42bd420cc2f99e68e60005fa7c28fc2f60e4e04ee160d9dd3b98e72fc2954f98.artifact.txt"); + var updater = createTimeStaticUpdater(localStorePath, DELEGATION_TRUSTED_ROOT); + updater.refresh(); + updater.downloadTarget("release/artifact.txt"); + assertNotNull(updater.getTargetStore().readTarget("release/artifact.txt")); + } + + /** + * Tests that a terminating delegation stops the search. The test data has: + * + *
    + *
  • release: paths=["release/*"], terminating=true + *
  • fallback: paths=["*"], terminating=false + *
+ * + *

Searching for "release/missing.txt" matches the release delegation's path pattern, but since + * the release targets don't contain it and release is terminating, the search should stop without + * checking fallback. + */ + @Test + public void testTerminatingDelegationStopsSearch() throws Exception { + setupMirror( + "synthetic/delegation-terminating", + "timestamp.json", + "1.snapshot.json", + "1.targets.json", + "1.release.json", + "1.fallback.json"); + var updater = createTimeStaticUpdater(localStorePath, DELEGATION_TRUSTED_ROOT); + updater.refresh(); + assertThrows( + TargetMetadataMissingException.class, () -> updater.downloadTarget("release/missing.txt")); + } + + /** + * Tests that a non-terminating delegation allows the search to continue. The test data has: + * + *

    + *
  • staging: paths=["*"], terminating=false + *
  • production: paths=["*"], terminating=false + *
+ * + *

"found.txt" matches staging's "*" but is not found there. Since staging is non-terminating, + * search continues to production where the target is found. + */ + @Test + public void testNonTerminatingDelegationContinuesSearch() throws Exception { + setupMirror( + "synthetic/delegation-non-terminating", + "timestamp.json", + "1.snapshot.json", + "1.targets.json", + "1.staging.json", + "1.production.json", + "targets/ce355849c5722b5468f50a99dab3c6031331140f63e02550ab2381aa0e2ee9e5.found.txt"); + var updater = createTimeStaticUpdater(localStorePath, DELEGATION_TRUSTED_ROOT); + updater.refresh(); + updater.downloadTarget("found.txt"); + assertNotNull(updater.getTargetStore().readTarget("found.txt")); + } + private long countFilesInTargetsDir(Updater updater) throws IOException { try (var filesStream = Files.list(((FileSystemTufStore) updater.getTargetStore()).getTargetsDir())) { @@ -909,6 +1031,78 @@ public void testUpdate_snapshotsAndTimestampHaveNoSizeAndNoHashesInMeta() throws snapshot.getSignedMeta().getMeta().get("targets.json").getLength().isEmpty()); } + @Test + public void testIsTargetInRole_pathHashPrefixes() { + Updater updater = createAlwaysVerifyingUpdater(); + String targetName = "foo.txt"; + // sha256 of "foo.txt" is ddab29ff2c393ee52855d21a240eb05f775df88e3ce347df759f0c4b80356c35 + String hash = + Hashing.sha256().hashString(targetName, java.nio.charset.StandardCharsets.UTF_8).toString(); + String prefix = hash.substring(0, 5); + + DelegationRole roleWithPrefix = + ImmutableDelegationRole.builder() + .name("role1") + .addKeyids("key1") + .threshold(1) + .isTerminating(false) + .addPathHashPrefixes(prefix) + .build(); + assertTrue(updater.isTargetInRole(roleWithPrefix, targetName)); + + DelegationRole roleWithBadPrefix = + ImmutableDelegationRole.builder() + .name("role2") + .addKeyids("key1") + .threshold(1) + .isTerminating(false) + .addPathHashPrefixes("bad") + .build(); + assertFalse(updater.isTargetInRole(roleWithBadPrefix, targetName)); + } + + @Test + public void testIsTargetInRole_paths() { + Updater updater = createAlwaysVerifyingUpdater(); + + DelegationRole roleWithMatchingPath = + ImmutableDelegationRole.builder() + .name("role1") + .addKeyids("key1") + .threshold(1) + .isTerminating(false) + .addPaths("*.txt") + .build(); + assertTrue(updater.isTargetInRole(roleWithMatchingPath, "foo.txt")); + assertFalse(updater.isTargetInRole(roleWithMatchingPath, "dir/foo.txt")); + + DelegationRole roleWithDirPath = + ImmutableDelegationRole.builder() + .name("role2") + .addKeyids("key1") + .threshold(1) + .isTerminating(false) + .addPaths("targets/*.tgz") + .build(); + assertTrue(updater.isTargetInRole(roleWithDirPath, "targets/foo.tgz")); + assertFalse(updater.isTargetInRole(roleWithDirPath, "targets/foo.txt")); + assertFalse(updater.isTargetInRole(roleWithDirPath, "foo.tgz")); + } + + @Test + public void testIsTargetInRole_neitherPathsNorPrefixes() { + Updater updater = createAlwaysVerifyingUpdater(); + + DelegationRole roleWithNothing = + ImmutableDelegationRole.builder() + .name("empty") + .addKeyids("key1") + .threshold(1) + .isTerminating(false) + .build(); + assertFalse(updater.isTargetInRole(roleWithNothing, "anything.txt")); + } + @Test public void canCreateMultipleUpdaters() throws IOException { createTimeStaticUpdater(localStorePath, UPDATER_SYNTHETIC_TRUSTED_ROOT); diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/README.md b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/README.md new file mode 100644 index 000000000..e32b6d848 --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/README.md @@ -0,0 +1,391 @@ +# Synthetic TUF Test Repos + +This directory contains synthetic TUF repositories used by `UpdaterTest` and `DelegationTest`. + +## Generating updater tests + +These were previously generated using a very old version of go-tuf. + +## Generating delegation repos + +The `delegation-basic/`, `delegation-terminating/`, and `delegation-non-terminating/` repos +(plus `delegation-trusted-root.json`) were generated using python-tuf. To regenerate them, +save the script below as `generate_delegation_repos.py` in this directory and run it: + +```bash +~/src/tuf-conformance/env/bin/python3 generate_delegation_repos.py +``` + +This requires `python-tuf` and `securesystemslib[crypto]` to be installed. The +tuf-conformance virtualenv already has these dependencies. + +After regenerating, update the target file hashes in `DelegationTest.java` to match +the new sha256 values (the hashes change because the keys are regenerated). + +### Repos produced + +- **delegation-basic/** - Top-level targets delegates `release/*` to a `release` role which + contains `release/artifact.txt`. Tests basic delegation resolution. +- **delegation-terminating/** - Delegates to `release` (terminating, paths `release/*`) then + `fallback` (paths `*`). The `release` role is empty, so searching for `release/missing.txt` + stops at the terminating role without reaching `fallback`. +- **delegation-non-terminating/** - Delegates to `staging` (non-terminating, paths `*`) then + `production` (paths `*`). `staging` is empty, so search continues past it to `production` + where `found.txt` is found. +- **delegation-trusted-root.json** - Shared bootstrap root used by all three repos. + +### Script + +```python +#!/usr/bin/env python3 +"""Generate synthetic TUF repos with delegations for DelegationTest.java. + +Usage (from tuf-conformance venv): + ~/src/tuf-conformance/env/bin/python3 generate_delegation_repos.py + +Generates 3 repos under the current directory: + delegation-basic/ - simple delegation with target found in child role + delegation-terminating/ - terminating delegation stops search + delegation-non-terminating/ - non-terminating delegation allows search to continue + +Also generates delegation-trusted-root.json (the bootstrap root). +""" + +import hashlib +import json +import os +import shutil +from datetime import datetime, timezone +from pathlib import Path + +from securesystemslib.signer import CryptoSigner +from tuf.api.metadata import ( + DelegatedRole, + Delegations, + Metadata, + MetaFile, + Root, + Snapshot, + TargetFile, + Targets, + Timestamp, +) +from tuf.api.serialization.json import JSONSerializer + +EXPIRY = datetime(2030, 1, 1, 0, 0, 0, tzinfo=timezone.utc) +SPEC_VERSION = "1.0" +SCRIPT_DIR = Path(__file__).parent + + +def generate_keys(): + """Generate signing keys for all top-level roles.""" + return { + "root": CryptoSigner.generate_ecdsa(), + "targets": CryptoSigner.generate_ecdsa(), + "snapshot": CryptoSigner.generate_ecdsa(), + "timestamp": CryptoSigner.generate_ecdsa(), + } + + +def create_root(signers): + """Create and sign root metadata.""" + root = Root(spec_version=SPEC_VERSION, expires=EXPIRY, consistent_snapshot=True) + for role_name, signer in signers.items(): + root.add_key(signer.public_key, role_name) + root_md = Metadata(root) + root_md.sign(signers["root"]) + return root_md + + +def create_targets_with_delegations(signers, delegation_signers, delegation_roles): + """Create top-level targets with delegation configuration.""" + keys = {} + for role_name, signer in delegation_signers.items(): + key = signer.public_key + keys[key.keyid] = key + + delegations = Delegations(keys=keys, roles=delegation_roles) + targets = Targets( + spec_version=SPEC_VERSION, expires=EXPIRY, delegations=delegations + ) + targets_md = Metadata(targets) + targets_md.sign(signers["targets"]) + return targets_md + + +def create_delegated_targets(delegation_signer, target_files=None): + """Create delegated targets metadata, optionally with target files.""" + targets = Targets(spec_version=SPEC_VERSION, expires=EXPIRY) + if target_files: + for name, content in target_files.items(): + targets.targets[name] = TargetFile.from_data(name, content) + targets_md = Metadata(targets) + targets_md.sign(delegation_signer) + return targets_md + + +def create_snapshot(signers, targets_version, delegated_roles_meta=None): + """Create snapshot metadata referencing targets and delegated roles.""" + snapshot = Snapshot(spec_version=SPEC_VERSION, expires=EXPIRY) + snapshot.meta["targets.json"].version = targets_version + + if delegated_roles_meta: + for role_name, (version, md_bytes) in delegated_roles_meta.items(): + snapshot.meta[f"{role_name}.json"] = MetaFile(version=version) + + snapshot_md = Metadata(snapshot) + snapshot_md.sign(signers["snapshot"]) + return snapshot_md + + +def create_timestamp(signers, snapshot_version): + """Create timestamp metadata.""" + timestamp = Timestamp(spec_version=SPEC_VERSION, expires=EXPIRY) + timestamp.snapshot_meta.version = snapshot_version + timestamp_md = Metadata(timestamp) + timestamp_md.sign(signers["timestamp"]) + return timestamp_md + + +def serialize(md): + """Serialize metadata to bytes.""" + serializer = JSONSerializer() + return md.to_bytes(serializer) + + +def write_target_file(repo_dir, target_name, content): + """Write a target file with hash-prefixed filename to targets/ dir. + + Uses sha256 prefix to match TargetFile.from_data() which only generates sha256. + The Updater falls back to sha256 when sha512 is not in the metadata. + """ + sha256 = hashlib.sha256(content).hexdigest() + targets_dir = repo_dir / "targets" + # Handle subdirectories in target names + target_subdir = targets_dir / Path(target_name).parent + target_subdir.mkdir(parents=True, exist_ok=True) + filename = Path(target_name).name + target_path = target_subdir / f"{sha256}.{filename}" + target_path.write_bytes(content) + + +def write_repo(repo_dir, root_md, targets_md, snapshot_md, timestamp_md, + delegated_mds=None, target_contents=None): + """Write all metadata files to the repo directory.""" + if repo_dir.exists(): + shutil.rmtree(repo_dir) + repo_dir.mkdir(parents=True) + + root_bytes = serialize(root_md) + targets_bytes = serialize(targets_md) + snapshot_bytes = serialize(snapshot_md) + timestamp_bytes = serialize(timestamp_md) + + root_version = root_md.signed.version + targets_version = targets_md.signed.version + snapshot_version = snapshot_md.signed.version + + # Root: versioned + unversioned + (repo_dir / f"{root_version}.root.json").write_bytes(root_bytes) + (repo_dir / "root.json").write_bytes(root_bytes) + + # Targets: versioned + (repo_dir / f"{targets_version}.targets.json").write_bytes(targets_bytes) + + # Snapshot: versioned + (repo_dir / f"{snapshot_version}.snapshot.json").write_bytes(snapshot_bytes) + + # Timestamp: unversioned + (repo_dir / "timestamp.json").write_bytes(timestamp_bytes) + + # Delegated targets: versioned + if delegated_mds: + for role_name, md in delegated_mds.items(): + version = md.signed.version + md_bytes = serialize(md) + (repo_dir / f"{version}.{role_name}.json").write_bytes(md_bytes) + + # Target files + if target_contents: + for name, content in target_contents.items(): + write_target_file(repo_dir, name, content) + + return root_bytes + + +def generate_delegation_basic(signers): + """Repo 1: delegation-basic + Top-level targets delegates 'release/*' to 'release' role. + 'release' role contains target 'release/artifact.txt'. + """ + release_signer = CryptoSigner.generate_ecdsa() + + delegation_role = DelegatedRole( + name="release", + keyids=[release_signer.public_key.keyid], + threshold=1, + terminating=False, + paths=["release/*"], + ) + + targets_md = create_targets_with_delegations( + signers, {"release": release_signer}, {delegation_role.name: delegation_role} + ) + + target_content = b"artifact content" + release_md = create_delegated_targets( + release_signer, {"release/artifact.txt": target_content} + ) + + delegated_roles_meta = {"release": (release_md.signed.version, serialize(release_md))} + snapshot_md = create_snapshot(signers, targets_md.signed.version, delegated_roles_meta) + timestamp_md = create_timestamp(signers, snapshot_md.signed.version) + + repo_dir = SCRIPT_DIR / "delegation-basic" + root_bytes = write_repo( + repo_dir, create_root(signers), targets_md, snapshot_md, timestamp_md, + {"release": release_md}, + {"release/artifact.txt": target_content}, + ) + return repo_dir + + +def generate_delegation_terminating(signers): + """Repo 2: delegation-terminating + Top-level targets delegates to 'release' (terminating=true, paths=['release/*']) + then 'fallback' (paths=['*']). + 'release' exists but does NOT contain 'release/missing.txt'. + Because 'release' is terminating and matches the path, search stops + without checking 'fallback'. + """ + release_signer = CryptoSigner.generate_ecdsa() + fallback_signer = CryptoSigner.generate_ecdsa() + + release_role = DelegatedRole( + name="release", + keyids=[release_signer.public_key.keyid], + threshold=1, + terminating=True, + paths=["release/*"], + ) + fallback_role = DelegatedRole( + name="fallback", + keyids=[fallback_signer.public_key.keyid], + threshold=1, + terminating=False, + paths=["*"], + ) + + targets_md = create_targets_with_delegations( + signers, + {"release": release_signer, "fallback": fallback_signer}, + {release_role.name: release_role, fallback_role.name: fallback_role}, + ) + + # release has no targets (empty) + release_md = create_delegated_targets(release_signer) + + # fallback has the target (but should never be reached) + fallback_content = b"fallback content" + fallback_md = create_delegated_targets( + fallback_signer, {"release/missing.txt": fallback_content} + ) + + delegated_roles_meta = { + "release": (release_md.signed.version, serialize(release_md)), + "fallback": (fallback_md.signed.version, serialize(fallback_md)), + } + snapshot_md = create_snapshot(signers, targets_md.signed.version, delegated_roles_meta) + timestamp_md = create_timestamp(signers, snapshot_md.signed.version) + + repo_dir = SCRIPT_DIR / "delegation-terminating" + write_repo( + repo_dir, create_root(signers), targets_md, snapshot_md, timestamp_md, + {"release": release_md, "fallback": fallback_md}, + {"release/missing.txt": fallback_content}, + ) + return repo_dir + + +def generate_delegation_non_terminating(signers): + """Repo 3: delegation-non-terminating + Top-level targets delegates to 'staging' (terminating=false, paths=['*']) + then 'production' (paths=['*']). + 'staging' exists but doesn't have the target 'found.txt'. + 'production' has 'found.txt'. + Search continues past non-terminating 'staging' to 'production'. + """ + staging_signer = CryptoSigner.generate_ecdsa() + production_signer = CryptoSigner.generate_ecdsa() + + staging_role = DelegatedRole( + name="staging", + keyids=[staging_signer.public_key.keyid], + threshold=1, + terminating=False, + paths=["*"], + ) + production_role = DelegatedRole( + name="production", + keyids=[production_signer.public_key.keyid], + threshold=1, + terminating=False, + paths=["*"], + ) + + targets_md = create_targets_with_delegations( + signers, + {"staging": staging_signer, "production": production_signer}, + {staging_role.name: staging_role, production_role.name: production_role}, + ) + + # staging has no targets + staging_md = create_delegated_targets(staging_signer) + + # production has the target + target_content = b"found content" + production_md = create_delegated_targets( + production_signer, {"found.txt": target_content} + ) + + delegated_roles_meta = { + "staging": (staging_md.signed.version, serialize(staging_md)), + "production": (production_md.signed.version, serialize(production_md)), + } + snapshot_md = create_snapshot(signers, targets_md.signed.version, delegated_roles_meta) + timestamp_md = create_timestamp(signers, snapshot_md.signed.version) + + repo_dir = SCRIPT_DIR / "delegation-non-terminating" + write_repo( + repo_dir, create_root(signers), targets_md, snapshot_md, timestamp_md, + {"staging": staging_md, "production": production_md}, + {"found.txt": target_content}, + ) + return repo_dir + + +def main(): + # Use shared keys for all repos so they share one trusted root + signers = generate_keys() + root_md = create_root(signers) + + # Write shared trusted root + trusted_root_path = SCRIPT_DIR / "delegation-trusted-root.json" + trusted_root_path.write_bytes(serialize(root_md)) + print(f"Wrote {trusted_root_path}") + + generate_delegation_basic(signers) + print("Generated delegation-basic/") + + generate_delegation_terminating(signers) + print("Generated delegation-terminating/") + + generate_delegation_non_terminating(signers) + print("Generated delegation-non-terminating/") + + print("Done!") + + +if __name__ == "__main__": + main() +``` diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/1.release.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/1.release.json new file mode 100644 index 000000000..4197c6667 --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/1.release.json @@ -0,0 +1,22 @@ +{ + "signatures": [ + { + "keyid": "f04bb0fd5e31cdfb17897c396ae0e1bf401ede45c40db0ac9fa7f0c92b4ce51b", + "sig": "304402202c7dcaa2e5609de8b3ab2c227eeb321d5fa790677caf67cecbb5ec6f2981c3e4022049c18f1767633bae833a1c67d4ac728f598e8e06d467bbef284cb665850b67d8" + } + ], + "signed": { + "_type": "targets", + "expires": "2030-01-01T00:00:00Z", + "spec_version": "1.0", + "targets": { + "release/artifact.txt": { + "hashes": { + "sha256": "42bd420cc2f99e68e60005fa7c28fc2f60e4e04ee160d9dd3b98e72fc2954f98" + }, + "length": 16 + } + }, + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/1.root.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/1.root.json new file mode 100644 index 000000000..23b0b3a10 --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/1.root.json @@ -0,0 +1,71 @@ +{ + "signatures": [ + { + "keyid": "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25", + "sig": "3046022100864db1f5fca8c68ebcf44e4178fdcf4e03337f702714e43abfdd9ad6e7afa520022100fec37a1e99fdc72e846ed09a665e4152c03617de73a43f4e45c609deff1f2daf" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": true, + "expires": "2030-01-01T00:00:00Z", + "keys": { + "37ba4cc5ea92910093e3c676170b108040abfbc57dcccea05b0699ca68898aff": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgm6jd0hu77Pqe++RRbVIyYQM2l4T\nmAhNE9rDhvq5ybnl3+WMc9fa2jtEqfvPAE5DFPJyg2Lz75dv+gu8DWKZBA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "641dd335e66b48e622030db93a685bef3641c880963241e4d2f8e1c2bdf8a534": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERFePU9B6XZwwnLUuFOpCSBVKD6f/\nBPh25tHw+SYBkEScquR1fnXDAfuYQmno8Cli6CKirkLQ+iNv8LKEA8DbdA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5PCsF9N/NctreorljL8eRPxxCMnJ\nIQSEVxnyUygaMlV32aUqRHbqu/+KSOkenY/hRH+1ZLJl/Prnl1KVPSD37Q==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "fe1f0eb51412ccc170809b699ad3be8aae5dd43197f4f137de0faa9719d64fe0": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnaP6BP0F1VxhNg+bnOjXXEtHiwJ4\nWjBxYtqg6SyIxGB9jDsHcphJbLQUzEfWPTFsla6BHURNY4mcB4TdjRhWQQ==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + } + }, + "roles": { + "root": { + "keyids": [ + "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "641dd335e66b48e622030db93a685bef3641c880963241e4d2f8e1c2bdf8a534" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "fe1f0eb51412ccc170809b699ad3be8aae5dd43197f4f137de0faa9719d64fe0" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "37ba4cc5ea92910093e3c676170b108040abfbc57dcccea05b0699ca68898aff" + ], + "threshold": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/1.snapshot.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/1.snapshot.json new file mode 100644 index 000000000..82040195a --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/1.snapshot.json @@ -0,0 +1,22 @@ +{ + "signatures": [ + { + "keyid": "641dd335e66b48e622030db93a685bef3641c880963241e4d2f8e1c2bdf8a534", + "sig": "3045022100eef7ad3def7d959a58dd105f4902b6fcbf8633b3930115d603e2d30cebfbef2102204a13847643b690f98a305de1c599c84e9ecd894c9c26284ed7b8d06e1f2b439b" + } + ], + "signed": { + "_type": "snapshot", + "expires": "2030-01-01T00:00:00Z", + "meta": { + "release.json": { + "version": 1 + }, + "targets.json": { + "version": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/1.targets.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/1.targets.json new file mode 100644 index 000000000..236372a5c --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/1.targets.json @@ -0,0 +1,39 @@ +{ + "signatures": [ + { + "keyid": "fe1f0eb51412ccc170809b699ad3be8aae5dd43197f4f137de0faa9719d64fe0", + "sig": "30450220652c8d595de0212f12a384602a6c52e19abc427b01e171b60e3526999acbd158022100ee65b88ca9f792814d8c2bdc344a07b94bcc0270c3dfee1ecbb7ac77fb1c67cc" + } + ], + "signed": { + "_type": "targets", + "delegations": { + "keys": { + "f04bb0fd5e31cdfb17897c396ae0e1bf401ede45c40db0ac9fa7f0c92b4ce51b": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1SXRpsjv6j7f00wCHICfD0g4GJGr\nHaZtdQkAthvCqaey56+jJnDF/4dHtuLjRHf6h3tBWNLkqNan7hMj1h7V0Q==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + } + }, + "roles": [ + { + "keyids": [ + "f04bb0fd5e31cdfb17897c396ae0e1bf401ede45c40db0ac9fa7f0c92b4ce51b" + ], + "name": "release", + "paths": [ + "release/*" + ], + "terminating": false, + "threshold": 1 + } + ] + }, + "expires": "2030-01-01T00:00:00Z", + "spec_version": "1.0", + "targets": {}, + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/root.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/root.json new file mode 100644 index 000000000..23b0b3a10 --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/root.json @@ -0,0 +1,71 @@ +{ + "signatures": [ + { + "keyid": "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25", + "sig": "3046022100864db1f5fca8c68ebcf44e4178fdcf4e03337f702714e43abfdd9ad6e7afa520022100fec37a1e99fdc72e846ed09a665e4152c03617de73a43f4e45c609deff1f2daf" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": true, + "expires": "2030-01-01T00:00:00Z", + "keys": { + "37ba4cc5ea92910093e3c676170b108040abfbc57dcccea05b0699ca68898aff": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgm6jd0hu77Pqe++RRbVIyYQM2l4T\nmAhNE9rDhvq5ybnl3+WMc9fa2jtEqfvPAE5DFPJyg2Lz75dv+gu8DWKZBA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "641dd335e66b48e622030db93a685bef3641c880963241e4d2f8e1c2bdf8a534": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERFePU9B6XZwwnLUuFOpCSBVKD6f/\nBPh25tHw+SYBkEScquR1fnXDAfuYQmno8Cli6CKirkLQ+iNv8LKEA8DbdA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5PCsF9N/NctreorljL8eRPxxCMnJ\nIQSEVxnyUygaMlV32aUqRHbqu/+KSOkenY/hRH+1ZLJl/Prnl1KVPSD37Q==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "fe1f0eb51412ccc170809b699ad3be8aae5dd43197f4f137de0faa9719d64fe0": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnaP6BP0F1VxhNg+bnOjXXEtHiwJ4\nWjBxYtqg6SyIxGB9jDsHcphJbLQUzEfWPTFsla6BHURNY4mcB4TdjRhWQQ==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + } + }, + "roles": { + "root": { + "keyids": [ + "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "641dd335e66b48e622030db93a685bef3641c880963241e4d2f8e1c2bdf8a534" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "fe1f0eb51412ccc170809b699ad3be8aae5dd43197f4f137de0faa9719d64fe0" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "37ba4cc5ea92910093e3c676170b108040abfbc57dcccea05b0699ca68898aff" + ], + "threshold": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/targets/release/42bd420cc2f99e68e60005fa7c28fc2f60e4e04ee160d9dd3b98e72fc2954f98.artifact.txt b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/targets/release/42bd420cc2f99e68e60005fa7c28fc2f60e4e04ee160d9dd3b98e72fc2954f98.artifact.txt new file mode 100644 index 000000000..a47c55114 --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/targets/release/42bd420cc2f99e68e60005fa7c28fc2f60e4e04ee160d9dd3b98e72fc2954f98.artifact.txt @@ -0,0 +1 @@ +artifact content \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/timestamp.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/timestamp.json new file mode 100644 index 000000000..5f6816525 --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-basic/timestamp.json @@ -0,0 +1,19 @@ +{ + "signatures": [ + { + "keyid": "37ba4cc5ea92910093e3c676170b108040abfbc57dcccea05b0699ca68898aff", + "sig": "30440220640dec4f361d9a460fb93f2a3e08280c85ad33e3d2cc0db1af91139a17cfede4022043af9dd2d6e734f88ff2481778ec4b3058f2ade11b45d1bbf392c54e8f8dfac1" + } + ], + "signed": { + "_type": "timestamp", + "expires": "2030-01-01T00:00:00Z", + "meta": { + "snapshot.json": { + "version": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.production.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.production.json new file mode 100644 index 000000000..2a8c6c76e --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.production.json @@ -0,0 +1,22 @@ +{ + "signatures": [ + { + "keyid": "90ea2df54a9135dd5545a58a9e21b870b30e09bc8acec974698ee4bc02bfbdae", + "sig": "3046022100c6011212aba4ad219bbfbb643ea9470f5154f902b024e022428cf30a86edb340022100fc6030bf845a811e5837ef9b76e90be916eeec14444a842d588e2636eaee5cfe" + } + ], + "signed": { + "_type": "targets", + "expires": "2030-01-01T00:00:00Z", + "spec_version": "1.0", + "targets": { + "found.txt": { + "hashes": { + "sha256": "ce355849c5722b5468f50a99dab3c6031331140f63e02550ab2381aa0e2ee9e5" + }, + "length": 13 + } + }, + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.root.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.root.json new file mode 100644 index 000000000..e975df80f --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.root.json @@ -0,0 +1,71 @@ +{ + "signatures": [ + { + "keyid": "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25", + "sig": "3045022100ebd7051992d6b53c809948d432921528f177ac97139788ef91a5235869855a9f02204c95204d52d7fc8a0fd67efa47810729be8731a871067cce70ef2e9e82c48930" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": true, + "expires": "2030-01-01T00:00:00Z", + "keys": { + "37ba4cc5ea92910093e3c676170b108040abfbc57dcccea05b0699ca68898aff": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgm6jd0hu77Pqe++RRbVIyYQM2l4T\nmAhNE9rDhvq5ybnl3+WMc9fa2jtEqfvPAE5DFPJyg2Lz75dv+gu8DWKZBA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "641dd335e66b48e622030db93a685bef3641c880963241e4d2f8e1c2bdf8a534": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERFePU9B6XZwwnLUuFOpCSBVKD6f/\nBPh25tHw+SYBkEScquR1fnXDAfuYQmno8Cli6CKirkLQ+iNv8LKEA8DbdA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5PCsF9N/NctreorljL8eRPxxCMnJ\nIQSEVxnyUygaMlV32aUqRHbqu/+KSOkenY/hRH+1ZLJl/Prnl1KVPSD37Q==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "fe1f0eb51412ccc170809b699ad3be8aae5dd43197f4f137de0faa9719d64fe0": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnaP6BP0F1VxhNg+bnOjXXEtHiwJ4\nWjBxYtqg6SyIxGB9jDsHcphJbLQUzEfWPTFsla6BHURNY4mcB4TdjRhWQQ==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + } + }, + "roles": { + "root": { + "keyids": [ + "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "641dd335e66b48e622030db93a685bef3641c880963241e4d2f8e1c2bdf8a534" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "fe1f0eb51412ccc170809b699ad3be8aae5dd43197f4f137de0faa9719d64fe0" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "37ba4cc5ea92910093e3c676170b108040abfbc57dcccea05b0699ca68898aff" + ], + "threshold": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.snapshot.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.snapshot.json new file mode 100644 index 000000000..e2d65e55f --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.snapshot.json @@ -0,0 +1,25 @@ +{ + "signatures": [ + { + "keyid": "641dd335e66b48e622030db93a685bef3641c880963241e4d2f8e1c2bdf8a534", + "sig": "3045022061b3342c91143ad928429486be03d7b2802136d903d479c23570afa7fbf4c9000221009389b3b4c87781266d4aaf1cdb92347075b7c8a00ebf8307be31fe53a9b629ed" + } + ], + "signed": { + "_type": "snapshot", + "expires": "2030-01-01T00:00:00Z", + "meta": { + "production.json": { + "version": 1 + }, + "staging.json": { + "version": 1 + }, + "targets.json": { + "version": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.staging.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.staging.json new file mode 100644 index 000000000..54e371e38 --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.staging.json @@ -0,0 +1,15 @@ +{ + "signatures": [ + { + "keyid": "8509519c38cd6ffe42a68629f8f57659e504ba0914c2b777e932736e4137c0fa", + "sig": "3046022100d649e6011f17f60ec9f7f67df36d2bb3702e44abb157fdb3bda529da3ca368f3022100f459a70210d2f7440d5473d8afb7b81842bc1d80470cb977dde443cdab28683a" + } + ], + "signed": { + "_type": "targets", + "expires": "2030-01-01T00:00:00Z", + "spec_version": "1.0", + "targets": {}, + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.targets.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.targets.json new file mode 100644 index 000000000..096e56668 --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/1.targets.json @@ -0,0 +1,57 @@ +{ + "signatures": [ + { + "keyid": "fe1f0eb51412ccc170809b699ad3be8aae5dd43197f4f137de0faa9719d64fe0", + "sig": "3045022100b6ee8dd2ca48ed3d9566bb5cc0a4fcae18d39464af0e316a6eba0197190822160220439141227defe6dec0981faa8e2886ebe4d9afc671a7a80acb0f2287a58800b6" + } + ], + "signed": { + "_type": "targets", + "delegations": { + "keys": { + "8509519c38cd6ffe42a68629f8f57659e504ba0914c2b777e932736e4137c0fa": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2XX0VeWv3ROuMwMzTndUqg+FBn7m\nElMMr31IPFyutAbCQmXhtdM04PrEAOXcedZcC3ZAKr6NSiPMIDIHyqXUiA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "90ea2df54a9135dd5545a58a9e21b870b30e09bc8acec974698ee4bc02bfbdae": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE93EiJ6AYFfydaHoanlFdnFMj6pJ4\nOyu6xGJXoJseswLwcsSfr72Txqh0EFUlS+MHSlPQtks+eoIR2pk7R2NdMg==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + } + }, + "roles": [ + { + "keyids": [ + "8509519c38cd6ffe42a68629f8f57659e504ba0914c2b777e932736e4137c0fa" + ], + "name": "staging", + "paths": [ + "*" + ], + "terminating": false, + "threshold": 1 + }, + { + "keyids": [ + "90ea2df54a9135dd5545a58a9e21b870b30e09bc8acec974698ee4bc02bfbdae" + ], + "name": "production", + "paths": [ + "*" + ], + "terminating": false, + "threshold": 1 + } + ] + }, + "expires": "2030-01-01T00:00:00Z", + "spec_version": "1.0", + "targets": {}, + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/root.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/root.json new file mode 100644 index 000000000..e975df80f --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/root.json @@ -0,0 +1,71 @@ +{ + "signatures": [ + { + "keyid": "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25", + "sig": "3045022100ebd7051992d6b53c809948d432921528f177ac97139788ef91a5235869855a9f02204c95204d52d7fc8a0fd67efa47810729be8731a871067cce70ef2e9e82c48930" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": true, + "expires": "2030-01-01T00:00:00Z", + "keys": { + "37ba4cc5ea92910093e3c676170b108040abfbc57dcccea05b0699ca68898aff": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgm6jd0hu77Pqe++RRbVIyYQM2l4T\nmAhNE9rDhvq5ybnl3+WMc9fa2jtEqfvPAE5DFPJyg2Lz75dv+gu8DWKZBA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "641dd335e66b48e622030db93a685bef3641c880963241e4d2f8e1c2bdf8a534": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERFePU9B6XZwwnLUuFOpCSBVKD6f/\nBPh25tHw+SYBkEScquR1fnXDAfuYQmno8Cli6CKirkLQ+iNv8LKEA8DbdA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5PCsF9N/NctreorljL8eRPxxCMnJ\nIQSEVxnyUygaMlV32aUqRHbqu/+KSOkenY/hRH+1ZLJl/Prnl1KVPSD37Q==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "fe1f0eb51412ccc170809b699ad3be8aae5dd43197f4f137de0faa9719d64fe0": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnaP6BP0F1VxhNg+bnOjXXEtHiwJ4\nWjBxYtqg6SyIxGB9jDsHcphJbLQUzEfWPTFsla6BHURNY4mcB4TdjRhWQQ==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + } + }, + "roles": { + "root": { + "keyids": [ + "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "641dd335e66b48e622030db93a685bef3641c880963241e4d2f8e1c2bdf8a534" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "fe1f0eb51412ccc170809b699ad3be8aae5dd43197f4f137de0faa9719d64fe0" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "37ba4cc5ea92910093e3c676170b108040abfbc57dcccea05b0699ca68898aff" + ], + "threshold": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/targets/ce355849c5722b5468f50a99dab3c6031331140f63e02550ab2381aa0e2ee9e5.found.txt b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/targets/ce355849c5722b5468f50a99dab3c6031331140f63e02550ab2381aa0e2ee9e5.found.txt new file mode 100644 index 000000000..0e66e9a21 --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/targets/ce355849c5722b5468f50a99dab3c6031331140f63e02550ab2381aa0e2ee9e5.found.txt @@ -0,0 +1 @@ +found content \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/timestamp.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/timestamp.json new file mode 100644 index 000000000..af759ea46 --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-non-terminating/timestamp.json @@ -0,0 +1,19 @@ +{ + "signatures": [ + { + "keyid": "37ba4cc5ea92910093e3c676170b108040abfbc57dcccea05b0699ca68898aff", + "sig": "3045022028bbe935f3220e1482541e105153ed6a043b82f810c667393ca9c85f225b9e990221008ed202ca5aca3a7dc6e4966b8fbf7b481a5169a2bdfe67440226a0cff6546930" + } + ], + "signed": { + "_type": "timestamp", + "expires": "2030-01-01T00:00:00Z", + "meta": { + "snapshot.json": { + "version": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.fallback.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.fallback.json new file mode 100644 index 000000000..6ef7ade44 --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.fallback.json @@ -0,0 +1,22 @@ +{ + "signatures": [ + { + "keyid": "7418e18fbce9d1cf585c05cc13d9478d9d129a10c9b99a21416f4a4497abdb56", + "sig": "3046022100dfd064fa0b4c635af09f576f6c84bb9f71e074a77c6de865e7737f6a2edf3870022100907662ada1d1c5b7910892f768969c93f99b8ad408694562de915ab10c624189" + } + ], + "signed": { + "_type": "targets", + "expires": "2030-01-01T00:00:00Z", + "spec_version": "1.0", + "targets": { + "release/missing.txt": { + "hashes": { + "sha256": "6c2814d5403ee7ccbd04f096f62cd1da362a03e7cbd6ff07b04b2b64855e1ce2" + }, + "length": 16 + } + }, + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.release.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.release.json new file mode 100644 index 000000000..a257d4078 --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.release.json @@ -0,0 +1,15 @@ +{ + "signatures": [ + { + "keyid": "056bc19ef355df5a8cc6f8bddba32e6faa9f1a48ad0a78a7a19e1c69a2f406b3", + "sig": "3044022052445c160097807395d30dab7288ff0a02608347b5ba1dac041282291497e6fd0220289e74cc977138cb64c956a33674a207d7ca3c8e88d632c64ea19042d7babe3a" + } + ], + "signed": { + "_type": "targets", + "expires": "2030-01-01T00:00:00Z", + "spec_version": "1.0", + "targets": {}, + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.root.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.root.json new file mode 100644 index 000000000..e6c04a3fb --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.root.json @@ -0,0 +1,71 @@ +{ + "signatures": [ + { + "keyid": "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25", + "sig": "3046022100fd7ddef3e65c49ed53e89f8deedc95e3c88b5e44a20373b1dcbce60ad70f77b6022100f57b9b1a0241ad39fd5ade7af111d96107a2ce7c4736af05e579dc157c266138" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": true, + "expires": "2030-01-01T00:00:00Z", + "keys": { + "37ba4cc5ea92910093e3c676170b108040abfbc57dcccea05b0699ca68898aff": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgm6jd0hu77Pqe++RRbVIyYQM2l4T\nmAhNE9rDhvq5ybnl3+WMc9fa2jtEqfvPAE5DFPJyg2Lz75dv+gu8DWKZBA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "641dd335e66b48e622030db93a685bef3641c880963241e4d2f8e1c2bdf8a534": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERFePU9B6XZwwnLUuFOpCSBVKD6f/\nBPh25tHw+SYBkEScquR1fnXDAfuYQmno8Cli6CKirkLQ+iNv8LKEA8DbdA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5PCsF9N/NctreorljL8eRPxxCMnJ\nIQSEVxnyUygaMlV32aUqRHbqu/+KSOkenY/hRH+1ZLJl/Prnl1KVPSD37Q==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "fe1f0eb51412ccc170809b699ad3be8aae5dd43197f4f137de0faa9719d64fe0": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnaP6BP0F1VxhNg+bnOjXXEtHiwJ4\nWjBxYtqg6SyIxGB9jDsHcphJbLQUzEfWPTFsla6BHURNY4mcB4TdjRhWQQ==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + } + }, + "roles": { + "root": { + "keyids": [ + "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "641dd335e66b48e622030db93a685bef3641c880963241e4d2f8e1c2bdf8a534" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "fe1f0eb51412ccc170809b699ad3be8aae5dd43197f4f137de0faa9719d64fe0" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "37ba4cc5ea92910093e3c676170b108040abfbc57dcccea05b0699ca68898aff" + ], + "threshold": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.snapshot.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.snapshot.json new file mode 100644 index 000000000..3979cc1ad --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.snapshot.json @@ -0,0 +1,25 @@ +{ + "signatures": [ + { + "keyid": "641dd335e66b48e622030db93a685bef3641c880963241e4d2f8e1c2bdf8a534", + "sig": "3045022013c60649fc57a0776ba11f8d45af05e40481bb3bc8116eda825de836853e8a4d022100947b97301928e1028b451fd7686015c959e7c6d697d87d028a1d912ba8743b03" + } + ], + "signed": { + "_type": "snapshot", + "expires": "2030-01-01T00:00:00Z", + "meta": { + "fallback.json": { + "version": 1 + }, + "release.json": { + "version": 1 + }, + "targets.json": { + "version": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.targets.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.targets.json new file mode 100644 index 000000000..7d226892d --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/1.targets.json @@ -0,0 +1,57 @@ +{ + "signatures": [ + { + "keyid": "fe1f0eb51412ccc170809b699ad3be8aae5dd43197f4f137de0faa9719d64fe0", + "sig": "304402205c1752361690d3ce275d64500ab2bc84dd766a9bd6c940bcf09139f3de3e765302201548854bb2b9a9939480cd19a6298d2d698172692f7c6b378c3360918e6d3876" + } + ], + "signed": { + "_type": "targets", + "delegations": { + "keys": { + "056bc19ef355df5a8cc6f8bddba32e6faa9f1a48ad0a78a7a19e1c69a2f406b3": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJjt87QffU0HjY/n5M0inE/cCFsOI\nwZNXgWXVIY/ei+7jrOSzIbke06vjrOLw0iLau86Y2Gx/xF0Z/UPdVBiWQg==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "7418e18fbce9d1cf585c05cc13d9478d9d129a10c9b99a21416f4a4497abdb56": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiT66qoGgAH+tvWzKCZ0hk7QylmG4\n2M+ma8+gIwC9JzxD/xhFSDcgsFJDYaDjL6wUYs2eUTTMXD1OKRQzTLZjQw==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + } + }, + "roles": [ + { + "keyids": [ + "056bc19ef355df5a8cc6f8bddba32e6faa9f1a48ad0a78a7a19e1c69a2f406b3" + ], + "name": "release", + "paths": [ + "release/*" + ], + "terminating": true, + "threshold": 1 + }, + { + "keyids": [ + "7418e18fbce9d1cf585c05cc13d9478d9d129a10c9b99a21416f4a4497abdb56" + ], + "name": "fallback", + "paths": [ + "*" + ], + "terminating": false, + "threshold": 1 + } + ] + }, + "expires": "2030-01-01T00:00:00Z", + "spec_version": "1.0", + "targets": {}, + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/root.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/root.json new file mode 100644 index 000000000..e6c04a3fb --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/root.json @@ -0,0 +1,71 @@ +{ + "signatures": [ + { + "keyid": "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25", + "sig": "3046022100fd7ddef3e65c49ed53e89f8deedc95e3c88b5e44a20373b1dcbce60ad70f77b6022100f57b9b1a0241ad39fd5ade7af111d96107a2ce7c4736af05e579dc157c266138" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": true, + "expires": "2030-01-01T00:00:00Z", + "keys": { + "37ba4cc5ea92910093e3c676170b108040abfbc57dcccea05b0699ca68898aff": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgm6jd0hu77Pqe++RRbVIyYQM2l4T\nmAhNE9rDhvq5ybnl3+WMc9fa2jtEqfvPAE5DFPJyg2Lz75dv+gu8DWKZBA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "641dd335e66b48e622030db93a685bef3641c880963241e4d2f8e1c2bdf8a534": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERFePU9B6XZwwnLUuFOpCSBVKD6f/\nBPh25tHw+SYBkEScquR1fnXDAfuYQmno8Cli6CKirkLQ+iNv8LKEA8DbdA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5PCsF9N/NctreorljL8eRPxxCMnJ\nIQSEVxnyUygaMlV32aUqRHbqu/+KSOkenY/hRH+1ZLJl/Prnl1KVPSD37Q==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "fe1f0eb51412ccc170809b699ad3be8aae5dd43197f4f137de0faa9719d64fe0": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnaP6BP0F1VxhNg+bnOjXXEtHiwJ4\nWjBxYtqg6SyIxGB9jDsHcphJbLQUzEfWPTFsla6BHURNY4mcB4TdjRhWQQ==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + } + }, + "roles": { + "root": { + "keyids": [ + "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "641dd335e66b48e622030db93a685bef3641c880963241e4d2f8e1c2bdf8a534" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "fe1f0eb51412ccc170809b699ad3be8aae5dd43197f4f137de0faa9719d64fe0" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "37ba4cc5ea92910093e3c676170b108040abfbc57dcccea05b0699ca68898aff" + ], + "threshold": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/targets/release/6c2814d5403ee7ccbd04f096f62cd1da362a03e7cbd6ff07b04b2b64855e1ce2.missing.txt b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/targets/release/6c2814d5403ee7ccbd04f096f62cd1da362a03e7cbd6ff07b04b2b64855e1ce2.missing.txt new file mode 100644 index 000000000..a62db6b5b --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/targets/release/6c2814d5403ee7ccbd04f096f62cd1da362a03e7cbd6ff07b04b2b64855e1ce2.missing.txt @@ -0,0 +1 @@ +fallback content \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/timestamp.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/timestamp.json new file mode 100644 index 000000000..4290246d4 --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-terminating/timestamp.json @@ -0,0 +1,19 @@ +{ + "signatures": [ + { + "keyid": "37ba4cc5ea92910093e3c676170b108040abfbc57dcccea05b0699ca68898aff", + "sig": "3045022048c4dd2d8950af1fa61e94ab2ebb053ef161720b54611e606f9514b093cf2c73022100d6dc768bae91c650eeb4b1735463face129a23f4db8d9a09ad6b25e8eb6c23fb" + } + ], + "signed": { + "_type": "timestamp", + "expires": "2030-01-01T00:00:00Z", + "meta": { + "snapshot.json": { + "version": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} \ No newline at end of file diff --git a/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-trusted-root.json b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-trusted-root.json new file mode 100644 index 000000000..c7b14ee6f --- /dev/null +++ b/sigstore-java/src/test/resources/dev/sigstore/tuf/synthetic/delegation-trusted-root.json @@ -0,0 +1,71 @@ +{ + "signatures": [ + { + "keyid": "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25", + "sig": "3045022100be7d03a00430dd0fb0deafdbc253bbeb4f04a54f1258c231048084b1583c056802201f84de162051bd905973b558b868068896ea4f9d5a373a58027f884b2dddb54f" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": true, + "expires": "2030-01-01T00:00:00Z", + "keys": { + "37ba4cc5ea92910093e3c676170b108040abfbc57dcccea05b0699ca68898aff": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgm6jd0hu77Pqe++RRbVIyYQM2l4T\nmAhNE9rDhvq5ybnl3+WMc9fa2jtEqfvPAE5DFPJyg2Lz75dv+gu8DWKZBA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "641dd335e66b48e622030db93a685bef3641c880963241e4d2f8e1c2bdf8a534": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERFePU9B6XZwwnLUuFOpCSBVKD6f/\nBPh25tHw+SYBkEScquR1fnXDAfuYQmno8Cli6CKirkLQ+iNv8LKEA8DbdA==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5PCsF9N/NctreorljL8eRPxxCMnJ\nIQSEVxnyUygaMlV32aUqRHbqu/+KSOkenY/hRH+1ZLJl/Prnl1KVPSD37Q==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + }, + "fe1f0eb51412ccc170809b699ad3be8aae5dd43197f4f137de0faa9719d64fe0": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnaP6BP0F1VxhNg+bnOjXXEtHiwJ4\nWjBxYtqg6SyIxGB9jDsHcphJbLQUzEfWPTFsla6BHURNY4mcB4TdjRhWQQ==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256" + } + }, + "roles": { + "root": { + "keyids": [ + "e0bf0d617f563ecc95bb3e0b8a9fbae5ead4320259915da7a5f31066ffb2ec25" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "641dd335e66b48e622030db93a685bef3641c880963241e4d2f8e1c2bdf8a534" + ], + "threshold": 1 + }, + "targets": { + "keyids": [ + "fe1f0eb51412ccc170809b699ad3be8aae5dd43197f4f137de0faa9719d64fe0" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "37ba4cc5ea92910093e3c676170b108040abfbc57dcccea05b0699ca68898aff" + ], + "threshold": 1 + } + }, + "spec_version": "1.0", + "version": 1 + } +} \ No newline at end of file diff --git a/tuf-cli/build.gradle.kts b/tuf-cli/build.gradle.kts index 5b89d49a3..2b31ff307 100644 --- a/tuf-cli/build.gradle.kts +++ b/tuf-cli/build.gradle.kts @@ -35,8 +35,9 @@ application { distributions.main { contents { - from("tuf-cli.xfails") { + from("tuf-cli-server.xfails") { into("bin") + rename { "tuf-cli.xfails" } } } } diff --git a/tuf-cli/src/main/java/dev/sigstore/tuf/cli/Download.java b/tuf-cli/src/main/java/dev/sigstore/tuf/cli/Download.java index c097899b7..ed9a17f93 100644 --- a/tuf-cli/src/main/java/dev/sigstore/tuf/cli/Download.java +++ b/tuf-cli/src/main/java/dev/sigstore/tuf/cli/Download.java @@ -52,9 +52,9 @@ public Integer call() throws Exception { .setTargetStore(fsStore) .setClock(clock) .build(); - // the java client isn't one shot like other clients, so downloadTarget doesn't call update - // for the sake of conformance updateMeta here - tuf.updateMeta(); + // the java client isn't one shot like other clients, so downloadTarget doesn't call refresh + // for the sake of conformance, refresh here + tuf.refresh(); tuf.downloadTarget(targetName); return 0; } diff --git a/tuf-cli/src/main/java/dev/sigstore/tuf/cli/Refresh.java b/tuf-cli/src/main/java/dev/sigstore/tuf/cli/Refresh.java index 66e7535c4..c2d92c502 100644 --- a/tuf-cli/src/main/java/dev/sigstore/tuf/cli/Refresh.java +++ b/tuf-cli/src/main/java/dev/sigstore/tuf/cli/Refresh.java @@ -47,7 +47,7 @@ public Integer call() throws Exception { .setMetaFetcher(MetaFetcher.newFetcher(HttpFetcher.newFetcher(metadataUrl))) .setClock(clock) .build(); - tuf.updateMeta(); + tuf.refresh(); return 0; } } diff --git a/tuf-cli/tuf-cli-server.xfails b/tuf-cli/tuf-cli-server.xfails index ff2a70ebd..862a64568 100644 --- a/tuf-cli/tuf-cli-server.xfails +++ b/tuf-cli/tuf-cli-server.xfails @@ -1,22 +1 @@ test_metadata_bytes_match -test_unusual_role_name[?] -test_unusual_role_name[#] -test_unusual_role_name[/delegatedrole] -test_unusual_role_name[../delegatedrole] -test_static_repository[tuf-on-ci-0.11] -test_graph_traversal[basic-delegation] -test_graph_traversal[single-level-delegations] -test_graph_traversal[two-level-delegations] -test_graph_traversal[two-level-test-DFS-order-of-traversal] -test_graph_traversal[three-level-delegation-test-DFS-order-of-traversal] -test_graph_traversal[two-level-terminating-ignores-all-but-roles-descendants] -test_graph_traversal[three-level-terminating-ignores-all-but-roles-descendants] -test_graph_traversal[two-level-ignores-all-branches-not-matching-paths] -test_graph_traversal[three-level-ignores-all-branches-not-matching-paths] -test_graph_traversal[cyclic-graph] -test_graph_traversal[two-roles-delegating-to-a-third] -test_graph_traversal[two-roles-delegating-to-a-third-different-paths] -test_targetfile_search[targetpath matches wildcard] -test_targetfile_search[targetpath with separators x] -test_targetfile_search[targetpath with separators y] -test_targetfile_search[targetpath is not delegated by all roles in the chain] diff --git a/tuf-cli/tuf-cli.xfails b/tuf-cli/tuf-cli.xfails deleted file mode 100644 index ff2a70ebd..000000000 --- a/tuf-cli/tuf-cli.xfails +++ /dev/null @@ -1,22 +0,0 @@ -test_metadata_bytes_match -test_unusual_role_name[?] -test_unusual_role_name[#] -test_unusual_role_name[/delegatedrole] -test_unusual_role_name[../delegatedrole] -test_static_repository[tuf-on-ci-0.11] -test_graph_traversal[basic-delegation] -test_graph_traversal[single-level-delegations] -test_graph_traversal[two-level-delegations] -test_graph_traversal[two-level-test-DFS-order-of-traversal] -test_graph_traversal[three-level-delegation-test-DFS-order-of-traversal] -test_graph_traversal[two-level-terminating-ignores-all-but-roles-descendants] -test_graph_traversal[three-level-terminating-ignores-all-but-roles-descendants] -test_graph_traversal[two-level-ignores-all-branches-not-matching-paths] -test_graph_traversal[three-level-ignores-all-branches-not-matching-paths] -test_graph_traversal[cyclic-graph] -test_graph_traversal[two-roles-delegating-to-a-third] -test_graph_traversal[two-roles-delegating-to-a-third-different-paths] -test_targetfile_search[targetpath matches wildcard] -test_targetfile_search[targetpath with separators x] -test_targetfile_search[targetpath with separators y] -test_targetfile_search[targetpath is not delegated by all roles in the chain]