Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add unit testing for tuf key verification. #146

Merged
merged 6 commits into from
Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@

/** Autodetection for verification algorithms based on public keys used. */
public class Verifiers {
@FunctionalInterface
public interface Supplier {
public Verifier newVerifier(PublicKey publicKey) throws NoSuchAlgorithmException;
}

/** Returns a new verifier for the provided public key to use during verificaiton. */
/** Returns a new verifier for the provided public key to use during verification. */
public static Verifier newVerifier(PublicKey publicKey) throws NoSuchAlgorithmException {
if (publicKey.getAlgorithm().equals("RSA")) {
return new RsaVerifier(publicKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

/** Thrown when the metadata has not been signed by enough of the allowed keys. */
public class SignatureVerificationException extends TufException {
final int requiredSignatures, verifiedSignatures;
private final int requiredSignatures, verifiedSignatures;

public SignatureVerificationException(int requiredSignatures, int verifiedSignatures) {
super(
Expand All @@ -27,4 +27,12 @@ public SignatureVerificationException(int requiredSignatures, int verifiedSignat
this.requiredSignatures = requiredSignatures;
this.verifiedSignatures = verifiedSignatures;
}

public int getRequiredSignatures() {
return requiredSignatures;
}

public int getVerifiedSignatures() {
return verifiedSignatures;
}
}
20 changes: 17 additions & 3 deletions sigstore-java/src/main/java/dev/sigstore/tuf/TufClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,21 @@ public class TufClient {
1024; // Limit the update loop to retrieve a max of 1024 subsequent versions as expressed in
// 5.3.3 of spec.

protected Clock clock = Clock.systemUTC();
private Clock clock;
private Verifiers.Supplier verifiers;

TufClient(Clock clock, Verifiers.Supplier verifiers) {
this.clock = clock;
this.verifiers = verifiers;
}

TufClient(Verifiers.Supplier verifiers) {
this(Clock.systemUTC(), verifiers);
}

TufClient() {
this(Clock.systemUTC(), Verifiers::newVerifier);
}

private ZonedDateTime updateStartTime;

Expand Down Expand Up @@ -185,7 +199,7 @@ private boolean hasNewKeys(RootRole oldRole, RootRole newRole) {
* @throws SignatureVerificationException if there are not enough verified signatures
*/
@VisibleForTesting
static void verifyDelegate(
void verifyDelegate(
List<Signature> signatures,
Map<String, Key> publicKeys,
Role role,
Expand All @@ -211,7 +225,7 @@ static void verifyDelegate(
var pubKey = Keys.constructTufPublicKey(keyBytes, key.getScheme());
byte[] signatureBytes = Hex.decode(signature.getSignature());
try {
if (Verifiers.newVerifier(pubKey).verify(verificationMaterial, signatureBytes)) {
if (verifiers.newVerifier(pubKey).verify(verificationMaterial, signatureBytes)) {
goodSigs.add(signature.getKeyId());
}
} catch (SignatureException e) {
Expand Down
230 changes: 217 additions & 13 deletions sigstore-java/src/test/java/dev/sigstore/tuf/TufClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,31 @@

import static org.junit.jupiter.api.Assertions.*;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import dev.sigstore.encryption.signers.Verifier;
import dev.sigstore.encryption.signers.Verifiers;
import dev.sigstore.json.GsonSupplier;
import dev.sigstore.tuf.model.Root;
import dev.sigstore.tuf.model.*;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ResourceHandler;
Expand Down Expand Up @@ -69,7 +79,7 @@ static void startRemoteResourceServer() throws Exception {
public void testRootUpdate_fromProdData()
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
setupMirror("remote-repo-prod", "1.root.json", "2.root.json", "3.root.json", "4.root.json");
var client = createTufClient();
var client = createTimeStaticTufClient();
Path trustedRoot = tufTestData.resolve("trusted-root.json");
client.updateRoot(trustedRoot, new URL(remoteUrl), localStore);
assertStoreContains("root.json");
Expand All @@ -83,21 +93,21 @@ public void testRootUpdate_fromProdData()
public void testRootUpdate_notEnoughSignatures()
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
setupMirror("remote-repo-unsigned", "2.root.json");
var client = createTufClient();
var client = createTimeStaticTufClient();
try {
client.updateRoot(tufTestData.resolve("trusted-root.json"), new URL(remoteUrl), localStore);
fail();
} catch (SignatureVerificationException e) {
assertEquals(3, e.requiredSignatures);
assertEquals(0, e.verifiedSignatures);
assertEquals(3, e.getRequiredSignatures());
assertEquals(0, e.getVerifiedSignatures());
}
}

@Test
public void testRootUpdate_expiredRoot()
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
setupMirror("remote-repo-expired", "2.root.json");
var client = createTufClient();
var client = createTimeStaticTufClient();
try {
client.updateRoot(tufTestData.resolve("trusted-root.json"), new URL(remoteUrl), localStore);
fail();
Expand All @@ -113,7 +123,7 @@ public void testRootUpdate_expiredRoot()
public void testRootUpdate_inconsistentVersion()
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
setupMirror("remote-repo-inconsistent-version", "2.root.json");
var client = createTufClient();
var client = createTimeStaticTufClient();
try {
client.updateRoot(tufTestData.resolve("trusted-root.json"), new URL(remoteUrl), localStore);
fail();
Expand All @@ -127,20 +137,214 @@ public void testRootUpdate_inconsistentVersion()
public void testRootUpdate_metaFileTooBig()
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
setupMirror("remote-repo-meta-file-too-big", "2.root.json");
var client = createTufClient();
var client = createTimeStaticTufClient();
try {
client.updateRoot(tufTestData.resolve("trusted-root.json"), new URL(remoteUrl), localStore);
fail();
} catch (MetaFileExceedsMaxException e) {
}
}

// sigs and keys with the same number have the same key ids.
static final Signature SIG_1 =
ImmutableSignature.builder()
.keyId("2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97")
.signature(
"3046022100f7d4abde3d694fba01af172466629249a6743efd04c3999f958494842a7aee1f022100d19a295f9225247f17650fdb4ad50b99c2326700aadd0afaec4ae418941c7c59")
.build();
static final Signature SIG_2 =
ImmutableSignature.builder()
.keyId("eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b")
.signature(
"3045022075ec28360b3e310db9d3de281a5286e37884aefd9f0b7193ad67c68ab6ee95a2022100aa08a93c58d74d9cb128cea765cae378efe86092f253b75fd427aede48ac7e22")
.build();
static final Pair<String, Key> PUB_KEY_1 =
Pair.of(
"2f64fb5eac0cf94dd39bb45308b98920055e9a0d8e012a7220787834c60aef97",
newKey(
"04cbc5cab2684160323c25cd06c3307178a6b1d1c9b949328453ae473c5ba7527e35b13f298b41633382241f3fd8526c262d43b45adee5c618fa0642c82b8a9803"));
static final Pair<String, Key> PUB_KEY_2 =
Pair.of(
"eaf22372f417dd618a46f6c627dbc276e9fd30a004fc94f9be946e73f8bd090b",
newKey(
"04117b33dd265715bf23315e368faa499728db8d1f0a377070a1c7b1aba2cc21be6ab1628e42f2cdd7a35479f2dce07b303a8ba646c55569a8d2a504ba7e86e447"));
static final Pair<String, Key> PUB_KEY_3 =
Pair.of(
"f40f32044071a9365505da3d1e3be6561f6f22d0e60cf51df783999f6c3429cb",
newKey(
"04cc1cd53a61c23e88cc54b488dfae168a257c34fac3e88811c55962b24cffbfecb724447999c54670e365883716302e49da57c79a33cd3e16f81fbc66f0bcdf48"));

@Test
public void testVerifyDelegate_verified()
throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
List<Signature> sigs = ImmutableList.of(SIG_1, SIG_2);

Map<String, Key> publicKeys =
ImmutableMap.of(
PUB_KEY_1.getLeft(), PUB_KEY_1.getRight(),
PUB_KEY_2.getLeft(), PUB_KEY_2.getRight());
Role delegate =
ImmutableRootRole.builder()
.addKeyids(PUB_KEY_1.getLeft(), PUB_KEY_2.getLeft())
.threshold(2)
.build();
byte[] verificationMaterial = "alksdjfas".getBytes(StandardCharsets.UTF_8);

createAlwaysVerifyingTufClient()
.verifyDelegate(sigs, publicKeys, delegate, verificationMaterial);
// we are good
}

@Test
public void testVerifyDelegate_verificationFailed()
throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
List<Signature> sigs = ImmutableList.of(SIG_1, SIG_2);

Map<String, Key> publicKeys = ImmutableMap.of(PUB_KEY_1.getLeft(), PUB_KEY_1.getRight());
Role delegate = ImmutableRootRole.builder().addKeyids(PUB_KEY_1.getLeft()).threshold(1).build();
byte[] verificationMaterial = "alksdjfas".getBytes(StandardCharsets.UTF_8);
var client =
new TufClient(
publicKey ->
new Verifier() {
@Override
public PublicKey getPublicKey() {
return null;
}

@Override
public boolean verify(byte[] artifact, byte[] signature)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
return false;
}

@Override
public boolean verifyDigest(byte[] artifactDigest, byte[] signature)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
return false;
}
});
try {
client.verifyDelegate(sigs, publicKeys, delegate, verificationMaterial);
fail("This should have failed since the public key for PUB_KEY_1 should fail to verify.");
} catch (SignatureVerificationException e) {
assertEquals(1, e.getRequiredSignatures());
assertEquals(0, e.getVerifiedSignatures());
}
}

@Test
public void testVerifyDelegate_belowThreshold()
throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
List<Signature> sigs = ImmutableList.of(SIG_1, SIG_2);

Map<String, Key> publicKeys = ImmutableMap.of(PUB_KEY_1.getLeft(), PUB_KEY_1.getRight());
Role delegate =
ImmutableRootRole.builder()
.addKeyids(PUB_KEY_1.getLeft(), PUB_KEY_2.getLeft())
.threshold(2)
.build();
byte[] verificationMaterial = "alksdjfas".getBytes(StandardCharsets.UTF_8);

try {
createAlwaysVerifyingTufClient()
.verifyDelegate(sigs, publicKeys, delegate, verificationMaterial);
fail(
"Test should have thrown SignatureVerificationException due to insufficient public keys");
} catch (SignatureVerificationException e) {
assertEquals(1, e.getVerifiedSignatures());
assertEquals(2, e.getRequiredSignatures());
}
}

// Just testing boundary conditions for iteration bugs.
@Test
public void testVerifyDelegate_emptyLists()
throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
List<Signature> sigs = ImmutableList.of();

Map<String, Key> publicKeys = ImmutableMap.of();
Role delegate = ImmutableRootRole.builder().addKeyids().threshold(2).build();
byte[] verificationMaterial = "alksdjfas".getBytes(StandardCharsets.UTF_8);

try {
createAlwaysVerifyingTufClient()
.verifyDelegate(sigs, publicKeys, delegate, verificationMaterial);
fail(
"Test should have thrown SignatureVerificationException due to insufficient public keys");
} catch (SignatureVerificationException e) {
assertEquals(0, e.getVerifiedSignatures());
assertEquals(2, e.getRequiredSignatures());
}
}

@Test
public void testVerifyDelegate_goodSigsAndKeysButNotInRole()
throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
List<Signature> sigs = ImmutableList.of(SIG_1, SIG_2);

Map<String, Key> publicKeys =
ImmutableMap.of(
PUB_KEY_1.getLeft(), PUB_KEY_1.getRight(),
PUB_KEY_2.getLeft(), PUB_KEY_2.getRight());
Role delegate =
ImmutableRootRole.builder()
.addKeyids(PUB_KEY_1.getLeft(), PUB_KEY_3.getLeft())
.threshold(2)
.build();
byte[] verificationMaterial = "alksdjfas".getBytes(StandardCharsets.UTF_8);

try {
createAlwaysVerifyingTufClient()
.verifyDelegate(sigs, publicKeys, delegate, verificationMaterial);
fail(
"Test should have thrown SignatureVerificationException due to insufficient public keys");
} catch (SignatureVerificationException e) {
// pub key #1 and #3 were allowed, but only #1 and #2 were present so verification only
// verified #1.
assertEquals(1, e.getVerifiedSignatures());
assertEquals(2, e.getRequiredSignatures());
}
}

static Key newKey(String keyContents) {
return ImmutableKey.builder()
.keyType("ecdsa-sha2-nistp256")
.addKeyIdHashAlgorithms("sha256", "sha513")
.scheme("ecdsa-sha2-nistp256")
.putKeyVal("public", keyContents)
.build();
}

@NotNull
private static TufClient createTufClient() {
TufClient client = new TufClient();
// set a fixed time to ensure test results are reproducible.
client.clock = Clock.fixed(Instant.parse(TEST_STATIC_UPDATE_TIME), ZoneOffset.UTC);
return client;
private static TufClient createTimeStaticTufClient() {
return new TufClient(
Clock.fixed(Instant.parse(TEST_STATIC_UPDATE_TIME), ZoneOffset.UTC),
Verifiers::newVerifier);
}

@NotNull
private static TufClient createAlwaysVerifyingTufClient() {
return new TufClient(
publicKey ->
new Verifier() {
@Override
public PublicKey getPublicKey() {
return null;
}

@Override
public boolean verify(byte[] artifact, byte[] signature)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
return true;
}

@Override
public boolean verifyDigest(byte[] artifactDigest, byte[] signature)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
return true;
}
});
}

private void setupMirror(String repoFolder, String... files) throws IOException {
Expand Down