netdoc: Parser for outer layer of onion service descriptors.
This commit is contained in:
parent
d35e8a751d
commit
6c4e9c8f1d
|
@ -10,6 +10,7 @@
|
|||
|
||||
#![allow(dead_code, unused_variables, clippy::missing_panics_doc)] // TODO hs: remove.
|
||||
mod desc_enc;
|
||||
mod outer_layer;
|
||||
|
||||
use std::time::SystemTime;
|
||||
|
||||
|
|
|
@ -0,0 +1,254 @@
|
|||
//! Implement parsing for the outer layer of an onion service descriptor.
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use tor_cert::Ed25519Cert;
|
||||
use tor_checkable::signed::SignatureGated;
|
||||
use tor_checkable::timed::TimerangeBound;
|
||||
use tor_checkable::Timebound;
|
||||
use tor_llcrypto::pk::ed25519::{self, ValidatableEd25519Signature};
|
||||
|
||||
use crate::parse::{keyword::Keyword, parser::SectionRules, tokenize::NetDocReader};
|
||||
use crate::types::misc::{UnvalidatedEdCert, B64};
|
||||
use crate::{Pos, Result};
|
||||
|
||||
/// A more-or-less verbatim representation of the outermost layer of an onion
|
||||
/// service descriptor.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct HsDescOuter {
|
||||
/// The lifetime of this descriptor, in minutes.
|
||||
///
|
||||
/// This doesn't actually list the starting time or the end time for the
|
||||
/// descriptor: presumably, because we didn't want to leak the onion
|
||||
/// service's view of the wallclock.
|
||||
pub(super) lifetime_minutes: u16,
|
||||
/// A certificate containing the descriptor-signing-key for this onion
|
||||
/// service (`KP_hs_desc_sign`) signed by the blinded ed25519 identity
|
||||
/// (`HS_blind_id`) for this onion service.
|
||||
pub(super) desc_signing_key_cert: Ed25519Cert,
|
||||
/// A revision counter to tell whether this descriptor is more or less recent
|
||||
/// than another one for the same blinded ID.
|
||||
pub(super) revision_counter: RevisionCounter,
|
||||
/// The encrypted contents of this onion service descriptor.
|
||||
///
|
||||
/// Clients will decrypt this; onion service directories cannot.
|
||||
//
|
||||
// TODO: it might be a good idea to just discard this immediately (after checking it)
|
||||
// for the directory case.
|
||||
pub(super) encrypted_body: Vec<u8>,
|
||||
}
|
||||
|
||||
/// An `HsDescOuter` whose signatures have all been verified, but which has not
|
||||
/// been checked for timeliness.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct HsDescOuterSigChecked(HsDescOuter);
|
||||
|
||||
/// An `HsDescOuter` whose signatures have not yet been verified, and whose
|
||||
/// timeliness has not been checked.
|
||||
pub(super) type UncheckedHsDescOuter = SignatureGated<TimerangeBound<HsDescOuter>>;
|
||||
|
||||
decl_keyword! {
|
||||
HsOuterKwd {
|
||||
"hs-descriptor" => HS_DESCRIPTOR,
|
||||
"descriptor-lifetime" => DESCRIPTOR_LIFETIME,
|
||||
"descriptor-signing-key-cert" => DESCRIPTOR_SIGNING_KEY_CERT,
|
||||
"revision-counter" => REVISION_COUNTER,
|
||||
"superencrypted" => SUPERENCRYPTED,
|
||||
"signature" => SIGNATURE
|
||||
}
|
||||
}
|
||||
|
||||
/// Rules about how keywords appear in the outer layer of an onion service
|
||||
/// descriptor.
|
||||
static HS_OUTER_RULES: Lazy<SectionRules<HsOuterKwd>> = Lazy::new(|| {
|
||||
use HsOuterKwd::*;
|
||||
|
||||
let mut rules = SectionRules::new();
|
||||
rules.add(HS_DESCRIPTOR.rule().required().args(1..));
|
||||
rules.add(DESCRIPTOR_LIFETIME.rule().required().args(1..));
|
||||
rules.add(DESCRIPTOR_SIGNING_KEY_CERT.rule().required().obj_required());
|
||||
rules.add(REVISION_COUNTER.rule().required().args(1..));
|
||||
rules.add(SUPERENCRYPTED.rule().required().obj_required());
|
||||
rules.add(SIGNATURE.rule().required().args(1..));
|
||||
rules.add(UNRECOGNIZED.rule().may_repeat().obj_optional());
|
||||
|
||||
rules
|
||||
});
|
||||
|
||||
impl HsDescOuter {
|
||||
/// Try to parse an outer layer of an onion service descriptor from a string.
|
||||
pub(super) fn parse(s: &str) -> Result<UncheckedHsDescOuter> {
|
||||
// XXXX needs to be unchecked.
|
||||
let mut reader = NetDocReader::new(s);
|
||||
let result = HsDescOuter::take_from_reader(&mut reader).map_err(|e| e.within(s))?;
|
||||
reader.should_be_exhausted()?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Extract an HsDescOuter from a reader.
|
||||
///
|
||||
/// The reader must contain a single HsDescOuter; we return an error if not.
|
||||
fn take_from_reader(reader: &mut NetDocReader<'_, HsOuterKwd>) -> Result<UncheckedHsDescOuter> {
|
||||
use crate::err::ParseErrorKind as EK;
|
||||
use HsOuterKwd::*;
|
||||
|
||||
let mut iter = reader.iter();
|
||||
let body = HS_OUTER_RULES.parse(&mut iter)?;
|
||||
|
||||
// Enforce that the object starts and ends with the right keywords, and
|
||||
// find the start and end of the signed material.
|
||||
let signed_text = {
|
||||
let first_item = body
|
||||
.first_item()
|
||||
.expect("Somehow parsing worked though no keywords were present‽");
|
||||
let last_item = body
|
||||
.last_item()
|
||||
.expect("Somehow parsing worked though no keywords were present‽");
|
||||
if first_item.kwd() != HS_DESCRIPTOR {
|
||||
return Err(EK::WrongStartingToken
|
||||
.with_msg(first_item.kwd_str().to_string())
|
||||
.at_pos(first_item.pos()));
|
||||
}
|
||||
if last_item.kwd() != SIGNATURE {
|
||||
return Err(EK::WrongEndingToken
|
||||
.with_msg(last_item.kwd_str().to_string())
|
||||
.at_pos(last_item.pos()));
|
||||
}
|
||||
let s = reader.str();
|
||||
let start_idx = first_item
|
||||
.pos()
|
||||
.offset_within(s)
|
||||
.expect("Token came from nowhere within the string‽");
|
||||
let end_idx = last_item
|
||||
.pos()
|
||||
.offset_within(s)
|
||||
.expect("Token came from nowhere within the string‽");
|
||||
// TODO: This way of handling prefixes does a needless
|
||||
// allocation. Someday we could make our signature-checking
|
||||
// logic even smarter.
|
||||
let mut signed_text = b"Tor onion service descriptor sig v3".to_vec();
|
||||
signed_text.extend_from_slice(
|
||||
s.get(start_idx..end_idx)
|
||||
.expect("Somehow the first item came after the last‽")
|
||||
.as_bytes(),
|
||||
);
|
||||
signed_text
|
||||
};
|
||||
|
||||
// Check that the hs-descriptor version is 3.
|
||||
{
|
||||
let version = body.required(HS_DESCRIPTOR)?.required_arg(0)?;
|
||||
if version != "3" {
|
||||
return Err(EK::BadDocumentVersion
|
||||
.with_msg(format!("Unexpected hsdesc version {}", version))
|
||||
.at_pos(Pos::at(version)));
|
||||
}
|
||||
}
|
||||
|
||||
// Parse lifetime.
|
||||
let lifetime_minutes = {
|
||||
let tok = body.required(DESCRIPTOR_LIFETIME)?;
|
||||
let lifetime_minutes: u16 = tok.parse_arg(0)?;
|
||||
if !(30..=720).contains(&lifetime_minutes) {
|
||||
return Err(EK::BadArgument
|
||||
.with_msg(format!("Invalid HsDesc lifetime {}", lifetime_minutes))
|
||||
.at_pos(tok.pos()));
|
||||
}
|
||||
lifetime_minutes
|
||||
};
|
||||
|
||||
// Parse certificate.
|
||||
let (unchecked_cert, desc_signing_key) = {
|
||||
let cert_tok = body.required(DESCRIPTOR_SIGNING_KEY_CERT)?;
|
||||
let cert = cert_tok
|
||||
.parse_obj::<UnvalidatedEdCert>("ED25519 CERT")?
|
||||
.check_cert_type(tor_cert::CertType::HS_BLINDED_ID_V_SIGNING)?
|
||||
.into_unchecked()
|
||||
.check_key(None) // require that the cert contains its signing key.
|
||||
.map_err(|err| {
|
||||
EK::BadObjectVal
|
||||
.err()
|
||||
.with_source(err)
|
||||
.at_pos(cert_tok.pos())
|
||||
})?;
|
||||
let key: ed25519::PublicKey = cert
|
||||
.peek_subject_key()
|
||||
.as_ed25519()
|
||||
.and_then(|id| id.try_into().ok())
|
||||
.ok_or_else(|| {
|
||||
EK::BadObjectVal
|
||||
.err()
|
||||
.with_msg("Invalid ed25519 subject key")
|
||||
.at_pos(cert_tok.pos())
|
||||
})?;
|
||||
(cert, key)
|
||||
};
|
||||
|
||||
// Parse remaining fields, which are nice and simple.
|
||||
let revision_counter = body.required(REVISION_COUNTER)?.parse_arg::<u64>(0)?.into();
|
||||
let encrypted_body: Vec<u8> = body.required(SUPERENCRYPTED)?.obj("MESSAGE")?;
|
||||
let signature = body.required(SIGNATURE)?.parse_arg::<B64>(0)?;
|
||||
let signature = ed25519::Signature::from_bytes(signature.as_bytes())?;
|
||||
|
||||
// Split apart the unchecked certificate: its constraints will become
|
||||
// our own.
|
||||
let (desc_signing_key_cert, cert_signature) = unchecked_cert
|
||||
.dangerously_split()
|
||||
// we already checked that there is a public key, so an error should be impossible.
|
||||
.map_err(|e| EK::Internal.err().with_source(e))?;
|
||||
let desc_signing_key_cert = desc_signing_key_cert.dangerously_assume_timely();
|
||||
|
||||
// Build our return value.
|
||||
let expiration = desc_signing_key_cert.expiry();
|
||||
let desc = HsDescOuter {
|
||||
lifetime_minutes,
|
||||
desc_signing_key_cert,
|
||||
revision_counter,
|
||||
encrypted_body,
|
||||
};
|
||||
// You can't have that until you check that it's timely.
|
||||
let desc = TimerangeBound::new(desc, ..expiration);
|
||||
// And you can't have _that_ until you check the signatures.
|
||||
let signatures: Vec<Box<dyn tor_llcrypto::pk::ValidatableSignature>> = vec![
|
||||
Box::new(cert_signature),
|
||||
Box::new(ValidatableEd25519Signature::new(
|
||||
desc_signing_key,
|
||||
signature,
|
||||
&signed_text[..],
|
||||
)),
|
||||
];
|
||||
Ok(SignatureGated::new(desc, signatures))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
// @@ begin test lint list maintained by maint/add_warning @@
|
||||
#![allow(clippy::bool_assert_comparison)]
|
||||
#![allow(clippy::clone_on_copy)]
|
||||
#![allow(clippy::dbg_macro)]
|
||||
#![allow(clippy::print_stderr)]
|
||||
#![allow(clippy::print_stdout)]
|
||||
#![allow(clippy::single_char_pattern)]
|
||||
#![allow(clippy::unwrap_used)]
|
||||
#![allow(clippy::unchecked_duration_subtraction)]
|
||||
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
|
||||
use super::*;
|
||||
use tor_checkable::SelfSigned;
|
||||
|
||||
const TESTDATA: &str = include_str!("../../../testdata/hsdesc1.txt");
|
||||
|
||||
#[test]
|
||||
fn parse_good() -> Result<()> {
|
||||
let desc = HsDescOuter::parse(TESTDATA)?;
|
||||
|
||||
let desc = desc
|
||||
.check_signature()?
|
||||
.check_valid_at(&humantime::parse_rfc3339("2023-01-23T15:00:00Z").unwrap())
|
||||
.unwrap();
|
||||
|
||||
// TODO HS: Add checks for the specific fields here once I'm more
|
||||
// confident that this is the example descriptor I'm using.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue