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 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 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 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 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}