did_method_plc/
audit.rs

1use std::str::FromStr;
2
3use crate::operation::{SignedOperation, SignedPLCOperation};
4use crate::{operation::PLCOperationType, util::op_from_json};
5use crate::{PLCError, PLCOperation};
6use chrono::{DateTime, Local, NaiveDateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9#[derive(Serialize, Deserialize, Clone)]
10#[serde(rename_all = "camelCase")]
11pub struct AuditLog {
12    pub cid: String,
13    pub created_at: NaiveDateTime,
14    pub did: String,
15    pub nullified: bool,
16    pub operation: PLCOperation,
17}
18
19impl AuditLog {
20    pub fn from_json(json: &str) -> Result<Self, PLCError> {
21        let json: serde_json::Value =
22            serde_json::from_str(json).map_err(|e| PLCError::Other(e.into()))?;
23        let op = op_from_json(
24            &serde_json::to_string(json.get("operation").unwrap())
25                .map_err(|e| PLCError::Other(e.into()))?,
26        )?;
27        Ok(AuditLog {
28            cid: json.get("cid").unwrap().as_str().unwrap().to_string(),
29            created_at: DateTime::<Utc>::from_str(json.get("createdAt").unwrap().as_str().unwrap())
30                .unwrap()
31                .naive_utc(),
32            did: json.get("did").unwrap().as_str().unwrap().to_string(),
33            nullified: json.get("nullified").unwrap().as_bool().unwrap(),
34            operation: op,
35        })
36    }
37
38    pub fn op_type(&self) -> PLCOperationType {
39        match &self.operation {
40            PLCOperation::SignedGenesis(_) => PLCOperationType::Operation,
41            PLCOperation::UnsignedGenesis(_) => PLCOperationType::Operation,
42            PLCOperation::SignedPLC(op) => op.unsigned.type_.clone(),
43            PLCOperation::UnsignedPLC(op) => op.type_.clone(),
44        }
45    }
46}
47
48#[derive(Serialize, Deserialize, Clone)]
49pub struct DIDAuditLogs(Vec<AuditLog>);
50
51impl DIDAuditLogs {
52    pub fn from_json(json: &str) -> Result<Self, PLCError> {
53        let json: serde_json::Value =
54            serde_json::from_str(json).map_err(|e| PLCError::Other(e.into()))?;
55        let logs = json
56            .as_array()
57            .unwrap()
58            .iter()
59            .map(|log| AuditLog::from_json(&serde_json::to_string(log).unwrap()).unwrap())
60            .collect();
61        Ok(Self(logs))
62    }
63
64    pub fn get_latest(&self) -> Result<String, PLCError> {
65        let last_op = self.0.last();
66        if last_op.is_none() {
67            return Err(PLCError::MisorderedOperation);
68        }
69        let mut last_op = last_op.unwrap();
70        if last_op.op_type() == PLCOperationType::Tombstone {
71            if self.0.len() == 1 {
72                return Err(PLCError::InvalidOperation);
73            }
74            last_op = &self.0[self.0.len() - 2];
75        }
76        Ok(last_op.cid.clone())
77    }
78
79    pub fn assure_valid(&self, proposed: SignedPLCOperation) -> Result<bool, PLCError> {
80        let cid = match &proposed.unsigned.prev {
81            Some(cid) => cid,
82            None => return Err(PLCError::MisorderedOperation),
83        };
84        let index_of_prev = self.0.iter().position(|log| log.cid == cid.to_string());
85
86        if index_of_prev.is_none() {
87            return Err(PLCError::MisorderedOperation);
88        }
89
90        let ops_in_history = self.0[0..index_of_prev.unwrap()].to_vec();
91        let nullified = self.0[index_of_prev.unwrap()..self.0.len() - 1].to_vec();
92
93        let last_op = ops_in_history.last();
94        if last_op.is_none() {
95            return Err(PLCError::MisorderedOperation);
96        }
97        let last_op = last_op.unwrap();
98        if last_op.op_type() == PLCOperationType::Tombstone {
99            return Err(PLCError::MisorderedOperation);
100        }
101
102        let last_op_normalized: SignedPLCOperation = match &last_op.operation {
103            PLCOperation::SignedGenesis(op) => op.normalize().unwrap().into(),
104            PLCOperation::SignedPLC(op) => op.clone(),
105            _ => {
106                unreachable!()
107            }
108        };
109
110        // No nullification is involved
111        let first_nullified = nullified.first();
112        if first_nullified.is_none() {
113            match last_op_normalized.verify_sig(None) {
114                Ok(_) => {
115                    return Ok(true);
116                }
117                Err(_) => {
118                    return Err(PLCError::InvalidSignature);
119                }
120            }
121        }
122        let first_nullified = first_nullified.unwrap();
123
124        let (_, disputed_key) = match last_op_normalized.verify_sig(None) {
125            Ok(result) => {
126                if !result.0 {
127                    return Err(PLCError::InvalidSignature);
128                }
129                result
130            }
131            Err(_) => {
132                return Err(PLCError::InvalidSignature);
133            }
134        };
135        let disputed_key = disputed_key.unwrap();
136
137        let signer_index = last_op_normalized
138            .unsigned
139            .rotation_keys
140            .iter()
141            .position(|key| key == &disputed_key)
142            .unwrap();
143        let more_powerful_keys = last_op_normalized
144            .unsigned
145            .rotation_keys
146            .split_at(signer_index)
147            .1
148            .to_vec();
149
150        match proposed.verify_sig(Some(more_powerful_keys)) {
151            Ok(result) => {
152                if !result.0 {
153                    return Err(PLCError::InvalidSignature);
154                }
155            }
156            Err(_) => {
157                return Err(PLCError::InvalidSignature);
158            }
159        }
160
161        if nullified.len() > 0 {
162            const RECOVERY_WINDOW: i64 = 72 * 60 * 60;
163            let local = Local::now().naive_utc();
164            let time_lapsed = local - first_nullified.created_at;
165            if time_lapsed.num_seconds() > RECOVERY_WINDOW {
166                return Err(PLCError::LateRecovery);
167            }
168        }
169
170        return Ok(true);
171    }
172
173    pub fn last(&self) -> Option<&AuditLog> {
174        self.0.last()
175    }
176
177    pub fn len(&self) -> usize {
178        self.0.len()
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    const TEST_AUDIT_LOG: &str = "[{\"did\":\"did:plc:z72i7hdynmk6r22z27h6tvur\",\"operation\":{\"sig\":\"9NuYV7AqwHVTc0YuWzNV3CJafsSZWH7qCxHRUIP2xWlB-YexXC1OaYAnUayiCXLVzRQ8WBXIqF-SvZdNalwcjA\",\"prev\":null,\"type\":\"plc_operation\",\"services\":{\"atproto_pds\":{\"type\":\"AtprotoPersonalDataServer\",\"endpoint\":\"https://bsky.social\"}},\"alsoKnownAs\":[\"at://bluesky-team.bsky.social\"],\"rotationKeys\":[\"did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg\",\"did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK\"],\"verificationMethods\":{\"atproto\":\"did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF\"}},\"cid\":\"bafyreigp6shzy6dlcxuowwoxz7u5nemdrkad2my5zwzpwilcnhih7bw6zm\",\"nullified\":false,\"createdAt\":\"2023-04-12T04:53:57.057Z\"},{\"did\":\"did:plc:z72i7hdynmk6r22z27h6tvur\",\"operation\":{\"sig\":\"1mEWzRtFOgeRXH-YCSPTxb990JOXxa__n8Qw6BOKl7Ndm6OFFmwYKiiMqMCpAbxpnGjF5abfIsKc7u3a77Cbnw\",\"prev\":\"bafyreigp6shzy6dlcxuowwoxz7u5nemdrkad2my5zwzpwilcnhih7bw6zm\",\"type\":\"plc_operation\",\"services\":{\"atproto_pds\":{\"type\":\"AtprotoPersonalDataServer\",\"endpoint\":\"https://bsky.social\"}},\"alsoKnownAs\":[\"at://bsky.app\"],\"rotationKeys\":[\"did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg\",\"did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK\"],\"verificationMethods\":{\"atproto\":\"did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF\"}},\"cid\":\"bafyreihmuvr3frdvd6vmdhucih277prdcfcezf67lasg5oekxoimnunjoq\",\"nullified\":false,\"createdAt\":\"2023-04-12T17:26:46.468Z\"},{\"did\":\"did:plc:z72i7hdynmk6r22z27h6tvur\",\"operation\":{\"sig\":\"OoDJihYhLUEWp2MGiAoCN1sRj9cgUEqNjZe6FIOePB8Ugp-IWAZplFRm-pU-fbYSpYV1_tQ9Gx8d_PR9f3NBAg\",\"prev\":\"bafyreihmuvr3frdvd6vmdhucih277prdcfcezf67lasg5oekxoimnunjoq\",\"type\":\"plc_operation\",\"services\":{\"atproto_pds\":{\"type\":\"AtprotoPersonalDataServer\",\"endpoint\":\"https://bsky.social\"}},\"alsoKnownAs\":[\"at://bsky.app\"],\"rotationKeys\":[\"did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg\",\"did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK\"],\"verificationMethods\":{\"atproto\":\"did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF\"}},\"cid\":\"bafyreiexwziulimyiw3qlhpwr2zljk5jtzdp2bgqbgoxuemjsf5a6tan3a\",\"nullified\":false,\"createdAt\":\"2023-06-01T20:05:52.008Z\"},{\"did\":\"did:plc:z72i7hdynmk6r22z27h6tvur\",\"operation\":{\"sig\":\"8Wj9Cf74dZFNKx7oucZSHbBDFOMJ3xx9lkvj5rT9xMErssWYl1D9n4PeGC0mNml7xDG7uoQqZ1JWoApGADUgXg\",\"prev\":\"bafyreiexwziulimyiw3qlhpwr2zljk5jtzdp2bgqbgoxuemjsf5a6tan3a\",\"type\":\"plc_operation\",\"services\":{\"atproto_pds\":{\"type\":\"AtprotoPersonalDataServer\",\"endpoint\":\"https://puffball.us-east.host.bsky.network\"}},\"alsoKnownAs\":[\"at://bsky.app\"],\"rotationKeys\":[\"did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg\",\"did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK\"],\"verificationMethods\":{\"atproto\":\"did:key:zQ3shQo6TF2moaqMTrUZEM1jeuYRQXeHEx4evX9751y2qPqRA\"}},\"cid\":\"bafyreifn4pkect7nymne3sxkdg7tn7534msyxcjkshmzqtijmn3enyxm3q\",\"nullified\":false,\"createdAt\":\"2023-11-09T21:49:10.793Z\"}]";
187
188    #[test]
189    fn test_did_audit_log_from_json() {
190        let audit_logs = DIDAuditLogs::from_json(TEST_AUDIT_LOG).unwrap();
191        assert_eq!(audit_logs.0.len(), 4);
192        assert_eq!(audit_logs.last().unwrap().cid, "bafyreifn4pkect7nymne3sxkdg7tn7534msyxcjkshmzqtijmn3enyxm3q");
193
194        match &audit_logs.last().unwrap().operation {
195            PLCOperation::SignedPLC(op) => {
196                assert_eq!(op.unsigned.type_, PLCOperationType::Operation);
197            }
198            _ => {
199                unreachable!()
200            }
201        }
202
203        assert_eq!(audit_logs.last().unwrap().nullified, false);
204        assert_eq!(audit_logs.last().unwrap().created_at, NaiveDateTime::parse_from_str("2023-11-09T21:49:10.793Z", "%Y-%m-%dT%H:%M:%S%.fZ").unwrap());
205    }
206}