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 sign and verify test vectors #292

Merged
merged 8 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 0 additions & 1 deletion bindings/web5_uniffi_wrapper/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,3 @@ impl From<InnerWeb5Error> for Web5Error {
}

pub type Result<T> = std::result::Result<T, Web5Error>;

Original file line number Diff line number Diff line change
@@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think with the latest on main you'll need to rebase and then this'll be...

Suggested change
this.rustCoreVerifier = RustCoreEd25519Verifier(privateKey)
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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package web5.sdk.crypto.verifiers

import web5.sdk.rust.Verifier as RustCoreVerifier

typealias Verifier = RustCoreVerifier
104 changes: 104 additions & 0 deletions bound/kt/src/test/kotlin/web5/sdk/TestVectorsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package web5.sdk

import com.fasterxml.jackson.core.type.TypeReference
import org.junit.jupiter.api.Test
import java.io.File
import org.junit.jupiter.api.Assertions.*
import web5.sdk.crypto.keys.Jwk
import web5.sdk.crypto.signers.Ed25519Signer
import web5.sdk.crypto.verifiers.Ed25519Verifier

class TestVectors<I, O>(
val description: String,
val vectors: List<TestVector<I, O>>
)

class TestVector<I, O>(
val description: String,
val input: I,
val output: O?,
val errors: Boolean? = false,
)

class Web5TestVectorsCryptoEd25519 {

data class SignTestInput(
val data: String,
val key: TestVectorJwk,
)

data class TestVectorJwk(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we're not testing the JWK in this test, but ideally we could use the real Jwk, but to your point on the other PR, we may need to add #297 first?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup after 297 is in we can use it

val crv: String,
val d: String?,
val kid: String,
val kty: String,
val x: String
)

@Test
fun sign() {
val typeRef = object : TypeReference<TestVectors<SignTestInput, String>>() {}
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)
}
}
}

data class VerifyTestInput(
val key: Map<String, Any>,
val signature: String,
val data: String
)

@Test
fun verify() {
val typeRef = object : TypeReference<TestVectors<VerifyTestInput, Boolean>>() {}
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) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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.signers.Ed25519Signer
import web5.sdk.rust.ed25519GeneratorGenerate as rustCoreEd25519GeneratorGenerate

class Ed25519VerifierTest {

@Test
fun `test verifier with valid signature`() {
val privateJwk = rustCoreEd25519GeneratorGenerate()
val ed25519Signer = Ed25519Signer(privateJwk)

val message = "abc".toByteArray()
val signature = ed25519Signer.sign(message)

val ed25519Verifier = Ed25519Verifier(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(privateJwk)

val message = "abc".toByteArray()
val signature = ed25519Signer.sign(message)

val modifiedMessage = "abcd".toByteArray()

val ed25519Verifier = Ed25519Verifier(privateJwk)
val isValid = ed25519Verifier.verify(modifiedMessage, signature)

assertFalse(isValid, "Signature should be invalid")
}
}
2 changes: 1 addition & 1 deletion crates/web5/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ pub mod rfc3339;
#[cfg(test)]
mod test_helpers;
#[cfg(test)]
mod test_vectors;
mod test_vectors;
97 changes: 97 additions & 0 deletions crates/web5/src/test_vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub struct TestVector<I, O> {
pub description: String,
pub input: I,
pub output: O,
pub errors: Option<bool>,
}

#[derive(Debug, serde::Deserialize)]
Expand Down Expand Up @@ -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<SignVectorInput, Option<String>> =
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised this works. ed25519 signatures are different every time because they use a random nonce to prevent computing the private key if the same private key is used for multiple signatures

Copy link
Contributor Author

@nitro-neal nitro-neal Aug 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh no..
This works in every sdk
Are we doing something wrong lol, let me check on this..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see Section 4.2.5 on Deterministic Nonce Generation

Ed25519 uses deterministic signing which removes the need for fresh random numbers during the signing process. This does not lead to any particular consequences for our security analysis since we model the key derivation function as a random oracle. However, it is well known not to reduce security


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<VerifyVectorInput, bool> =
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");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the difference between Err() and Ok(false) for verifier.verify?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chad says:
Ok(false): The operation itself succeeded, but the verification result is false. This means that the signature is valid in terms of format and process, but it does not match the data or is otherwise invalid according to the verification logic.

and yea then err is an exception basically if I'm understnading rust correctly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh and as far as the implementation yea just verified false is invalid signature vs catastrophic failure

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI in #300 I'm moving in favor of the monad pattern so verify returns a void and utilizes the error case for cryptographic failure

no actionable items in this PR, just making you aware (and I'll be sure to remediate test vectors prior to merging #300 )

assert_eq!(
is_valid, expected_output,
"Verification result does not match expected output: {}",
vector.description
);
}
}

fn hex_string_to_byte_array(s: String) -> Vec<u8> {
s.chars()
.collect::<Vec<char>>()
.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::<String>()
}
}
}
2 changes: 1 addition & 1 deletion web5-spec
Loading