feat(interpreter-data)!: allow only deterministic signature algorithms (#734)

Some public signature algorithms require a RNG, but it is not
available in certain environments like smartcontracts.
This commit is contained in:
Ivan Boldyrev 2023-11-02 18:43:35 +04:00 committed by GitHub
parent 55da7a64aa
commit 15ce40a1cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 243 additions and 35 deletions

4
Cargo.lock generated
View File

@ -144,6 +144,7 @@ dependencies = [
"bs58 0.5.0",
"fluence-keypair",
"serde",
"thiserror",
]
[[package]]
@ -179,6 +180,7 @@ dependencies = [
"air-interpreter-cid",
"air-interpreter-data",
"air-interpreter-interface",
"air-interpreter-signatures",
"aquavm-air",
"avm-interface",
"avm-server",
@ -202,6 +204,7 @@ dependencies = [
"air-interpreter-signatures",
"air-test-utils",
"aquavm-air-parser",
"fluence-keypair",
"itertools",
"maplit",
"nom",
@ -341,6 +344,7 @@ dependencies = [
"air-utils",
"aquavm-air-parser",
"borsh 0.10.3",
"bs58 0.5.0",
"concat-idents",
"criterion 0.3.6",
"csv",

View File

@ -52,6 +52,7 @@ fluence-app-service = "0.29.0"
marine-rs-sdk = { version = "0.10.0", features = ["logger"] }
borsh = "0.10.3"
bs58 = "0.5.0"
# the feature just silence a warning in the criterion 0.3.x.
criterion = { version = "0.3.3", features = ["html_reports"] }
csv = "1.1.5"

View File

@ -24,9 +24,9 @@ use crate::INTERPRETER_SUCCESS;
use air_interpreter_data::InterpreterData;
use air_interpreter_interface::CallRequests;
use air_interpreter_signatures::KeyPair;
use air_utils::measure;
use fluence_keypair::error::SigningError;
use fluence_keypair::KeyPair;
use std::fmt::Debug;
use std::hash::Hash;
@ -138,7 +138,7 @@ fn sign_result(exec_ctx: &mut ExecutionCtx<'_>, keypair: &KeyPair) -> Result<(),
.map_err(signing_error_into_outcome)?;
let current_pubkey = keypair.public();
exec_ctx.signature_store.put(current_pubkey.into(), current_signature);
exec_ctx.signature_store.put(current_pubkey, current_signature);
Ok(())
}

View File

@ -81,11 +81,8 @@ pub enum PreparationError {
},
/// Malformed keypair format data.
#[error("malformed keypair format: {error:?}")]
MalformedKeyPairData {
#[from]
error: fluence_keypair::error::DecodingError,
},
#[error("malformed keypair format: {0}")]
MalformedKeyPairData(#[from] air_interpreter_signatures::KeyError),
/// Failed to verify CidStore contents of the current data.
#[error(transparent)]

View File

@ -21,10 +21,11 @@ use crate::execution_step::TraceHandler;
use air_interpreter_data::InterpreterData;
use air_interpreter_interface::RunParameters;
use air_interpreter_signatures::KeyError;
use air_interpreter_signatures::KeyPair;
use air_interpreter_signatures::SignatureStore;
use air_parser::ast::Instruction;
use fluence_keypair::KeyFormat;
use fluence_keypair::KeyPair;
use std::convert::TryFrom;
@ -88,7 +89,7 @@ pub(crate) fn prepare<'i>(
)?;
let trace_handler = TraceHandler::from_trace(prev_data.trace, current_data.trace);
let key_format = KeyFormat::try_from(run_parameters.key_format)?;
let key_format = KeyFormat::try_from(run_parameters.key_format).map_err(KeyError::from)?;
let keypair = KeyPair::from_secret_key(run_parameters.secret_key_bytes, key_format)?;
let result = PreparationDescriptor {

View File

@ -16,7 +16,9 @@
use crate::ExecutionError;
use air_interpreter_signatures::{PeerCidTracker, SignatureStore};
use air_interpreter_signatures::KeyPair;
use air_interpreter_signatures::PeerCidTracker;
use air_interpreter_signatures::SignatureStore;
#[cfg(feature = "gen_signatures")]
#[tracing::instrument(skip_all)]
@ -24,7 +26,7 @@ pub(crate) fn sign_produced_cids(
signature_tracker: &mut PeerCidTracker,
signature_store: &mut SignatureStore,
salt: &str,
keypair: &fluence_keypair::KeyPair,
keypair: &KeyPair,
) -> Result<(), ExecutionError> {
use crate::UncatchableError;
@ -42,7 +44,7 @@ pub(crate) fn sign_produced_cids(
_signature_tracker: &mut PeerCidTracker,
_signature_store: &mut SignatureStore,
_salt: &str,
_keypair: &fluence_keypair::KeyPair,
_keypair: &KeyPair,
) -> Result<(), ExecutionError> {
Ok(())
}

View File

@ -14,6 +14,8 @@
* limitations under the License.
*/
#[cfg(feature = "check_signatures")]
mod algorithms;
#[cfg(feature = "check_signatures")]
mod attacks;
#[cfg(feature = "check_signatures")]

View File

@ -0,0 +1,95 @@
/*
* Copyright 2023 Fluence Labs Limited
*
* 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.
*/
use air::{min_supported_version, PreparationError};
use air_interpreter_data::{verification::DataVerifierError, InterpreterData};
use air_interpreter_signatures::KeyError;
use air_test_utils::{
assert_error_eq,
prelude::{request_sent_by, unit_call_service},
test_runner::{create_avm, create_avm_with_key, NativeAirRunner, TestRunParameters},
};
use fluence_keypair::KeyFormat;
use serde_json::json;
/// Checking that other peers' key algorithms are valid.
#[test]
fn test_banned_signature() {
let air_script = r#"(call "other_peer_id" ("" "") [])"#;
let bad_algo_keypair = fluence_keypair::KeyPair::generate_secp256k1();
let bad_algo_pk = bad_algo_keypair.public();
let bad_algo_signature: air_interpreter_signatures::Signature =
air_interpreter_signatures::sign_cids(vec![], "particle_id", &bad_algo_keypair)
.unwrap()
.into();
let bad_algo_pk_ser = bs58::encode(bad_algo_pk.encode()).into_string();
let bad_signature_store = json!({
bad_algo_pk_ser: bad_algo_signature,
});
let bad_peer_id = bad_algo_pk.to_peer_id().to_string();
let trace = vec![request_sent_by("init_peer_fake_id")];
let mut data = serde_json::to_value(InterpreterData::from_execution_result(
trace.into(),
<_>::default(),
<_>::default(),
<_>::default(),
min_supported_version().clone(),
))
.unwrap();
data["signatures"] = bad_signature_store;
let current_data = data.to_string();
let mut avm = create_avm(unit_call_service(), "other_peer_id");
let res = avm
.call(
air_script,
"",
current_data,
TestRunParameters::from_init_peer_id("init_peer_fake_id"),
)
.unwrap();
assert_error_eq!(
&res,
PreparationError::DataSignatureCheckError(DataVerifierError::MalformedKey {
error: KeyError::AlgorithmNotWhitelisted(KeyFormat::Secp256k1),
peer_id: bad_peer_id
})
);
}
/// Checking that local key is valid.
#[test]
fn test_banned_signing_key() {
let air_script = "(null)";
let bad_algo_keypair = fluence_keypair::KeyPair::generate_secp256k1();
let mut avm = create_avm_with_key::<NativeAirRunner>(bad_algo_keypair, unit_call_service());
let res = avm
.call(air_script, "", "", TestRunParameters::from_init_peer_id("init_peer_id"))
.unwrap();
assert_error_eq!(
&res,
PreparationError::MalformedKeyPairData(KeyError::AlgorithmNotWhitelisted(KeyFormat::Secp256k1))
);
}

View File

@ -46,7 +46,7 @@ fn issue_310() {
0,
None,
call_results,
&key_pair,
key_pair.as_inner(),
particle_id.to_owned(),
)
.unwrap()

View File

@ -17,11 +17,12 @@
use std::rc::Rc;
use air_interpreter_cid::CidRef;
use air_interpreter_signatures::KeyError;
use thiserror::Error as ThisError;
#[derive(Debug, ThisError)]
pub enum DataVerifierError {
#[error(transparent)]
MalformedKey(fluence_keypair::error::DecodingError),
#[error("malformed key at peer: {peer_id:?}: {error}")]
MalformedKey { error: KeyError, peer_id: String },
#[error(transparent)]
MalformedSignature(fluence_keypair::error::DecodingError),

View File

@ -42,6 +42,16 @@ impl<'data> DataVerifier<'data> {
// it can be further optimized if only required parts are passed
// SignatureStore is not used elsewhere
pub fn new(data: &'data InterpreterData, salt: &'data str) -> Result<Self, DataVerifierError> {
// validate key algoritms
for (public_key, _) in data.signatures.iter() {
public_key
.validate()
.map_err(|error| DataVerifierError::MalformedKey {
error,
peer_id: public_key.to_peer_id(),
})?;
}
// it contains signature too; if we try to add a value to a peer w/o signature, it is an immediate error
let mut grouped_cids: HashMap<Box<str>, PeerInfo<'data>> = data
.signatures

View File

@ -18,3 +18,8 @@ bs58 = "0.5.0"
borsh = { version = "0.10.3", features = ["rc"]}
borsh-derive = "0.10.3"
serde = { version = "1.0.190", features = ["derive"] }
thiserror = "1.0.49"
[features]
default = ["rand"]
rand = []

View File

@ -33,14 +33,24 @@ mod trackers;
pub use crate::stores::*;
pub use crate::trackers::*;
pub use fluence_keypair::KeyPair;
pub use fluence_keypair::KeyFormat;
use borsh::BorshSerialize;
use fluence_keypair::error::SigningError;
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::hash::Hash;
use std::ops::Deref;
#[derive(thiserror::Error, Debug)]
pub enum KeyError {
#[error("signature algorithm {0:?} not whitelisted")]
AlgorithmNotWhitelisted(fluence_keypair::KeyFormat),
#[error("invalid key data: {0}")]
InvalidKeyData(#[from] fluence_keypair::error::DecodingError),
}
/// An opaque serializable representation of a public key.
///
/// It can be a string or a binary, you shouldn't care about it unless you change serialization format.
@ -55,6 +65,10 @@ pub struct PublicKey(
);
impl PublicKey {
pub fn new(inner: fluence_keypair::PublicKey) -> Self {
Self(inner)
}
pub fn verify<T: BorshSerialize + ?Sized>(
&self,
value: &T,
@ -66,6 +80,15 @@ impl PublicKey {
let serialized_value = SaltedData::new(&value, salt).serialize();
pk.verify(&serialized_value, signature)
}
pub fn to_peer_id(&self) -> String {
self.0.to_peer_id().to_string()
}
pub fn validate(&self) -> Result<(), KeyError> {
let key_format = self.get_key_format();
validate_with_key_format((), key_format)
}
}
impl Deref for PublicKey {
@ -82,9 +105,72 @@ impl Hash for PublicKey {
}
}
impl From<fluence_keypair::PublicKey> for PublicKey {
fn from(value: fluence_keypair::PublicKey) -> Self {
Self(value)
#[derive(Clone)]
pub struct KeyPair(fluence_keypair::KeyPair);
impl KeyPair {
pub fn new(inner: fluence_keypair::KeyPair) -> Result<Self, KeyError> {
let key_format = inner.key_format();
validate_with_key_format((), key_format)?;
Ok(Self(inner))
}
pub fn from_secret_key(secret_key: Vec<u8>, key_format: KeyFormat) -> Result<Self, KeyError> {
let inner = fluence_keypair::KeyPair::from_secret_key(secret_key, key_format)?;
Self::new(inner)
}
pub fn public(&self) -> PublicKey {
PublicKey::new(self.0.public())
}
pub fn key_format(&self) -> KeyFormat {
self.0.key_format()
}
pub fn sign(&self, msg: &[u8]) -> Result<Signature, SigningError> {
self.0.sign(msg).map(Signature::new)
}
pub fn secret(&self) -> Vec<u8> {
self.0.secret().expect("cannot fail on supported formats")
}
pub fn into_inner(self) -> fluence_keypair::KeyPair {
self.0
}
pub fn as_inner(&self) -> &fluence_keypair::KeyPair {
&self.0
}
#[cfg(feature = "rand")]
pub fn generate(key_format: KeyFormat) -> Result<Self, KeyError> {
validate_with_key_format((), key_format)?;
Ok(Self(fluence_keypair::KeyPair::generate(key_format)))
}
}
impl TryFrom<fluence_keypair::KeyPair> for KeyPair {
type Error = KeyError;
fn try_from(value: fluence_keypair::KeyPair) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<KeyPair> for fluence_keypair::KeyPair {
fn from(value: KeyPair) -> Self {
value.0
}
}
pub(crate) fn validate_with_key_format<V>(inner: V, key_format: KeyFormat) -> Result<V, KeyError> {
match key_format {
fluence_keypair::KeyFormat::Ed25519 => Ok(inner),
_ => Err(KeyError::AlgorithmNotWhitelisted(key_format)),
}
}

View File

@ -34,7 +34,7 @@ impl serde::de::Visitor<'_> for PublicKeyVisitor {
type Value = fluence_keypair::PublicKey;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("expecting a base58-encoded public key string")
formatter.write_str("a base58-encoded public key string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
@ -43,9 +43,8 @@ impl serde::de::Visitor<'_> for PublicKeyVisitor {
{
use serde::de;
let public_key = fluence_keypair::PublicKey::from_base58(v)
.map_err(|_| de::Error::invalid_value(de::Unexpected::Str(v), &self))?;
Ok(public_key)
fluence_keypair::PublicKey::from_base58(v)
.map_err(|_| de::Error::invalid_value(de::Unexpected::Str(v), &self))
}
}

View File

@ -14,11 +14,11 @@
* limitations under the License.
*/
use crate::KeyPair;
use crate::SaltedData;
use air_interpreter_cid::{CidRef, CID};
use fluence_keypair::error::SigningError;
use fluence_keypair::KeyPair;
use std::rc::Rc;
@ -48,17 +48,17 @@ impl PeerCidTracker {
salt: &str,
keypair: &KeyPair,
) -> Result<crate::Signature, SigningError> {
sign_cids(self.cids.clone(), salt, keypair)
sign_cids(self.cids.clone(), salt, &keypair.0).map(Into::into)
}
}
fn sign_cids(
pub fn sign_cids(
mut cids: Vec<Rc<CidRef>>,
salt: &str,
keypair: &KeyPair,
) -> Result<crate::Signature, SigningError> {
keypair: &fluence_keypair::KeyPair,
) -> Result<fluence_keypair::Signature, SigningError> {
cids.sort_unstable();
let serialized_cids = SaltedData::new(&cids, salt).serialize();
keypair.sign(&serialized_cids).map(crate::Signature::new)
keypair.sign(&serialized_cids)
}

View File

@ -19,6 +19,7 @@ aquavm-air = { version = "0.54.0", path = "../../../air" }
air-interpreter-cid = { version = "0.6.0", path = "../interpreter-cid" }
air-interpreter-data = { version = "0.14.0", path = "../interpreter-data" }
air-interpreter-interface = { version = "0.15.1", path = "../interpreter-interface" }
air-interpreter-signatures = { version = "0.1.3", path = "../interpreter-signatures" }
avm-interface = { version = "0.29.2", path = "../../../avm/interface" }
avm-server = { version = "0.33.3", path = "../../../avm/server" }
marine-rs-sdk = "0.10.0"

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
use fluence_keypair::KeyPair;
use air_interpreter_signatures::KeyPair;
use rand_chacha::rand_core::SeedableRng;
/// Derive fake keypair for testing proposes.
@ -25,6 +25,7 @@ use rand_chacha::rand_core::SeedableRng;
// Should be moved to test lib when keypair interface PR is merged.
pub fn derive_dummy_keypair(seed: &str) -> (KeyPair, String) {
use sha2::{Digest as _, Sha256};
use std::convert::TryFrom;
let mut rng = {
let mut hasher = Sha256::new();
@ -33,7 +34,8 @@ pub fn derive_dummy_keypair(seed: &str) -> (KeyPair, String) {
};
let keypair_ed25519 = ed25519_dalek::Keypair::generate(&mut rng);
let keypair: KeyPair = KeyPair::Ed25519(keypair_ed25519.into());
let keypair = fluence_keypair::KeyPair::Ed25519(keypair_ed25519.into());
let keypair = KeyPair::try_from(keypair).expect("cannot happen");
let peer_id = keypair.public().to_peer_id().to_string();
(keypair, peer_id)

View File

@ -177,14 +177,15 @@ pub fn create_custom_avm<R: AirRunner>(
TestRunner {
runner,
call_service,
keypair,
keypair: keypair.into_inner(),
}
}
pub fn create_avm_with_key<R: AirRunner>(
keypair: KeyPair,
keypair: impl Into<KeyPair>,
call_service: CallServiceClosure,
) -> TestRunner<R> {
let keypair = keypair.into();
let current_peer_id = keypair.public().to_peer_id().to_string();
let runner = R::new(current_peer_id);

View File

@ -18,6 +18,7 @@ air-test-utils = { version = "0.12.1", path = "../air-lib/test-utils" }
aquavm-air-parser = { version = "0.10.0", path = "../air-lib/air-parser" }
itertools = "0.10.5"
fluence-keypair = "0.10.1"
strum = { version="0.24.1", features=["derive"] }
nom = "7.1.3"
nom_locate = "4.1.0"

View File

@ -22,7 +22,6 @@ use crate::{
services::{services_to_call_service_closure, MarineServiceHandle, NetworkServices},
};
use air_interpreter_signatures::KeyPair;
use air_test_utils::{
key_utils::derive_dummy_keypair,
test_runner::{
@ -30,6 +29,7 @@ use air_test_utils::{
},
RawAVMOutcome,
};
use fluence_keypair::KeyPair;
use std::{borrow::Borrow, cell::RefCell, collections::HashMap, hash::Hash, ops::Deref, rc::Rc};
@ -86,7 +86,7 @@ pub struct Peer<R> {
}
impl<R: AirRunner> Peer<R> {
pub fn new(keypair: KeyPair, services: Rc<[MarineServiceHandle]>) -> Self {
pub fn new(keypair: impl Into<KeyPair>, services: Rc<[MarineServiceHandle]>) -> Self {
let call_service = services_to_call_service_closure(services);
let runner = create_avm_with_key::<R>(keypair, call_service);