rebase/flow/
github_verification.rs

1use crate::{
2    content::github_verification::GitHubVerificationContent as Ctnt,
3    proof::github_verification::GitHubVerificationProof as Prf,
4    statement::github_verification::GitHubVerificationStatement as Stmt,
5    types::{
6        error::FlowError,
7        defs::{Flow, StatementResponse, Issuer, Proof, Statement, Subject, Instructions},
8    },
9};
10
11use async_trait::async_trait;
12use regex::Regex;
13use reqwest::{
14    Client,
15    header::{HeaderMap, USER_AGENT}
16};
17use schemars::schema_for;
18use serde::{Deserialize, Serialize};
19use serde_json::map::Map;
20use tsify::Tsify;
21use url::Url;
22use wasm_bindgen::prelude::*;
23
24#[derive(Clone, Debug, Deserialize, Serialize, Tsify)]
25#[tsify(into_wasm_abi, from_wasm_abi)]
26pub struct GitHubVerificationFlow {
27    pub user_agent: String,
28    pub delimiter: String,
29}
30
31#[derive(Deserialize, Debug, Serialize)]
32pub struct GitHubResponse {
33    // This value here is { content: String }
34    // TODO: Use serde_with and get better typing?
35    pub files: Map<String, serde_json::value::Value>,
36    pub owner: Owner,
37    pub history: Vec<History>,
38}
39
40#[derive(Deserialize, Debug, Serialize)]
41pub struct Owner {
42    pub login: String,
43}
44
45#[derive(Deserialize, Debug, Serialize)]
46pub struct History {
47    pub version: String,
48}
49
50#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
51#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
52impl Flow<Ctnt, Stmt, Prf> for GitHubVerificationFlow {
53    fn instructions(&self) -> Result<Instructions, FlowError> {
54        Ok(Instructions { 
55            statement: "Enter your GitHub account handle to verify and include in a signed message using your wallet.".to_string(),
56            statement_schema: schema_for!(Stmt),
57            signature: "Sign the message presented to you containing your GitHub handle and addtional information.".to_string(),
58            witness: "Create a Gist with this message to create a link between your identifier and your GitHub handle.".to_string(),
59            witness_schema: schema_for!(Prf) 
60        })
61    }
62
63    async fn statement<I: Issuer + Send + Clone>(
64        &self,
65        statement: Stmt,
66        _issuer: I,
67    ) -> Result<StatementResponse, FlowError> {
68        Ok(StatementResponse {
69            statement: statement.generate_statement()?,
70            delimiter: Some(self.delimiter.to_owned())
71        })
72    }
73
74    async fn validate_proof<I: Issuer + Send>(&self, proof: Prf, _issuer: I) -> Result<Ctnt, FlowError> {
75        let client = Client::new();
76        let request_url = format!("https://api.github.com/gists/{}", proof.gist_id);
77        let re = Regex::new(r"^[a-zA-Z0-9]{32}$")
78            .map_err(|_| FlowError::BadLookup("could not generate gist id regex".to_string()))?;
79
80        if !re.is_match(&proof.gist_id) {
81            return Err(FlowError::BadLookup("gist id invalid".to_string()));
82        }
83
84        let mut headers = HeaderMap::new();
85        headers.insert(
86            USER_AGENT,
87            self.user_agent.to_string().parse().map_err(|_| {
88                FlowError::BadLookup("could not generate header for lookup".to_string())
89            })?,
90        );
91
92        let res: GitHubResponse = client
93            .get(Url::parse(&request_url).map_err(|e| FlowError::BadLookup(e.to_string()))?)
94            .headers(headers)
95            .send()
96            .await
97            .map_err(|e| FlowError::BadLookup(e.to_string()))?
98            .json()
99            .await
100            .map_err(|e| FlowError::BadLookup(e.to_string()))?;
101
102        if proof.statement.handle.to_lowercase() != res.owner.login.to_lowercase() {
103            return Err(FlowError::BadLookup(format!(
104                "handle mismatch, expected: {}, got: {}",
105                proof.statement.handle.to_lowercase(),
106                res.owner.login.to_lowercase()
107            )));
108        };
109        let s = serde_json::to_string(&res.files)
110            .map_err(|e| FlowError::BadLookup(e.to_string()))?;
111
112        for (_k, v) in res.files {
113            let object = match v.as_object() {
114                None => continue,
115                Some(x) => x,
116            };
117
118            let str_val = match object.get("content") {
119                None => continue,
120                Some(x) => x,
121            };
122
123            let p = match str_val.as_str() {
124                None => continue,
125                Some(x) => x,
126            };
127
128            let mut a = p.split(&self.delimiter); 
129            let txt = a.next(); 
130            let txt_sig = a.next();
131
132            match (txt, txt_sig) {
133                (Some(stmt), Some(sig)) => {
134                    if stmt != proof.statement.generate_statement()? {
135                        continue;
136                    }
137                    proof.statement.subject.valid_signature(stmt, sig).await?;
138                    return Ok(proof.to_content(stmt, sig)?)
139                }
140                _ => continue
141            }
142            
143        }
144
145        Err(FlowError::BadLookup(
146            // "Failed to find properly formatted gist".to_string(),
147            format!("Failed to find files in: {}", s),
148        ))
149    }
150}
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::{
155        test_util::util::{
156            test_ed25519_did, test_did_keypair, 
157            test_eth_did, test_solana_did, test_witness_signature, MockFlow,
158            MockIssuer, TestKey, TestWitness, test_witness_statement,
159        },
160        types::{
161            enums::subject::Subjects,
162            defs::{Issuer, Proof, Statement, Subject},
163        },
164    };
165
166    fn mock_proof(key: fn() -> Subjects) -> Prf {
167        Prf {
168            statement: Stmt {
169                subject: key(),
170                handle: "foo".to_owned(),
171            },
172            gist_id: "not_tested".to_owned(),
173        }
174    }
175
176    #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
177    #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
178    impl Flow<Ctnt, Stmt, Prf> for MockFlow {
179        fn instructions(&self) -> Result<Instructions, FlowError> {
180            Ok(Instructions {
181                statement: "Unimplemented".to_string(),
182                statement_schema: schema_for!(Stmt),
183                signature: "Unimplemented".to_string(),
184                witness: "Unimplemented".to_string(),
185                witness_schema: schema_for!(Prf)
186            })
187        }
188
189        async fn statement<I: Issuer + Send + Clone>(
190            &self,
191            statement: Stmt,
192            _issuer: I,
193        ) -> Result<StatementResponse, FlowError> {
194            Ok(StatementResponse {
195                statement: statement.generate_statement()?,
196                delimiter: Some("\n\n".to_string())
197            })
198        }
199
200        async fn validate_proof<I: Issuer + Send>(
201            &self,
202            proof: Prf,
203            _issuer: I,
204        ) -> Result<Ctnt, FlowError> {
205            // NOTE: This just passes through, instead of looking up!!!
206            if self.statement != proof.statement.generate_statement()? {
207                return Err(FlowError::BadLookup("Mismatched statements".to_string()))
208            }
209
210            proof.statement.subject.valid_signature(&self.statement, &self.signature).await?;
211
212            Ok(proof
213                .to_content(&self.statement, &self.signature)
214                .map_err(FlowError::Proof)?)
215        }
216    }
217
218    #[tokio::test]
219    async fn mock_github() {
220        let did = mock_proof(test_eth_did);
221        let signature = test_witness_signature(TestWitness::GitHub, TestKey::Eth).unwrap();
222        let statement = test_witness_statement(TestWitness::GitHub, TestKey::Eth).unwrap();
223
224        let flow = MockFlow {
225            statement,
226            signature,
227        };
228        let i = MockIssuer {};
229        flow.unsigned_credential(did.clone(), test_eth_did(), i.clone())
230            .await
231            .unwrap();
232
233        let did = mock_proof(test_ed25519_did);
234
235        let signature = test_witness_signature(TestWitness::GitHub, TestKey::Ed25519).unwrap();
236        let statement = test_witness_statement(TestWitness::GitHub, TestKey::Ed25519).unwrap();
237        let flow = MockFlow {
238            statement,
239            signature,
240        };   
241        flow.unsigned_credential(did.clone(), test_ed25519_did(), i.clone())
242            .await
243            .unwrap();
244
245        let did = mock_proof(test_solana_did);
246        let signature = test_witness_signature(TestWitness::GitHub, TestKey::Solana).unwrap();
247        let statement = test_witness_statement(TestWitness::GitHub, TestKey::Solana).unwrap();
248        let flow = MockFlow {
249            statement,
250            signature,
251        };
252        flow.unsigned_credential(did.clone(), test_solana_did(), i.clone())
253            .await
254            .unwrap();
255    }
256
257    #[tokio::test]
258    async fn mock_github_on_the_fly() {
259        let i = MockIssuer {};
260        let (subj1, iss1) = test_did_keypair().await.unwrap();
261
262        let ver_proof1 = Prf {
263            statement: Stmt {
264                subject: subj1.clone(),
265                handle: "foo".to_owned(),
266            },
267            gist_id: "unused".to_owned(),
268        };
269
270        let statement = ver_proof1.generate_statement().unwrap();
271        let signature = iss1.sign(&statement).await.unwrap();
272        let flow = MockFlow {
273            statement,
274            signature,
275        };
276
277        flow.unsigned_credential(ver_proof1.clone(), subj1.clone(), i.clone())
278            .await
279            .unwrap();
280
281        let (subj2, iss2) = test_did_keypair().await.unwrap();
282
283        let ver_proof2 = Prf {
284            statement: Stmt {
285                subject: subj2.clone(),
286                handle: "foo".to_owned(),
287            },
288            gist_id: "unused".to_owned(),
289        };
290
291        let statement = ver_proof2.generate_statement().unwrap();
292        let signature = iss2.sign(&statement).await.unwrap();
293        let flow = MockFlow {
294            statement,
295            signature,
296        };
297
298        flow.unsigned_credential(ver_proof2.clone(), subj2.clone(), i.clone())
299            .await
300            .unwrap();
301
302        // Make sure it fails correctly:
303        let statement = ver_proof2.generate_statement().unwrap();
304        let signature = iss1.sign(&statement).await.unwrap();
305        let flow = MockFlow {
306            statement,
307            signature,
308        };
309
310        if flow
311            .unsigned_credential(ver_proof2.clone(), subj2.clone(), i.clone())
312            .await
313            .is_ok()
314        {
315            panic!("Approved bad signature");
316        };
317    }
318}