diff --git a/Cargo.lock b/Cargo.lock index 1ddb4e761..7bce34614 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -454,6 +454,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + [[package]] name = "base64-simd" version = "0.7.0" @@ -463,6 +469,12 @@ dependencies = [ "simd-abstraction", ] +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "1.3.2" @@ -585,6 +597,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "const-oid" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" + [[package]] name = "core-foundation" version = "0.9.3" @@ -717,6 +735,17 @@ dependencies = [ "syn", ] +[[package]] +name = "der" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56acb310e15652100da43d130af8d97b509e95af61aab1c5a7939ef24337ee17" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "diff" version = "0.1.13" @@ -1470,7 +1499,16 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" dependencies = [ - "base64", + "base64 0.13.1", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", ] [[package]] @@ -1658,7 +1696,7 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" dependencies = [ - "base64", + "base64 0.13.1", "bytes", "encoding_rs", "futures-core", @@ -1774,7 +1812,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" dependencies = [ - "base64", + "base64 0.13.1", ] [[package]] @@ -1983,6 +2021,16 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "strsim" version = "0.10.0" @@ -2183,6 +2231,7 @@ dependencies = [ name = "tough" version = "0.13.0" dependencies = [ + "base64 0.21.2", "chrono", "dyn-clone", "globset", @@ -2201,6 +2250,7 @@ dependencies = [ "serde_json", "serde_plain", "snafu", + "spki", "tempfile", "untrusted", "url", @@ -2215,7 +2265,7 @@ dependencies = [ "aws-sdk-kms", "aws-smithy-client", "aws-smithy-http", - "base64", + "base64 0.13.1", "bytes", "http", "pem", diff --git a/olpc-cjson/src/lib.rs b/olpc-cjson/src/lib.rs index 3e2ff6aa1..a0c9f81ca 100644 --- a/olpc-cjson/src/lib.rs +++ b/olpc-cjson/src/lib.rs @@ -187,15 +187,17 @@ impl Formatter for CanonicalFormatter { } // Only quotes and backslashes are escaped in canonical JSON. + // TRX: Our cjson implementation is in fact valid json and therefore allows literal \n (0x5c6c) + // on a json string, following the json spec. fn write_char_escape( &mut self, writer: &mut W, char_escape: CharEscape, ) -> Result<()> { match char_escape { - CharEscape::Quote | CharEscape::ReverseSolidus => { + CharEscape::Quote | CharEscape::ReverseSolidus | CharEscape::LineFeed => { self.writer(writer).write_all(b"\\")?; - } + }, _ => {} } self.writer(writer).write_all(&[match char_escape { @@ -204,7 +206,7 @@ impl Formatter for CanonicalFormatter { CharEscape::Solidus => b'/', CharEscape::Backspace => b'\x08', CharEscape::FormFeed => b'\x0c', - CharEscape::LineFeed => b'\n', + CharEscape::LineFeed => b'n',// TRX CharEscape::CarriageReturn => b'\r', CharEscape::Tab => b'\t', CharEscape::AsciiControl(byte) => byte, diff --git a/tough/Cargo.toml b/tough/Cargo.toml index 5b06cdf86..3819b7133 100644 --- a/tough/Cargo.toml +++ b/tough/Cargo.toml @@ -9,6 +9,7 @@ keywords = ["tuf", "update", "repository"] edition = "2018" [dependencies] +base64 = "0.21.2" chrono = { version = "0.4", default-features = false, features = ["std", "alloc", "serde", "clock"] } dyn-clone = "1" globset = { version = "0.4" } @@ -24,6 +25,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" serde_plain = "1" snafu = "0.7" +spki = { version = "0.7.2", features = ["alloc", "pem"] } tempfile = "3" untrusted = "0.7" url = "2" diff --git a/tough/src/editor/mod.rs b/tough/src/editor/mod.rs index 1f24ede07..fda93c43b 100644 --- a/tough/src/editor/mod.rs +++ b/tough/src/editor/mod.rs @@ -104,7 +104,7 @@ impl RepositoryEditor { for (roletype, rolekeys) in &root.signed.roles { if rolekeys.threshold.get() > rolekeys.keyids.len() as u64 { return Err(error::Error::UnstableRoot { - role: *roletype, + role: roletype, threshold: rolekeys.threshold.get(), actual: rolekeys.keyids.len(), }); diff --git a/tough/src/lib.rs b/tough/src/lib.rs index 98ab7a1aa..f408bf529 100644 --- a/tough/src/lib.rs +++ b/tough/src/lib.rs @@ -60,6 +60,7 @@ pub use crate::transport::{ use chrono::{DateTime, Utc}; use log::warn; use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC}; +use schema::RemoteSessions; use snafu::{ensure, OptionExt, ResultExt}; use std::collections::HashMap; use std::fs::create_dir_all; @@ -195,6 +196,11 @@ impl RepositoryLoader { Repository::load(self) } + /// Load and verify Uptane repository metadata. + pub fn load_uptane(self) -> Result { + UptaneRepository::load(self) + } + /// Set the transport. If no transport has been set, [`DefaultTransport`] will be used. #[must_use] pub fn transport(mut self, transport: T) -> Self { @@ -310,7 +316,71 @@ pub struct Repository { expiration_enforcement: ExpirationEnforcement, } +/// An Uptane repository +/// +/// You can create a `Repository` using a [`RepositoryLoader`]. +#[derive(Debug, Clone)] +pub struct UptaneRepository { + root: Signed, + remote_sessions: std::result::Result, String>, +} + +impl UptaneRepository { + fn load(loader: RepositoryLoader) -> Result { + let datastore = Datastore::new(loader.datastore)?; + let transport = loader + .transport + .unwrap_or_else(|| Box::new(DefaultTransport::new())); + let limits = loader.limits.unwrap_or_default(); + let expiration_enforcement = loader.expiration_enforcement.unwrap_or_default(); + let metadata_base_url = parse_url(loader.metadata_base_url)?; + + // 0. Load the trusted root metadata file + 1. Update the root metadata file + let root = load_root( + transport.as_ref(), + loader.root, + &datastore, + limits.max_root_size, + limits.max_root_updates, + &metadata_base_url, + expiration_enforcement, + )?; + + let remote_sessions = match root.signed.roles.get(&RoleType::RemoteSessions) { + Some(_) => load_remote_sessions( + transport.as_ref(), + &root, + &datastore, + limits.max_timestamp_size, + &metadata_base_url, + expiration_enforcement + ).map_err(|err| + format!("{}", err) + ), + None => + Err("remote-sessions not set in root.json".to_string()) + }; + + Ok(Self { + root, + remote_sessions, + }) + } + + /// Returns a reference to the signed root + pub fn root(&self) -> &Signed { + &self.root + } + + /// Returns a reference to the signed remote sessions + pub fn remote_sessions(&self) -> std::result::Result<&Signed, String> { + self.remote_sessions.as_ref().map_err(|err|err.to_owned()) + } + +} + impl Repository { + /// Load and verify TUF repository metadata using a [`RepositoryLoader`] for the settings. fn load(loader: RepositoryLoader) -> Result { let datastore = Datastore::new(loader.datastore)?; @@ -1211,6 +1281,64 @@ fn load_delegations( Ok(()) } +fn load_remote_sessions( + transport: &dyn Transport, + root: &Signed, + datastore: &Datastore, + max_remote_sessions_size: u64, + metadata_base_url: &Url, + expiration_enforcement: ExpirationEnforcement, +) -> Result> { + let path = "remote-sessions.json"; + let reader = fetch_max_size( + transport, + metadata_base_url.join(path).context(error::JoinUrlSnafu { + path, + url: metadata_base_url.clone(), + })?, + max_remote_sessions_size, + "max_timestamp_size argument", + )?; + + + let remote_sessions: Signed = + serde_json::from_reader(reader).context(error::ParseMetadataSnafu { + role: RoleType::RemoteSessions, + })?; + + root.signed + .verify_role(&remote_sessions) + .context(error::VerifyMetadataSnafu { + role: RoleType::RemoteSessions, + })?; + + // 2.2. Check for a rollback attack. + if let Some(Ok(old_remote_sessions)) = datastore + .reader("remote-sessions.json")? + .map(serde_json::from_reader::<_, Signed>) + { + if root.signed.verify_role(&old_remote_sessions).is_ok() { + ensure!( + old_remote_sessions.signed.version <= old_remote_sessions.signed.version, + error::OlderMetadataSnafu { + role: RoleType::Timestamp, + current_version: old_remote_sessions.signed.version, + new_version: old_remote_sessions.signed.version + } + ); + } + } + + // TUF v1.0.16, 5.3.3. Check for a freeze attack + if expiration_enforcement == ExpirationEnforcement::Safe { + check_expired(datastore, &remote_sessions.signed)?; + } + + datastore.create("remote-sessions.json", &remote_sessions)?; + + Ok(remote_sessions) +} + #[cfg(test)] mod tests { use super::*; diff --git a/tough/src/schema/decoded.rs b/tough/src/schema/decoded.rs index a8655640e..ab9529458 100644 --- a/tough/src/schema/decoded.rs +++ b/tough/src/schema/decoded.rs @@ -68,6 +68,25 @@ pub trait Encode { fn encode(b: &[u8]) -> String; } + +/// [`Decode`]/[`Encode`] implementation for base64-encoded strings. +#[derive(Debug, Clone, Copy)] +pub struct Base64; + +use base64::prelude::{Engine as _, BASE64_STANDARD}; + +impl Decode for Base64 { + fn decode(s: &str) -> Result, Error> { + BASE64_STANDARD.decode(s).context(error::Base64DecodeSnafu) + } +} + +impl Encode for Base64 { + fn encode(b: &[u8]) -> String { + BASE64_STANDARD.encode(b) + } +} + /// [`Decode`]/[`Encode`] implementation for hex-encoded strings. #[derive(Debug, Clone, Copy)] pub struct Hex; diff --git a/tough/src/schema/error.rs b/tough/src/schema/error.rs index eb61e603e..9652057fe 100644 --- a/tough/src/schema/error.rs +++ b/tough/src/schema/error.rs @@ -64,6 +64,13 @@ pub enum Error { backtrace: Backtrace, }, + /// Failed to decode a base64-encoded string. + #[snafu(display("Invalid base64 string: {}", source))] + Base64Decode { + source: base64::DecodeError, + backtrace: Backtrace, + }, + /// The library failed to serialize an object to JSON. #[snafu(display("Failed to serialize {} to JSON: {}", what, source))] JsonSerialization { diff --git a/tough/src/schema/key.rs b/tough/src/schema/key.rs index 15f51ac7a..4d14b658c 100644 --- a/tough/src/schema/key.rs +++ b/tough/src/schema/key.rs @@ -2,17 +2,19 @@ //! Handles cryptographic keys and their serialization in TUF metadata files. -use crate::schema::decoded::{Decoded, EcdsaFlex, Hex, RsaPem}; -use crate::schema::error::{self, Result}; -use olpc_cjson::CanonicalFormatter; +use crate::schema::decoded::{Decoded, EcdsaFlex, Hex, RsaPem, Encode}; +use crate::schema::error::Result; use ring::digest::{digest, SHA256}; -use ring::signature::VerificationAlgorithm; +use ring::signature::{VerificationAlgorithm}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use snafu::ResultExt; + use std::collections::HashMap; use std::fmt; use std::str::FromStr; +use snafu::OptionExt; +use super::error; + /// Serializes signing keys as defined by the TUF specification. All keys have the format /// ```json @@ -34,7 +36,8 @@ use std::str::FromStr; /// * `Ed25519`: PUBLIC is a 64-byte hex encoded string. /// * `Ecdsa`: PUBLIC is in PEM format and a string. #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] -#[serde(rename_all = "kebab-case")] +// TRX We use SCREAMING-KEBAB-CASE +#[serde(rename_all = "SCREAMING-KEBAB-CASE")] #[serde(tag = "keytype")] pub enum Key { /// An RSA key. @@ -42,6 +45,8 @@ pub enum Key { /// The RSA key. keyval: RsaKey, /// Denotes the key's signature scheme. + // TRX: We don't use this field, need to skip serializing when generating canonical jon + #[serde(skip)] scheme: RsaScheme, /// Any additional fields read during deserialization; will not be used. #[serde(flatten)] @@ -52,6 +57,8 @@ pub enum Key { /// The Ed25519 key. keyval: Ed25519Key, /// Denotes the key's signature scheme. + // TRX: We don't use this field, need to skip serializing when generating canonical jon + #[serde(skip)] scheme: Ed25519Scheme, /// Any additional fields read during deserialization; will not be used. #[serde(flatten)] @@ -78,6 +85,13 @@ pub enum RsaScheme { RsassaPssSha256, } +// TRX: Required to skip (de)serializing +impl Default for RsaScheme { + fn default() -> Self { + RsaScheme::RsassaPssSha256 + } +} + /// Represents a deserialized (decoded) RSA public key. #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] pub struct RsaKey { @@ -97,6 +111,13 @@ pub enum Ed25519Scheme { Ed25519, } +// TRX: Required to skip (de)serializing +impl Default for Ed25519Scheme { + fn default() -> Self { + Ed25519Scheme::Ed25519 + } +} + /// Represents a deserialized (decoded) Ed25519 public key. #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] pub struct Ed25519Key { @@ -130,14 +151,51 @@ pub struct EcdsaKey { impl Key { /// Calculate the key ID for this key. + // TRX For legacy reasons, we calculate key ids based on the getEncoded/getAByte + // methods in sun.security.rsa.RSAPublicKeyImpl and net.i2p.crypto.eddsa.EdDSAPublicKey respectively, + // so we need to do the same here, instead of using the cjson representation pub fn key_id(&self) -> Result> { - let mut buf = Vec::new(); - let mut ser = serde_json::Serializer::with_formatter(&mut buf, CanonicalFormatter::new()); - self.serialize(&mut ser) - .context(error::JsonSerializationSnafu { - what: "key".to_owned(), - })?; - Ok(digest(&SHA256, &buf).as_ref().to_vec().into()) + + let keyval: Vec = match self { + Key::Ecdsa { + keyval, + .. + } => + keyval.public.to_vec(), + Key::Ed25519 { + keyval, + .. + } =>{ + let mut der_encoded = Vec::with_capacity(44); + + der_encoded.push(0x30); + der_encoded.push((10 + keyval.public.len()) as u8); + der_encoded.extend(&[0x30, 5, 0x06, 3, 43, 101, 112, 0x03, 1 + keyval.public.len() as u8, 0]); + der_encoded.extend(keyval.public.iter()); + + dbg!(&der_encoded); + + der_encoded + }, + Key::Rsa { + keyval, + .. + } => { + let as_pem = RsaPem::encode(&keyval.public); + let (_, as_der) = spki::Document::from_pem(&as_pem).ok().context(error::SpkiDecodeSnafu)?; + as_der.to_vec() + }, + }; + + // TRX: See comment on key_id, we dont use cjson to calculate key_id + // let mut buf = Vec::new(); + // let mut ser = serde_json::Serializer::with_formatter(&mut buf, CanonicalFormatter::new()); + // self.serialize(&mut ser) + // .context(error::JsonSerializationSnafu { + // what: "key".to_owned(), + // })?; + + Ok(digest(&SHA256, &keyval).as_ref().to_vec().into()) } /// Verify a signature of an object made with this key. diff --git a/tough/src/schema/mod.rs b/tough/src/schema/mod.rs index fed41ec52..9b9bb4b98 100644 --- a/tough/src/schema/mod.rs +++ b/tough/src/schema/mod.rs @@ -35,6 +35,8 @@ use std::ops::{Deref, DerefMut}; use std::path::Path; use std::str::FromStr; +use self::decoded::Base64; + /// The type of metadata role. #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Hash)] #[serde(rename_all = "kebab-case")] @@ -53,6 +55,10 @@ pub enum RoleType { Timestamp, /// A delegated targets role DelegatedTargets, + + /// RemoteSessions role + // TRX: Only in uptane + RemoteSessions, } derive_display_from_serialize!(RoleType); @@ -113,7 +119,7 @@ pub struct Signature { /// The key ID (listed in root.json) that made this signature. pub keyid: Decoded, /// A hex-encoded signature of the canonical JSON form of a role. - pub sig: Decoded, + pub sig: Decoded, } /// A `KeyHolder` is metadata that is responsible for verifying the signatures of a role. @@ -134,10 +140,11 @@ pub enum KeyHolder { /// roles in this file. #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] #[serde(tag = "_type")] -#[serde(rename = "root")] +#[serde(rename = "Root")] pub struct Root { /// A string that contains the version number of the TUF specification. Its format follows the /// Semantic Versioning 2.0.0 (semver) specification. + #[serde(skip, default = "String::new")] pub spec_version: String, /// A boolean indicating whether the repository supports consistent snapshots. When consistent @@ -160,7 +167,8 @@ pub struct Root { /// A list of roles, the keys associated with each role, and the threshold of signatures used /// for each role. - pub roles: HashMap, + // TRX: We need to support unknown RoleTypes so we use a custom type for `roles` + pub roles: Roles, /// Extra arguments found during deserialization. /// @@ -172,6 +180,41 @@ pub struct Root { pub _extra: HashMap, } + +/// Hold roles in Root +// TRX: This allows storing unknown roles for deserialization +#[derive(Eq, PartialEq, Clone, Serialize, Deserialize, Debug)] +pub struct Roles(HashMap); + +impl Roles { + /// Delegate to underlying map + pub fn get(&self, role: &RoleType) -> Option<&RoleKeys> { + let role_str = role.to_string(); // Uses serde via derived Display + self.0.get(&role_str) + } + + /// Create a Roles from an HashMap + /// Relies on to_string/Display and from_string derived for RoleType using serde + pub fn new(roles: &HashMap) -> Self { + Self(roles.iter().map(|(k,v)| (k.to_string(), v.clone()) ).collect()) + } +} + +impl<'a> IntoIterator for &'a Roles { + type Item = (RoleType, &'a RoleKeys); + + type IntoIter = Box + 'a>; + + fn into_iter(self) -> Self::IntoIter { + let it = self.0.iter().flat_map(|(k, v)| { + let role_type = RoleType::from_str(&k); + role_type.ok().map(|rt| (rt, v)) + }); + + Box::new(it) + } +} + /// Represents the key IDs used for a role and the threshold of signatures required to validate it. /// TUF 4.3: A ROLE is one of "root", "snapshot", "targets", "timestamp", or "mirrors". A role for /// each of "root", "snapshot", "timestamp", and "targets" MUST be specified in the key list. @@ -243,10 +286,12 @@ impl Role for Root { /// lengths and file hashes. #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] #[serde(tag = "_type")] -#[serde(rename = "snapshot")] +// TRX: We use Snapshot, not snapshot +#[serde(rename = "Snapshot")] pub struct Snapshot { /// A string that contains the version number of the TUF specification. Its format follows the /// Semantic Versioning 2.0.0 (semver) specification. + #[serde(skip, default = "String::new")] pub spec_version: String, /// An integer that is greater than 0. Clients MUST NOT replace a metadata file with a version @@ -320,6 +365,43 @@ pub struct SnapshotMeta { pub _extra: HashMap, } +/// A role describing allowed remote sessions for this repository +#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] +pub struct RemoteSessions { + /// A json blob describing the allowed remote sessions + pub remote_sessions: Value, + + /// Determines when metadata should be considered expired and no longer trusted by clients. + pub expires: DateTime, + + /// An integer that is greater than 0. + pub version: NonZeroU64, + + /// Extra arguments found during deserialization + /// + /// We must store these to correctly verify signatures for this object. + /// + /// If you're instantiating this struct, you should make this `HashMap::empty()`. + #[serde(flatten)] + pub _extra: HashMap, +} + +impl Role for RemoteSessions { + const TYPE: RoleType = RoleType::RemoteSessions; + + fn expires(&self) -> DateTime { + self.expires + } + + fn version(&self) -> NonZeroU64 { + self.version + } + + fn filename(&self, _consistent_snapshot: bool) -> String { + format!("{}.remote-sessions.json", self.version()) + } +} + /// Represents the hash dictionary in a `snapshot.json` file. #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] pub struct Hashes { @@ -384,10 +466,13 @@ impl Role for Snapshot { /// #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[serde(tag = "_type")] -#[serde(rename = "targets")] +// TRX: We use Targets, not targets +#[serde(rename = "Targets")] pub struct Targets { /// A string that contains the version number of the TUF specification. Its format follows the /// Semantic Versioning 2.0.0 (semver) specification. + // #[serde(skip_serializing_if = "Option::is_none")] + #[serde(skip, default = "String::new")] pub spec_version: String, /// An integer that is greater than 0. Clients MUST NOT replace a metadata file with a version @@ -1091,10 +1176,12 @@ impl DelegatedRole { /// unaware of interference with obtaining updates. #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] #[serde(tag = "_type")] -#[serde(rename = "timestamp")] +// TRX: We use Timestamp, not timestamp +#[serde(rename = "Timestamp")] pub struct Timestamp { /// A string that contains the version number of the TUF specification. Its format follows the /// Semantic Versioning 2.0.0 (semver) specification. + #[serde(skip, default = "String::new")] pub spec_version: String, /// An integer that is greater than 0. Clients MUST NOT replace a metadata file with a version