diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/DuplicateKeyIdsException.java b/sigstore-java/src/main/java/dev/sigstore/tuf/DuplicateKeyIdsException.java
new file mode 100644
index 00000000..0a227063
--- /dev/null
+++ b/sigstore-java/src/main/java/dev/sigstore/tuf/DuplicateKeyIdsException.java
@@ -0,0 +1,52 @@
+/*
+ * 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 dev.sigstore.tuf.model.Signature;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+/**
+ * Thrown when the metadata has signatures from the same key even if the threshold is met. 4.2.1
+ */
+public class DuplicateKeyIdsException extends TufException {
+
+ private final List signatures;
+ private final String keyId;
+
+ public DuplicateKeyIdsException(List signatures, String keyId) {
+ super(
+ String.format(
+ Locale.ROOT,
+ "The role has multiple signatures with the same key_id. [Signatures: %s, KeyId: %s]",
+ signatures.stream()
+ .map(Signature::getSignature)
+ .collect(Collectors.joining(",", "(", ")")),
+ keyId));
+ this.signatures = signatures;
+ this.keyId = keyId;
+ }
+
+ public List getSignatures() {
+ return signatures;
+ }
+
+ public String getKeyId() {
+ return keyId;
+ }
+}
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 d71919ff..9bde6c9c 100644
--- a/sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java
+++ b/sigstore-java/src/main/java/dev/sigstore/tuf/Updater.java
@@ -42,6 +42,7 @@
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
+import java.util.stream.Collectors;
import org.bouncycastle.util.encoders.DecoderException;
import org.bouncycastle.util.encoders.Hex;
@@ -211,8 +212,7 @@ private boolean hasNewKeys(RootRole oldRole, RootRole newRole) {
}
void verifyDelegate(Root trustedRoot, SignedTufMeta extends TufMeta> delegate)
- throws SignatureVerificationException, IOException, NoSuchAlgorithmException,
- InvalidKeySpecException {
+ throws SignatureVerificationException, IOException {
verifyDelegate(
delegate.getSignatures(),
trustedRoot.getSignedMeta().getKeys(),
@@ -228,6 +228,7 @@ void verifyDelegate(Root trustedRoot, SignedTufMeta extends TufMeta> delegate)
* @param role the key ids and threshold values for role signing
* @param verificationMaterial the contents to be verified for authenticity
* @throws SignatureVerificationException if there are not enough verified signatures
+ * @throws IOException if an error occurred parsing a key
*/
@VisibleForTesting
void verifyDelegate(
@@ -235,16 +236,23 @@ void verifyDelegate(
Map publicKeys,
Role role,
byte[] verificationMaterial)
- throws InvalidKeySpecException, IOException, NoSuchAlgorithmException {
+ throws IOException {
// use set to not count the same key multiple times towards the threshold.
var goodSigs = new HashSet<>(role.getKeyids().size() * 4 / 3);
// role.getKeyIds() defines the keys allowed to sign for this role.
for (String keyid : role.getKeyids()) {
- Optional signatureMaybe =
- signatures.stream().filter(sig -> sig.getKeyId().equals(keyid)).findFirst();
+ List matchingSignatures =
+ signatures.stream()
+ .filter(sig -> sig.getKeyId().equals(keyid))
+ .collect(Collectors.toList());
+ // check for any duplicate key_ids:
+ // https://theupdateframework.github.io/specification/latest/#file-formats-object-format
+ if (matchingSignatures.size() > 1) {
+ throw new DuplicateKeyIdsException(matchingSignatures, keyid);
+ }
// only verify if we find a signature that matches an allowed key id.
- if (signatureMaybe.isPresent()) {
- var signature = signatureMaybe.get();
+ if (matchingSignatures.size() == 1) {
+ var signature = matchingSignatures.get(0);
// look for the public key that matches the key ID and use it for verification.
var key = publicKeys.get(signature.getKeyId());
if (key != null) {
diff --git a/tuf-cli/tuf-cli.xfails b/tuf-cli/tuf-cli.xfails
index ca109077..77eabad5 100644
--- a/tuf-cli/tuf-cli.xfails
+++ b/tuf-cli/tuf-cli.xfails
@@ -1,5 +1,4 @@
test_metadata_bytes_match
-test_duplicate_sig_keyids
test_unusual_role_name[?]
test_unusual_role_name[#]
test_unusual_role_name[/delegatedrole]