tor_netdoc/doc/hsdesc/build/
inner.rs1use crate::NetdocBuilder;
8use crate::doc::hsdesc::IntroAuthType;
9use crate::doc::hsdesc::IntroPointDesc;
10use crate::doc::hsdesc::inner::HsInnerKwd;
11use crate::doc::hsdesc::pow::PowParams;
12use crate::doc::hsdesc::pow::v1::PowParamsV1;
13use crate::encode::ItemArgument;
14use crate::encode::NetdocEncoder;
15use crate::types::misc::Iso8601TimeNoSp;
16
17use rand::CryptoRng;
18use rand::Rng;
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};
28use tor_protover::Protocols;
29
30use std::sync::LazyLock;
31use std::time::SystemTime;
32
33use smallvec::SmallVec;
34
35#[derive(Debug)]
39pub(super) struct HsDescInner<'a> {
40 pub(super) hs_desc_sign: &'a ed25519::Keypair,
42 pub(super) create2_formats: &'a [HandshakeType],
44 pub(super) auth_required: Option<&'a SmallVec<[IntroAuthType; 2]>>,
46 pub(super) is_single_onion_service: bool,
48 pub(super) intro_points: &'a [IntroPointDesc],
50 pub(super) intro_auth_key_cert_expiry: SystemTime,
52 pub(super) intro_enc_key_cert_expiry: SystemTime,
54
55 pub(super) flow_control: Option<&'a (Protocols, u8)>,
63
64 pub(super) protos: Protocols,
66
67 #[cfg(feature = "hs-pow-full")]
69 pub(super) pow_params: Option<&'a PowParams>,
70}
71
72#[cfg(feature = "hs-pow-full")]
74fn encode_pow_params(
75 encoder: &mut NetdocEncoder,
76 pow_params: &PowParamsV1,
77) -> Result<(), EncodeError> {
78 let mut pow_params_enc = encoder.item(HsInnerKwd::POW_PARAMS);
79 pow_params_enc.add_arg(&"v1");
80
81 let (seed, expiration) = pow_params.seed().clone().dangerously_into_parts();
84 let expiration = expiration.end();
85
86 seed.write_arg_onto(&mut pow_params_enc)?;
87
88 pow_params
89 .suggested_effort()
90 .write_arg_onto(&mut pow_params_enc)?;
91
92 let expiration = if let Some(expiration) = expiration {
93 expiration
94 } else {
95 return Err(internal!("PoW seed should always have expiration").into());
96 };
97
98 Iso8601TimeNoSp::from(expiration).write_arg_onto(&mut pow_params_enc)?;
99
100 Ok(())
101}
102
103impl<'a> NetdocBuilder for HsDescInner<'a> {
104 fn build_sign<R: Rng + CryptoRng>(self, _: &mut R) -> Result<String, EncodeError> {
105 use HsInnerKwd::*;
106
107 let HsDescInner {
108 hs_desc_sign,
109 create2_formats,
110 auth_required,
111 is_single_onion_service,
112 intro_points,
113 intro_auth_key_cert_expiry,
114 intro_enc_key_cert_expiry,
115 flow_control,
116 protos,
117 #[cfg(feature = "hs-pow-full")]
118 pow_params,
119 } = self;
120
121 let mut encoder = NetdocEncoder::new();
122
123 {
124 let mut create2_formats_enc = encoder.item(CREATE2_FORMATS);
125 for fmt in create2_formats {
126 let fmt: u16 = (*fmt).into();
127 create2_formats_enc = create2_formats_enc.arg(&fmt);
128 }
129 }
130
131 {
132 if let Some(auth_required) = auth_required {
133 let mut auth_required_enc = encoder.item(INTRO_AUTH_REQUIRED);
134 for auth in auth_required {
135 auth_required_enc = auth_required_enc.arg(&auth.to_string());
136 }
137 }
138 }
139
140 if is_single_onion_service {
141 encoder.item(SINGLE_ONION_SERVICE);
142 }
143
144 if let Some((fcp, inc)) = flow_control {
145 let fcp = flowctrl_protocols(fcp);
146 encoder.item(FLOW_CONTROL).arg(&fcp).arg(inc);
147 }
148
149 if !protos.is_empty() {
150 encoder.item(PROTO).args_raw_string(&protos.to_string());
151 }
152
153 #[cfg(feature = "hs-pow-full")]
154 if let Some(pow_params) = pow_params {
155 match pow_params {
156 #[cfg(feature = "hs-pow-full")]
157 PowParams::V1(pow_params) => encode_pow_params(&mut encoder, pow_params)?,
158 #[cfg(not(feature = "hs-pow-full"))]
159 PowParams::V1(_) => {
160 return Err(internal!(
161 "Got a V1 PoW params but support for V1 is disabled."
162 ));
163 }
164 }
165 }
166
167 let mut sorted_ip: Vec<_> = intro_points.iter().collect();
175 sorted_ip.sort_by_key(|key| key.ipt_ntor_key.as_bytes());
176 for intro_point in sorted_ip {
177 let nspec: u8 = intro_point
180 .link_specifiers
181 .len()
182 .try_into()
183 .map_err(into_bad_api_usage!("Too many link specifiers."))?;
184
185 let mut link_specifiers = vec![];
186 link_specifiers.write_u8(nspec);
187
188 for link_spec in &intro_point.link_specifiers {
189 link_specifiers.write(link_spec)?;
190 }
191
192 encoder
193 .item(INTRODUCTION_POINT)
194 .arg(&Base64::encode_string(&link_specifiers));
195 encoder
196 .item(ONION_KEY)
197 .arg(&"ntor")
198 .arg(&Base64::encode_string(&intro_point.ipt_ntor_key.to_bytes()));
199
200 let signed_auth_key = Ed25519Cert::builder()
203 .cert_type(CertType::HS_IP_V_SIGNING)
204 .expiration(intro_auth_key_cert_expiry)
205 .signing_key(ed25519::Ed25519Identity::from(hs_desc_sign.verifying_key()))
206 .cert_key(CertifiedKey::Ed25519((*intro_point.ipt_sid_key).into()))
207 .encode_and_sign(hs_desc_sign)
208 .map_err(into_bad_api_usage!("failed to sign the intro auth key"))?;
209
210 encoder
211 .item(AUTH_KEY)
212 .object_bytes("ED25519 CERT", signed_auth_key.as_ref());
213
214 encoder
220 .item(ENC_KEY)
221 .arg(&"ntor")
222 .arg(&Base64::encode_string(
223 &intro_point.svc_ntor_key.as_bytes()[..],
224 ));
225
226 let signbit = 0;
235 let ed_svc_ntor_key =
236 convert_curve25519_to_ed25519_public(&intro_point.svc_ntor_key, signbit)
237 .ok_or_else(|| {
238 bad_api_usage!("failed to convert curve25519 pk to ed25519 pk")
239 })?;
240
241 let signed_enc_key = Ed25519Cert::builder()
244 .cert_type(CertType::HS_IP_CC_SIGNING)
245 .expiration(intro_enc_key_cert_expiry)
246 .signing_key(ed25519::Ed25519Identity::from(hs_desc_sign.verifying_key()))
247 .cert_key(CertifiedKey::Ed25519(ed25519::Ed25519Identity::from(
248 &ed_svc_ntor_key,
249 )))
250 .encode_and_sign(hs_desc_sign)
251 .map_err(into_bad_api_usage!(
252 "failed to sign the intro encryption key"
253 ))?;
254
255 encoder
256 .item(ENC_KEY_CERT)
257 .object_bytes("ED25519 CERT", signed_enc_key.as_ref());
258 }
259
260 encoder.finish().map_err(|e| e.into())
261 }
262}
263
264fn flowctrl_protocols(p: &Protocols) -> String {
271 use tor_protover::ProtoKind;
272 static ALL_FLOWCTRL: LazyLock<Protocols> = LazyLock::new(|| {
276 Protocols::from_kind_and_versions(ProtoKind::FlowCtrl, "1-2")
277 .expect("Internal protocol list could not be parsed")
278 });
279
280 ALL_FLOWCTRL
281 .intersection(p)
282 .to_string()
283 .strip_prefix("FlowCtrl=")
284 .expect("FlowCtrl protocols were not encoded correctly.")
285 .to_string()
286}
287
288#[cfg(test)]
289mod test {
290 #![allow(clippy::bool_assert_comparison)]
292 #![allow(clippy::clone_on_copy)]
293 #![allow(clippy::dbg_macro)]
294 #![allow(clippy::mixed_attributes_style)]
295 #![allow(clippy::print_stderr)]
296 #![allow(clippy::print_stdout)]
297 #![allow(clippy::single_char_pattern)]
298 #![allow(clippy::unwrap_used)]
299 #![allow(clippy::unchecked_time_subtraction)]
300 #![allow(clippy::useless_vec)]
301 #![allow(clippy::needless_pass_by_value)]
302 #![allow(clippy::string_slice)] use super::*;
306 use crate::doc::hsdesc::IntroAuthType;
307 use crate::doc::hsdesc::build::test::{create_intro_point_descriptor, expect_bug};
308 use crate::doc::hsdesc::pow::v1::PowParamsV1;
309
310 use smallvec::SmallVec;
311 use std::net::Ipv4Addr;
312 use std::time::UNIX_EPOCH;
313 use tor_basic_utils::test_rng::Config;
314 use tor_checkable::timed::TimeRangeBound;
315 #[cfg(feature = "hs-pow-full")]
316 use tor_hscrypto::pow::v1::{Effort, Seed};
317 use tor_linkspec::LinkSpec;
318
319 fn create_inner_desc(
321 create2_formats: &[HandshakeType],
322 auth_required: Option<&SmallVec<[IntroAuthType; 2]>>,
323 is_single_onion_service: bool,
324 intro_points: &[IntroPointDesc],
325 flow_control: Option<(Protocols, u8)>,
326 protos: Protocols,
327 pow_params: Option<&PowParams>,
328 ) -> Result<String, EncodeError> {
329 let hs_desc_sign = ed25519::Keypair::generate(&mut Config::Deterministic.into_rng());
330
331 HsDescInner {
332 hs_desc_sign: &hs_desc_sign,
333 create2_formats,
334 auth_required,
335 is_single_onion_service,
336 intro_points,
337 intro_auth_key_cert_expiry: UNIX_EPOCH,
338 intro_enc_key_cert_expiry: UNIX_EPOCH,
339 flow_control: flow_control.as_ref(),
340 protos,
341 #[cfg(feature = "hs-pow-full")]
342 pow_params,
343 }
344 .build_sign(&mut rand::rng())
345 }
346
347 #[test]
348 fn inner_hsdesc_no_intro_auth() {
349 let hs_desc = create_inner_desc(
351 &[HandshakeType::NTOR], None, true, &[], None,
356 Default::default(),
357 None,
358 )
359 .unwrap();
360
361 assert_eq!(hs_desc, "create2-formats 2\nsingle-onion-service\n");
362
363 let hs_desc = create_inner_desc(
365 &[HandshakeType::NTOR], None, false, &[], None,
370 Default::default(),
371 None,
372 )
373 .unwrap();
374
375 assert_eq!(hs_desc, "create2-formats 2\n");
376
377 let link_specs1 = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 1234)];
378 let link_specs2 = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 5679)];
379 let link_specs3 = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 8901)];
380
381 let mut rng = Config::Deterministic.into_rng();
382 let intros = &[
383 create_intro_point_descriptor(&mut rng, link_specs1),
384 create_intro_point_descriptor(&mut rng, link_specs2),
385 create_intro_point_descriptor(&mut rng, link_specs3),
386 ];
387
388 let hs_desc = create_inner_desc(
389 &[
390 HandshakeType::TAP,
391 HandshakeType::NTOR,
392 HandshakeType::NTOR_V3,
393 ], None, false, intros, None,
398 Default::default(),
399 None,
400 )
401 .unwrap();
402
403 assert_eq!(
404 hs_desc,
405 r#"create2-formats 0 2 3
406introduction-point AQAGfwAAASLF
407onion-key ntor CJi8nDPhIFA7X9Q+oP7+jzxNo044cblmagk/d7oKWGc=
408auth-key
409-----BEGIN ED25519 CERT-----
410AQkAAAAAAU4J4xGrMt9q5eHYZSmbOZTi1iKl59nd3ItYXAa/ASlRAQAgBACQKRtN
411eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61CGkJzc/ECYHzJeeAKIkRFV/6jr9
412zAB5XnEFghZmXdDTQdqcPXAFydyeHWW4uR+Uii0wPI8VokbU0NoLTNYJGAM=
413-----END ED25519 CERT-----
414enc-key ntor TL7GcN+B++pB6eRN/0nBZGmWe125qh7ccQJ/Hhku+x8=
415enc-key-cert
416-----BEGIN ED25519 CERT-----
417AQsAAAAAAabaCv4gv9ddyIztD1J8my9mgotmWnkHX94buLAtt15aAQAgBACQKRtN
418eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61GxlI6caS8iFp2bLmg1+Pkgij47f
419eetKn+yDC5Q3eo/hJLDBGAQNOX7jFMdr9HjotjXIt6/Khfmg58CZC/gKhAw=
420-----END ED25519 CERT-----
421introduction-point AQAGfwAAAQTS
422onion-key ntor HWIigEAdcOgqgHPDFmzhhkeqvYP/GcMT2fKb5JY6ey8=
423auth-key
424-----BEGIN ED25519 CERT-----
425AQkAAAAAAZZVJwNlzVw1ZQGO7MTzC5MsySASd+fswAcjdTJJOifXAQAgBACQKRtN
426eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61IVW0XivcAKhvUvNUsU1CFznk3Mz
427KSsp/mBoKi2iY4f4eN2SXx8U6pmnxnXFxYP6obi+tc5QWj1Jbfl1Aci3TAA=
428-----END ED25519 CERT-----
429enc-key ntor 9Upi9XNWyqx3ZwHeQ5r3+Dh116k+C4yHeE9BcM68HDc=
430enc-key-cert
431-----BEGIN ED25519 CERT-----
432AQsAAAAAAcH+1K5m7pRnMc01mPp5AYVnJK1iZ/fKHwK0tVR/jtBvAQAgBACQKRtN
433eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61Hectpha37ioha85fpNt+/yDfebh
4346BKUUQ0jf3SMXuNgX8SV9NSabn14WCSdKG/8RoYBCTR+yRJX0dy55mjg+go=
435-----END ED25519 CERT-----
436introduction-point AQAGfwAAARYv
437onion-key ntor x/stThC6cVWJJUR7WERZj5VYVPTAOA/UDjHdtprJkiE=
438auth-key
439-----BEGIN ED25519 CERT-----
440AQkAAAAAAVMhalzZJ8txKHuCX8TEhmO3LbCvDgV0zMT4eQ49SDpBAQAgBACQKRtN
441eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61GdVAiMag0dquEx4IywKDLEhxA7N
4422RZFTS2QI+Sk3dyz46WO+epj1YBlgfOYCZlBEx+oFkRlUJdOc0Eu0sDlAw8=
443-----END ED25519 CERT-----
444enc-key ntor XI/a9NGh/7ClaFcKqtdI9DoP8da5ovwPDdgCHUr3xX0=
445enc-key-cert
446-----BEGIN ED25519 CERT-----
447AQsAAAAAAZYGETSx12Og2xqJNMS9kGOHTEFeBkFPi7k0UaFv5HNKAQAgBACQKRtN
448eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61E8vxB5lB83+rQnWmHLzpfuMUZjG
449o7Ct/ZB0j8YRB5lKSd07YAjA6Zo8kMnuZYX2Mb67TxWDQ/zlYJGOwLlj7A8=
450-----END ED25519 CERT-----
451"#
452 );
453 }
454
455 #[test]
456 fn inner_hsdesc_too_many_link_specifiers() {
457 let link_spec = LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 9999);
458 let link_specifiers =
459 std::iter::repeat_n(link_spec, u8::MAX as usize + 1).collect::<Vec<_>>();
460
461 let intros = &[create_intro_point_descriptor(
462 &mut Config::Deterministic.into_rng(),
463 &link_specifiers,
464 )];
465
466 let err = create_inner_desc(
469 &[HandshakeType::NTOR], None, false, intros, None,
474 Default::default(),
475 None,
476 )
477 .unwrap_err();
478
479 assert!(expect_bug(err).contains("Too many link specifiers."));
480 }
481
482 #[test]
483 fn inner_hsdesc_intro_auth() {
484 let mut rng = Config::Deterministic.into_rng();
485 let link_specs = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 8080)];
486 let intros = &[create_intro_point_descriptor(&mut rng, link_specs)];
487 let auth = SmallVec::from([IntroAuthType::Ed25519, IntroAuthType::Ed25519]);
488
489 let hs_desc = create_inner_desc(
492 &[HandshakeType::NTOR], Some(&auth), false, intros, None,
497 Default::default(),
498 None,
499 )
500 .unwrap();
501
502 assert_eq!(
503 hs_desc,
504 r#"create2-formats 2
505intro-auth-required ed25519 ed25519
506introduction-point AQAGfwAAAR+Q
507onion-key ntor HWIigEAdcOgqgHPDFmzhhkeqvYP/GcMT2fKb5JY6ey8=
508auth-key
509-----BEGIN ED25519 CERT-----
510AQkAAAAAAZZVJwNlzVw1ZQGO7MTzC5MsySASd+fswAcjdTJJOifXAQAgBACQKRtN
511eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61IVW0XivcAKhvUvNUsU1CFznk3Mz
512KSsp/mBoKi2iY4f4eN2SXx8U6pmnxnXFxYP6obi+tc5QWj1Jbfl1Aci3TAA=
513-----END ED25519 CERT-----
514enc-key ntor 9Upi9XNWyqx3ZwHeQ5r3+Dh116k+C4yHeE9BcM68HDc=
515enc-key-cert
516-----BEGIN ED25519 CERT-----
517AQsAAAAAAcH+1K5m7pRnMc01mPp5AYVnJK1iZ/fKHwK0tVR/jtBvAQAgBACQKRtN
518eNThmyleMYdmFucrbgPcZNDO6S81MZD1r7q61Hectpha37ioha85fpNt+/yDfebh
5196BKUUQ0jf3SMXuNgX8SV9NSabn14WCSdKG/8RoYBCTR+yRJX0dy55mjg+go=
520-----END ED25519 CERT-----
521"#
522 );
523 }
524
525 #[test]
526 #[cfg(feature = "hs-pow-full")]
527 fn inner_hsdesc_pow_params() {
528 use humantime::parse_rfc3339;
529
530 let mut rng = Config::Deterministic.into_rng();
531 let link_specs = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 8080)];
532 let intros = &[create_intro_point_descriptor(&mut rng, link_specs)];
533
534 let pow_expiration = parse_rfc3339("1994-04-29T00:00:00Z").unwrap();
535 let pow_params = PowParams::V1(PowParamsV1::new(
536 TimeRangeBound::new(Seed::from([0; 32]), ..pow_expiration),
537 Effort::new(64),
538 ));
539
540 let hs_desc = create_inner_desc(
541 &[HandshakeType::NTOR], None, false, intros, None,
546 Default::default(),
547 Some(&pow_params),
548 )
549 .unwrap();
550
551 assert!(hs_desc.contains(
552 "\npow-params v1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 64 1994-04-29T00:00:00\n"
553 ));
554 }
555
556 #[test]
557 fn inner_hsdesc_protos_flowctl() {
558 let mut rng = Config::Deterministic.into_rng();
559 let link_specs = &[LinkSpec::OrPort(Ipv4Addr::LOCALHOST.into(), 8080)];
560 let intros = &[create_intro_point_descriptor(&mut rng, link_specs)];
561
562 let protos = "Relay=1-20 Wombat=12 Link=4".parse().unwrap();
563 let fcp = "FlowCtrl=1-6".parse().unwrap();
564 let flow_control = Some((fcp, 33));
565
566 let hs_desc = create_inner_desc(
567 &[HandshakeType::NTOR],
568 None,
569 false,
570 intros,
571 flow_control,
572 protos,
573 None,
574 )
575 .unwrap();
576
577 assert!(hs_desc.contains("\nflow-control 1-2 33\n"));
579 assert!(hs_desc.contains("\nproto Link=4 Relay=1-20 Wombat=12\n"));
581 }
582}