Skip to content

Commit

Permalink
[cryptotest] Add ECDSA test host-side driver
Browse files Browse the repository at this point in the history
Signed-off-by: Ryan Torok <[email protected]>
  • Loading branch information
RyanTorok committed Feb 15, 2024
1 parent 442df23 commit dcd75b6
Show file tree
Hide file tree
Showing 5 changed files with 335 additions and 0 deletions.
8 changes: 8 additions & 0 deletions sw/host/cryptotest/ujson_lib/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,28 @@ ujson_rust(
srcs = ["//sw/device/tests/crypto/cryptotest/json:aes_commands"],
)

ujson_rust(
name = "ecdsa_commands_rust",
srcs = ["//sw/device/tests/crypto/cryptotest/json:ecdsa_commands"],
)

rust_library(
name = "cryptotest_commands",
srcs = [
"src/aes_commands.rs",
"src/commands.rs",
"src/ecdsa_commands.rs",
"src/lib.rs",
],
compile_data = [
":commands_rust",
":aes_commands_rust",
":ecdsa_commands_rust",
],
rustc_env = {
"commands_loc": "$(execpath :commands_rust)",
"aes_commands_loc": "$(execpath :aes_commands_rust)",
"ecdsa_commands_loc": "$(execpath :ecdsa_commands_rust)",
},
deps = [
"//sw/host/opentitanlib",
Expand Down
4 changes: 4 additions & 0 deletions sw/host/cryptotest/ujson_lib/src/ecdsa_commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
include!(env!("ecdsa_commands_loc"));
1 change: 1 addition & 0 deletions sw/host/cryptotest/ujson_lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
// SPDX-License-Identifier: Apache-2.0
pub mod aes_commands;
pub mod commands;
pub mod ecdsa_commands;
27 changes: 27 additions & 0 deletions sw/host/tests/crypto/ecdsa_kat/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

load("//rules:ujson.bzl", "ujson_rust")
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library")

package(default_visibility = ["//visibility:public"])

rust_binary(
name = "harness",
srcs = ["src/main.rs"],
deps = [
"//sw/host/cryptotest/ujson_lib:cryptotest_commands",
"//sw/host/opentitanlib",
"@crate_index//:anyhow",
"@crate_index//:arrayvec",
"@crate_index//:clap",
"@crate_index//:humantime",
"@crate_index//:log",
"@crate_index//:num-bigint-dig",
"@crate_index//:num-traits",
"@crate_index//:rust-crypto",
"@crate_index//:serde",
"@crate_index//:serde_json",
],
)
295 changes: 295 additions & 0 deletions sw/host/tests/crypto/ecdsa_kat/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
// Copyright lowRISC contributors. Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0

use anyhow::Result;
use arrayvec::ArrayVec;
use clap::Parser;
use crypto::{
digest::Digest,
sha2::{Sha256, Sha384, Sha512},
sha3::Sha3,
};
use num_bigint_dig::BigInt;
use num_traits::Num;
use serde::Deserialize;
use std::fs;
use std::time::Duration;

use cryptotest_commands::commands::CryptotestCommand;
use cryptotest_commands::ecdsa_commands::{
CryptotestEcdsaCoordinate, CryptotestEcdsaCurve, CryptotestEcdsaHashAlg,
CryptotestEcdsaMessage, CryptotestEcdsaOperation, CryptotestEcdsaSignature,
CryptotestEcdsaVerifyOutput,
};

use opentitanlib::app::TransportWrapper;
use opentitanlib::execute_test;
use opentitanlib::test_utils::init::InitializeTest;
use opentitanlib::test_utils::rpc::{UartRecv, UartSend};
use opentitanlib::uart::console::UartConsole;

#[derive(Debug, Parser)]
struct Opts {
#[command(flatten)]
init: InitializeTest,

// Console receive timeout.
#[arg(long, value_parser = humantime::parse_duration, default_value = "10s")]
timeout: Duration,

#[arg(long, num_args = 1..)]
ecdsa_json: Vec<String>,
}

#[derive(Debug, Deserialize)]
struct EcdsaTestCase {
vendor: String,
test_case_id: usize,
algorithm: String,
operation: String,
curve: String,
hash_alg: String,
message: Vec<u8>,
qx: String,
qy: String,
r: String,
s: String,
result: bool,
}

const ECDSA_CMD_MAX_SIGNATURE_SCALAR_BYTES_P256: usize = 32;
const ECDSA_CMD_MAX_COORDINATE_BYTES_P256: usize = 32;
const ECDSA_CMD_MAX_SIGNATURE_SCALAR_BYTES_P384: usize = 48;
const ECDSA_CMD_MAX_COORDINATE_BYTES_P384: usize = 48;

fn run_ecdsa_testcase(
test_case: &EcdsaTestCase,
opts: &Opts,
transport: &TransportWrapper,
fail_counter: &mut u32,
) -> Result<()> {
log::info!(
"vendor: {}, test case: {}",
test_case.vendor,
test_case.test_case_id
);
let uart = transport.uart("console")?;
assert_eq!(test_case.algorithm.as_str(), "ecdsa");
let qx: Vec<u8> = BigInt::from_str_radix(&test_case.qx, 16)
.unwrap()
.to_bytes_le()
.1;
let qy: Vec<u8> = BigInt::from_str_radix(&test_case.qy, 16)
.unwrap()
.to_bytes_le()
.1;
let r: Vec<u8> = BigInt::from_str_radix(&test_case.r, 16)
.unwrap()
.to_bytes_le()
.1;
let s: Vec<u8> = BigInt::from_str_radix(&test_case.s, 16)
.unwrap()
.to_bytes_le()
.1;

// Get the hash function and hash the message to get the digest (unfortunately this code is
// challenging to deduplicate because the `Digest` trait is not object safe).
let (hash_alg, message_digest): (CryptotestEcdsaHashAlg, Vec<u8>) =
match test_case.hash_alg.as_str() {
"sha-256" => {
let mut hasher = Sha256::new();
hasher.input(test_case.message.as_slice());
let mut out = vec![0u8; hasher.output_bytes()];
hasher.result(&mut out);
(CryptotestEcdsaHashAlg::Sha256, out)
}
"sha-384" => {
let mut hasher = Sha384::new();
hasher.input(test_case.message.as_slice());
let mut out = vec![0u8; hasher.output_bytes()];
hasher.result(&mut out);
(CryptotestEcdsaHashAlg::Sha384, out)
}
"sha-512" => {
let mut hasher = Sha512::new();
hasher.input(test_case.message.as_slice());
let mut out = vec![0u8; hasher.output_bytes()];
hasher.result(&mut out);
(CryptotestEcdsaHashAlg::Sha512, out)
}
"sha3-256" => {
let mut hasher = Sha3::sha3_256();
hasher.input(test_case.message.as_slice());
let mut out = vec![0u8; hasher.output_bytes()];
hasher.result(&mut out);
(CryptotestEcdsaHashAlg::Sha3_256, out)
}
"sha3-512" => {
let mut hasher = Sha3::sha3_512();
hasher.input(test_case.message.as_slice());
let mut out = vec![0u8; hasher.output_bytes()];
hasher.result(&mut out);
(CryptotestEcdsaHashAlg::Sha3_512, out)
}
_ => panic!("Invalid ECDSA hash mode"),
};

// Determine the curve and check the lengths of the other arguments based on the curve choice
let curve = match test_case.curve.as_str() {
"p256" => {
assert!(
qx.len() <= ECDSA_CMD_MAX_COORDINATE_BYTES_P256,
"ECDSA qx value was too long for curve p256 (got: {}, max: {})",
qx.len(),
ECDSA_CMD_MAX_COORDINATE_BYTES_P256,
);
assert!(
qy.len() <= ECDSA_CMD_MAX_COORDINATE_BYTES_P256,
"ECDSA qy value was too long for curve p256 (got: {}, max: {})",
qy.len(),
ECDSA_CMD_MAX_COORDINATE_BYTES_P256,
);
assert!(
r.len() <= ECDSA_CMD_MAX_SIGNATURE_SCALAR_BYTES_P256,
"ECDSA signature value r was too long for curve p256 (got: {}, max: {})",
r.len(),
ECDSA_CMD_MAX_SIGNATURE_SCALAR_BYTES_P256,
);
assert!(
s.len() <= ECDSA_CMD_MAX_SIGNATURE_SCALAR_BYTES_P256,
"ECDSA signature value s was too long for curve p256 (got: {}, max: {})",
s.len(),
ECDSA_CMD_MAX_SIGNATURE_SCALAR_BYTES_P256,
);
CryptotestEcdsaCurve::P256
}
"p384" => {
assert!(
qx.len() <= ECDSA_CMD_MAX_COORDINATE_BYTES_P384,
"ECDSA qx value was too long for curve p384 (got: {}, max: {})",
qx.len(),
ECDSA_CMD_MAX_COORDINATE_BYTES_P384,
);
assert!(
qy.len() <= ECDSA_CMD_MAX_COORDINATE_BYTES_P384,
"ECDSA qy value was too long for curve p384 (got: {}, max: {})",
qy.len(),
ECDSA_CMD_MAX_COORDINATE_BYTES_P384,
);
assert!(
r.len() <= ECDSA_CMD_MAX_SIGNATURE_SCALAR_BYTES_P384,
"ECDSA signature value r was too long for curve p384 (got: {}, max: {})",
r.len(),
ECDSA_CMD_MAX_SIGNATURE_SCALAR_BYTES_P384,
);
assert!(
s.len() <= ECDSA_CMD_MAX_SIGNATURE_SCALAR_BYTES_P384,
"ECDSA signature value s was too long for curve p384 (got: {}, max: {})",
s.len(),
ECDSA_CMD_MAX_SIGNATURE_SCALAR_BYTES_P384,
);
CryptotestEcdsaCurve::P384
}
_ => panic!("Invalid ECDSA curve name"),
};

// Send everything
CryptotestCommand::Ecdsa.send(&*uart)?;
match test_case.operation.as_str() {
"verify" => CryptotestEcdsaOperation::Verify,
_ => panic!("Invalid ECDSA operation"),
}
.send(&*uart)?;
hash_alg.send(&*uart)?;
curve.send(&*uart)?;

// Size of `input` is determined at compile-time by type inference
let mut input = ArrayVec::new();
// Fill the buffer until we run out of bytes, truncating the rightmost bytes if we have too
// many
let msg_len = message_digest.len();
let mut message_digest = message_digest.into_iter();
while !input.is_full() {
input.push(message_digest.next().unwrap_or(0u8));
}
// `unwrap()` operations are safe here because we checked the sizes above.
let msg = CryptotestEcdsaMessage {
input,
input_len: msg_len,
};
msg.send(&*uart)?;

CryptotestEcdsaSignature {
r: ArrayVec::try_from(r.as_slice()).unwrap(),
r_len: r.len(),
s: ArrayVec::try_from(s.as_slice()).unwrap(),
s_len: s.len(),
}
.send(&*uart)?;

CryptotestEcdsaCoordinate {
coordinate: ArrayVec::try_from(qx.as_slice()).unwrap(),
coordinate_len: qx.len(),
}
.send(&*uart)?;

CryptotestEcdsaCoordinate {
coordinate: ArrayVec::try_from(qy.as_slice()).unwrap(),
coordinate_len: qy.len(),
}
.send(&*uart)?;

let ecdsa_output = CryptotestEcdsaVerifyOutput::recv(&*uart, opts.timeout, false)?;
let success = match ecdsa_output {
CryptotestEcdsaVerifyOutput::Success => true,
CryptotestEcdsaVerifyOutput::Failure => false,
CryptotestEcdsaVerifyOutput::IntValue(i) => panic!("Invalid ECDSA verify result: {}", i),
};
if test_case.result != success {
log::info!(
"FAILED test #{}: expected = {}, actual = {}",
test_case.test_case_id,
test_case.result,
success
);
}
if success != test_case.result {
*fail_counter += 1;
}
Ok(())
}

fn test_ecdsa(opts: &Opts, transport: &TransportWrapper) -> Result<()> {
let uart = transport.uart("console")?;
uart.set_flow_control(true)?;
let _ = UartConsole::wait_for(&*uart, r"Running [^\r\n]*", opts.timeout)?;

let mut test_counter = 0u32;
let mut fail_counter = 0u32;
let test_vector_files = &opts.ecdsa_json;
for file in test_vector_files {
let raw_json = fs::read_to_string(file)?;
let ecdsa_tests: Vec<EcdsaTestCase> = serde_json::from_str(&raw_json)?;

for ecdsa_test in &ecdsa_tests {
test_counter += 1;
log::info!("Test counter: {}", test_counter);
run_ecdsa_testcase(ecdsa_test, opts, transport, &mut fail_counter)?;
}
}
assert_eq!(
0, fail_counter,
"Failed {} out of {} tests.",
fail_counter, test_counter
);
Ok(())
}

fn main() -> Result<()> {
let opts = Opts::parse();
opts.init.init_logging();

let transport = opts.init.init_target()?;
execute_test!(test_ecdsa, &opts, &transport);
Ok(())
}

0 comments on commit dcd75b6

Please sign in to comment.