1pub const MAX_CHALLENGES: usize = 8;
5pub const MAX_SCHEME_BYTES: usize = 64;
7pub const MAX_VALUE_BYTES: usize = 8_192;
9pub const MAX_DESCRIPTION_BYTES: usize = 512;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum BoundError {
15 Count,
17 Scheme,
19 Value,
21 Description,
23}
24
25#[must_use]
27pub fn is_valid_scheme(scheme: &str) -> bool {
28 let bytes = scheme.as_bytes();
29 (1..=MAX_SCHEME_BYTES).contains(&bytes.len())
30 && (bytes[0].is_ascii_lowercase() || bytes[0].is_ascii_digit())
31 && bytes[1..]
32 .iter()
33 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || b".-".contains(byte))
34}
35
36pub fn validate_challenges(
41 challenges: &[(&str, &str)],
42 description: &str,
43) -> Result<(), BoundError> {
44 if !(1..=MAX_CHALLENGES).contains(&challenges.len()) {
45 return Err(BoundError::Count);
46 }
47 for (scheme, value) in challenges {
48 if !is_valid_scheme(scheme) {
49 return Err(BoundError::Scheme);
50 }
51 if value.len() > MAX_VALUE_BYTES || value.chars().any(forbidden_control) {
52 return Err(BoundError::Value);
53 }
54 }
55 if description.len() > MAX_DESCRIPTION_BYTES || description.chars().any(forbidden_control) {
56 return Err(BoundError::Description);
57 }
58 Ok(())
59}
60
61fn forbidden_control(ch: char) -> bool {
62 ch != '\t' && ch.is_control()
63}
64
65fn varint(mut value: usize, out: &mut Vec<u8>) {
66 while value >= 0x80 {
67 out.push((value.to_le_bytes()[0] & 0x7f) | 0x80);
68 value >>= 7;
69 }
70 out.push(value.to_le_bytes()[0]);
71}
72
73fn field(number: u8, value: &[u8], out: &mut Vec<u8>) {
74 out.push((number << 3) | 2);
75 varint(value.len(), out);
76 out.extend_from_slice(value);
77}
78
79#[must_use]
81pub fn encode_admission_challenge(challenges: &[(&str, &str)], description: &str) -> Vec<u8> {
82 let mut out = Vec::new();
83 for (scheme, value) in challenges {
84 let mut entry = Vec::new();
85 if !scheme.is_empty() {
86 field(1, scheme.as_bytes(), &mut entry);
87 }
88 if !value.is_empty() {
89 field(2, value.as_bytes(), &mut entry);
90 }
91 field(1, &entry, &mut out);
92 }
93 if !description.is_empty() {
94 field(2, description.as_bytes(), &mut out);
95 }
96 out
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn golden_bytes() {
105 let challenges = [
106 (
107 "mpp",
108 "Payment id=\"fake-example-not-valid\", method=\"tempo\", intent=\"charge\", request=\"fake-example-request-not-valid\"",
109 ),
110 ("x402", "fake-example-payment-required-not-valid"),
111 ];
112 assert_eq!(
113 encode_admission_challenge(&challenges, "Example upload payment required."),
114 include_bytes!("../../../tests/golden/transport/admission-challenge.bin")
115 );
116 }
117
118 #[test]
119 fn bounds() {
120 for good in ["a", "0", "mpp.v1-2", &"a".repeat(64)] {
121 assert!(is_valid_scheme(good));
122 }
123 for bad in ["", "A", "mpp_1", "-mpp", &"a".repeat(65)] {
124 assert!(!is_valid_scheme(bad));
125 }
126 assert_eq!(validate_challenges(&[], ""), Err(BoundError::Count));
127 assert!(validate_challenges(&vec![("mpp", "v"); 8], "").is_ok());
128 assert_eq!(
129 validate_challenges(&vec![("mpp", "v"); 9], ""),
130 Err(BoundError::Count)
131 );
132 assert!(validate_challenges(&[("mpp", &"v".repeat(8192))], &"d".repeat(512)).is_ok());
133 assert_eq!(
134 validate_challenges(&[("mpp", &"v".repeat(8193))], ""),
135 Err(BoundError::Value)
136 );
137 assert_eq!(
138 validate_challenges(&[("mpp", "v")], &"d".repeat(513)),
139 Err(BoundError::Description)
140 );
141 assert_eq!(
142 validate_challenges(&[("mpp", "a\r")], ""),
143 Err(BoundError::Value)
144 );
145 assert_eq!(
146 validate_challenges(&[("mpp", "a")], "a\n"),
147 Err(BoundError::Description)
148 );
149 assert_eq!(
150 validate_challenges(&[("mpp", "a\u{85}")], ""),
151 Err(BoundError::Value)
152 );
153 }
154}