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