diff --git a/fuzzing/src/main/java/fuzzing/TufVerifierFuzzer.java b/fuzzing/src/main/java/fuzzing/TufVerifierFuzzer.java new file mode 100644 index 00000000..8cb8efa0 --- /dev/null +++ b/fuzzing/src/main/java/fuzzing/TufVerifierFuzzer.java @@ -0,0 +1,45 @@ +/* + * Copyright 2023 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 fuzzing; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import dev.sigstore.tuf.encryption.Verifiers; +import dev.sigstore.tuf.model.ImmutableKey; +import dev.sigstore.tuf.model.Key; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.util.Map; + +public class TufVerifierFuzzer { + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + try { + String keyType = data.consumeString(10); + String scheme = data.consumeString(20); + String keyData = data.consumeRemainingAsString(); + + Key key = + ImmutableKey.builder() + .keyType(keyType) + .keyVal(Map.of("public", keyData)) + .scheme(scheme) + .build(); + + Verifiers.newVerifier(key); + } catch (IOException | InvalidKeyException e) { + // known exceptions + } + } +} diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/encryption/EcdsaVerifier.java b/sigstore-java/src/main/java/dev/sigstore/tuf/encryption/EcdsaVerifier.java new file mode 100644 index 00000000..2b4bfe3e --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/encryption/EcdsaVerifier.java @@ -0,0 +1,41 @@ +/* + * 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.encryption; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; + +/** ECDSA verifier, instantiated in {@link Verifiers}. */ +class EcdsaVerifier implements Verifier { + + private final PublicKey publicKey; + + EcdsaVerifier(PublicKey publicKey) { + this.publicKey = publicKey; + } + + @Override + public boolean verify(byte[] artifact, byte[] signature) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + var verifier = Signature.getInstance("SHA256withECDSA"); + verifier.initVerify(publicKey); + verifier.update(artifact); + return verifier.verify(signature); + } +} diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/encryption/Ed25519Verifier.java b/sigstore-java/src/main/java/dev/sigstore/tuf/encryption/Ed25519Verifier.java new file mode 100644 index 00000000..ad29e6f9 --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/encryption/Ed25519Verifier.java @@ -0,0 +1,42 @@ +/* + * 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.encryption; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; + +/** Ed25519 verifier, instantiated by {@link Verifiers}. */ +class Ed25519Verifier implements Verifier { + + private final PublicKey publicKey; + + Ed25519Verifier(PublicKey publicKey) { + this.publicKey = publicKey; + } + + /** EdDSA verifiers hash implicitly for ed25519 keys. */ + @Override + public boolean verify(byte[] artifact, byte[] signature) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + var verifier = Signature.getInstance("Ed25519"); + verifier.initVerify(publicKey); + verifier.update(artifact); + return verifier.verify(signature); + } +} diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/encryption/RsaPssVerifier.java b/sigstore-java/src/main/java/dev/sigstore/tuf/encryption/RsaPssVerifier.java new file mode 100644 index 00000000..7b0f11e1 --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/encryption/RsaPssVerifier.java @@ -0,0 +1,41 @@ +/* + * 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.encryption; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; + +/** RSA verifier using PSS and MGF1, instantiated by {@link Verifiers}. */ +class RsaPssVerifier implements Verifier { + + private final PublicKey publicKey; + + RsaPssVerifier(PublicKey publicKey) { + this.publicKey = publicKey; + } + + @Override + public boolean verify(byte[] artifact, byte[] signature) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + var verifier = Signature.getInstance("SHA256withRSAandMGF1"); + verifier.initVerify(publicKey); + verifier.update(artifact); + return verifier.verify(signature); + } +} diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/encryption/Verifier.java b/sigstore-java/src/main/java/dev/sigstore/tuf/encryption/Verifier.java new file mode 100644 index 00000000..1398c1ca --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/encryption/Verifier.java @@ -0,0 +1,35 @@ +/* + * 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.encryption; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; + +/** A verifier interface specifying verification for a raw artifact (no hashing). */ +public interface Verifier { + + /** + * Verify an artifact. Implementations may hash the artifact with sha256 before verifying unless + * they have an implicit hashing algorithm. + * + * @param artifact the artifact that was signed + * @param signature the signature associated with the artifact + * @return true if the signature is valid, false otherwise + */ + boolean verify(byte[] artifact, byte[] signature) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException; +} diff --git a/sigstore-java/src/main/java/dev/sigstore/tuf/encryption/Verifiers.java b/sigstore-java/src/main/java/dev/sigstore/tuf/encryption/Verifiers.java new file mode 100644 index 00000000..16494540 --- /dev/null +++ b/sigstore-java/src/main/java/dev/sigstore/tuf/encryption/Verifiers.java @@ -0,0 +1,113 @@ +/* + * Copyright 2024 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.encryption; + +import dev.sigstore.tuf.model.Key; +import java.io.IOException; +import java.io.StringReader; +import java.security.InvalidKeyException; +import java.security.PublicKey; +import java.security.Security; +import org.bouncycastle.asn1.edec.EdECObjectIdentifiers; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.crypto.params.ECKeyParameters; +import org.bouncycastle.crypto.params.RSAKeyParameters; +import org.bouncycastle.crypto.util.PublicKeyFactory; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.util.encoders.DecoderException; +import org.bouncycastle.util.encoders.Hex; + +public class Verifiers { + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + @FunctionalInterface + public interface Supplier { + Verifier newVerifier(Key key) throws IOException, InvalidKeyException; + } + + public static Verifier newVerifier(Key key) throws IOException, InvalidKeyException { + + PublicKey publicKey = parsePublicKey(key); + if (key.getKeyType().equals("rsa") && key.getScheme().equals("rsassa-pss-sha256")) { + return new RsaPssVerifier(publicKey); + } + if (isEcdsaKey(key) && key.getScheme().equals("ecdsa-sha2-nistp256")) { + return new EcdsaVerifier(publicKey); + } + if (key.getKeyType().equals("ed25519") && key.getScheme().equals("ed25519")) { + return new Ed25519Verifier(publicKey); + } + throw new InvalidKeyException( + "Unsupported tuf key type and scheme combination: " + + key.getKeyType() + + "/" + + key.getScheme()); + } + + private static PublicKey parsePublicKey(Key key) throws IOException, InvalidKeyException { + var keyType = key.getKeyType(); + if (keyType.equals("rsa") || isEcdsaKey(key)) { + try (PEMParser pemParser = new PEMParser(new StringReader(key.getKeyVal().get("public")))) { + var keyObj = pemParser.readObject(); // throws DecoderException + if (keyObj == null) { + throw new InvalidKeyException( + "tuf " + key.getKeyType() + " keys must be a single PEM encoded section"); + } + if (keyObj instanceof SubjectPublicKeyInfo) { + var keyInfo = PublicKeyFactory.createKey((SubjectPublicKeyInfo) keyObj); + if ((keyType.equals("rsa") && keyInfo instanceof RSAKeyParameters) + || (isEcdsaKey(key) && keyInfo instanceof ECKeyParameters)) { + JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); + return converter.getPublicKey((SubjectPublicKeyInfo) keyObj); + } + } + throw new InvalidKeyException( + "Could not parse PEM section into " + keyType + " public key"); + } catch (DecoderException e) { + throw new InvalidKeyException("Could not parse PEM section in " + keyType + " public key"); + } + } + // tuf allows raw keys only for ed25519 (non PEM): + // https://github.com/theupdateframework/specification/blob/c51875f445d8a57efca9dadfbd5dbdece06d87e6/tuf-spec.md#key-objects--file-formats-keys + else if (keyType.equals("ed25519")) { + byte[] keyContents; + try { + keyContents = Hex.decode(key.getKeyVal().get("public")); + } catch (DecoderException e) { + throw new InvalidKeyException("Could not parse hex encoded ed25519 public key"); + } + var params = + new SubjectPublicKeyInfo( + new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), keyContents); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); + return converter.getPublicKey(params); + } else { + throw new InvalidKeyException("Unsupported tuf key type" + key.getKeyType()); + } + } + + // this is a hack to handle keytypes of ecdsa-sha2-nistp256 + // context: https://github.com/awslabs/tough/issues/754 + private static boolean isEcdsaKey(Key key) { + return key.getKeyType().equals("ecdsa-sha2-nistp256") || key.getKeyType().equals("ecdsa"); + } +} diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/encryption/EcdsaVerifierTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/encryption/EcdsaVerifierTest.java new file mode 100644 index 00000000..95becd64 --- /dev/null +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/encryption/EcdsaVerifierTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024 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.encryption; + +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Security; +import java.security.Signature; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class EcdsaVerifierTest { + + private static final byte[] CONTENT = "abcdef".getBytes(StandardCharsets.UTF_8); + + @Test + public void testVerify_ECDSA() throws Exception { + Security.addProvider(new BouncyCastleProvider()); + + var keyPair = genKeyPair(); + var signature = genSignature(keyPair); + var verifier = new EcdsaVerifier(keyPair.getPublic()); + Assertions.assertTrue(verifier.verify(CONTENT, signature)); + } + + private KeyPair genKeyPair() throws Exception { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("ECDSA"); + keyGen.initialize(256); + return keyGen.generateKeyPair(); + } + + private byte[] genSignature(KeyPair keyPair) throws Exception { + Signature signature = Signature.getInstance("SHA256withECDSA"); + signature.initSign(keyPair.getPrivate()); + signature.update(CONTENT); + return signature.sign(); + } +} diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/encryption/Ed25519VerifierTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/encryption/Ed25519VerifierTest.java new file mode 100644 index 00000000..27843cde --- /dev/null +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/encryption/Ed25519VerifierTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 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.encryption; + +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Security; +import java.security.Signature; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class Ed25519VerifierTest { + + private static final byte[] CONTENT = "abcdef".getBytes(StandardCharsets.UTF_8); + + @Test + public void testVerify_EdDSA() throws Exception { + Security.addProvider(new BouncyCastleProvider()); + + var keyPair = genKeyPair(); + var signature = genSignature(keyPair); + var verifier = new Ed25519Verifier(keyPair.getPublic()); + Assertions.assertTrue(verifier.verify(CONTENT, signature)); + } + + private KeyPair genKeyPair() throws Exception { + KeyPairGenerator kpGen = KeyPairGenerator.getInstance("ed25519"); + return kpGen.generateKeyPair(); + } + + private byte[] genSignature(KeyPair keyPair) throws Exception { + Signature signature = Signature.getInstance("ed25519"); + signature.initSign(keyPair.getPrivate()); + signature.update(CONTENT); + return signature.sign(); + } +} diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/encryption/RsaPssVerifierTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/encryption/RsaPssVerifierTest.java new file mode 100644 index 00000000..54d856f5 --- /dev/null +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/encryption/RsaPssVerifierTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024 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.encryption; + +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.Security; +import java.security.Signature; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class RsaPssVerifierTest { + + private static final byte[] CONTENT = "abcdef".getBytes(StandardCharsets.UTF_8); + + @Test + public void testVerify_RsaPss() throws Exception { + Security.addProvider(new BouncyCastleProvider()); + + var keyPair = genKeyPair(); + var signature = genSignature(keyPair); + var verifier = new RsaPssVerifier(keyPair.getPublic()); + Assertions.assertTrue(verifier.verify(CONTENT, signature)); + } + + private KeyPair genKeyPair() throws Exception { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + return keyGen.genKeyPair(); + } + + private byte[] genSignature(KeyPair keyPair) throws Exception { + Signature signature = Signature.getInstance("SHA256withRSAandMGF1"); + signature.initSign(keyPair.getPrivate()); + signature.update(CONTENT); + return signature.sign(); + } +} diff --git a/sigstore-java/src/test/java/dev/sigstore/tuf/encryption/VerifiersTest.java b/sigstore-java/src/test/java/dev/sigstore/tuf/encryption/VerifiersTest.java new file mode 100644 index 00000000..8c8eebb9 --- /dev/null +++ b/sigstore-java/src/test/java/dev/sigstore/tuf/encryption/VerifiersTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2024 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.encryption; + +import com.google.common.io.Resources; +import dev.sigstore.tuf.model.ImmutableKey; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class VerifiersTest { + + static final String RSA_PUB_PATH = "dev/sigstore/samples/keys/test-rsa.pub"; + static final String EC_PUB_PATH = "dev/sigstore/samples/keys/test-ec.pub"; + + @Test + public void newVerifierRSA() throws Exception { + var key = + ImmutableKey.builder() + .keyType("rsa") + .keyVal( + Map.of( + "public", + Resources.toString( + Resources.getResource(RSA_PUB_PATH), StandardCharsets.UTF_8))) + .scheme("rsassa-pss-sha256") + .build(); + var verifier = Verifiers.newVerifier(key); + Assertions.assertTrue(verifier instanceof RsaPssVerifier); + } + + @Test + public void newVerifierRSA_unsupportedScheme() throws Exception { + var key = + ImmutableKey.builder() + .keyType("rsa") + .keyVal( + Map.of( + "public", + Resources.toString( + Resources.getResource(RSA_PUB_PATH), StandardCharsets.UTF_8))) + .scheme("rsa-junk") + .build(); + Assertions.assertThrows(InvalidKeyException.class, () -> Verifiers.newVerifier(key)); + } + + @Test + public void newVerifierECDSA() throws Exception { + var key = + ImmutableKey.builder() + .keyType("ecdsa") + .keyVal( + Map.of( + "public", + Resources.toString(Resources.getResource(EC_PUB_PATH), StandardCharsets.UTF_8))) + .scheme("ecdsa-sha2-nistp256") + .build(); + var verifier = Verifiers.newVerifier(key); + Assertions.assertTrue(verifier instanceof EcdsaVerifier); + } + + @Test + public void newVerifierECDSA_unsupportedScheme() throws Exception { + var key = + ImmutableKey.builder() + .keyType("ecdsa") + .keyVal( + Map.of( + "public", + Resources.toString( + Resources.getResource(RSA_PUB_PATH), StandardCharsets.UTF_8))) + .scheme("ecdsa-junk") + .build(); + Assertions.assertThrows(InvalidKeyException.class, () -> Verifiers.newVerifier(key)); + } + + @Test + public void newVerifierEd25519() throws Exception { + var key = + ImmutableKey.builder() + .keyType("ed25519") + .keyVal( + Map.of( + "public", "2d7218ce609f85de4b0d29d9e679cfd73e96756652f7069a0cf00acb752e5d3c")) + .scheme("ed25519") + .build(); + var verifier = Verifiers.newVerifier(key); + Assertions.assertTrue(verifier instanceof Ed25519Verifier); + } + + @Test + public void newVerifierEd25519_unsupportedScheme() { + var key = + ImmutableKey.builder() + .keyType("ed25519") + .keyVal( + Map.of( + "public", "2d7218ce609f85de4b0d29d9e679cfd73e96756652f7069a0cf00acb752e5d3c")) + .scheme("ed25519junk") + .build(); + Assertions.assertThrows(InvalidKeyException.class, () -> Verifiers.newVerifier(key)); + } +}