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

feat: verify particle signatures [NET-539] #1811

Merged
merged 12 commits into from
Oct 11, 2023
12 changes: 12 additions & 0 deletions aquamarine/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use humantime::FormattedDuration;
use thiserror::Error;

use particle_protocol::ParticleError;

#[derive(Debug, Error)]
pub enum AquamarineApiError {
#[error("AquamarineApiError::ParticleExpired: particle_id = {particle_id}")]
Expand Down Expand Up @@ -44,6 +46,11 @@ pub enum AquamarineApiError {
"AquamarineApiError::AquamarineQueueFull: can't send particle {particle_id:?} to Aquamarine"
)]
AquamarineQueueFull { particle_id: Option<String> },
#[error("AquamarineApiError::SignatureVerificationFailed: particle_id = {particle_id}, error = {err}")]
SignatureVerificationFailed {
particle_id: String,
err: ParticleError,
},
}

impl AquamarineApiError {
Expand All @@ -52,6 +59,11 @@ impl AquamarineApiError {
AquamarineApiError::ParticleExpired { particle_id } => Some(particle_id),
AquamarineApiError::OneshotCancelled { particle_id } => Some(particle_id),
AquamarineApiError::ExecutionTimedOut { particle_id, .. } => Some(particle_id),
// Should it be `None` considering usage of signature as particle id?
// It can compromise valid particles into thinking they are invalid.
// But still there can be a case when signature was generated wrong
// and client will never know about it.
akim-bow marked this conversation as resolved.
Show resolved Hide resolved
AquamarineApiError::SignatureVerificationFailed { .. } => None,
AquamarineApiError::AquamarineDied { particle_id } => particle_id,
AquamarineApiError::AquamarineQueueFull { particle_id, .. } => particle_id,
}
Expand Down
10 changes: 10 additions & 0 deletions aquamarine/src/plumber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ impl<RT: AquaRuntime, F: ParticleFunctionStatic> Plumber<RT, F> {
return;
}

if let Err(err) = particle.verify() {
justprosh marked this conversation as resolved.
Show resolved Hide resolved
tracing::warn!(target: "signature", particle_id = particle.id, "Particle signature verification failed: {err:?}");
self.events
.push_back(Err(AquamarineApiError::SignatureVerificationFailed {
particle_id: particle.id,
err,
}));
return;
}

let builtins = &self.builtins;
let key = (particle.id.clone(), worker_id);
let entry = self.actors.entry(key);
Expand Down
8 changes: 6 additions & 2 deletions crates/local-vm/src/local_vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,15 +303,19 @@ pub fn make_particle(

tracing::info!(particle_id = id, "Made a particle");

Particle {
let mut particle = Particle {
id,
init_peer_id: peer_id,
timestamp,
ttl,
script,
signature: vec![],
data: particle_data,
}
};

particle.sign(key_pair).expect("sign particle");

particle
}

pub fn read_args(
Expand Down
2 changes: 1 addition & 1 deletion crates/nox-tests/tests/local_vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ fn make() {
&mut local_vm_a,
false,
Duration::from_secs(20),
&keypair_b,
&keypair_a,
);

let args = read_args(particle, client_b, &mut local_vm_b, &keypair_b)
Expand Down
2 changes: 1 addition & 1 deletion crates/system-services/src/distro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ impl SystemServiceDistros {
fn versions_from(packages: &HashMap<String, PackageDistro>) -> Versions {
let mut versions = Self::default_versions();
for (name, package) in packages {
match ServiceKey::from_string(&name) {
match ServiceKey::from_string(name) {
Some(AquaIpfs) => {
versions.aqua_ipfs_version = package.version;
}
Expand Down
1 change: 0 additions & 1 deletion nox/src/effectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ impl Effectors {
let sent = connectivity.send(contact, particle).await;
if sent {
// resolved and sent, exit
return;
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion particle-protocol/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ pub enum ParticleError {
err: SigningError,
particle_id: String,
},
#[error("Failed to verify particle {particle_id} signature: {err}")]
#[error("Failed to verify particle {particle_id} by {peer_id} with signature: {err}")]
SignatureVerificationFailed {
#[source]
err: VerificationError,
particle_id: String,
peer_id: String,
},
#[error("Failed to decode public key from init_peer_id of particle {particle_id}: {err}")]
DecodingError {
Expand Down
54 changes: 52 additions & 2 deletions particle-protocol/src/particle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,12 @@ impl Particle {
/// return immutable particle fields in bytes for signing
/// concatenation of:
/// - id as bytes
/// - init_peer_id in base58 as bytes
/// - timestamp u64 as little-endian bytes
/// - ttl u32 as little-endian bytes
/// - script as bytes
fn as_bytes(&self) -> Vec<u8> {
let mut bytes = vec![];
bytes.extend(self.id.as_bytes());
bytes.extend(self.init_peer_id.to_base58().as_bytes());
bytes.extend(self.timestamp.to_le_bytes());
bytes.extend(self.ttl.to_le_bytes());
bytes.extend(self.script.as_bytes());
Expand Down Expand Up @@ -135,6 +133,7 @@ impl Particle {
.map_err(|err| SignatureVerificationFailed {
err,
particle_id: self.id.clone(),
peer_id: self.init_peer_id.to_base58(),
})
}
}
Expand All @@ -159,3 +158,54 @@ impl std::fmt::Display for Particle {
)
}
}

#[cfg(test)]
mod tests {
use crate::Particle;
use base64::{engine::general_purpose::STANDARD as base64, Engine};
use fluence_keypair::{KeyFormat, KeyPair};

#[test]
fn test_signature() {
let kp_bytes = base64
.decode("7h48PQ/f1rS9TxacmgODxbD42Il9B3KC117jvOPppPE=")
.unwrap();
assert_eq!(kp_bytes.len(), 32);

let kp = KeyPair::from_secret_key(kp_bytes, KeyFormat::Ed25519).unwrap();

// assert peer id
assert_eq!(
kp.get_peer_id().to_base58(),
"12D3KooWANqfCDrV79MZdMnMqTvDdqSAPSxdgFY1L6DCq2DVGB4D"
);

// test simple signature
let message = "message".to_string();

let signature = kp.sign(message.as_bytes()).unwrap();
assert!(kp.public().verify(message.as_bytes(), &signature).is_ok());
assert_eq!(base64.encode(signature.to_vec()), "sBW7H6/1fwAwF86ldwVm9BDu0YH3w30oFQjTWX0Tiu9yTVZHmxkV2OX4GL5jn0Iz0CrasGcOfozzkZwtJBPMBg==");

// test particle signature
let mut p = Particle {
id: "2883f959-e9e7-4843-8c37-205d393ca372".to_string(),
init_peer_id: kp.get_peer_id(),
timestamp: 1696934545662,
ttl: 7000,
script: "abc".to_string(),
signature: vec![],
data: vec![],
};

let particle_bytes = p.as_bytes();
assert_eq!(
base64.encode(&particle_bytes),
"Mjg4M2Y5NTktZTllNy00ODQzLThjMzctMjA1ZDM5M2NhMzcy/kguGYsBAABYGwAAYWJj"
);

p.sign(&kp).unwrap();
assert!(p.verify().is_ok());
assert_eq!(base64.encode(&p.signature), "KceXDnOfqe0dOnAxiDsyWBIvUq6WHoT0ge+VMHXOZsjZvCNH7/10oufdlYfcPomfv28On6E87ZhDcHGBZcb7Bw==");
}
}
Loading