fetch: Configure Minimum Feature Level
Add a configuration knob `node.fetch.signedReferences.featureLevel.minimum` to allow node operators to enforce more strict verification when fetching Signed References.
This commit is contained in:
parent
47063057a8
commit
e245e3115b
|
|
@ -30,6 +30,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
the node's inventory, where the node has a reference
|
the node's inventory, where the node has a reference
|
||||||
`refs/namespaces/<NID>/refs/rad/sigrefs`. The migration will only take place
|
`refs/namespaces/<NID>/refs/rad/sigrefs`. The migration will only take place
|
||||||
if the `rad/sigrefs` found were below the latest feature level, i.e. `parent`.
|
if the `rad/sigrefs` found were below the latest feature level, i.e. `parent`.
|
||||||
|
- The fetch process, between nodes, can reject `refs/namespaces` that have a
|
||||||
|
signed references feature level that is below an expected minimum. This
|
||||||
|
minimum can be configured in the Radicle configuration file under
|
||||||
|
`node.fetch.signedReferences.featureLevel.minimum`.
|
||||||
|
|
||||||
## 1.7.1
|
## 1.7.1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -315,6 +315,10 @@ $ rad config schema
|
||||||
"description": "Database configuration.",
|
"description": "Database configuration.",
|
||||||
"$ref": "#/$defs/Config"
|
"$ref": "#/$defs/Config"
|
||||||
},
|
},
|
||||||
|
"fetch": {
|
||||||
|
"description": "Configuration for fetching from other nodes.",
|
||||||
|
"$ref": "#/$defs/Fetch"
|
||||||
|
},
|
||||||
"secret": {
|
"secret": {
|
||||||
"description": "Path to a file containing an Ed25519 secret key, in OpenSSH format, i.e./nwith the `-----BEGIN OPENSSH PRIVATE KEY-----` header. The corresponding/npublic key will be used as the Node ID./n/nA decryption password cannot be configured, but passed at runtime via/nthe environment variable `RAD_PASSPHRASE`.",
|
"description": "Path to a file containing an Ed25519 secret key, in OpenSSH format, i.e./nwith the `-----BEGIN OPENSSH PRIVATE KEY-----` header. The corresponding/npublic key will be used as the Node ID./n/nA decryption password cannot be configured, but passed at runtime via/nthe environment variable `RAD_PASSPHRASE`.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|
@ -720,6 +724,52 @@ $ rad config schema
|
||||||
"NORMAL",
|
"NORMAL",
|
||||||
"OFF"
|
"OFF"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"Fetch": {
|
||||||
|
"description": "Configuration for fetching repositories from/nother nodes.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"signedReferences": {
|
||||||
|
"$ref": "#/$defs/SignedReferencesConfig"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SignedReferencesConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"featureLevel": {
|
||||||
|
"$ref": "#/$defs/FeatureLevelConfig"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"FeatureLevelConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"minimum": {
|
||||||
|
"description": "The minimum feature level required to accept incoming/nreferences from other users. This value is compared/nagainst the feature level detected on refs as they are/nfetched./n/nNote that by increasing this value, security can be/ntraded for compatibility. The higher the value,/nthe less backward compatible, but the more secure, fetches will be.",
|
||||||
|
"$ref": "#/$defs/FeatureLevel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"FeatureLevel": {
|
||||||
|
"description": "The Signed References feature has evolved over time./nThis enum captures the corresponding /"feature level/"./n/nFeature levels are monotonic, in the sense that a greater feature level/nencompasses all the features of smaller ones.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"description": "The lowest feature level, with least security. It is vulnerable to/ngraft attacks and replay attacks.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "An intermediate feature level, which protects against graft attacks but is vulnerable to replay attacks. Introduced in Radicle 1.1.0, in commit `989edacd564fa658358f5ccfd08c243c5ebd8cda`.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "root"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "The highest feature level known, which protects against graft attacks and replay attacks. Introduced in Radicle 1.7.0, in commit `d3bc868e84c334f113806df1737f52cc57c5453d`.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "parent"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ pub use gix_protocol::{transport::bstr::ByteSlice, RemoteProgress};
|
||||||
pub use handle::Handle;
|
pub use handle::Handle;
|
||||||
pub use policy::{Allowed, BlockList, Scope};
|
pub use policy::{Allowed, BlockList, Scope};
|
||||||
use radicle::storage::git::Repository;
|
use radicle::storage::git::Repository;
|
||||||
pub use state::{FetchLimit, FetchResult};
|
pub use state::{Config, FetchLimit, FetchResult};
|
||||||
pub use transport::Transport;
|
pub use transport::Transport;
|
||||||
|
|
||||||
use radicle::crypto::PublicKey;
|
use radicle::crypto::PublicKey;
|
||||||
|
|
@ -65,7 +65,7 @@ pub enum HandshakeError {
|
||||||
/// [`clone`] should be used.
|
/// [`clone`] should be used.
|
||||||
pub fn pull<R, S>(
|
pub fn pull<R, S>(
|
||||||
handle: &mut Handle<R, S>,
|
handle: &mut Handle<R, S>,
|
||||||
limit: FetchLimit,
|
config: Config,
|
||||||
remote: PublicKey,
|
remote: PublicKey,
|
||||||
refs_at: Option<Vec<RefsAt>>,
|
refs_at: Option<Vec<RefsAt>>,
|
||||||
) -> Result<FetchResult, Error>
|
) -> Result<FetchResult, Error>
|
||||||
|
|
@ -84,7 +84,7 @@ where
|
||||||
// N.b. ensure that we ignore the local peer's key.
|
// N.b. ensure that we ignore the local peer's key.
|
||||||
handle.blocked.extend([local]);
|
handle.blocked.extend([local]);
|
||||||
let result = state
|
let result = state
|
||||||
.run(handle, &handshake, limit, remote, refs_at)
|
.run(handle, &handshake, config, remote, refs_at)
|
||||||
.map_err(Error::Protocol);
|
.map_err(Error::Protocol);
|
||||||
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
|
|
@ -101,7 +101,7 @@ where
|
||||||
/// they want to populate with the `remote`'s view of the project.
|
/// they want to populate with the `remote`'s view of the project.
|
||||||
pub fn clone<R, S>(
|
pub fn clone<R, S>(
|
||||||
handle: &mut Handle<R, S>,
|
handle: &mut Handle<R, S>,
|
||||||
limit: FetchLimit,
|
config: Config,
|
||||||
remote: PublicKey,
|
remote: PublicKey,
|
||||||
) -> Result<FetchResult, Error>
|
) -> Result<FetchResult, Error>
|
||||||
where
|
where
|
||||||
|
|
@ -115,7 +115,7 @@ where
|
||||||
let handshake = perform_handshake(handle)?;
|
let handshake = perform_handshake(handle)?;
|
||||||
let state = FetchState::default();
|
let state = FetchState::default();
|
||||||
let result = state
|
let result = state
|
||||||
.run(handle, &handshake, limit, remote, None)
|
.run(handle, &handshake, config, remote, None)
|
||||||
.map_err(Error::Protocol);
|
.map_err(Error::Protocol);
|
||||||
let elapsed = start.elapsed().as_millis();
|
let elapsed = start.elapsed().as_millis();
|
||||||
let rid = handle.repository().id();
|
let rid = handle.repository().id();
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,12 @@ pub struct FetchLimit {
|
||||||
pub refs: u64,
|
pub refs: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct Config {
|
||||||
|
pub limit: FetchLimit,
|
||||||
|
pub level_min: FeatureLevel,
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for FetchLimit {
|
impl Default for FetchLimit {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -358,7 +364,7 @@ impl FetchState {
|
||||||
mut self,
|
mut self,
|
||||||
handle: &mut Handle<R, S>,
|
handle: &mut Handle<R, S>,
|
||||||
handshake: &Handshake,
|
handshake: &Handshake,
|
||||||
limit: FetchLimit,
|
config: Config,
|
||||||
remote: PublicKey,
|
remote: PublicKey,
|
||||||
refs_at: Option<Vec<RefsAt>>,
|
refs_at: Option<Vec<RefsAt>>,
|
||||||
) -> Result<FetchResult, error::Protocol>
|
) -> Result<FetchResult, error::Protocol>
|
||||||
|
|
@ -375,7 +381,7 @@ impl FetchState {
|
||||||
handshake,
|
handshake,
|
||||||
&stage::CanonicalId {
|
&stage::CanonicalId {
|
||||||
remote,
|
remote,
|
||||||
limit: limit.special,
|
limit: config.limit.special,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
log::debug!("Fetched rad/id ({}ms)", start.elapsed().as_millis());
|
log::debug!("Fetched rad/id ({}ms)", start.elapsed().as_millis());
|
||||||
|
|
@ -413,7 +419,7 @@ impl FetchState {
|
||||||
handshake,
|
handshake,
|
||||||
delegates.clone(),
|
delegates.clone(),
|
||||||
threshold,
|
threshold,
|
||||||
&limit,
|
&config.limit,
|
||||||
remote,
|
remote,
|
||||||
refs_at,
|
refs_at,
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -500,6 +506,26 @@ impl FetchState {
|
||||||
source: err,
|
source: err,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
(Ok(Some(refs)), delegate) if refs.feature_level() < config.level_min => {
|
||||||
|
log::debug!(
|
||||||
|
"Pruning {remote} tips due to insufficient feature level '{}' < '{}'",
|
||||||
|
refs.feature_level(),
|
||||||
|
config.level_min
|
||||||
|
);
|
||||||
|
|
||||||
|
failures.push(sigrefs::Validation::InsufficientFeatureLevel {
|
||||||
|
remote,
|
||||||
|
actual: refs.feature_level(),
|
||||||
|
minimum: config.level_min,
|
||||||
|
});
|
||||||
|
|
||||||
|
if delegate {
|
||||||
|
valid_delegates.remove(&remote);
|
||||||
|
failed_delegates.insert(remote);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.prune(&remote);
|
||||||
|
}
|
||||||
(Ok(Some(refs)), false) => {
|
(Ok(Some(refs)), false) => {
|
||||||
if let Some(SignedRefsAt { at, .. }) =
|
if let Some(SignedRefsAt { at, .. }) =
|
||||||
SignedRefsAt::load(remote, handle.repository())?
|
SignedRefsAt::load(remote, handle.repository())?
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ use cyphernet::Ecdh;
|
||||||
use radicle::cob::migrate;
|
use radicle::cob::migrate;
|
||||||
use radicle::crypto;
|
use radicle::crypto;
|
||||||
use radicle::node::device::Device;
|
use radicle::node::device::Device;
|
||||||
use radicle_fetch::FetchLimit;
|
|
||||||
use radicle_signals::Signal;
|
use radicle_signals::Signal;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
|
@ -236,7 +235,10 @@ impl Runtime {
|
||||||
|
|
||||||
let nid = *signer.public_key();
|
let nid = *signer.public_key();
|
||||||
let fetch = worker::FetchConfig {
|
let fetch = worker::FetchConfig {
|
||||||
limit: FetchLimit::default(),
|
config: radicle_fetch::Config {
|
||||||
|
limit: radicle_fetch::FetchLimit::default(),
|
||||||
|
level_min: config.fetch.feature_level_min(),
|
||||||
|
},
|
||||||
local: nid,
|
local: nid,
|
||||||
expiry: worker::garbage::Expiry::default(),
|
expiry: worker::garbage::Expiry::default(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ use radicle::prelude::NodeId;
|
||||||
use radicle::storage::refs::RefsAt;
|
use radicle::storage::refs::RefsAt;
|
||||||
use radicle::storage::{ReadRepository, ReadStorage};
|
use radicle::storage::{ReadRepository, ReadStorage};
|
||||||
use radicle::{cob, crypto, Storage};
|
use radicle::{cob, crypto, Storage};
|
||||||
use radicle_fetch::FetchLimit;
|
|
||||||
|
|
||||||
pub use radicle_protocol::worker::{
|
pub use radicle_protocol::worker::{
|
||||||
AuthorizationError, FetchError, FetchRequest, FetchResult, UploadError,
|
AuthorizationError, FetchError, FetchRequest, FetchResult, UploadError,
|
||||||
|
|
@ -60,8 +59,8 @@ pub struct TaskResult {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FetchConfig {
|
pub struct FetchConfig {
|
||||||
/// Data limits when fetching from a remote.
|
/// Configuration passed to the fetch protocol.
|
||||||
pub limit: FetchLimit,
|
pub config: radicle_fetch::Config,
|
||||||
/// Public key of the local peer.
|
/// Public key of the local peer.
|
||||||
pub local: crypto::PublicKey,
|
pub local: crypto::PublicKey,
|
||||||
/// Configuration for `git gc` garbage collection. Defaults to `1
|
/// Configuration for `git gc` garbage collection. Defaults to `1
|
||||||
|
|
@ -243,7 +242,7 @@ impl Worker {
|
||||||
notifs: notifications::StoreWriter,
|
notifs: notifications::StoreWriter,
|
||||||
) -> Result<fetch::FetchResult, FetchError> {
|
) -> Result<fetch::FetchResult, FetchError> {
|
||||||
let FetchConfig {
|
let FetchConfig {
|
||||||
limit,
|
config,
|
||||||
local,
|
local,
|
||||||
expiry,
|
expiry,
|
||||||
} = &self.fetch_config;
|
} = &self.fetch_config;
|
||||||
|
|
@ -267,7 +266,7 @@ impl Worker {
|
||||||
&self.storage,
|
&self.storage,
|
||||||
&mut cache,
|
&mut cache,
|
||||||
&mut self.db,
|
&mut self.db,
|
||||||
*limit,
|
*config,
|
||||||
remote,
|
remote,
|
||||||
refs_at,
|
refs_at,
|
||||||
)?;
|
)?;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ use radicle::storage::{
|
||||||
};
|
};
|
||||||
use radicle::{cob, git, node, Storage};
|
use radicle::{cob, git, node, Storage};
|
||||||
use radicle_fetch::git::refs::Applied;
|
use radicle_fetch::git::refs::Applied;
|
||||||
use radicle_fetch::{Allowed, BlockList, FetchLimit};
|
use radicle_fetch::{Allowed, BlockList};
|
||||||
pub use radicle_protocol::worker::fetch::{FetchResult, UpdatedCanonicalRefs};
|
pub use radicle_protocol::worker::fetch::{FetchResult, UpdatedCanonicalRefs};
|
||||||
|
|
||||||
use super::channels::ChannelsFlush;
|
use super::channels::ChannelsFlush;
|
||||||
|
|
@ -67,20 +67,24 @@ impl Handle {
|
||||||
storage: &Storage,
|
storage: &Storage,
|
||||||
cache: &mut cob::cache::StoreWriter,
|
cache: &mut cob::cache::StoreWriter,
|
||||||
refsdb: &mut D,
|
refsdb: &mut D,
|
||||||
limit: FetchLimit,
|
config: radicle_fetch::Config,
|
||||||
remote: PublicKey,
|
remote: PublicKey,
|
||||||
refs_at: Option<Vec<RefsAt>>,
|
refs_at: Option<Vec<RefsAt>>,
|
||||||
) -> Result<FetchResult, error::Fetch> {
|
) -> Result<FetchResult, error::Fetch> {
|
||||||
let (result, clone, notifs) = match self {
|
let (result, clone, notifs) = match self {
|
||||||
Self::Clone { mut handle } => {
|
Self::Clone { mut handle } => {
|
||||||
log::debug!(target: "worker", "{} cloning from {remote}", handle.local());
|
log::debug!(target: "worker", "{} cloning from {remote}", handle.local());
|
||||||
match radicle_fetch::clone(&mut handle, limit, remote) {
|
match radicle_fetch::clone(&mut handle, config, remote) {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
handle.into_inner().cleanup();
|
handle.into_inner().cleanup();
|
||||||
return Err(err.into());
|
return Err(err.into());
|
||||||
}
|
}
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
handle.into_inner().mv(storage.path_of(&rid))?;
|
if result.is_success() {
|
||||||
|
handle.into_inner().mv(storage.path_of(&rid))?;
|
||||||
|
} else {
|
||||||
|
handle.into_inner().cleanup();
|
||||||
|
}
|
||||||
(result, true, None)
|
(result, true, None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +94,7 @@ impl Handle {
|
||||||
notifications,
|
notifications,
|
||||||
} => {
|
} => {
|
||||||
log::debug!(target: "worker", "{} pulling from {remote}", handle.local());
|
log::debug!(target: "worker", "{} pulling from {remote}", handle.local());
|
||||||
let result = radicle_fetch::pull(&mut handle, limit, remote, refs_at)?;
|
let result = radicle_fetch::pull(&mut handle, config, remote, refs_at)?;
|
||||||
(result, false, Some(notifications))
|
(result, false, Some(notifications))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use serde_json as json;
|
||||||
use crate::node;
|
use crate::node;
|
||||||
use crate::node::policy::SeedingPolicy;
|
use crate::node::policy::SeedingPolicy;
|
||||||
use crate::node::{Address, Alias, NodeId};
|
use crate::node::{Address, Alias, NodeId};
|
||||||
|
use crate::storage::refs::FeatureLevel;
|
||||||
|
|
||||||
use super::policy;
|
use super::policy;
|
||||||
|
|
||||||
|
|
@ -460,6 +461,56 @@ impl From<DefaultSeedingPolicy> for SeedingPolicy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
pub struct FeatureLevelConfig {
|
||||||
|
/// The minimum feature level required to accept incoming
|
||||||
|
/// references from other users. This value is compared
|
||||||
|
/// against the feature level detected on refs as they are
|
||||||
|
/// fetched.
|
||||||
|
///
|
||||||
|
/// Note that by increasing this value, security can be
|
||||||
|
/// traded for compatibility. The higher the value,
|
||||||
|
/// the less backward compatible, but the more secure, fetches will be.
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
rename = "minimum",
|
||||||
|
skip_serializing_if = "crate::serde_ext::is_default"
|
||||||
|
)]
|
||||||
|
min: FeatureLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeatureLevelConfig {
|
||||||
|
pub fn min(&self) -> FeatureLevel {
|
||||||
|
self.min
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for fetching repositories from
|
||||||
|
/// other nodes.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
pub struct Fetch {
|
||||||
|
#[serde(default, skip_serializing_if = "crate::serde_ext::is_default")]
|
||||||
|
signed_references: SignedReferencesConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
pub struct SignedReferencesConfig {
|
||||||
|
#[serde(default, skip_serializing_if = "crate::serde_ext::is_default")]
|
||||||
|
feature_level: FeatureLevelConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fetch {
|
||||||
|
pub fn feature_level_min(&self) -> FeatureLevel {
|
||||||
|
self.signed_references.feature_level.min()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Service configuration.
|
/// Service configuration.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|
@ -512,6 +563,9 @@ pub struct Config {
|
||||||
/// Database configuration.
|
/// Database configuration.
|
||||||
#[serde(default, skip_serializing_if = "crate::serde_ext::is_default")]
|
#[serde(default, skip_serializing_if = "crate::serde_ext::is_default")]
|
||||||
pub database: node::db::config::Config,
|
pub database: node::db::config::Config,
|
||||||
|
/// Configuration for fetching from other nodes.
|
||||||
|
#[serde(default, skip_serializing_if = "crate::serde_ext::is_default")]
|
||||||
|
pub fetch: Fetch,
|
||||||
/// Extra fields that aren't supported.
|
/// Extra fields that aren't supported.
|
||||||
#[serde(flatten, skip_serializing)]
|
#[serde(flatten, skip_serializing)]
|
||||||
pub extra: json::Map<String, json::Value>,
|
pub extra: json::Map<String, json::Value>,
|
||||||
|
|
@ -550,6 +604,7 @@ impl Config {
|
||||||
seeding_policy: DefaultSeedingPolicy::default(),
|
seeding_policy: DefaultSeedingPolicy::default(),
|
||||||
database: node::db::config::Config::default(),
|
database: node::db::config::Config::default(),
|
||||||
extra: json::Map::default(),
|
extra: json::Map::default(),
|
||||||
|
fetch: Fetch::default(),
|
||||||
secret: None,
|
secret: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -861,4 +916,25 @@ mod test {
|
||||||
assert_eq!(got.alias, expected.alias);
|
assert_eq!(got.alias, expected.alias);
|
||||||
assert_eq!(got.external_addresses, expected.external_addresses);
|
assert_eq!(got.external_addresses, expected.external_addresses);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fetch_level_min() {
|
||||||
|
let config = json!({
|
||||||
|
"alias": "radicle",
|
||||||
|
"fetch": {
|
||||||
|
"signedReferences": {
|
||||||
|
"featureLevel": {
|
||||||
|
"minimum": "parent"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let got: super::Config = serde_json::from_value(config).unwrap();
|
||||||
|
let expected = super::Config::new(Alias::new("radicle"));
|
||||||
|
assert_eq!(got.alias, expected.alias);
|
||||||
|
assert_eq!(
|
||||||
|
got.fetch.feature_level_min(),
|
||||||
|
crate::storage::refs::FeatureLevel::Parent
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ use crate::identity::{Identity, Project};
|
||||||
use crate::node::device::Device;
|
use crate::node::device::Device;
|
||||||
use crate::node::SyncedAt;
|
use crate::node::SyncedAt;
|
||||||
use crate::storage::refs;
|
use crate::storage::refs;
|
||||||
use crate::storage::refs::{Refs, SignedRefs, SignedRefsAt};
|
use crate::storage::refs::{FeatureLevel, Refs, SignedRefs, SignedRefsAt};
|
||||||
use crate::storage::{
|
use crate::storage::{
|
||||||
ReadRepository, ReadStorage, Remote, Remotes, RepositoryInfo, SetHead, SignRepository,
|
ReadRepository, ReadStorage, Remote, Remotes, RepositoryInfo, SetHead, SignRepository,
|
||||||
WriteRepository, WriteStorage,
|
WriteRepository, WriteStorage,
|
||||||
|
|
@ -400,6 +400,14 @@ pub enum Validation {
|
||||||
#[source]
|
#[source]
|
||||||
source: crate::storage::refs::sigrefs::read::error::Read,
|
source: crate::storage::refs::sigrefs::read::error::Read,
|
||||||
},
|
},
|
||||||
|
#[error(
|
||||||
|
"rejecting `refs/namespaces/{remote}/refs/rad/sigrefs` on feature level '{actual}', below required minimum '{minimum}'"
|
||||||
|
)]
|
||||||
|
InsufficientFeatureLevel {
|
||||||
|
remote: RemoteId,
|
||||||
|
actual: FeatureLevel,
|
||||||
|
minimum: FeatureLevel,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Repository {
|
impl Repository {
|
||||||
|
|
|
||||||
|
|
@ -281,19 +281,38 @@ where
|
||||||
///
|
///
|
||||||
/// Feature levels are monotonic, in the sense that a greater feature level
|
/// Feature levels are monotonic, in the sense that a greater feature level
|
||||||
/// encompasses all the features of smaller ones.
|
/// encompasses all the features of smaller ones.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum FeatureLevel {
|
pub enum FeatureLevel {
|
||||||
/// References are stored without additional metadata.
|
/// The lowest feature level, with least security. It is vulnerable to
|
||||||
|
/// graft attacks and replay attacks.
|
||||||
#[default]
|
#[default]
|
||||||
None,
|
None,
|
||||||
/// Introduced in Radicle 1.1.0, in commit
|
|
||||||
/// `989edacd564fa658358f5ccfd08c243c5ebd8cda`,
|
#[cfg_attr(
|
||||||
/// this requires [`IDENTITY_ROOT`].
|
feature = "schemars",
|
||||||
|
schemars(description = "\
|
||||||
|
An intermediate feature level, which protects against graft attacks \
|
||||||
|
but is vulnerable to replay attacks. \
|
||||||
|
Introduced in Radicle 1.1.0, in commit \
|
||||||
|
`989edacd564fa658358f5ccfd08c243c5ebd8cda`.\
|
||||||
|
")
|
||||||
|
)]
|
||||||
|
/// Requires [`IDENTITY_ROOT`].
|
||||||
Root,
|
Root,
|
||||||
/// Introduced in Radicle 1.7.0, in commit
|
|
||||||
/// `d3bc868e84c334f113806df1737f52cc57c5453d`,
|
#[cfg_attr(
|
||||||
/// this requires [`SIGREFS_PARENT`].
|
feature = "schemars",
|
||||||
|
schemars(description = "\
|
||||||
|
The highest feature level known, which protects against graft attacks \
|
||||||
|
and replay attacks. \
|
||||||
|
Introduced in Radicle 1.7.0, in commit \
|
||||||
|
`d3bc868e84c334f113806df1737f52cc57c5453d`.\
|
||||||
|
")
|
||||||
|
)]
|
||||||
|
/// Requires [`SIGREFS_PARENT`].
|
||||||
Parent,
|
Parent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue