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
10/// The id of a relying party.
11#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct RpId(u64);
13
14impl RpId {
15    /// Converts the RP id to an u64
16    #[must_use]
17    pub const fn into_inner(self) -> u64 {
18        self.0
19    }
20
21    /// Creates a new `RpId` by wrapping a `u64`
22    #[must_use]
23    pub const fn new(value: u64) -> Self {
24        Self(value)
25    }
26}
27
28impl fmt::Display for RpId {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        write!(f, "rp_{:016x}", self.0)
31    }
32}
33
34impl FromStr for RpId {
35    type Err = String;
36
37    fn from_str(s: &str) -> Result<Self, Self::Err> {
38        if let Some(id) = s.strip_prefix("rp_") {
39            Ok(Self(u64::from_str_radix(id, 16).map_err(|_| {
40                "Invalid RP ID format: expected hex string".to_string()
41            })?))
42        } else {
43            Err("A valid RP ID must start with 'rp_'".to_string())
44        }
45    }
46}
47
48impl From<u64> for RpId {
49    fn from(value: u64) -> Self {
50        Self(value)
51    }
52}
53
54impl From<RpId> for FieldElement {
55    fn from(value: RpId) -> Self {
56        Self::from(value.0)
57    }
58}
59
60impl Serialize for RpId {
61    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
62    where
63        S: Serializer,
64    {
65        if serializer.is_human_readable() {
66            serializer.serialize_str(&self.to_string())
67        } else {
68            u64::serialize(&self.0, serializer)
69        }
70    }
71}
72
73impl<'de> Deserialize<'de> for RpId {
74    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
75    where
76        D: Deserializer<'de>,
77    {
78        if deserializer.is_human_readable() {
79            let s = String::deserialize(deserializer)?;
80            Self::from_str(&s).map_err(D::Error::custom)
81        } else {
82            let value = u64::deserialize(deserializer)?;
83            Ok(Self(value))
84        }
85    }
86}
87
88/// Computes the message to be signed for the RP signature.
89///
90/// The message format is: `nonce || created_at || expires_at` (48 bytes total).
91/// - `nonce`: 32 bytes (big-endian)
92/// - `created_at`: 8 bytes (big-endian)
93/// - `expires_at`: 8 bytes (big-endian)
94#[must_use]
95pub fn compute_rp_signature_msg(
96    nonce: ark_babyjubjub::Fq,
97    created_at: u64,
98    expires_at: u64,
99) -> Vec<u8> {
100    let mut msg = Vec::with_capacity(48);
101    msg.extend(nonce.into_bigint().to_bytes_be());
102    msg.extend(created_at.to_be_bytes());
103    msg.extend(expires_at.to_be_bytes());
104    msg
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_rpid_display() {
113        let rp_id = RpId::new(0x123456789abcdef0);
114        assert_eq!(rp_id.to_string(), "rp_123456789abcdef0");
115
116        let rp_id = RpId::new(u64::MAX);
117        assert_eq!(rp_id.to_string(), "rp_ffffffffffffffff");
118
119        let rp_id = RpId::new(0);
120        assert_eq!(rp_id.to_string(), "rp_0000000000000000");
121    }
122
123    #[test]
124    fn test_rpid_from_str() {
125        let rp_id = "rp_123456789abcdef0".parse::<RpId>().unwrap();
126        assert_eq!(rp_id.0, 0x123456789abcdef0);
127
128        let rp_id = "rp_ffffffffffffffff".parse::<RpId>().unwrap();
129        assert_eq!(rp_id.0, u64::MAX);
130
131        let rp_id = "rp_0000000000000000".parse::<RpId>().unwrap();
132        assert_eq!(rp_id.0, 0);
133
134        let rp_id = "rp_123456789ABCDEF0".parse::<RpId>().unwrap();
135        assert_eq!(rp_id.0, 0x123456789abcdef0);
136    }
137
138    #[test]
139    fn test_rpid_from_str_errors() {
140        assert!("123456789abcdef0".parse::<RpId>().is_err());
141        assert!("rp_invalid".parse::<RpId>().is_err());
142        assert!("rp_".parse::<RpId>().is_err());
143    }
144
145    #[test]
146    fn test_rpid_roundtrip() {
147        let original = RpId::new(0x123456789abcdef0);
148        let s = original.to_string();
149        let parsed = s.parse::<RpId>().unwrap();
150        assert_eq!(original, parsed);
151    }
152
153    #[test]
154    fn test_rpid_json_serialization() {
155        let rp_id = RpId::new(0x123456789abcdef0);
156        let json = serde_json::to_string(&rp_id).unwrap();
157        assert_eq!(json, "\"rp_123456789abcdef0\"");
158
159        let deserialized: RpId = serde_json::from_str(&json).unwrap();
160        assert_eq!(rp_id, deserialized);
161    }
162
163    #[test]
164    fn test_rpid_binary_serialization() {
165        let rp_id = RpId::new(0x123456789abcdef0);
166
167        let mut buffer = Vec::new();
168        ciborium::into_writer(&rp_id, &mut buffer).unwrap();
169
170        let decoded: RpId = ciborium::from_reader(&buffer[..]).unwrap();
171
172        assert_eq!(rp_id, decoded);
173    }
174
175    #[test]
176    fn test_compute_rp_signature_msg_fixed_length() {
177        // Test with small values that would have leading zeros in variable-length encoding
178        // to ensure we always get fixed 32-byte field elements
179        let nonce = ark_babyjubjub::Fq::from(1u64);
180        let created_at = 1000u64;
181        let expires_at = 2000u64;
182
183        let msg = compute_rp_signature_msg(nonce, created_at, expires_at);
184
185        // Message must always be exactly 80 bytes: 32 (nonce) + 8 (created_at) + 8 (expires_at)
186        assert_eq!(
187            msg.len(),
188            48,
189            "RP signature message must be exactly 48 bytes"
190        );
191    }
192}