Skip to main content

tor_netdoc/doc/hsdesc/build/
inner.rs

1//! Functionality for encoding the inner document of an onion service descriptor.
2//!
3//! NOTE: `HsDescInner` is a private helper for building hidden service descriptors, and is
4//! not meant to be used directly. Hidden services will use `HsDescBuilder` to build and encode
5//! hidden service descriptors.
6
7use 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/// The representation of the inner document of an onion service descriptor.
36///
37/// The plaintext format of this document is described in section 2.5.2.2. of rend-spec-v3.
38#[derive(Debug)]
39pub(super) struct HsDescInner<'a> {
40    /// The descriptor signing key.
41    pub(super) hs_desc_sign: &'a ed25519::Keypair,
42    /// A list of recognized CREATE handshakes that this onion service supports.
43    pub(super) create2_formats: &'a [HandshakeType],
44    /// A list of authentication types that this onion service supports.
45    pub(super) auth_required: Option<&'a SmallVec<[IntroAuthType; 2]>>,
46    /// If true, this a "single onion service" and is not trying to keep its own location private.
47    pub(super) is_single_onion_service: bool,
48    /// One or more introduction points used to contact the onion service.
49    pub(super) intro_points: &'a [IntroPointDesc],
50    /// The expiration time of an introduction point authentication key certificate.
51    pub(super) intro_auth_key_cert_expiry: SystemTime,
52    /// The expiration time of an introduction point encryption key certificate.
53    pub(super) intro_enc_key_cert_expiry: SystemTime,
54
55    /// If present, a sendme increment and a set of FlowCtrl capabilities to
56    /// advertise with it.
57    ///
58    /// If this is None, we don't advertise any flowctrl capabilities.
59    ///
60    /// For historical reasons, the protocols capabilities here are separate
61    /// from `supported_protos`.    
62    pub(super) flow_control: Option<&'a (Protocols, u8)>,
63
64    /// If present, a set of subprotocol capabilities that we want to advertise.
65    pub(super) protos: Protocols,
66
67    /// Proof-of-work parameters
68    #[cfg(feature = "hs-pow-full")]
69    pub(super) pow_params: Option<&'a PowParams>,
70}
71
72/// Encode the pow-params line.
73#[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    // It's safe to call dangerously_into_parts here, since we encode the
82    // expiration alongside the value.
83    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        // We sort the introduction points here so as not to expose
168        // detail about the order in which they were added, which might
169        // be useful to an attacker somehow.  The choice of ntor
170        // key is arbitrary; we could sort by anything, really.
171        //
172        // TODO SPEC: Either specify that we should sort by ntor key,
173        // or sort by something else and specify that.
174        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            // rend-spec-v3 0.4. "Protocol building blocks [BUILDING-BLOCKS]": the number of link
178            // specifiers (NPSEC) must fit in a single byte.
179            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            // For compatibility with c-tor, the introduction point authentication key is signed by
201            // the descriptor signing key.
202            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            // "The key is a base64 encoded curve25519 public key used to encrypt the introduction
215            // request to service. (`KP_hss_ntor`)"
216            //
217            // TODO: The spec allows for multiple enc-key lines, but we currently only ever encode
218            // a single one.
219            encoder
220                .item(ENC_KEY)
221                .arg(&"ntor")
222                .arg(&Base64::encode_string(
223                    &intro_point.svc_ntor_key.as_bytes()[..],
224                ));
225
226            // The subject key is the ed25519 equivalent of the svc_ntor_key
227            // curve25519 public encryption key, with its sign bit set to 0.
228            //
229            // (Setting the sign bit to zero has a 50% chance of making the
230            // ed25519 public key useless for checking signatures, but that's
231            // okay: since this cert is generated with its signing/subject keys
232            // reversed (for compatibility reasons), we never actually generate
233            // or check any signatures using this key.)
234            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            // For compatibility with c-tor, the encryption key is signed with the descriptor
242            // signing key.
243            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
264/// Return a string encoding all of the `FlowCtrl` protocols in `p` that we should
265/// encode in a `flow-control` item.
266///
267/// This is an inelegant function because the `flow-control` item pre-dates
268/// the `proto` item by some time.  Don't make any more functions like this one!
269/// Instead, put new subprotocol capabilities into the `proto` item.
270fn flowctrl_protocols(p: &Protocols) -> String {
271    use tor_protover::ProtoKind;
272    // We only encode FlowCtrl protocols 1-2 here.  If we decide that we would like to implement
273    // more, we will decide later whether to advertise them in a flow-control line or in a 'proto'
274    // line.
275    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    // @@ begin test lint list maintained by maint/add_warning @@
291    #![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)] // See arti#2571
303    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
304
305    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    /// Build an inner document using the specified parameters.
320    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        // A descriptor for a "single onion service"
350        let hs_desc = create_inner_desc(
351            &[HandshakeType::NTOR], /* create2_formats */
352            None,                   /* auth_required */
353            true,                   /* is_single_onion_service */
354            &[],                    /* intro_points */
355            None,
356            Default::default(),
357            None,
358        )
359        .unwrap();
360
361        assert_eq!(hs_desc, "create2-formats 2\nsingle-onion-service\n");
362
363        // A descriptor for a location-hidden service
364        let hs_desc = create_inner_desc(
365            &[HandshakeType::NTOR], /* create2_formats */
366            None,                   /* auth_required */
367            false,                  /* is_single_onion_service */
368            &[],                    /* intro_points */
369            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            ], /* create2_formats */
394            None,   /* auth_required */
395            false,  /* is_single_onion_service */
396            intros, /* intro_points */
397            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        // A descriptor for a location-hidden service with an introduction point with too many link
467        // specifiers
468        let err = create_inner_desc(
469            &[HandshakeType::NTOR], /* create2_formats */
470            None,                   /* auth_required */
471            false,                  /* is_single_onion_service */
472            intros,                 /* intro_points */
473            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        // A descriptor for a location-hidden service with 1 introduction points which requires
490        // auth.
491        let hs_desc = create_inner_desc(
492            &[HandshakeType::NTOR], /* create2_formats */
493            Some(&auth),            /* auth_required */
494            false,                  /* is_single_onion_service */
495            intros,                 /* intro_points */
496            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], /* create2_formats */
542            None,                   /* auth_required */
543            false,                  /* is_single_onion_service */
544            intros,                 /* intro_points */
545            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        // Only FlowCtrl 1-2 can make it through.
578        assert!(hs_desc.contains("\nflow-control 1-2 33\n"));
579        // All declared protocols make it into the `proto` line.
580        assert!(hs_desc.contains("\nproto Link=4 Relay=1-20 Wombat=12\n"));
581    }
582}