tor_netdoc/doc/hsdesc/build/
inner.rs1use crate::build::ItemArgument;
8use crate::build::NetdocEncoder;
9use crate::doc::hsdesc::inner::HsInnerKwd;
10use crate::doc::hsdesc::pow::v1::PowParamsV1;
11use crate::doc::hsdesc::pow::PowParams;
12use crate::doc::hsdesc::IntroAuthType;
13use crate::doc::hsdesc::IntroPointDesc;
14use crate::types::misc::Iso8601TimeNoSp;
15use crate::NetdocBuilder;
16
17use rand::CryptoRng;
18use rand::RngCore;
19use tor_bytes::{EncodeError, Writer};
20use tor_cell::chancell::msg::HandshakeType;
21use tor_cert::{CertType, CertifiedKey, Ed25519Cert};
22use tor_error::internal;
23use tor_error::{bad_api_usage, into_bad_api_usage};
24use tor_llcrypto::pk::ed25519;
25use tor_llcrypto::pk::keymanip::convert_curve25519_to_ed25519_public;
26
27use base64ct::{Base64, Encoding};
28
29use std::time::SystemTime;
30
31use smallvec::SmallVec;
32
33#[derive(Debug)]
37pub(super) struct HsDescInner<'a> {
38 pub(super) hs_desc_sign: &'a ed25519::Keypair,
40 pub(super) create2_formats: &'a [HandshakeType],
42 pub(super) auth_required: Option<&'a SmallVec<[IntroAuthType; 2]>>,
44 pub(super) is_single_onion_service: bool,
46 pub(super) intro_points: &'a [IntroPointDesc],
48 pub(super) intro_auth_key_cert_expiry: SystemTime,
50 pub(super) intro_enc_key_cert_expiry: SystemTime,
52 #[cfg(feature = "hs-pow-full")]
54 pub(super) pow_params: Option<&'a PowParams>,
55}
56
57#[cfg(feature = "hs-pow-full")]
58fn encode_pow_params(
59 encoder: &mut NetdocEncoder,
60 pow_params: &PowParamsV1,
61) -> Result<(), EncodeError> {
62 let mut pow_params_enc = encoder.item(HsInnerKwd::POW_PARAMS);
63 pow_params_enc.add_arg(&"v1");
64
65 let (seed, (_, expiration)) = pow_params.seed().clone().dangerously_into_parts();
68
69 seed.write_onto(&mut pow_params_enc)?;
70
71 pow_params
72 .suggested_effort()
73 .write_onto(&mut pow_params_enc)?;
74
75 let expiration = if let Some(expiration) = expiration {
76 expiration
77 } else {
78 return Err(internal!("PoW seed should always have expiration").into());
79 };
80
81 Iso8601TimeNoSp::from(expiration).write_onto(&mut pow_params_enc)?;
82
83 Ok(())
84}
85
86impl<'a> NetdocBuilder for HsDescInner<'a> {
87 fn build_sign<R: RngCore + CryptoRng>(self, _: &mut R) -> Result<String, EncodeError> {
88 use HsInnerKwd::*;
89
90 let HsDescInner {
91 hs_desc_sign,
92 create2_formats,
93 auth_required,
94 is_single_onion_service,
95 intro_points,
96 intro_auth_key_cert_expiry,
97 intro_enc_key_cert_expiry,
98 #[cfg(feature = "hs-pow-full")]
99 pow_params,
100 } = self;
101
102 let mut encoder = NetdocEncoder::new();
103
104 {
105 let mut create2_formats_enc = encoder.item(CREATE2_FORMATS);
106 for fmt in create2_formats {
107 let fmt: u16 = (*fmt).into();
108 create2_formats_enc = create2_formats_enc.arg(&fmt);
109 }
110 }
111
112 {
113 if let Some(auth_required) = auth_required {
114 let mut auth_required_enc = encoder.item(INTRO_AUTH_REQUIRED);
115 for auth in auth_required {
116 auth_required_enc = auth_required_enc.arg(&auth.to_string());
117 }
118 }
119 }
120
121 if is_single_onion_service {
122 encoder.item(SINGLE_ONION_SERVICE);
123 }
124
125 #[cfg(feature = "hs-pow-full")]
126 if let Some(pow_params) = pow_params {
127 match pow_params {
128 #[cfg(feature = "hs-pow-full")]
129 PowParams::V1(pow_params) => encode_pow_params(&mut encoder, pow_params)?,
130 #[cfg(not(feature = "hs-pow-full"))]
131 PowParams::V1(_) => {
132 return Err(internal!(
133 "Got a V1 PoW params but support for V1 is disabled."
134 ))
135 }
136 }
137 }
138
139 let mut sorted_ip: Vec<_> = intro_points.iter().collect();
147 sorted_ip.sort_by_key(|key| key.ipt_ntor_key.as_bytes());
148 for intro_point in sorted_ip {
149 let nspec: u8 = intro_point
152 .link_specifiers
153 .len()
154 .try_into()
155 .map_err(into_bad_api_usage!("Too many link specifiers."))?;
156
157 let mut link_specifiers = vec![];
158 link_specifiers.write_u8(nspec);
159
160 for link_spec in &intro_point.link_specifiers {
161 link_specifiers.write(link_spec)?;
162 }
163
164 encoder
165 .item(INTRODUCTION_POINT)
166 .arg(&Base64::encode_string(&link_specifiers));
167 encoder
168 .item(ONION_KEY)
169 .arg(&"ntor")
170 .arg(&Base64::encode_string(&intro_point.ipt_ntor_key.to_bytes()));
171
172 let signed_auth_key = Ed25519Cert::constructor()
175 .cert_type(CertType::HS_IP_V_SIGNING)
176 .expiration(intro_auth_key_cert_expiry)
177 .signing_key(ed25519::Ed25519Identity::from(hs_desc_sign.verifying_key()))
178 .cert_key(CertifiedKey::Ed25519((*intro_point.ipt_sid_key).into()))
179 .encode_and_sign(hs_desc_sign)
180 .map_err(into_bad_api_usage!("failed to sign the intro auth key"))?;
181
182 encoder
183 .item(AUTH_KEY)
184 .object("ED25519 CERT", signed_auth_key.as_ref());
185
186 encoder
192 .item(ENC_KEY)
193 .arg(&"ntor")
194 .arg(&Base64::encode_string(
195 &intro_point.svc_ntor_key.as_bytes()[..],
196 ));
197
198 let signbit = 0;
207 let ed_svc_ntor_key =
208 convert_curve25519_to_ed25519_public(&intro_point.svc_ntor_key, signbit)
209 .ok_or_else(|| {
210 bad_api_usage!("failed to convert curve25519 pk to ed25519 pk")
211 })?;
212
213 let signed_enc_key = Ed25519Cert::constructor()
216 .cert_type(CertType::HS_IP_CC_SIGNING)
217 .expiration(intro_enc_key_cert_expiry)
218 .signing_key(ed25519::Ed25519Identity::from(hs_desc_sign.verifying_key()))
219 .cert_key(CertifiedKey::Ed25519(ed25519::Ed25519Identity::from(
220 &ed_svc_ntor_key,
221 )))
222 .encode_and_sign(hs_desc_sign)
223 .map_err(into_bad_api_usage!(
224 "failed to sign the intro encryption key"
225 ))?;
226
227 encoder
228 .item(ENC_KEY_CERT)
229 .object("ED25519 CERT", signed_enc_key.as_ref());
230 }
231
232 encoder.finish().map_err(|e| e.into())
233 }
234}
235
236#[cfg(test)]
237mod test {
238 #![allow(clippy::bool_assert_comparison)]
240 #![allow(clippy::clone_on_copy)]
241 #![allow(clippy::dbg_macro)]
242 #![allow(clippy::mixed_attributes_style)]
243 #![allow(clippy::print_stderr)]
244 #![allow(clippy::print_stdout)]
245 #![allow(clippy::single_char_pattern)]
246 #![allow(clippy::unwrap_used)]
247 #![allow(clippy::unchecked_duration_subtraction)]
248 #![allow(clippy::useless_vec)]
249 #![allow(clippy::needless_pass_by_value)]
250 use super::*;
253 use crate::doc::hsdesc::build::test::{create_intro_point_descriptor, expect_bug};
254 use crate::doc::hsdesc::pow::v1::PowParamsV1;
255 use crate::doc::hsdesc::IntroAuthType;
256
257 use smallvec::SmallVec;
258 use std::net::Ipv4Addr;
259 use std::time::UNIX_EPOCH;
260 use tor_basic_utils::test_rng::Config;
261 use tor_checkable::timed::TimerangeBound;
262 #[cfg(feature = "hs-pow-full")]
263 use tor_hscrypto::pow::v1::{Effort, Seed};
264 use tor_linkspec::LinkSpec;
265
266 fn create_inner_desc(
268 create2_formats: &[HandshakeType],
269 auth_required: Option<&SmallVec<[IntroAuthType; 2]>>,
270 is_single_onion_service: bool,
271 intro_points: &[IntroPointDesc],
272 pow_params: Option<&PowParams>,
273 ) -> Result<String, EncodeError> {
274 let hs_desc_sign = ed25519::Keypair::generate(&mut Config::Deterministic.into_rng());
275
276 HsDescInner {
277 hs_desc_sign: &hs_desc_sign,
278 create2_formats,
279 auth_required,
280 is_single_onion_service,
281 intro_points,
282 intro_auth_key_cert_expiry: UNIX_EPOCH,
283 intro_enc_key_cert_expiry: UNIX_EPOCH,
284 #[cfg(feature = "hs-pow-full")]
285 pow_params,
286 }
287 .build_sign(&mut rand::rng())
288 }
289
290 #[test]
291 fn inner_hsdesc_no_intro_auth() {
292 let hs_desc = create_inner_desc(
294 &[HandshakeType::NTOR], None, true, &[], None,
299 )
300 .unwrap();
301
302 assert_eq!(hs_desc, "create2-formats 2\nsingle-onion-service\n");
303
304 let hs_desc = create_inner_desc(
306 &[HandshakeType::NTOR], None, false, &[], None,
311 )
312 .unwrap();
313
314 assert_eq!(hs_desc, "create2-formats 2\n");
315
316 let link_specs1 = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 1234)];
317 let link_specs2 = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 5679)];
318 let link_specs3 = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 8901)];
319
320 let mut rng = Config::Deterministic.into_rng();
321 let intros = &[
322 create_intro_point_descriptor(&mut rng, link_specs1),
323 create_intro_point_descriptor(&mut rng, link_specs2),
324 create_intro_point_descriptor(&mut rng, link_specs3),
325 ];
326
327 let hs_desc = create_inner_desc(
328 &[
329 HandshakeType::TAP,
330 HandshakeType::NTOR,
331 HandshakeType::NTOR_V3,
332 ], None, false, intros, None,
337 )
338 .unwrap();
339
340 assert_eq!(
341 hs_desc,
342 r#"create2-formats 0 2 3
343introduction-point AQAGfwAAASLF
344onion-key ntor CJi8nDPhIFA7X9Q+oP7+jzxNo044cblmagk/d7oKWGc=
345auth-key
346-----BEGIN ED25519 CERT-----
347AQkAAAAAAU4J4xGrMt9q5eHYZSmbOZTi1iKl59nd3ItYXAa/ASlRAQAgBACQKRtN
348eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61CGkJzc/ECYHzJeeAKIkRFV/6jr9
349zAB5XnEFghZmXdDTQdqcPXAFydyeHWW4uR+Uii0wPI8VokbU0NoLTNYJGAM=
350-----END ED25519 CERT-----
351enc-key ntor TL7GcN+B++pB6eRN/0nBZGmWe125qh7ccQJ/Hhku+x8=
352enc-key-cert
353-----BEGIN ED25519 CERT-----
354AQsAAAAAAabaCv4gv9ddyIztD1J8my9mgotmWnkHX94buLAtt15aAQAgBACQKRtN
355eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61GxlI6caS8iFp2bLmg1+Pkgij47f
356eetKn+yDC5Q3eo/hJLDBGAQNOX7jFMdr9HjotjXIt6/Khfmg58CZC/gKhAw=
357-----END ED25519 CERT-----
358introduction-point AQAGfwAAAQTS
359onion-key ntor HWIigEAdcOgqgHPDFmzhhkeqvYP/GcMT2fKb5JY6ey8=
360auth-key
361-----BEGIN ED25519 CERT-----
362AQkAAAAAAZZVJwNlzVw1ZQGO7MTzC5MsySASd+fswAcjdTJJOifXAQAgBACQKRtN
363eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61IVW0XivcAKhvUvNUsU1CFznk3Mz
364KSsp/mBoKi2iY4f4eN2SXx8U6pmnxnXFxYP6obi+tc5QWj1Jbfl1Aci3TAA=
365-----END ED25519 CERT-----
366enc-key ntor 9Upi9XNWyqx3ZwHeQ5r3+Dh116k+C4yHeE9BcM68HDc=
367enc-key-cert
368-----BEGIN ED25519 CERT-----
369AQsAAAAAAcH+1K5m7pRnMc01mPp5AYVnJK1iZ/fKHwK0tVR/jtBvAQAgBACQKRtN
370eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61Hectpha37ioha85fpNt+/yDfebh
3716BKUUQ0jf3SMXuNgX8SV9NSabn14WCSdKG/8RoYBCTR+yRJX0dy55mjg+go=
372-----END ED25519 CERT-----
373introduction-point AQAGfwAAARYv
374onion-key ntor x/stThC6cVWJJUR7WERZj5VYVPTAOA/UDjHdtprJkiE=
375auth-key
376-----BEGIN ED25519 CERT-----
377AQkAAAAAAVMhalzZJ8txKHuCX8TEhmO3LbCvDgV0zMT4eQ49SDpBAQAgBACQKRtN
378eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61GdVAiMag0dquEx4IywKDLEhxA7N
3792RZFTS2QI+Sk3dyz46WO+epj1YBlgfOYCZlBEx+oFkRlUJdOc0Eu0sDlAw8=
380-----END ED25519 CERT-----
381enc-key ntor XI/a9NGh/7ClaFcKqtdI9DoP8da5ovwPDdgCHUr3xX0=
382enc-key-cert
383-----BEGIN ED25519 CERT-----
384AQsAAAAAAZYGETSx12Og2xqJNMS9kGOHTEFeBkFPi7k0UaFv5HNKAQAgBACQKRtN
385eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61E8vxB5lB83+rQnWmHLzpfuMUZjG
386o7Ct/ZB0j8YRB5lKSd07YAjA6Zo8kMnuZYX2Mb67TxWDQ/zlYJGOwLlj7A8=
387-----END ED25519 CERT-----
388"#
389 );
390 }
391
392 #[test]
393 fn inner_hsdesc_too_many_link_specifiers() {
394 let link_spec = LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 9999);
395 let link_specifiers =
396 std::iter::repeat_n(link_spec, u8::MAX as usize + 1).collect::<Vec<_>>();
397
398 let intros = &[create_intro_point_descriptor(
399 &mut Config::Deterministic.into_rng(),
400 &link_specifiers,
401 )];
402
403 let err = create_inner_desc(
406 &[HandshakeType::NTOR], None, false, intros, None,
411 )
412 .unwrap_err();
413
414 assert!(expect_bug(err).contains("Too many link specifiers."));
415 }
416
417 #[test]
418 fn inner_hsdesc_intro_auth() {
419 let mut rng = Config::Deterministic.into_rng();
420 let link_specs = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 8080)];
421 let intros = &[create_intro_point_descriptor(&mut rng, link_specs)];
422 let auth = SmallVec::from([IntroAuthType::Ed25519, IntroAuthType::Ed25519]);
423
424 let hs_desc = create_inner_desc(
427 &[HandshakeType::NTOR], Some(&auth), false, intros, None,
432 )
433 .unwrap();
434
435 assert_eq!(
436 hs_desc,
437 r#"create2-formats 2
438intro-auth-required ed25519 ed25519
439introduction-point AQAGfwAAAR+Q
440onion-key ntor HWIigEAdcOgqgHPDFmzhhkeqvYP/GcMT2fKb5JY6ey8=
441auth-key
442-----BEGIN ED25519 CERT-----
443AQkAAAAAAZZVJwNlzVw1ZQGO7MTzC5MsySASd+fswAcjdTJJOifXAQAgBACQKRtN
444eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61IVW0XivcAKhvUvNUsU1CFznk3Mz
445KSsp/mBoKi2iY4f4eN2SXx8U6pmnxnXFxYP6obi+tc5QWj1Jbfl1Aci3TAA=
446-----END ED25519 CERT-----
447enc-key ntor 9Upi9XNWyqx3ZwHeQ5r3+Dh116k+C4yHeE9BcM68HDc=
448enc-key-cert
449-----BEGIN ED25519 CERT-----
450AQsAAAAAAcH+1K5m7pRnMc01mPp5AYVnJK1iZ/fKHwK0tVR/jtBvAQAgBACQKRtN
451eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61Hectpha37ioha85fpNt+/yDfebh
4526BKUUQ0jf3SMXuNgX8SV9NSabn14WCSdKG/8RoYBCTR+yRJX0dy55mjg+go=
453-----END ED25519 CERT-----
454"#
455 );
456 }
457
458 #[test]
459 #[cfg(feature = "hs-pow-full")]
460 fn inner_hsdesc_pow_params() {
461 use humantime::parse_rfc3339;
462
463 let mut rng = Config::Deterministic.into_rng();
464 let link_specs = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 8080)];
465 let intros = &[create_intro_point_descriptor(&mut rng, link_specs)];
466
467 let pow_expiration = parse_rfc3339("1994-04-29T00:00:00Z").unwrap();
468 let pow_params = PowParams::V1(PowParamsV1::new(
469 TimerangeBound::new(Seed::from([0; 32]), ..pow_expiration),
470 Effort::new(64),
471 ));
472
473 let hs_desc = create_inner_desc(
474 &[HandshakeType::NTOR], None, false, intros, Some(&pow_params),
479 )
480 .unwrap();
481
482 assert!(hs_desc.contains(
483 "\npow-params v1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 64 1994-04-29T00:00:00\n"
484 ));
485 }
486}