siws_recap/
lib.rs

1mod capability;
2
3pub use capability::{Capability, DecodingError, EncodingError, VerificationError};
4pub use ucan_capabilities_object::{
5    AbilityName, AbilityNameRef, AbilityNamespace, AbilityNamespaceRef, AbilityRef, CapsInner,
6    ConvertError, NotaBeneCollection,
7};
8
9/// The prefix for a ReCap uri.
10pub const RESOURCE_PREFIX: &str = "urn:recap:";
11
12#[cfg(test)]
13mod test {
14    use super::*;
15    use serde_json::Value;
16    use siws::message::SiwsMessage;
17
18    const SIWE_WITH_INTERLEAVED_RES: &str =
19        include_str!("../tests/siwe_with_interleaved_resources.txt");
20    const SIWE_WITH_STATEMENT_NO_CAPS: &str =
21        include_str!("../tests/siwe_with_statement_no_caps.txt");
22    const SIWE_WITH_STATEMENT: &str = include_str!("../tests/siwe_with_statement.txt");
23    const SIWE_NO_CAPS: &str = include_str!("../tests/siwe_with_no_caps.txt");
24    const SIWE: &str = include_str!("../tests/siwe_with_caps.txt");
25
26    #[test]
27    fn no_caps_statement_append() {
28        let msg = Capability::<Value>::default()
29            .build_message(SiwsMessage {
30                domain: "example.com".parse().unwrap(),
31                address: "0000000000000000000000000000000000000000".into(),
32                statement: Some("Some custom statement.".into()),
33                uri: Some("did:key:example".parse().unwrap()),
34                version: Some("1".into()),
35                chain_id: Some("testnet".into()),
36                nonce: Some("mynonce1".into()),
37                issued_at: Some("2022-06-21T12:00:00.000Z".parse().unwrap()),
38                expiration_time: None,
39                not_before: None,
40                request_id: None,
41                resources: vec![],
42            })
43            .expect("failed to build SIWE delegation");
44
45        assert_eq!(
46            SIWE_WITH_STATEMENT_NO_CAPS,
47            msg.to_string(),
48            "generated SIWE message did not match expectation"
49        );
50    }
51
52    #[test]
53    fn build_delegation_statement_append() {
54        let mut cap = Capability::<Value>::default();
55        cap.with_action_convert("credential:*", "credential/present", [])
56            .unwrap();
57
58        let msg = cap
59            .build_message(SiwsMessage {
60                domain: "example.com".parse().unwrap(),
61                address: "0000000000000000000000000000000000000000".into(),
62                statement: Some("Some custom statement.".into()),
63                uri: Some("did:key:example".parse().unwrap()),
64                version: Some("1".into()),
65                chain_id: Some("testnet".into()),
66                nonce: Some("mynonce1".into()),
67                issued_at: Some("2022-06-21T12:00:00.000Z".parse().unwrap()),
68                expiration_time: None,
69                not_before: None,
70                request_id: None,
71                resources: vec!["http://example.com".parse().unwrap()],
72            })
73            .expect("failed to build SIWE delegation");
74
75        assert_eq!(
76            SIWE_WITH_STATEMENT.trim(),
77            msg.to_string(),
78            "generated SIWE message did not match expectation"
79        );
80    }
81
82    #[test]
83    fn no_caps() {
84        let msg = Capability::<Value>::default()
85            .build_message(SiwsMessage {
86                domain: "example.com".parse().unwrap(),
87                address: "0000000000000000000000000000000000000000".into(),
88                statement: None,
89                uri: Some("did:key:example".parse().unwrap()),
90                version: Some("1".into()),
91                chain_id: Some("testnet".into()),
92                nonce: Some("mynonce1".into()),
93                issued_at: Some("2022-06-21T12:00:00.000Z".parse().unwrap()),
94                expiration_time: None,
95                not_before: None,
96                request_id: None,
97                resources: vec![],
98            })
99            .expect("failed to build SIWE delegation");
100
101        assert_eq!(
102            SIWE_NO_CAPS,
103            msg.to_string(),
104            "generated SIWE message did not match expectation"
105        );
106    }
107
108    #[test]
109    fn build_delegation() {
110        let msg = Capability::<Value>::default()
111            .with_actions_convert("urn:credential:type:type1", [("credential/present", [])])
112            .unwrap()
113            .with_actions_convert(
114                "kepler:ens:example.eth://default/kv",
115                [("kv/list", []), ("kv/get", []), ("kv/metadata", [])],
116            )
117            .unwrap()
118            .with_actions_convert(
119                "kepler:ens:example.eth://default/kv/public",
120                [
121                    ("kv/list", []),
122                    ("kv/get", []),
123                    ("kv/metadata", []),
124                    ("kv/put", []),
125                    ("kv/delete", []),
126                ],
127            )
128            .unwrap()
129            .with_actions_convert(
130                "kepler:ens:example.eth://default/kv/dapp-space",
131                [
132                    ("kv/list", []),
133                    ("kv/get", []),
134                    ("kv/metadata", []),
135                    ("kv/put", []),
136                    ("kv/delete", []),
137                ],
138            )
139            .unwrap()
140            .build_message(SiwsMessage {
141                domain: "example.com".parse().unwrap(),
142                address: "0000000000000000000000000000000000000000".into(),
143                statement: None,
144                uri: Some("did:key:example".parse().unwrap()),
145                version: Some("1".into()),
146                chain_id: Some("testnet".into()),
147                nonce: Some("mynonce1".into()),
148                issued_at: Some("2022-06-21T12:00:00.000Z".parse().unwrap()),
149                expiration_time: None,
150                not_before: None,
151                request_id: None,
152                resources: vec![],
153            })
154            .expect("failed to build SIWE delegation");
155
156        assert_eq!(
157            SIWE.trim(),
158            msg.to_string(),
159            "generated SIWE message did not match expectation"
160        );
161    }
162
163    #[test]
164    fn verify() {
165        let msg: SiwsMessage = SIWE.trim().parse().unwrap();
166        assert!(
167            Capability::<Value>::extract_and_verify(&msg)
168                .transpose()
169                .expect("unable to parse resources as capabilities")
170                .is_ok(),
171            "statement did not match capabilities"
172        );
173
174        let mut altered_msg_1 = msg.clone();
175        altered_msg_1
176            .statement
177            .iter_mut()
178            .for_each(|statement| statement.push_str(" I am the walrus!"));
179        assert!(
180            Capability::<Value>::extract_and_verify(&altered_msg_1).is_err(),
181            "altered statement incorrectly matched capabilities"
182        );
183    }
184
185    #[test]
186    fn verify_interleaved_resources() {
187        let msg: SiwsMessage = SIWE_WITH_INTERLEAVED_RES.trim().parse().unwrap();
188        assert!(
189            Capability::<Value>::extract_and_verify(&msg)
190                .unwrap()
191                .is_none(),
192            "recap resource should come last"
193        );
194    }
195}