rebase/flow/
poap_ownership_verification.rs

1use crate::{
2    content::poap_ownership_verification::PoapOwnershipVerificationContent as Ctnt,
3    proof::poap_ownership_verification::PoapOwnershipVerificationProof as Prf,
4    statement::poap_ownership_verification::PoapOwnershipVerificationStatement as Stmt,
5    types::{
6        defs::{Flow, Instructions, Issuer, Proof, Statement, StatementResponse, Subject},
7        enums::subject::{Pkh, Subjects},
8        error::FlowError,
9    },
10};
11
12use async_trait::async_trait;
13use chrono::{DateTime, Duration, Utc};
14use reqwest::{
15    header::{HeaderMap, HeaderName, HeaderValue},
16    Client,
17};
18use schemars::schema_for;
19use serde::{Deserialize, Serialize};
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 PoapOwnershipVerificationFlow {
27    pub api_key: String,
28    pub challenge_delimiter: String,
29    // The amount of time that can pass before the witness
30    // wants a new flow initiated. In demo, set to 15 mins.
31    // This is checked for a negative value or 0 and errs if one is found
32    // Alternative is casting u64 to i64 and risking UB.
33    pub max_elapsed_minutes: i64,
34}
35
36impl PoapOwnershipVerificationFlow {
37    // This makes sure the timestamps the client supplies make sense and are
38    // with in the limits of configured expration and that the max elapsed
39    // minutes are greater than 0.
40    pub fn sanity_check(&self, timestamp: &str) -> Result<(), FlowError> {
41        if self.max_elapsed_minutes <= 0 {
42            return Err(FlowError::Validation(
43                "Max elapsed minutes must be set to a number greater than 0".to_string(),
44            ));
45        }
46
47        let now = Utc::now();
48        let then = DateTime::parse_from_rfc3339(timestamp)
49            .map_err(|e| FlowError::Validation(e.to_string()))?;
50
51        if then > now {
52            return Err(FlowError::Validation(
53                "Timestamp provided comes from the future".to_string(),
54            ));
55        }
56
57        if now - Duration::minutes(self.max_elapsed_minutes) > then {
58            return Err(FlowError::Validation(
59                "Validation window has expired".to_string(),
60            ));
61        };
62        Ok(())
63    }
64}
65
66#[derive(Deserialize, Serialize)]
67struct PoapResEntry {
68    event: PoapEventEntry,
69    #[serde(rename = "tokenId")]
70    token_id: String,
71    owner: String,
72    chain: String,
73    // NOTE: This date is in the format "YYYY-MM-DD HH-mm-ss"
74    created: String,
75}
76
77#[derive(Deserialize, Serialize)]
78struct PoapEventEntry {
79    id: i64,
80    fancy_id: String,
81    name: String,
82    event_url: String,
83    image_url: String,
84    country: String,
85    city: String,
86    description: String,
87    year: u64,
88    // NOTE: These dates are in the format "DD-MonthShortName-YYYY"
89    start_date: String,
90    end_date: String,
91    // TODO: Test for this?
92    expiry_date: String,
93    supply: u64,
94}
95
96#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
97#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
98impl Flow<Ctnt, Stmt, Prf> for PoapOwnershipVerificationFlow {
99    fn instructions(&self) -> Result<Instructions, FlowError> {
100        Ok(Instructions {
101            statement: "Enter the event id of the POAP you want to verify ownership of."
102                .to_string(),
103            signature: "Sign a statement attesting to ownership of the POAP.".to_string(),
104            witness: "Send the attestation and the signature to the witness and issue a credential"
105                .to_string(),
106            statement_schema: schema_for!(Stmt),
107            witness_schema: schema_for!(Prf),
108        })
109    }
110
111    async fn statement<I: Issuer + Send + Clone>(
112        &self,
113        stmt: Stmt,
114        issuer: I,
115    ) -> Result<StatementResponse, FlowError> {
116        self.sanity_check(&stmt.issued_at)?;
117
118        // TODO: Investigate!
119        // Can POAPs be attached to non EIP155 DIDs?
120        if let Subjects::Pkh(Pkh::Eip155(_)) = stmt.subject {
121        } else {
122            return Err(FlowError::Validation(
123                "Currently only supports Ethereum Addresses for POAP Ownership flow".to_string(),
124            ));
125        }
126
127        let s = stmt.generate_statement()?;
128
129        // The witness takes the statement which is bound to a specific time by the "issued_at"
130        // timestamp, places the challenge delimiter in the middle, then adds their own version
131        // of the challenge. This ensures that the expected address is the one making this
132        // request and this request isn't being replayed from an interaction older than the
133        // max_elapsed_minutes.
134        let f = issuer.sign(&s);
135        let sig = f.await?;
136        Ok(StatementResponse {
137            statement: format!("{}{}{}", s, self.challenge_delimiter, sig),
138            delimiter: None,
139        })
140    }
141
142    async fn validate_proof<I: Issuer + Send>(
143        &self,
144        proof: Prf,
145        issuer: I,
146    ) -> Result<Ctnt, FlowError> {
147        self.sanity_check(&proof.statement.issued_at)?;
148
149        let u = Url::parse(&format!(
150            "https://api.poap.tech/actions/scan/{}",
151            proof.statement.subject.display_id()?
152        ))
153        .map_err(|e| FlowError::BadLookup(format!("Failed in API request: {}", e)))?;
154
155        let mut headers = HeaderMap::new();
156        let hv: HeaderValue = self
157            .api_key
158            .parse()
159            .map_err(|_e| FlowError::BadLookup("Could not parse Header value".to_string()))?;
160        let hn: HeaderName = "X-API-KEY"
161            .to_string()
162            .parse()
163            .map_err(|_e| FlowError::BadLookup("Could not parse Header name".to_string()))?;
164        headers.insert(hn, hv);
165
166        let client = Client::new();
167        let f = client.get(u).headers(headers).send();
168        let h = f.await.map_err(|e| FlowError::BadLookup(e.to_string()))?;
169        let f = h.json();
170        let res: Vec<PoapResEntry> = f.await.map_err(|e| FlowError::BadLookup(e.to_string()))?;
171
172        let mut found = false;
173        for entry in res {
174            if entry.event.id == proof.statement.event_id {
175                found = true;
176                break;
177            }
178        }
179
180        if !found {
181            return Err(FlowError::BadLookup(format!(
182                "Found no event with id {} in user's POAPs.",
183                proof.statement.event_id
184            )));
185        }
186
187        let s = proof.statement.generate_statement()?;
188        let f = issuer.sign(&s);
189        let sig = f.await?;
190
191        proof
192            .statement
193            .subject
194            .valid_signature(
195                // Because the timestamp is within the expected bounds, the witness
196                // then can recreate the statement by recreating the challenge.
197                // This is not vulnerable to replay attacks after the
198                // max_elapsed_minutes has elapsed.
199                &format!("{}{}{}", s, &self.challenge_delimiter, sig),
200                &proof.signature,
201            )
202            .await?;
203
204        Ok(proof.to_content(&s, &proof.signature)?)
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::{
212        test_util::util::{
213            test_eth_did, test_witness_signature, test_witness_statement, MockFlow, MockIssuer,
214            TestKey, TestWitness,
215        },
216        types::{
217            defs::{Issuer, Proof, Statement, Subject},
218            enums::subject::Subjects,
219        },
220    };
221
222    fn mock_proof(key: fn() -> Subjects, signature: String) -> Prf {
223        Prf {
224            statement: Stmt {
225                subject: key(),
226                event_id: 102213,
227                issued_at: "2023-09-27T16:36:33.696Z".to_string(),
228            },
229            signature,
230        }
231    }
232
233    #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
234    #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
235    impl Flow<Ctnt, Stmt, Prf> for MockFlow {
236        fn instructions(&self) -> Result<Instructions, FlowError> {
237            Ok(Instructions {
238                statement: "Unimplemented".to_string(),
239                statement_schema: schema_for!(Stmt),
240                signature: "Unimplemented".to_string(),
241                witness: "Unimplemented".to_string(),
242                witness_schema: schema_for!(Prf),
243            })
244        }
245
246        async fn statement<I: Issuer + Send + Clone>(
247            &self,
248            statement: Stmt,
249            _issuer: I,
250        ) -> Result<StatementResponse, FlowError> {
251            Ok(StatementResponse {
252                statement: statement.generate_statement()?,
253                delimiter: Some("\n\n".to_string()),
254            })
255        }
256
257        async fn validate_proof<I: Issuer + Send>(
258            &self,
259            proof: Prf,
260            _issuer: I,
261        ) -> Result<Ctnt, FlowError> {
262            proof
263                .statement
264                .subject
265                .valid_signature(&self.statement, &self.signature)
266                .await?;
267
268            Ok(proof
269                .to_content(&self.statement, &self.signature)
270                .map_err(FlowError::Proof)?)
271        }
272    }
273
274    #[tokio::test]
275    async fn mock_poap_ownership() {
276        let signature = test_witness_signature(TestWitness::NftOwnership, TestKey::Eth).unwrap();
277        let statement = test_witness_statement(TestWitness::NftOwnership, TestKey::Eth).unwrap();
278
279        let p = mock_proof(test_eth_did, signature.clone());
280
281        let flow = MockFlow {
282            statement,
283            signature,
284        };
285
286        let i = MockIssuer {};
287        flow.unsigned_credential(p, test_eth_did(), i)
288            .await
289            .unwrap();
290    }
291}