1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
//! Types associated with holochain reporting.
use kitsune2_api::Timestamp;
/// Holochain reporting entry.
///
/// When encoded as json, the tag property is "k" (kind) for brevity.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "k", rename_all = "camelCase")]
pub enum ReportEntry {
/// Indicates that the holochain process has started.
Start(ReportEntryStart),
/// Reports a receipt indicating that a peer received fetched ops from us.
FetchedOps(ReportEntryFetchedOps),
}
impl ReportEntry {
/// Create a new "start" entry.
pub fn start() -> Self {
Self::Start(ReportEntryStart {
timestamp: Timestamp::now().as_micros().to_string(),
})
}
}
/// Indicates that the holochain process has started.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReportEntryStart {
/// Timestamp microseconds since unix epoch.
///
/// When encoded as json, the property is "t" (timestamp) for brevity.
#[serde(rename = "t")]
pub timestamp: String,
}
/// Reports a receipt indicating that a peer received fetched ops from us.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReportEntryFetchedOps {
/// Timestamp microseconds since unix epoch.
///
/// When encoded as json, the property is "t" (timestamp) for brevity.
#[serde(rename = "t")]
pub timestamp: String,
/// Space id as a base64url string.
///
/// When encoded as json, the property is "d" (dna) for brevity.
#[serde(rename = "d")]
pub space: String,
/// Op count as a string.
///
/// When encoded as json, the property is "c" (count) for brevity.
#[serde(rename = "c")]
pub op_count: String,
/// Total byte length of all ops as a string.
///
/// When encoded as json, the property is "b" (bytes) for brevity.
#[serde(rename = "b")]
pub total_bytes: String,
/// List of base64url agent pubkeys from the node that received the op data.
///
/// When encoded as json, the property is "a" (agent) for brevity.
#[serde(rename = "a")]
pub agent_pubkeys: Vec<String>,
/// Signatures generated and verifyable by the listed agent pubkeys.
///
/// Concat the utf8 bytes of timestamp, space, op_count, total_bytes and
/// agent_pubkeys in order to generate/validate the signatures.
///
/// When encoded as json, the property is "s" (signatures) for brevity.
#[serde(rename = "s")]
pub signatures: Vec<String>,
}
impl ReportEntryFetchedOps {
/// Generate the canonical encoded byte array of this entry
/// for signing and verification.
pub fn encode_for_verification(&self) -> Vec<u8> {
let mut len =
self.timestamp.len() + self.space.len() + self.op_count.len() + self.total_bytes.len();
for a in self.agent_pubkeys.iter() {
len += a.len();
}
let mut out = Vec::with_capacity(len);
out.extend_from_slice(self.timestamp.as_bytes());
out.extend_from_slice(self.space.as_bytes());
out.extend_from_slice(self.op_count.as_bytes());
out.extend_from_slice(self.total_bytes.as_bytes());
for a in self.agent_pubkeys.iter() {
out.extend_from_slice(a.as_bytes());
}
out
}
/// Verify the signatures.
pub async fn verify(&self) -> bool {
use base64::prelude::*;
let to_verify = self.encode_for_verification();
tracing::trace!(to_verify = %String::from_utf8_lossy(&to_verify), report = ?self, "verify");
if self.agent_pubkeys.is_empty() || self.agent_pubkeys.len() != self.signatures.len() {
tracing::trace!("report signatures invalid");
return false;
}
for (agent, sig) in self.agent_pubkeys.iter().zip(self.signatures.iter()) {
let agent = agent.trim_start_matches("u");
let agent = match BASE64_URL_SAFE_NO_PAD.decode(agent) {
Ok(agent) => agent,
Err(_) => return false,
};
if agent.len() != 39 {
return false;
}
let sig = match BASE64_URL_SAFE_NO_PAD.decode(sig) {
Ok(sig) => sig,
Err(_) => return false,
};
if sig.len() != 64 {
return false;
}
let sig: [u8; 64] = sig.try_into().expect("array conversion failed");
let pk = crate::prelude::AgentPubKey::from_raw_39(agent);
use holochain_keystore::AgentPubKeyExt;
match pk
.verify_signature_raw(&sig.into(), to_verify.clone().into())
.await
{
Ok(true) => (),
_ => return false,
}
}
true
}
}