Verify plugin signatures
This commit is contained in:
@@ -56,6 +56,12 @@ pub enum DomainError {
|
||||
#[error("invalid plugin signature algorithm: {value}")]
|
||||
InvalidPluginSignatureAlgorithm { value: String },
|
||||
|
||||
#[error("invalid plugin signature key id: {value}")]
|
||||
InvalidPluginSignatureKeyId { value: String },
|
||||
|
||||
#[error("invalid plugin signature public key: {value}")]
|
||||
InvalidPluginSignaturePublicKey { value: String },
|
||||
|
||||
#[error("invalid plugin signature: {value}")]
|
||||
InvalidPluginSignature { value: String },
|
||||
}
|
||||
|
||||
@@ -6,6 +6,11 @@ use url::Url;
|
||||
|
||||
use crate::DomainError;
|
||||
|
||||
mod signature;
|
||||
|
||||
use signature::RawPluginSignature;
|
||||
pub use signature::{PluginSignature, PluginSignatureAlgorithm};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PluginId(String);
|
||||
|
||||
@@ -69,17 +74,6 @@ pub enum PluginContributionPoint {
|
||||
ReadingModeExporter,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum PluginSignatureAlgorithm {
|
||||
Ed25519,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PluginSignature {
|
||||
algorithm: PluginSignatureAlgorithm,
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RawPluginManifest {
|
||||
@@ -95,13 +89,6 @@ struct RawPluginManifest {
|
||||
signature: RawPluginSignature,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RawPluginSignature {
|
||||
algorithm: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl PluginId {
|
||||
pub fn parse(value: impl Into<String>) -> Result<Self, DomainError> {
|
||||
let value = value.into();
|
||||
@@ -329,40 +316,6 @@ impl PluginContributionPoint {
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginSignature {
|
||||
fn from_raw(raw: RawPluginSignature) -> Result<Self, DomainError> {
|
||||
let algorithm = PluginSignatureAlgorithm::parse(raw.algorithm.as_str())?;
|
||||
let value = plugin_signature_value(&algorithm, raw.value)?;
|
||||
Ok(Self { algorithm, value })
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn algorithm(&self) -> &PluginSignatureAlgorithm {
|
||||
&self.algorithm
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn value(&self) -> &str {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginSignatureAlgorithm {
|
||||
fn parse(value: &str) -> Result<Self, DomainError> {
|
||||
match value {
|
||||
"ed25519" => Ok(Self::Ed25519),
|
||||
_ => Err(DomainError::InvalidPluginSignatureAlgorithm { value: value.to_string() }),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Ed25519 => "ed25519",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_unique_permissions(values: &[String]) -> Result<Vec<PluginPermission>, DomainError> {
|
||||
let mut seen = BTreeSet::new();
|
||||
let mut permissions = Vec::with_capacity(values.len());
|
||||
@@ -430,20 +383,6 @@ fn plugin_checksum(value: impl Into<String>) -> Result<String, DomainError> {
|
||||
Ok(value.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
fn plugin_signature_value(
|
||||
algorithm: &PluginSignatureAlgorithm,
|
||||
value: impl Into<String>,
|
||||
) -> Result<String, DomainError> {
|
||||
let value = non_empty_plugin_field("signature", value)?;
|
||||
let valid = match algorithm {
|
||||
PluginSignatureAlgorithm::Ed25519 => is_hex_of_len(value.as_str(), 128),
|
||||
};
|
||||
if !valid {
|
||||
return Err(DomainError::InvalidPluginSignature { value });
|
||||
}
|
||||
Ok(value.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
fn is_valid_plugin_id(value: &str) -> bool {
|
||||
(3..=128).contains(&value.len())
|
||||
&& value.split('.').all(|segment| {
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::DomainError;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum PluginSignatureAlgorithm {
|
||||
Ed25519,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PluginSignature {
|
||||
algorithm: PluginSignatureAlgorithm,
|
||||
key_id: String,
|
||||
public_key: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(super) struct RawPluginSignature {
|
||||
pub(super) algorithm: String,
|
||||
pub(super) key_id: String,
|
||||
pub(super) public_key: String,
|
||||
pub(super) value: String,
|
||||
}
|
||||
|
||||
impl PluginSignature {
|
||||
pub(super) fn from_raw(raw: RawPluginSignature) -> Result<Self, DomainError> {
|
||||
let algorithm = PluginSignatureAlgorithm::parse(raw.algorithm.as_str())?;
|
||||
let key_id = plugin_signature_key_id(raw.key_id)?;
|
||||
let public_key = plugin_signature_public_key(raw.public_key)?;
|
||||
let value = plugin_signature_value(&algorithm, raw.value)?;
|
||||
Ok(Self { algorithm, key_id, public_key, value })
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn algorithm(&self) -> &PluginSignatureAlgorithm {
|
||||
&self.algorithm
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn key_id(&self) -> &str {
|
||||
&self.key_id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn public_key(&self) -> &str {
|
||||
&self.public_key
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn value(&self) -> &str {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginSignatureAlgorithm {
|
||||
fn parse(value: &str) -> Result<Self, DomainError> {
|
||||
match value {
|
||||
"ed25519" => Ok(Self::Ed25519),
|
||||
_ => Err(DomainError::InvalidPluginSignatureAlgorithm { value: value.to_string() }),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Ed25519 => "ed25519",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_signature_key_id(value: impl Into<String>) -> Result<String, DomainError> {
|
||||
let value = super::non_empty_plugin_field("signature.key_id", value)?;
|
||||
let valid = (3..=128).contains(&value.len())
|
||||
&& value.chars().all(|ch| {
|
||||
ch.is_ascii_lowercase() || ch.is_ascii_digit() || matches!(ch, '.' | '-' | '_')
|
||||
});
|
||||
if !valid {
|
||||
return Err(DomainError::InvalidPluginSignatureKeyId { value });
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn plugin_signature_public_key(value: impl Into<String>) -> Result<String, DomainError> {
|
||||
let value = super::non_empty_plugin_field("signature.public_key", value)?;
|
||||
if !super::is_hex_of_len(value.as_str(), 64) {
|
||||
return Err(DomainError::InvalidPluginSignaturePublicKey { value });
|
||||
}
|
||||
Ok(value.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
fn plugin_signature_value(
|
||||
algorithm: &PluginSignatureAlgorithm,
|
||||
value: impl Into<String>,
|
||||
) -> Result<String, DomainError> {
|
||||
let value = super::non_empty_plugin_field("signature", value)?;
|
||||
let valid = match algorithm {
|
||||
PluginSignatureAlgorithm::Ed25519 => super::is_hex_of_len(value.as_str(), 128),
|
||||
};
|
||||
if !valid {
|
||||
return Err(DomainError::InvalidPluginSignature { value });
|
||||
}
|
||||
Ok(value.to_ascii_lowercase())
|
||||
}
|
||||
@@ -26,6 +26,11 @@ fn parses_signed_plugin_manifest() -> Result<(), Box<dyn Error>> {
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
);
|
||||
assert_eq!(manifest.signature().algorithm(), &PluginSignatureAlgorithm::Ed25519);
|
||||
assert_eq!(manifest.signature().key_id(), "elydora-alpha-plugins");
|
||||
assert_eq!(
|
||||
manifest.signature().public_key(),
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
);
|
||||
assert_eq!(
|
||||
manifest.signature().value(),
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
@@ -100,6 +105,36 @@ fn rejects_unsigned_plugin_manifest() -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_plugin_signature_key_id() -> Result<(), Box<dyn Error>> {
|
||||
let manifest = valid_manifest()
|
||||
.replace("key_id = \"elydora-alpha-plugins\"", "key_id = \"Elydora Alpha Plugins\"");
|
||||
|
||||
let error = parse_error(manifest.as_str())?;
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
DomainError::InvalidPluginSignatureKeyId { value } if value == "Elydora Alpha Plugins"
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_plugin_signature_public_key() -> Result<(), Box<dyn Error>> {
|
||||
let manifest = valid_manifest().replace(
|
||||
"public_key = \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"",
|
||||
"public_key = \"abcd\"",
|
||||
);
|
||||
|
||||
let error = parse_error(manifest.as_str())?;
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
DomainError::InvalidPluginSignaturePublicKey { value } if value == "abcd"
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_mismatched_plugin_checksum() -> Result<(), Box<dyn Error>> {
|
||||
let manifest = valid_manifest().replace(
|
||||
@@ -163,6 +198,8 @@ checksum = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
|
||||
[signature]
|
||||
algorithm = "ed25519"
|
||||
key_id = "elydora-alpha-plugins"
|
||||
public_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
value = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
|
||||
"#
|
||||
.to_string()
|
||||
|
||||
Reference in New Issue
Block a user