Skip to main content

world_id_primitives/
rp.rs

1#![allow(clippy::unreadable_literal)]
2
3use std::{fmt, str::FromStr};
4
5use ark_ff::{BigInteger as _, PrimeField as _};
6use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
7
8use crate::FieldElement;
9
10const RP_SIGNATURE_MSG_VERSION: u8 = 0x01;
11
12#[expect(unused_imports, reason = "used in doc comments")]
13use crate::ProofRequest;
14
15/// The id of a relying party.
16#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct RpId(u64);
18
19impl RpId {
20    /// Converts the RP id to an u64
21    #[must_use]
22    pub const fn into_inner(self) -> u64 {
23        self.0
24    }
25
26    /// Creates a new `RpId` by wrapping a `u64`
27    #[must_use]
28    pub const fn new(value: u64) -> Self {
29        Self(value)
30    }
31}
32
33impl fmt::Display for RpId {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        write!(f, "rp_{:016x}", self.0)
36    }
37}
38
39impl FromStr for RpId {
40    type Err = String;
41
42    fn from_str(s: &str) -> Result<Self, Self::Err> {
43        if let Some(id) = s.strip_prefix("rp_") {
44            Ok(Self(u64::from_str_radix(id, 16).map_err(|_| {
45                "Invalid RP ID format: expected hex string".to_string()
46            })?))
47        } else {
48            Err("A valid RP ID must start with 'rp_'".to_string())
49        }
50    }
51}
52
53impl From<u64> for RpId {
54    fn from(value: u64) -> Self {
55        Self(value)
56    }
57}
58
59impl From<RpId> for FieldElement {
60    fn from(value: RpId) -> Self {
61        Self::from(value.0)
62    }
63}
64
65impl Serialize for RpId {
66    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
67    where
68        S: Serializer,
69    {
70        if serializer.is_human_readable() {
71            serializer.serialize_str(&self.to_string())
72        } else {
73            u64::serialize(&self.0, serializer)
74        }
75    }
76}
77
78impl<'de> Deserialize<'de> for RpId {
79    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
80    where
81        D: Deserializer<'de>,
82    {
83        if deserializer.is_human_readable() {
84            let s = String::deserialize(deserializer)?;
85            Self::from_str(&s).map_err(D::Error::custom)
86        } else {
87            let value = u64::deserialize(deserializer)?;
88            Ok(Self(value))
89        }
90    }
91}
92
93/// Computes the message to be signed for the RP signature that must be included in every [`ProofRequest`].
94///
95/// The message format is: `version || nonce || created_at || expires_at || action` (49 - 81 bytes total).
96/// - `version`: 1 byte (currently hardcoded to `0x01`)
97/// - `nonce`: 32 bytes (big-endian)
98/// - `created_at`: 8 bytes (big-endian)
99/// - `expires_at`: 8 bytes (big-endian)
100/// - `action`: optional (see Session Proofs for more details); 32 bytes (big-endian)
101///
102/// # Session Proofs
103/// Session Proofs don't require the RP to specify an `action`, as such, the `action` is not included
104/// in the signature. For other proofs, the action must always be included, otherwise the OPRF Nodes will reject
105/// the request.
106#[must_use]
107pub fn compute_rp_signature_msg(
108    nonce: ark_babyjubjub::Fq,
109    created_at: u64,
110    expires_at: u64,
111    action: Option<ark_babyjubjub::Fq>,
112) -> Vec<u8> {
113    let mut msg = Vec::with_capacity(81);
114    msg.push(RP_SIGNATURE_MSG_VERSION);
115    msg.extend(nonce.into_bigint().to_bytes_be());
116    msg.extend(created_at.to_be_bytes());
117    msg.extend(expires_at.to_be_bytes());
118
119    if let Some(action) = action {
120        msg.extend(action.into_bigint().to_bytes_be());
121    }
122
123    msg
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_rpid_display() {
132        let rp_id = RpId::new(0x123456789abcdef0);
133        assert_eq!(rp_id.to_string(), "rp_123456789abcdef0");
134
135        let rp_id = RpId::new(u64::MAX);
136        assert_eq!(rp_id.to_string(), "rp_ffffffffffffffff");
137
138        let rp_id = RpId::new(0);
139        assert_eq!(rp_id.to_string(), "rp_0000000000000000");
140    }
141
142    #[test]
143    fn test_rpid_from_str() {
144        let rp_id = "rp_123456789abcdef0".parse::<RpId>().unwrap();
145        assert_eq!(rp_id.0, 0x123456789abcdef0);
146
147        let rp_id = "rp_ffffffffffffffff".parse::<RpId>().unwrap();
148        assert_eq!(rp_id.0, u64::MAX);
149
150        let rp_id = "rp_0000000000000000".parse::<RpId>().unwrap();
151        assert_eq!(rp_id.0, 0);
152
153        let rp_id = "rp_123456789ABCDEF0".parse::<RpId>().unwrap();
154        assert_eq!(rp_id.0, 0x123456789abcdef0);
155    }
156
157    #[test]
158    fn test_rpid_from_str_errors() {
159        assert!("123456789abcdef0".parse::<RpId>().is_err());
160        assert!("rp_invalid".parse::<RpId>().is_err());
161        assert!("rp_".parse::<RpId>().is_err());
162    }
163
164    #[test]
165    fn test_rpid_roundtrip() {
166        let original = RpId::new(0x123456789abcdef0);
167        let s = original.to_string();
168        let parsed = s.parse::<RpId>().unwrap();
169        assert_eq!(original, parsed);
170    }
171
172    #[test]
173    fn test_rpid_json_serialization() {
174        let rp_id = RpId::new(0x123456789abcdef0);
175        let json = serde_json::to_string(&rp_id).unwrap();
176        assert_eq!(json, "\"rp_123456789abcdef0\"");
177
178        let deserialized: RpId = serde_json::from_str(&json).unwrap();
179        assert_eq!(rp_id, deserialized);
180    }
181
182    #[test]
183    fn test_rpid_binary_serialization() {
184        let rp_id = RpId::new(0x123456789abcdef0);
185
186        let mut buffer = Vec::new();
187        ciborium::into_writer(&rp_id, &mut buffer).unwrap();
188
189        let decoded: RpId = ciborium::from_reader(&buffer[..]).unwrap();
190
191        assert_eq!(rp_id, decoded);
192    }
193
194    #[test]
195    fn test_compute_rp_signature_msg_fixed_length() {
196        // Test with small values that would have leading zeros in variable-length encoding
197        // to ensure we always get fixed 32-byte field elements
198        let nonce = ark_babyjubjub::Fq::from(1u64);
199        let created_at = 1000u64;
200        let expires_at = 2000u64;
201
202        let msg = compute_rp_signature_msg(nonce, created_at, expires_at, None);
203
204        // Message must always be exactly 49 bytes:
205        // 1 (version) + 32 (nonce) + 8 (created_at) + 8 (expires_at)
206        assert_eq!(
207            msg.len(),
208            49,
209            "RP signature message must be exactly 49 bytes"
210        );
211        assert_eq!(
212            msg[0], RP_SIGNATURE_MSG_VERSION,
213            "RP signature message version must be 0x01"
214        );
215    }
216
217    #[test]
218    fn test_compute_rp_signature_msg_with_action() {
219        // Test with small values that would have leading zeros in variable-length encoding
220        // to ensure we always get fixed 32-byte field elements
221        let nonce = ark_babyjubjub::Fq::from(1u64);
222        let created_at = 1000u64;
223        let expires_at = 2000u64;
224        let action = ark_babyjubjub::Fq::from(2u64);
225
226        let msg = compute_rp_signature_msg(nonce, created_at, expires_at, Some(action));
227
228        // Message must always be exactly 81 bytes:
229        // 1 (version) + 32 (nonce) + 8 (created_at) + 8 (expires_at) + 32 (action)
230        assert_eq!(
231            msg.len(),
232            81,
233            "RP signature message must be exactly 81 bytes"
234        );
235        assert_eq!(
236            msg[0], RP_SIGNATURE_MSG_VERSION,
237            "RP signature message version must be 0x01"
238        );
239    }
240}