From 7042127772591b8d43887ecb92281816b5cae2b5 Mon Sep 17 00:00:00 2001 From: nitro-neal <5314059+nitro-neal@users.noreply.github.com> Date: Tue, 20 Aug 2024 16:44:43 -0500 Subject: [PATCH] add sign and verify test vectors (#292) * add sign and verify test vectors * test vectors to kotlin * rename * updates * update * updates * rename --- .../sdk/crypto/verifiers/Ed25519Verifier.kt | 27 +++ .../web5/sdk/crypto/verifiers/Verifier.kt | 5 + .../kotlin/web5/sdk/Web5TestVectorsTest.kt | 212 ++++++++++++++++++ .../crypto/verifiers/Ed25519VerifierTest.kt | 41 ++++ crates/web5/src/test_vectors.rs | 97 ++++++++ web5-spec | 2 +- 6 files changed, 383 insertions(+), 1 deletion(-) create mode 100644 bound/kt/src/main/kotlin/web5/sdk/crypto/verifiers/Ed25519Verifier.kt create mode 100644 bound/kt/src/main/kotlin/web5/sdk/crypto/verifiers/Verifier.kt create mode 100644 bound/kt/src/test/kotlin/web5/sdk/Web5TestVectorsTest.kt create mode 100644 bound/kt/src/test/kotlin/web5/sdk/crypto/verifiers/Ed25519VerifierTest.kt diff --git a/bound/kt/src/main/kotlin/web5/sdk/crypto/verifiers/Ed25519Verifier.kt b/bound/kt/src/main/kotlin/web5/sdk/crypto/verifiers/Ed25519Verifier.kt new file mode 100644 index 00000000..e5cd7b80 --- /dev/null +++ b/bound/kt/src/main/kotlin/web5/sdk/crypto/verifiers/Ed25519Verifier.kt @@ -0,0 +1,27 @@ +package web5.sdk.crypto.verifiers + +import web5.sdk.crypto.keys.Jwk +import web5.sdk.rust.Ed25519Verifier as RustCoreEd25519Verifier + +class Ed25519Verifier : Verifier { + private val rustCoreVerifier: RustCoreEd25519Verifier + + constructor(privateKey: Jwk) { + this.rustCoreVerifier = RustCoreEd25519Verifier(privateKey.rustCoreJwkData) + } + + private constructor(rustCoreVerifier: RustCoreEd25519Verifier) { + this.rustCoreVerifier = rustCoreVerifier + } + + /** + * Implementation of Signer's verify instance method for Ed25519. + * + * @param message the data to be verified. + * @param signature the signature to be verified. + * @return ByteArray the signature. + */ + override fun verify(message: ByteArray, signature: ByteArray): Boolean { + return rustCoreVerifier.verify(message, signature); + } +} \ No newline at end of file diff --git a/bound/kt/src/main/kotlin/web5/sdk/crypto/verifiers/Verifier.kt b/bound/kt/src/main/kotlin/web5/sdk/crypto/verifiers/Verifier.kt new file mode 100644 index 00000000..35a83ff1 --- /dev/null +++ b/bound/kt/src/main/kotlin/web5/sdk/crypto/verifiers/Verifier.kt @@ -0,0 +1,5 @@ +package web5.sdk.crypto.verifiers + +import web5.sdk.rust.Verifier as RustCoreVerifier + +typealias Verifier = RustCoreVerifier \ No newline at end of file diff --git a/bound/kt/src/test/kotlin/web5/sdk/Web5TestVectorsTest.kt b/bound/kt/src/test/kotlin/web5/sdk/Web5TestVectorsTest.kt new file mode 100644 index 00000000..1bc2d9c2 --- /dev/null +++ b/bound/kt/src/test/kotlin/web5/sdk/Web5TestVectorsTest.kt @@ -0,0 +1,212 @@ +package web5.sdk + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.PropertyNamingStrategy +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.databind.cfg.MapperConfig +import com.fasterxml.jackson.databind.introspect.AnnotatedField +import com.fasterxml.jackson.databind.introspect.AnnotatedMethod +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import org.junit.jupiter.api.Test +import java.io.File +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Nested +import web5.sdk.crypto.keys.Jwk +import web5.sdk.crypto.signers.Ed25519Signer +import web5.sdk.crypto.verifiers.Ed25519Verifier +import web5.sdk.dids.Document +import web5.sdk.rust.DocumentMetadataData +import web5.sdk.rust.ResolutionMetadataData + +class Web5TestVectorsTest { + + class TestVectors( + val description: String, + val vectors: List> + ) + + class TestVector( + val description: String, + val input: I, + val output: O?, + val errors: Boolean? = false, + ) + + data class SignTestInput( + val data: String, + val key: TestVectorJwk, + ) + + data class TestVectorJwk( + val crv: String, + val d: String?, + val kid: String, + val kty: String, + val x: String + ) + + data class VerifyTestInput( + val key: Map, + val signature: String, + val data: String + ) + + data class DidJwkResolveTestOutput( + val context: String?, + val didDocument: Document?, + val didDocumentMetadata: DocumentMetadataData, + val didResolutionMetadata: ResolutionMetadataData? + ) + + @Nested + inner class Web5TestVectorsCryptoEd25519 { + @Test + fun sign() { + val typeRef = object : TypeReference>() {} + val testVectors = + Json.jsonMapper.readValue(File("../../web5-spec/test-vectors/crypto_ed25519/sign.json"), typeRef) + + testVectors.vectors.forEach { vector -> + val inputByteArray = hexStringToByteArray(vector.input.data) + val testVectorJwk = vector.input.key + + val ed25519Jwk = Jwk( + kty = testVectorJwk.kty, + crv = testVectorJwk.crv, + d = testVectorJwk.d, + x = testVectorJwk.x, + alg = null, + y = null + ) + val signer = Ed25519Signer(ed25519Jwk) + + if (vector.errors == true) { + assertThrows(Exception::class.java) { + signer.sign(inputByteArray) + } + } else { + val signedByteArray = signer.sign(inputByteArray) + val signedHex = byteArrayToHexString(signedByteArray) + assertEquals(vector.output, signedHex) + } + } + } + + @Test + fun verify() { + val typeRef = object : TypeReference>() {} + val testVectors = + Json.jsonMapper.readValue(File("../../web5-spec/test-vectors/crypto_ed25519/verify.json"), typeRef) + + testVectors.vectors.forEach { vector -> + val inputByteArray = hexStringToByteArray(vector.input.data) + val signatureByteArray = hexStringToByteArray(vector.input.signature) + val testVectorJwk = Json.jsonMapper.convertValue(vector.input.key, TestVectorJwk::class.java) + + val ed25519Jwk = Jwk( + kty = testVectorJwk.kty, + crv = testVectorJwk.crv, + d = null, + x = testVectorJwk.x, + alg = null, + y = null + ) + val verifier = Ed25519Verifier(ed25519Jwk) + + if (vector.errors == true) { + assertThrows(Exception::class.java) { + verifier.verify(inputByteArray, signatureByteArray) + } + } else { + val verified = verifier.verify(inputByteArray, signatureByteArray) + assertEquals(vector.output, verified) + } + } + } + + private fun hexStringToByteArray(s: String): ByteArray { + val len = s.length + val data = ByteArray(len / 2) + for (i in 0 until len step 2) { + data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte() + } + return data + } + + private fun byteArrayToHexString(bytes: ByteArray): String { + return bytes.joinToString("") { "%02x".format(it) } + } + } + + @Nested + inner class Web5TestVectorsDidJwk { + + // This is so we can parse the test vector converting metadata errors in the test vectors like invalidDid to the INVALID-DID enum + internal inner class CustomEnumNamingStrategy : PropertyNamingStrategy() { + override fun nameForField(config: MapperConfig<*>?, field: AnnotatedField?, defaultName: String?): String? { + if (field?.type?.isEnumType == true) { + return convertToUpperSnakeCase(defaultName) + } + return defaultName + } + + override fun nameForGetterMethod(config: MapperConfig<*>?, method: AnnotatedMethod?, defaultName: String?): String { + if (method?.rawReturnType?.isEnum == true) { + return convertToUpperSnakeCase(defaultName) + } + return defaultName!! + } + + override fun nameForSetterMethod(config: MapperConfig<*>?, method: AnnotatedMethod?, defaultName: String?): String { + if (method?.rawParameterTypes?.firstOrNull()?.isEnum == true) { + return convertToUpperSnakeCase(defaultName) + } + return defaultName!! + } + + private fun convertToUpperSnakeCase(input: String?): String { + return input?.replace("([a-z])([A-Z]+)".toRegex(), "$1_$2")?.uppercase() ?: "" + } + } + + val jsonMapper: ObjectMapper = jacksonObjectMapper() + .registerKotlinModule() + .findAndRegisterModules() + .setPropertyNamingStrategy(CustomEnumNamingStrategy()) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .disable((DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + + @Test + fun resolve() { + val typeRef = object : TypeReference>() {} + + val testVectors = + jsonMapper.readValue(File("../../web5-spec/test-vectors/did_jwk/resolve.json"), typeRef) + + testVectors.vectors.forEach { vector -> + if (vector.errors == true) { + val resolvedDid = web5.sdk.dids.methods.jwk.DidJwk.resolve(vector.input) + + assertTrue(resolvedDid.resolutionMetadata.error != null) + + // TODO: parse resolutionMetadata from the test vector correctly +// assertEquals(resolvedDid.resolutionMetadata, vector.output!!.didResolutionMetadata) + } else { + val resolvedDid = web5.sdk.dids.methods.jwk.DidJwk.resolve(vector.input) + + assertEquals(resolvedDid.document!!.id, vector.output!!.didDocument!!.id) + assertEquals(resolvedDid.document!!.verificationMethod, vector.output.didDocument!!.verificationMethod) + assertEquals(resolvedDid.document!!.authentication, vector.output.didDocument.authentication) + assertEquals(resolvedDid.document!!.assertionMethod, vector.output.didDocument.assertionMethod) + assertEquals(resolvedDid.document!!.capabilityDelegation, vector.output.didDocument.capabilityDelegation) + assertEquals(resolvedDid.document!!.capabilityInvocation, vector.output.didDocument.capabilityInvocation) + } + } + } + } +} \ No newline at end of file diff --git a/bound/kt/src/test/kotlin/web5/sdk/crypto/verifiers/Ed25519VerifierTest.kt b/bound/kt/src/test/kotlin/web5/sdk/crypto/verifiers/Ed25519VerifierTest.kt new file mode 100644 index 00000000..339eb7db --- /dev/null +++ b/bound/kt/src/test/kotlin/web5/sdk/crypto/verifiers/Ed25519VerifierTest.kt @@ -0,0 +1,41 @@ +package web5.sdk.crypto.verifiers + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.assertFalse +import web5.sdk.crypto.keys.Jwk +import web5.sdk.crypto.signers.Ed25519Signer +import web5.sdk.rust.ed25519GeneratorGenerate as rustCoreEd25519GeneratorGenerate + +class Ed25519VerifierTest { + + @Test + fun `test verifier with valid signature`() { + val privateJwk = rustCoreEd25519GeneratorGenerate() + val ed25519Signer = Ed25519Signer(Jwk.fromRustCoreJwkData(privateJwk)) + + val message = "abc".toByteArray() + val signature = ed25519Signer.sign(message) + + val ed25519Verifier = Ed25519Verifier(Jwk.fromRustCoreJwkData(privateJwk)) + val isValid = ed25519Verifier.verify(message, signature) + + assertTrue(isValid, "Signature should be valid") + } + + @Test + fun `test verifier with invalid signature`() { + val privateJwk = rustCoreEd25519GeneratorGenerate() + val ed25519Signer = Ed25519Signer(Jwk.fromRustCoreJwkData(privateJwk)) + + val message = "abc".toByteArray() + val signature = ed25519Signer.sign(message) + + val modifiedMessage = "abcd".toByteArray() + + val ed25519Verifier = Ed25519Verifier(Jwk.fromRustCoreJwkData(privateJwk)) + val isValid = ed25519Verifier.verify(modifiedMessage, signature) + + assertFalse(isValid, "Signature should be invalid") + } +} diff --git a/crates/web5/src/test_vectors.rs b/crates/web5/src/test_vectors.rs index 09d51d15..ae51d679 100644 --- a/crates/web5/src/test_vectors.rs +++ b/crates/web5/src/test_vectors.rs @@ -6,6 +6,7 @@ pub struct TestVector { pub description: String, pub input: I, pub output: O, + pub errors: Option, } #[derive(Debug, serde::Deserialize)] @@ -207,4 +208,100 @@ mod test_vectors { } } } + + mod crypto_ed25519 { + use super::*; + use crate::crypto::dsa::ed25519::{Ed25519Signer, Ed25519Verifier}; + use crate::crypto::dsa::{Signer, Verifier}; + use crate::crypto::jwk::Jwk; + + #[derive(Debug, serde::Deserialize)] + struct SignVectorInput { + data: String, + key: Jwk, + } + + #[derive(Debug, serde::Deserialize)] + struct VerifyVectorInput { + data: String, + key: Jwk, + signature: String, + } + + #[test] + fn sign() { + let path = "crypto_ed25519/sign.json"; + let vectors: TestVectorFile> = + TestVectorFile::load_from_path(path); + + for vector in vectors.vectors { + let input = vector.input; + let expected_output = vector.output; + + let signer = Ed25519Signer::new(input.key); + + let data = hex_string_to_byte_array(input.data.to_string()); + let result = signer.sign(&data); + + if matches!(vector.errors, Some(true)) { + assert!(result.is_err(), "Expected an error, but signing succeeded"); + } else { + let signature = result.expect("Signing should not fail"); + + // Convert the signature to a hex string + let signature_hex = byte_array_to_hex_string(&signature); + + assert_eq!( + signature_hex, + expected_output.unwrap(), + "Signature does not match expected output" + ); + } + } + } + + #[test] + fn verify() { + let path = "crypto_ed25519/verify.json"; + let vectors: TestVectorFile = + TestVectorFile::load_from_path(path); + + for vector in vectors.vectors { + let input = vector.input; + let expected_output = vector.output; + + let verifier = Ed25519Verifier::new(input.key); + + let data = hex_string_to_byte_array(input.data.to_string()); + let signature = hex_string_to_byte_array(input.signature.to_string()); + + let result = verifier.verify(&data, &signature); + + let is_valid = result.expect("Verification should not fail"); + assert_eq!( + is_valid, expected_output, + "Verification result does not match expected output: {}", + vector.description + ); + } + } + + fn hex_string_to_byte_array(s: String) -> Vec { + s.chars() + .collect::>() + .chunks(2) + .map(|chunk| { + let hex_pair: String = chunk.iter().collect(); + u8::from_str_radix(&hex_pair, 16).unwrap() + }) + .collect() + } + + fn byte_array_to_hex_string(bytes: &[u8]) -> String { + bytes + .iter() + .map(|b| format!("{:02x}", b)) + .collect::() + } + } } diff --git a/web5-spec b/web5-spec index cb662117..748a115b 160000 --- a/web5-spec +++ b/web5-spec @@ -1 +1 @@ -Subproject commit cb6621177190b08ff38be7a1a4ab84e4a6cbcca7 +Subproject commit 748a115bf607f7ddf02996b678a5b09b4ccbf243