Skip to content

Commit

Permalink
Deterministic serialization #27
Browse files Browse the repository at this point in the history
  • Loading branch information
joepio committed Oct 25, 2020
1 parent ca38310 commit f71183d
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 36 deletions.
125 changes: 92 additions & 33 deletions lib/src/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ pub struct Commit {
/// The subject URL that is to be modified by this Delta
pub subject: String,
/// The date it was created, as a unix timestamp
pub created_at: u128,
/// The URL of the one suggesting this Commit
pub created_at: u64,
/// The URL of the one signing this Commit
pub signer: String,
/// The set of PropVals that need to be added.
/// Overwrites existing values
Expand All @@ -25,14 +25,19 @@ pub struct Commit {
/// If set to true, deletes the entire resource
pub destroy: Option<bool>,
/// Base64 encoded signature of the JSON serialized Commit
pub signature: String,
pub signature: Option<String>,
}

impl Commit {
/// Converts the Commit into a HashMap of strings.
/// Creates an identifier using the base_url or a default.
pub fn into_resource<'a>(self, store: &'a dyn Storelike) -> AtomicResult<Resource<'a>> {
let subject = format!("{}commits/{}", store.get_base_url(), self.signature);
let subject = match self.signature.as_ref() {
Some(sig) => format!("{}commits/{}", store.get_base_url(), sig),
None => {
return Err("No signature set".into());
}
};
let mut resource = Resource::new_instance(urls::COMMIT, store)?;
resource.set_subject(subject);
resource.set_propval(
Expand Down Expand Up @@ -67,18 +72,62 @@ impl Commit {
urls::SIGNER.into(),
Value::new(&self.signer, &DataType::AtomicUrl).unwrap(),
)?;
resource.set_propval(urls::SIGNATURE.into(), self.signature.into())?;
resource.set_propval(urls::SIGNATURE.into(), self.signature.unwrap().into())?;
Ok(resource)
}

/// Generates a deterministic serialized JSON representation of the Commit.
/// Does not contain the signature, since this function is used to check if the signature is correct.
pub fn serialize_deterministically(&self) -> AtomicResult<String> {
let mut obj = serde_json::Map::new();
obj.insert(
"subject".into(),
serde_json::Value::String(self.subject.clone()),
);
obj.insert(
"createdAt".into(),
serde_json::Value::Number(self.created_at.into()),
);
obj.insert(
"signer".into(),
serde_json::Value::String(self.signer.clone()),
);
if let Some(set) = self.set.clone() {
if !set.is_empty() {
let mut collect: Vec<(String, String)> = set.into_iter().collect();
// All keys should be ordered alphabetically
collect.sort();
// Make sure that the serializer does not mess up the order!
let mut set_map = serde_json::Map::new();
for (k, v) in collect.iter() {
set_map.insert(k.into(), serde_json::Value::String(v.into()));
}
obj.insert("set".into(), serde_json::Value::Object(set_map));
}
}
if let Some(mut remove) = self.remove.clone() {
if !remove.is_empty() {
// These, too, should be sorted alphabetically
remove.sort();
obj.insert("remove".into(), remove.into());
}
}
if let Some(destroy) = self.destroy {
// Only include this key if it is true
if destroy {
obj.insert("destroy".into(), serde_json::Value::Bool(true));
}
}
let string = serde_json::to_string(&obj)?;
Ok(string)
}
}

/// Use this for creating Commits
#[derive(Serialize)]
pub struct CommitBuilder {
/// The subject URL that is to be modified by this Delta
subject: String,
/// The date it was created, as a unix timestamp
created_at: Option<u128>,
/// The URL of the one suggesting this Commit
signer: String,
/// The set of PropVals that need to be added.
Expand All @@ -95,7 +144,6 @@ impl CommitBuilder {
pub fn new(subject: String, signer: String) -> Self {
CommitBuilder {
subject,
created_at: None,
signer,
set: HashMap::new(),
remove: HashSet::new(),
Expand All @@ -105,33 +153,34 @@ impl CommitBuilder {

/// Creates the Commit and signs it using a signature.
/// Does not send it - see atomic_lib::client::post_commit
pub fn sign(mut self, private_key: &str) -> AtomicResult<Commit> {
self.created_at = Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("You're a time traveler")
.as_millis(),
);
/// Private key is the base64 encoded pkcs8 for the signer
pub fn sign(self, private_key: &str) -> AtomicResult<Commit> {
let created_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("You're a time traveler")
.as_millis();

let mut commit = Commit {
subject: self.subject,
signer: self.signer,
set: Some(self.set),
remove: Some(self.remove.into_iter().collect()),
destroy: Some(self.destroy),
created_at: created_at as u64,
signature: None,
};

// TODO: use actual stringified resource, also change in Storelike::commit
// let stringified = serde_json::to_string(&self)?;
let stringified = "full_resource";
// let stringified = "full_resource";
let stringified = commit.serialize_deterministically()?;
let private_key_bytes = base64::decode(private_key)?;
let key_pair = ring::signature::Ed25519KeyPair::from_pkcs8(&private_key_bytes)
.map_err(|_| "Can't create keypair")?;
// let signature = some_lib::sign(string, private_key);
// let signax ture = some_lib::sign(string, private_key);
let signature = base64::encode(key_pair.sign(&stringified.as_bytes()));

Ok(Commit {
subject: self.subject,
signer: self.signer,
set: Some(self.set),
remove: Some(self.remove.into_iter().collect()),
destroy: Some(self.destroy),
created_at: self.created_at.unwrap(),
// TODO: Hashing signature logic
signature,
})
commit.signature = Some(signature);
Ok(commit)
}

/// Set Property / Value combinations that will either be created or overwritten.
Expand Down Expand Up @@ -163,17 +212,27 @@ mod test {
let (agent_subject, private_key) = store.create_agent("test_actor").unwrap();
let subject = "https://localhost/new_thing";
let mut commitbuiler = crate::commit::CommitBuilder::new(subject.into(), agent_subject);
let property = crate::urls::DESCRIPTION;
let value = "Some value";
commitbuiler.set(property.into(), value.into());
let property1 = crate::urls::DESCRIPTION;
let value1 = "Some value";
commitbuiler.set(property1.into(), value1.into());
let property2 = crate::urls::SHORTNAME;
let value2 = "someval";
commitbuiler.set(property2.into(), value2.into());
let commit = commitbuiler.sign(&private_key).unwrap();
let commit_subject = commit.subject.clone();
let _created_resource = store.commit(commit).unwrap();

let resource = store.get_resource(&subject).unwrap();
assert!(resource.get(property).unwrap().to_string() == value);
assert!(resource.get(property1).unwrap().to_string() == value1);
let found_commit = store.get_resource(&commit_subject).unwrap();
println!("{}", found_commit.get_subject());
assert!(found_commit.get_shortname("description").unwrap().to_string() == value);

assert!(
found_commit
.get_shortname("description")
.unwrap()
.to_string()
== value1
);
}
}
10 changes: 7 additions & 3 deletions lib/src/storelike.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,21 +83,25 @@ pub trait Storelike {
Ok(rs) => rs,
Err(_) => Resource::new(commit.subject.clone(), self),
};
let signature = match commit.signature.as_ref() {
Some(sig) => sig,
None => return Err("No signature set".into())
};
// TODO: Check if commit.agent has the rights to update the resource
let pubkey_b64 = self.get_resource(&commit.signer)?
.get(urls::PUBLIC_KEY)?.to_string();
let agent_pubkey = base64::decode(pubkey_b64)?;
// TODO: actually use the stringified resource
let stringified = "full_resource";
let stringified = commit.serialize_deterministically()?;
let peer_public_key =
ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, agent_pubkey);
let signature_bytes = base64::decode(commit.signature.clone())?;
let signature_bytes = base64::decode(signature.clone())?;
peer_public_key.verify(stringified.as_bytes(), &signature_bytes).map_err(|_| "Incorrect signature")?;
// Check if the created_at lies in the past
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("Time went backwards")
.as_millis();
.as_millis() as u64;
if commit.created_at > now {
return Err("Commit created_at timestamp must lie in the past.".into());
// TODO: also check that no younger commits exist
Expand Down

0 comments on commit f71183d

Please sign in to comment.