mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-08 01:05:47 +00:00
[Remote signer] Fold signer into Lighthouse repository (#1852)
The remote signer relies on the `types` and `crypto/bls` crates from Lighthouse. Moreover, a number of tests of the remote signer consumption of LH leverages this very signer, making any important update a potential dependency nightmare. Co-authored-by: Paul Hauner <paul@paulhauner.com>
This commit is contained in:
20
remote_signer/backend/Cargo.toml
Normal file
20
remote_signer/backend/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "remote_signer_backend"
|
||||
version = "0.2.0"
|
||||
authors = ["Herman Junge <herman@sigmaprime.io>"]
|
||||
edition = "2018"
|
||||
|
||||
[dev-dependencies]
|
||||
helpers = { path = "../../testing/remote_signer_test", package = "remote_signer_test" }
|
||||
sloggers = "1.0.1"
|
||||
tempdir = "0.3.7"
|
||||
|
||||
[dependencies]
|
||||
bls = { path = "../../crypto/bls" }
|
||||
clap = "2.33.3"
|
||||
hex = "0.4.2"
|
||||
lazy_static = "1.4.0"
|
||||
regex = "1.3.9"
|
||||
slog = "2.5.2"
|
||||
types = { path = "../../consensus/types" }
|
||||
zeroize = { version = "1.1.1", features = ["zeroize_derive"] }
|
||||
46
remote_signer/backend/src/error.rs
Normal file
46
remote_signer/backend/src/error.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
#[derive(Debug)]
|
||||
pub enum BackendError {
|
||||
/// Parameter is not a hexadecimal representation of a BLS public key.
|
||||
InvalidPublicKey(String),
|
||||
|
||||
/// Retrieved value is not a hexadecimal representation of a BLS secret key.
|
||||
InvalidSecretKey(String),
|
||||
|
||||
/// Public and Secret key won't match.
|
||||
KeyMismatch(String),
|
||||
|
||||
/// Item requested by its public key is not found.
|
||||
KeyNotFound(String),
|
||||
|
||||
/// Errors from the storage medium.
|
||||
///
|
||||
/// When converted from `std::io::Error`, stores `std::io::ErrorKind`
|
||||
/// and `std::io::Error` both formatted to string.
|
||||
StorageError(String, String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BackendError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
BackendError::InvalidPublicKey(e) => write!(f, "Invalid public key: {}", e),
|
||||
|
||||
// Feed it with the public key value used to retrieve it.
|
||||
BackendError::InvalidSecretKey(e) => write!(f, "Invalid secret key: {}", e),
|
||||
|
||||
// Feed it with the public key value used to retrieve it.
|
||||
BackendError::KeyMismatch(e) => write!(f, "Key mismatch: {}", e),
|
||||
|
||||
BackendError::KeyNotFound(e) => write!(f, "Key not found: {}", e),
|
||||
|
||||
// Only outputs to string the first component of the tuple, accounting
|
||||
// for potential differences on error displays between OS distributions.
|
||||
BackendError::StorageError(e, _) => write!(f, "Storage error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for BackendError {
|
||||
fn from(e: std::io::Error) -> BackendError {
|
||||
BackendError::StorageError(format!("{:?}", e.kind()), format!("{}", e))
|
||||
}
|
||||
}
|
||||
336
remote_signer/backend/src/lib.rs
Normal file
336
remote_signer/backend/src/lib.rs
Normal file
@@ -0,0 +1,336 @@
|
||||
mod error;
|
||||
mod storage;
|
||||
mod storage_raw_dir;
|
||||
mod utils;
|
||||
mod zeroize_string;
|
||||
|
||||
use crate::zeroize_string::ZeroizeString;
|
||||
use bls::SecretKey;
|
||||
use clap::ArgMatches;
|
||||
pub use error::BackendError;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use slog::{info, Logger};
|
||||
pub use storage::Storage;
|
||||
use storage_raw_dir::StorageRawDir;
|
||||
use types::Hash256;
|
||||
use utils::{bytes96_to_hex_string, validate_bls_pair};
|
||||
|
||||
lazy_static! {
|
||||
static ref PUBLIC_KEY_REGEX: Regex = Regex::new(r"[0-9a-fA-F]{96}").unwrap();
|
||||
}
|
||||
|
||||
/// A backend to be used by the Remote Signer HTTP API.
|
||||
///
|
||||
/// Designed to support several types of storages.
|
||||
#[derive(Clone)]
|
||||
pub struct Backend<T> {
|
||||
storage: T,
|
||||
}
|
||||
|
||||
impl Backend<StorageRawDir> {
|
||||
/// Creates a Backend with the given storage type at the CLI arguments.
|
||||
///
|
||||
/// # Storage types supported
|
||||
///
|
||||
/// * Raw files in directory: `--storage-raw-dir <DIR>`
|
||||
///
|
||||
pub fn new(cli_args: &ArgMatches<'_>, log: &Logger) -> Result<Self, String> {
|
||||
// Storage types are mutually exclusive.
|
||||
if let Some(path) = cli_args.value_of("storage-raw-dir") {
|
||||
info!(
|
||||
log,
|
||||
"Loading Backend";
|
||||
"storage type" => "raw dir",
|
||||
"directory" => path
|
||||
);
|
||||
|
||||
StorageRawDir::new(path)
|
||||
.map(|storage| Self { storage })
|
||||
.map_err(|e| format!("Storage Raw Dir: {}", e))
|
||||
} else {
|
||||
Err("No storage type supplied.".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Storage> Backend<T> {
|
||||
/// Returns the available public keys in storage.
|
||||
pub fn get_keys(&self) -> Result<Vec<String>, BackendError> {
|
||||
self.storage.get_keys()
|
||||
}
|
||||
|
||||
/// Signs the message with the requested key in storage.
|
||||
pub fn sign_message(
|
||||
&self,
|
||||
public_key: &str,
|
||||
signing_root: Hash256,
|
||||
) -> Result<String, BackendError> {
|
||||
if !PUBLIC_KEY_REGEX.is_match(public_key) || public_key.len() != 96 {
|
||||
return Err(BackendError::InvalidPublicKey(public_key.to_string()));
|
||||
}
|
||||
|
||||
let secret_key: ZeroizeString = self.storage.get_secret_key(public_key)?;
|
||||
let secret_key: SecretKey = validate_bls_pair(public_key, secret_key)?;
|
||||
|
||||
let signature = secret_key.sign(signing_root);
|
||||
|
||||
let signature: String = bytes96_to_hex_string(signature.serialize())
|
||||
.expect("Writing to a string should never error.");
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests_commons {
|
||||
use super::*;
|
||||
pub use crate::Storage;
|
||||
use helpers::*;
|
||||
use sloggers::{null::NullLoggerBuilder, Build};
|
||||
use tempdir::TempDir;
|
||||
|
||||
type T = StorageRawDir;
|
||||
|
||||
pub fn new_storage_with_tmp_dir() -> (T, TempDir) {
|
||||
let tmp_dir = TempDir::new("bls-remote-signer-test").unwrap();
|
||||
let storage = StorageRawDir::new(tmp_dir.path().to_str().unwrap()).unwrap();
|
||||
(storage, tmp_dir)
|
||||
}
|
||||
|
||||
pub fn get_null_logger() -> Logger {
|
||||
let log_builder = NullLoggerBuilder;
|
||||
log_builder.build().unwrap()
|
||||
}
|
||||
|
||||
pub fn new_backend_for_get_keys() -> (Backend<T>, TempDir) {
|
||||
let tmp_dir = TempDir::new("bls-remote-signer-test").unwrap();
|
||||
|
||||
let matches = set_matches(vec![
|
||||
"this_test",
|
||||
"--storage-raw-dir",
|
||||
tmp_dir.path().to_str().unwrap(),
|
||||
]);
|
||||
|
||||
let backend = match Backend::new(&matches, &get_null_logger()) {
|
||||
Ok(backend) => (backend),
|
||||
Err(e) => panic!("We should not be getting an err here: {}", e),
|
||||
};
|
||||
|
||||
(backend, tmp_dir)
|
||||
}
|
||||
|
||||
pub fn new_backend_for_signing() -> (Backend<T>, TempDir) {
|
||||
let (backend, tmp_dir) = new_backend_for_get_keys();
|
||||
|
||||
// This one has the whole fauna.
|
||||
add_sub_dirs(&tmp_dir);
|
||||
add_key_files(&tmp_dir);
|
||||
add_non_key_files(&tmp_dir);
|
||||
add_mismatched_key_file(&tmp_dir);
|
||||
|
||||
(backend, tmp_dir)
|
||||
}
|
||||
|
||||
pub fn assert_backend_new_error(matches: &ArgMatches, error_msg: &str) {
|
||||
match Backend::new(matches, &get_null_logger()) {
|
||||
Ok(_) => panic!("This invocation to Backend::new() should return error"),
|
||||
Err(e) => assert_eq!(e.to_string(), error_msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod backend_new {
|
||||
use super::*;
|
||||
use crate::tests_commons::*;
|
||||
use helpers::*;
|
||||
use tempdir::TempDir;
|
||||
|
||||
#[test]
|
||||
fn no_storage_type_supplied() {
|
||||
let matches = set_matches(vec!["this_test"]);
|
||||
|
||||
assert_backend_new_error(&matches, "No storage type supplied.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn given_path_does_not_exist() {
|
||||
let matches = set_matches(vec!["this_test", "--storage-raw-dir", "/dev/null/foo"]);
|
||||
|
||||
assert_backend_new_error(&matches, "Storage Raw Dir: Path does not exist.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn given_path_is_not_a_dir() {
|
||||
let matches = set_matches(vec!["this_test", "--storage-raw-dir", "/dev/null"]);
|
||||
|
||||
assert_backend_new_error(&matches, "Storage Raw Dir: Path is not a directory.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn given_inaccessible() {
|
||||
let tmp_dir = TempDir::new("bls-remote-signer-test").unwrap();
|
||||
set_permissions(tmp_dir.path(), 0o40311);
|
||||
|
||||
let matches = set_matches(vec![
|
||||
"this_test",
|
||||
"--storage-raw-dir",
|
||||
tmp_dir.path().to_str().unwrap(),
|
||||
]);
|
||||
|
||||
let result = Backend::new(&matches, &get_null_logger());
|
||||
|
||||
// A `d-wx--x--x` directory is innaccesible but not unwrittable.
|
||||
// By switching back to `drwxr-xr-x` we can get rid of the
|
||||
// temporal directory once we leave this scope.
|
||||
set_permissions(tmp_dir.path(), 0o40755);
|
||||
|
||||
match result {
|
||||
Ok(_) => panic!("This invocation to Backend::new() should return error"),
|
||||
Err(e) => assert_eq!(e.to_string(), "Storage Raw Dir: PermissionDenied",),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn happy_path() {
|
||||
let (_backend, _tmp_dir) = new_backend_for_get_keys();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod backend_raw_dir_get_keys {
|
||||
use crate::tests_commons::*;
|
||||
use helpers::*;
|
||||
|
||||
#[test]
|
||||
fn empty_dir() {
|
||||
let (backend, _tmp_dir) = new_backend_for_get_keys();
|
||||
|
||||
assert_eq!(backend.get_keys().unwrap().len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn some_files_are_not_public_keys() {
|
||||
let (backend, tmp_dir) = new_backend_for_get_keys();
|
||||
|
||||
add_sub_dirs(&tmp_dir);
|
||||
add_key_files(&tmp_dir);
|
||||
add_non_key_files(&tmp_dir);
|
||||
|
||||
assert_eq!(backend.get_keys().unwrap().len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_files_are_public_keys() {
|
||||
let (backend, tmp_dir) = new_backend_for_get_keys();
|
||||
add_key_files(&tmp_dir);
|
||||
|
||||
assert_eq!(backend.get_keys().unwrap().len(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod backend_raw_dir_sign_message {
|
||||
use crate::tests_commons::*;
|
||||
use helpers::*;
|
||||
use types::Hash256;
|
||||
|
||||
#[test]
|
||||
fn invalid_public_key() {
|
||||
let (backend, _tmp_dir) = new_backend_for_signing();
|
||||
|
||||
let test_case = |public_key_param: &str| {
|
||||
assert_eq!(
|
||||
backend
|
||||
.clone()
|
||||
.sign_message(
|
||||
public_key_param,
|
||||
Hash256::from_slice(&hex::decode(SIGNING_ROOT).unwrap())
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
format!("Invalid public key: {}", public_key_param)
|
||||
);
|
||||
};
|
||||
|
||||
test_case("abcdef"); // Length < 96.
|
||||
test_case(&format!("{}55", PUBLIC_KEY_1)); // Length > 96.
|
||||
test_case(SILLY_FILE_NAME_1); // Length == 96; Invalid hex characters.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storage_error() {
|
||||
let (backend, tmp_dir) = new_backend_for_signing();
|
||||
|
||||
set_permissions(tmp_dir.path(), 0o40311);
|
||||
set_permissions(&tmp_dir.path().join(PUBLIC_KEY_1), 0o40311);
|
||||
|
||||
let result = backend.sign_message(
|
||||
PUBLIC_KEY_1,
|
||||
Hash256::from_slice(&hex::decode(SIGNING_ROOT).unwrap()),
|
||||
);
|
||||
|
||||
set_permissions(tmp_dir.path(), 0o40755);
|
||||
set_permissions(&tmp_dir.path().join(PUBLIC_KEY_1), 0o40755);
|
||||
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
"Storage error: PermissionDenied"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_not_found() {
|
||||
let (backend, _tmp_dir) = new_backend_for_signing();
|
||||
|
||||
assert_eq!(
|
||||
backend
|
||||
.sign_message(
|
||||
ABSENT_PUBLIC_KEY,
|
||||
Hash256::from_slice(&hex::decode(SIGNING_ROOT).unwrap())
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
format!("Key not found: {}", ABSENT_PUBLIC_KEY)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_mismatch() {
|
||||
let (backend, _tmp_dir) = new_backend_for_signing();
|
||||
|
||||
assert_eq!(
|
||||
backend
|
||||
.sign_message(
|
||||
MISMATCHED_PUBLIC_KEY,
|
||||
Hash256::from_slice(&hex::decode(SIGNING_ROOT).unwrap())
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
format!("Key mismatch: {}", MISMATCHED_PUBLIC_KEY)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn happy_path() {
|
||||
let (backend, _tmp_dir) = new_backend_for_signing();
|
||||
|
||||
let test_case = |public_key: &str, signature: &str| {
|
||||
assert_eq!(
|
||||
backend
|
||||
.clone()
|
||||
.sign_message(
|
||||
public_key,
|
||||
Hash256::from_slice(&hex::decode(SIGNING_ROOT).unwrap())
|
||||
)
|
||||
.unwrap(),
|
||||
signature
|
||||
);
|
||||
};
|
||||
|
||||
test_case(PUBLIC_KEY_1, EXPECTED_SIGNATURE_1);
|
||||
test_case(PUBLIC_KEY_2, EXPECTED_SIGNATURE_2);
|
||||
test_case(PUBLIC_KEY_3, EXPECTED_SIGNATURE_3);
|
||||
}
|
||||
}
|
||||
10
remote_signer/backend/src/storage.rs
Normal file
10
remote_signer/backend/src/storage.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use crate::{BackendError, ZeroizeString};
|
||||
|
||||
/// The storage medium for the secret keys used by a `Backend`.
|
||||
pub trait Storage: 'static + Clone + Send + Sync {
|
||||
/// Queries storage for the available keys to sign.
|
||||
fn get_keys(&self) -> Result<Vec<String>, BackendError>;
|
||||
|
||||
/// Retrieves secret key from storage, using its public key as reference.
|
||||
fn get_secret_key(&self, input: &str) -> Result<ZeroizeString, BackendError>;
|
||||
}
|
||||
181
remote_signer/backend/src/storage_raw_dir.rs
Normal file
181
remote_signer/backend/src/storage_raw_dir.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use crate::{BackendError, Storage, ZeroizeString, PUBLIC_KEY_REGEX};
|
||||
use std::fs::read_dir;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::Read;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StorageRawDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl StorageRawDir {
|
||||
/// Initializes the storage with the given path, verifying
|
||||
/// whether it is a directory and if its available to the user.
|
||||
/// Does not list, nor verify the contents of the directory.
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, String> {
|
||||
let path = path.as_ref();
|
||||
|
||||
if !path.exists() {
|
||||
return Err("Path does not exist.".to_string());
|
||||
}
|
||||
|
||||
if !path.is_dir() {
|
||||
return Err("Path is not a directory.".to_string());
|
||||
}
|
||||
|
||||
read_dir(path).map_err(|e| format!("{:?}", e.kind()))?;
|
||||
|
||||
Ok(Self {
|
||||
path: path.to_path_buf(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Storage for StorageRawDir {
|
||||
/// List all the files in the directory having a BLS public key name.
|
||||
/// This function DOES NOT check the contents of each file.
|
||||
fn get_keys(&self) -> Result<Vec<String>, BackendError> {
|
||||
let entries = read_dir(&self.path).map_err(BackendError::from)?;
|
||||
|
||||
// We are silently suppressing errors in this chain
|
||||
// because we only care about files actually passing these filters.
|
||||
let keys: Vec<String> = entries
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| !entry.path().is_dir())
|
||||
.map(|entry| entry.file_name().into_string())
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|name| PUBLIC_KEY_REGEX.is_match(name))
|
||||
.collect();
|
||||
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
/// Gets a requested secret key by their reference, its public key.
|
||||
/// This function DOES NOT check the contents of the retrieved file.
|
||||
fn get_secret_key(&self, input: &str) -> Result<ZeroizeString, BackendError> {
|
||||
let file = File::open(self.path.join(input)).map_err(|e| match e.kind() {
|
||||
std::io::ErrorKind::NotFound => BackendError::KeyNotFound(input.to_string()),
|
||||
_ => e.into(),
|
||||
})?;
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
|
||||
let mut secret_key = String::new();
|
||||
buf_reader.read_to_string(&mut secret_key)?;
|
||||
|
||||
// Remove that `\n` without cloning.
|
||||
secret_key.pop();
|
||||
|
||||
Ok(ZeroizeString::from(secret_key))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod get_keys {
|
||||
use crate::tests_commons::*;
|
||||
use helpers::*;
|
||||
|
||||
#[test]
|
||||
fn problem_with_path() {
|
||||
let (storage, tmp_dir) = new_storage_with_tmp_dir();
|
||||
add_key_files(&tmp_dir);
|
||||
|
||||
// All good and fancy, let's make the dir innacessible now.
|
||||
set_permissions(tmp_dir.path(), 0o40311);
|
||||
|
||||
let result = storage.get_keys();
|
||||
|
||||
// Give permissions back, we want the tempdir to be deleted.
|
||||
set_permissions(tmp_dir.path(), 0o40755);
|
||||
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
"Storage error: PermissionDenied"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_files_in_dir() {
|
||||
let (storage, _tmp_dir) = new_storage_with_tmp_dir();
|
||||
|
||||
assert_eq!(storage.get_keys().unwrap().len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_files_in_dir_are_public_keys() {
|
||||
let (storage, tmp_dir) = new_storage_with_tmp_dir();
|
||||
add_sub_dirs(&tmp_dir);
|
||||
add_non_key_files(&tmp_dir);
|
||||
|
||||
assert_eq!(storage.get_keys().unwrap().len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_all_files_have_public_key_names() {
|
||||
let (storage, tmp_dir) = new_storage_with_tmp_dir();
|
||||
add_sub_dirs(&tmp_dir);
|
||||
add_key_files(&tmp_dir);
|
||||
add_non_key_files(&tmp_dir);
|
||||
|
||||
assert_eq!(storage.get_keys().unwrap().len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_files_do_have_public_key_names() {
|
||||
let (storage, tmp_dir) = new_storage_with_tmp_dir();
|
||||
add_key_files(&tmp_dir);
|
||||
|
||||
assert_eq!(storage.get_keys().unwrap().len(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod get_secret_key {
|
||||
use crate::tests_commons::*;
|
||||
use helpers::*;
|
||||
|
||||
#[test]
|
||||
fn unaccessible_file() {
|
||||
let (storage, tmp_dir) = new_storage_with_tmp_dir();
|
||||
add_key_files(&tmp_dir);
|
||||
|
||||
set_permissions(tmp_dir.path(), 0o40311);
|
||||
set_permissions(&tmp_dir.path().join(PUBLIC_KEY_1), 0o40311);
|
||||
|
||||
let result = storage.get_secret_key(PUBLIC_KEY_1);
|
||||
|
||||
set_permissions(tmp_dir.path(), 0o40755);
|
||||
set_permissions(&tmp_dir.path().join(PUBLIC_KEY_1), 0o40755);
|
||||
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
"Storage error: PermissionDenied"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_does_not_exist() {
|
||||
let (storage, _tmp_dir) = new_storage_with_tmp_dir();
|
||||
|
||||
assert_eq!(
|
||||
storage
|
||||
.get_secret_key(PUBLIC_KEY_1)
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
format!("Key not found: {}", PUBLIC_KEY_1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn happy_path() {
|
||||
let (storage, tmp_dir) = new_storage_with_tmp_dir();
|
||||
add_key_files(&tmp_dir);
|
||||
|
||||
assert_eq!(
|
||||
storage.get_secret_key(PUBLIC_KEY_1).unwrap().as_ref(),
|
||||
SECRET_KEY_1.as_bytes()
|
||||
);
|
||||
}
|
||||
}
|
||||
123
remote_signer/backend/src/utils.rs
Normal file
123
remote_signer/backend/src/utils.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use crate::{BackendError, ZeroizeString};
|
||||
use bls::SecretKey;
|
||||
use hex::decode;
|
||||
use std::fmt::{Error, Write};
|
||||
use std::str;
|
||||
|
||||
// hex::encode only allows up to 32 bytes.
|
||||
pub fn bytes96_to_hex_string(data: [u8; 96]) -> Result<String, Error> {
|
||||
static CHARS: &[u8] = b"0123456789abcdef";
|
||||
let mut s = String::with_capacity(96 * 2 + 2);
|
||||
|
||||
s.write_char('0')?;
|
||||
s.write_char('x')?;
|
||||
|
||||
for &byte in data.iter() {
|
||||
s.write_char(CHARS[(byte >> 4) as usize].into())?;
|
||||
s.write_char(CHARS[(byte & 0xf) as usize].into())?;
|
||||
}
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// Validates the match as a BLS pair of the public and secret keys given,
|
||||
/// consuming the secret key parameter, and returning a deserialized SecretKey.
|
||||
pub fn validate_bls_pair(
|
||||
public_key: &str,
|
||||
secret_key: ZeroizeString,
|
||||
) -> Result<SecretKey, BackendError> {
|
||||
let secret_key: SecretKey = secret_key.into_bls_sk().map_err(|e| {
|
||||
BackendError::InvalidSecretKey(format!("public_key: {}; {}", public_key, e))
|
||||
})?;
|
||||
|
||||
let pk_param_as_bytes = decode(&public_key)
|
||||
.map_err(|e| BackendError::InvalidPublicKey(format!("{}; {}", public_key, e)))?;
|
||||
|
||||
if &secret_key.public_key().serialize()[..] != pk_param_as_bytes.as_slice() {
|
||||
return Err(BackendError::KeyMismatch(public_key.to_string()));
|
||||
}
|
||||
|
||||
Ok(secret_key)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod functions {
|
||||
use super::*;
|
||||
use helpers::*;
|
||||
|
||||
#[test]
|
||||
fn fn_bytes96_to_hex_string() {
|
||||
assert_eq!(
|
||||
bytes96_to_hex_string(EXPECTED_SIGNATURE_1_BYTES).unwrap(),
|
||||
EXPECTED_SIGNATURE_1
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
bytes96_to_hex_string(EXPECTED_SIGNATURE_2_BYTES).unwrap(),
|
||||
EXPECTED_SIGNATURE_2
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
bytes96_to_hex_string(EXPECTED_SIGNATURE_3_BYTES).unwrap(),
|
||||
EXPECTED_SIGNATURE_3
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_validate_bls_pair() {
|
||||
let test_ok_case = |pk: &str, sk: ZeroizeString, sk_bytes: &[u8; 32]| {
|
||||
let serialized_secret_key = validate_bls_pair(pk, sk).unwrap().serialize();
|
||||
assert_eq!(serialized_secret_key.as_bytes().to_vec(), sk_bytes.to_vec());
|
||||
};
|
||||
|
||||
test_ok_case(
|
||||
PUBLIC_KEY_1,
|
||||
ZeroizeString::from(SECRET_KEY_1.to_string()),
|
||||
&SECRET_KEY_1_BYTES,
|
||||
);
|
||||
|
||||
let test_error_case = |pk: &str, sk: ZeroizeString, expected_error: &str| {
|
||||
assert_eq!(
|
||||
validate_bls_pair(pk, sk).err().unwrap().to_string(),
|
||||
expected_error
|
||||
);
|
||||
};
|
||||
|
||||
test_error_case(
|
||||
PUBLIC_KEY_2,
|
||||
ZeroizeString::from("TamperedKey%#$#%#$$&##00£$%$$£%$".to_string()),
|
||||
&format!(
|
||||
"Invalid secret key: public_key: {}; Invalid hex character: T at index 0",
|
||||
PUBLIC_KEY_2
|
||||
),
|
||||
);
|
||||
|
||||
test_error_case(
|
||||
PUBLIC_KEY_2,
|
||||
ZeroizeString::from("deadbeef".to_string()),
|
||||
&format!(
|
||||
"Invalid secret key: public_key: {}; InvalidSecretKeyLength {{ got: 4, expected: 32 }}",
|
||||
PUBLIC_KEY_2
|
||||
),
|
||||
);
|
||||
|
||||
let bad_pk_param = "not_validated_by_the_api_handler!";
|
||||
test_error_case(
|
||||
bad_pk_param,
|
||||
ZeroizeString::from(SECRET_KEY_1.to_string()),
|
||||
&format!("Invalid public key: {}; Odd number of digits", bad_pk_param),
|
||||
);
|
||||
|
||||
test_error_case(
|
||||
PUBLIC_KEY_1,
|
||||
ZeroizeString::from(SECRET_KEY_2.to_string()),
|
||||
&format!("Key mismatch: {}", PUBLIC_KEY_1),
|
||||
);
|
||||
|
||||
test_error_case(
|
||||
PUBLIC_KEY_2,
|
||||
ZeroizeString::from(SECRET_KEY_3.to_string()),
|
||||
&format!("Key mismatch: {}", PUBLIC_KEY_2),
|
||||
);
|
||||
}
|
||||
}
|
||||
222
remote_signer/backend/src/zeroize_string.rs
Normal file
222
remote_signer/backend/src/zeroize_string.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
use bls::SecretKey;
|
||||
use std::str;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// Provides a new-type wrapper around `String` that is zeroized on `Drop`.
|
||||
///
|
||||
/// Useful for ensuring that secret key memory is zeroed-out on drop.
|
||||
#[derive(Debug, Zeroize)]
|
||||
#[zeroize(drop)]
|
||||
pub struct ZeroizeString(String);
|
||||
|
||||
impl From<String> for ZeroizeString {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for ZeroizeString {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl ZeroizeString {
|
||||
/// Consumes the ZeroizeString, attempting to return a BLS SecretKey.
|
||||
pub fn into_bls_sk(self) -> Result<SecretKey, String> {
|
||||
let mut decoded_bytes = hex_string_to_bytes(&self.0)?;
|
||||
|
||||
let secret_key = SecretKey::deserialize(&decoded_bytes).map_err(|e| format!("{:?}", e))?;
|
||||
decoded_bytes.zeroize();
|
||||
|
||||
Ok(secret_key)
|
||||
}
|
||||
}
|
||||
|
||||
// An alternative to `hex::decode`, to allow for more control of
|
||||
// the objects created while decoding the secret key.
|
||||
fn hex_string_to_bytes(data: &str) -> Result<Vec<u8>, String> {
|
||||
if data.len() % 2 != 0 {
|
||||
return Err("Odd length".to_string());
|
||||
}
|
||||
|
||||
let mut vec: Vec<u8> = Vec::new();
|
||||
for i in 0..data.len() / 2 {
|
||||
vec.push(
|
||||
val(&data.as_bytes()[2 * i], 2 * i)? << 4
|
||||
| val(&data.as_bytes()[2 * i + 1], 2 * i + 1)?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(vec)
|
||||
}
|
||||
|
||||
// Auxiliar function for `hex_string_to_bytes`.
|
||||
fn val(c: &u8, idx: usize) -> Result<u8, String> {
|
||||
match c {
|
||||
b'A'..=b'F' => Ok(c - b'A' + 10),
|
||||
b'a'..=b'f' => Ok(c - b'a' + 10),
|
||||
b'0'..=b'9' => Ok(c - b'0'),
|
||||
_ => Err(format!(
|
||||
"Invalid hex character: {} at index {}",
|
||||
*c as char, idx
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod object {
|
||||
use super::*;
|
||||
use helpers::*;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[test]
|
||||
fn v_u8_zeroized() {
|
||||
// Create from `hex_string_to_bytes`, and record the pointer to its buffer.
|
||||
let mut decoded_bytes = hex_string_to_bytes(&SECRET_KEY_1.to_string()).unwrap();
|
||||
let old_pointer = decoded_bytes.as_ptr() as usize;
|
||||
|
||||
// Do something with the borrowed vector, and zeroize.
|
||||
let _ = SecretKey::deserialize(&decoded_bytes)
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
.unwrap();
|
||||
decoded_bytes.zeroize();
|
||||
|
||||
// Check it is pointing to the same buffer, and that it was deleted.
|
||||
assert_eq!(old_pointer as usize, decoded_bytes.as_ptr() as usize);
|
||||
assert!(decoded_bytes.is_empty());
|
||||
|
||||
// Check if the underlying bytes were zeroized.
|
||||
for i in 0..SECRET_KEY_1.len() / 2 {
|
||||
unsafe {
|
||||
assert_eq!(*((old_pointer + i) as *const u8), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_to_bls_sk() {
|
||||
let test_ok_case = |sk: &str, sk_b: &[u8]| {
|
||||
let z = ZeroizeString::from(sk.to_string());
|
||||
let sk: SecretKey = z.into_bls_sk().unwrap();
|
||||
assert_eq!(sk.serialize().as_bytes(), sk_b);
|
||||
};
|
||||
|
||||
let test_error_case = |sk: &str, err_msg: &str| {
|
||||
let z = ZeroizeString::from(sk.to_string());
|
||||
let err = z.into_bls_sk().err();
|
||||
assert_eq!(err, Some(err_msg.to_string()));
|
||||
};
|
||||
|
||||
test_ok_case(SECRET_KEY_1, &SECRET_KEY_1_BYTES);
|
||||
|
||||
test_error_case("Trolololololo", "Odd length");
|
||||
test_error_case("Trololololol", "Invalid hex character: T at index 0");
|
||||
test_error_case(
|
||||
"そんなことないでしょうけどう",
|
||||
"Invalid hex character: ã at index 0",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zeroized_after_drop() {
|
||||
let some_scope = |s: &str| -> usize {
|
||||
// Convert our literal into a String, then store the pointer
|
||||
// to the first byte of its slice.
|
||||
let s: String = s.to_string();
|
||||
let s_ptr = s.as_ptr();
|
||||
|
||||
// Just to make sure that the pointer of the string is NOT
|
||||
// the same as the pointer of the underlying buffer.
|
||||
assert_ne!(&s as *const String as usize, s_ptr as usize);
|
||||
|
||||
let z = ZeroizeString::from(s);
|
||||
|
||||
// Get the pointer to the underlying buffer,
|
||||
// We want to make sure is the same as the received string literal.
|
||||
// That is, no copies of the contents.
|
||||
let ptr_to_buf = z.as_ref().as_ptr();
|
||||
assert_eq!(ptr_to_buf, s_ptr);
|
||||
|
||||
// We exit this scope, returning to the caller the pointer to
|
||||
// the buffer, that we'll use to verify the zeroization.
|
||||
ptr_to_buf as usize
|
||||
};
|
||||
|
||||
// Call the closure.
|
||||
let ptr_to_buf = some_scope(SECRET_KEY_1);
|
||||
|
||||
// Check if the underlying bytes were zeroized.
|
||||
// At this point the first half is already reclaimed and assigned,
|
||||
// so we will just examine the other half.
|
||||
for i in SECRET_KEY_1.len() / 2..SECRET_KEY_1.len() {
|
||||
unsafe {
|
||||
assert_eq!(*((ptr_to_buf + i) as *const u8), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod functions {
|
||||
use super::*;
|
||||
use helpers::*;
|
||||
|
||||
#[test]
|
||||
fn fn_hex_string_to_bytes() {
|
||||
assert_eq!(
|
||||
hex_string_to_bytes(&"0aa".to_string()).err(),
|
||||
Some("Odd length".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
hex_string_to_bytes(&"0xdeadbeef".to_string()).err(),
|
||||
Some("Invalid hex character: x at index 1".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
hex_string_to_bytes(&"n00bn00b".to_string()).err(),
|
||||
Some("Invalid hex character: n at index 0".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
hex_string_to_bytes(&"abcdefgh".to_string()).err(),
|
||||
Some("Invalid hex character: g at index 6".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
hex_string_to_bytes(&SECRET_KEY_1).unwrap(),
|
||||
SECRET_KEY_1_BYTES
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
hex_string_to_bytes(&PUBLIC_KEY_1).unwrap(),
|
||||
PUBLIC_KEY_1_BYTES.to_vec()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
hex_string_to_bytes(&SIGNING_ROOT).unwrap(),
|
||||
SIGNING_ROOT_BYTES.to_vec()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
hex_string_to_bytes(&EXPECTED_SIGNATURE_1[2..]).unwrap(),
|
||||
EXPECTED_SIGNATURE_1_BYTES.to_vec()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
hex_string_to_bytes(&EXPECTED_SIGNATURE_2[2..]).unwrap(),
|
||||
EXPECTED_SIGNATURE_2_BYTES.to_vec()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
hex_string_to_bytes(&EXPECTED_SIGNATURE_3[2..]).unwrap(),
|
||||
EXPECTED_SIGNATURE_3_BYTES.to_vec()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
hex_string_to_bytes(&"0a0b11".to_string()).unwrap(),
|
||||
vec![10, 11, 17]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user