Verify plugin signatures

This commit is contained in:
2026-05-07 23:41:08 -04:00
parent eddd694ae9
commit e30d14728b
13 changed files with 610 additions and 144 deletions
+1
View File
@@ -7,6 +7,7 @@ rust-version.workspace = true
[dependencies]
directories.workspace = true
ed25519-dalek.workspace = true
ely_browser_core = { path = "../ely_browser_core" }
ely_design_system = { path = "../ely_design_system" }
ely_domain = { path = "../ely_domain" }
+4
View File
@@ -2,3 +2,7 @@ pub mod download_checksums;
pub mod download_files;
pub mod plugin_package_store;
pub mod plugin_packages;
pub mod plugin_signatures;
#[cfg(test)]
mod plugin_package_fixtures;
@@ -0,0 +1,93 @@
use std::{
error::Error,
fs,
path::{Path, PathBuf},
};
use ed25519_dalek::{Signer, SigningKey};
use ely_domain::PluginManifest;
use sha2::{Digest, Sha256};
use super::plugin_signatures::signing_payload;
const KEY_ID: &str = "elydora-alpha-plugins";
const TEST_SIGNING_KEY: [u8; 32] = [9; 32];
pub(crate) fn write_signed_package(
root: &Path,
name: &str,
component: &[u8],
) -> Result<PathBuf, Box<dyn Error>> {
let package = root.join(format!("{name}.rplug"));
fs::create_dir_all(package.join("signatures"))?;
fs::write(package.join("component.wasm"), component)?;
sign_package_in_place(package.as_path(), name)?;
Ok(package)
}
pub(crate) fn sign_package_in_place(package: &Path, name: &str) -> Result<(), Box<dyn Error>> {
fs::create_dir_all(package.join("signatures"))?;
let signing_key = SigningKey::from_bytes(&TEST_SIGNING_KEY);
let public_key = hex_bytes(signing_key.verifying_key().to_bytes().as_slice());
let component = fs::read(package.join("component.wasm"))?;
let checksum = format!("{:x}", Sha256::digest(component.as_slice()));
let placeholder_signature = "0".repeat(128);
fs::write(
package.join("plugin.toml"),
manifest_toml(name, checksum.as_str(), public_key.as_str(), placeholder_signature.as_str()),
)?;
let manifest_text = fs::read_to_string(package.join("plugin.toml"))?;
let manifest = PluginManifest::from_toml(manifest_text.as_str())?;
let payload = signing_payload(package, &manifest)?;
let signature = signing_key.sign(payload.as_slice());
let signature = hex_bytes(signature.to_bytes().as_slice());
fs::write(package.join("signatures").join("ed25519.sig"), signature.as_str())?;
fs::write(
package.join("plugin.toml"),
manifest_toml(name, checksum.as_str(), public_key.as_str(), signature.as_str()),
)?;
Ok(())
}
fn manifest_toml(name: &str, checksum: &str, public_key: &str, signature: &str) -> String {
format!(
r#"
id = "com.elydora.{name}"
name = "Verified Plugin"
description = "Exports verified content."
author = "Elydora"
homepage = "https://elydora.com/plugins/{name}"
permissions = ["page:metadata"]
contributes = ["command-bar-command"]
min_ely_build = "0.1.0"
checksum = "{checksum}"
[signature]
algorithm = "ed25519"
key_id = "{KEY_ID}"
public_key = "{public_key}"
value = "{signature}"
"#
)
}
fn hex_bytes(bytes: &[u8]) -> String {
let mut encoded = String::with_capacity(bytes.len() * 2);
for byte in bytes {
encoded.push(hex_char(byte >> 4));
encoded.push(hex_char(byte & 0x0f));
}
encoded
}
fn hex_char(value: u8) -> char {
match value {
0..=9 => (b'0' + value) as char,
_ => (b'a' + value - 10) as char,
}
}
@@ -261,9 +261,8 @@ mod tests {
time::{SystemTime, UNIX_EPOCH},
};
use sha2::{Digest, Sha256};
use super::{PluginPackageStore, PluginPackageStoreError};
use crate::services::plugin_package_fixtures::write_signed_package;
use crate::services::plugin_packages::PluginPackageReader;
#[test]
@@ -324,43 +323,7 @@ mod tests {
}
fn write_package(root: &Path, name: &str, component: &[u8]) -> Result<PathBuf, Box<dyn Error>> {
let package = root.join(format!("{name}.rplug"));
fs::create_dir_all(package.join("signatures"))?;
fs::write(package.join("component.wasm"), component)?;
let checksum = sha256_bytes(component);
let signature = "b".repeat(128);
fs::write(package.join("signatures").join("ed25519.sig"), &signature)?;
fs::write(
package.join("plugin.toml"),
manifest_toml(name, checksum.as_str(), signature.as_str()),
)?;
Ok(package)
}
fn manifest_toml(name: &str, checksum: &str, signature: &str) -> String {
format!(
r#"
id = "com.elydora.{name}"
name = "Verified Plugin"
description = "Exports verified content."
author = "Elydora"
homepage = "https://elydora.com/plugins/{name}"
permissions = ["page:metadata"]
contributes = ["command-bar-command"]
min_ely_build = "0.1.0"
checksum = "{checksum}"
[signature]
algorithm = "ed25519"
value = "{signature}"
"#
)
}
fn sha256_bytes(bytes: &[u8]) -> String {
format!("{:x}", Sha256::digest(bytes))
write_signed_package(root, name, component)
}
struct TempTree {
+22 -39
View File
@@ -9,6 +9,8 @@ use ely_domain::PluginManifest;
use sha2::{Digest, Sha256};
use thiserror::Error;
use super::plugin_signatures::{PluginSignatureVerificationError, PluginSignatureVerifier};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VerifiedPluginPackage {
source_path: PathBuf,
@@ -48,6 +50,9 @@ pub enum PluginPackageError {
#[error("plugin package signature mismatch for signatures/ed25519.sig")]
SignatureMismatch,
#[error(transparent)]
SignatureVerification(#[from] PluginSignatureVerificationError),
#[error(transparent)]
Manifest(#[from] ely_domain::DomainError),
}
@@ -78,6 +83,7 @@ impl PluginPackageReader {
if signature.trim().to_ascii_lowercase() != manifest.signature().value() {
return Err(PluginPackageError::SignatureMismatch);
}
PluginSignatureVerifier::verify(path, &manifest)?;
let package_hash = sha256_directory(path)?;
Ok(VerifiedPluginPackage::new(path.to_path_buf(), manifest, package_hash))
@@ -304,9 +310,8 @@ mod tests {
time::{SystemTime, UNIX_EPOCH},
};
use sha2::{Digest, Sha256};
use super::PluginPackageReader;
use crate::services::plugin_package_fixtures::{sign_package_in_place, write_signed_package};
#[test]
fn reads_verified_directory_package() -> Result<(), Box<dyn Error>> {
@@ -346,6 +351,19 @@ mod tests {
Ok(())
}
#[test]
fn rejects_package_signature_verification_failure() -> Result<(), Box<dyn Error>> {
let package = write_package("verification", b"wasm component")?;
fs::write(package.join("README.md"), "unsigned package metadata")?;
let error = PluginPackageReader::read_directory_package(&package)
.err()
.ok_or_else(|| std::io::Error::other("package read succeeded"))?;
assert!(matches!(error, super::PluginPackageError::SignatureVerification(_)));
Ok(())
}
#[test]
fn rejects_non_package_directory() -> Result<(), Box<dyn Error>> {
let package = temp_root()?.join("invalid");
@@ -365,6 +383,7 @@ mod tests {
let first_hash =
PluginPackageReader::read_directory_package(&package)?.package_hash().to_string();
fs::write(package.join("README.md"), "updated package metadata")?;
sign_package_in_place(package.as_path(), "package-hash")?;
let second_hash =
PluginPackageReader::read_directory_package(&package)?.package_hash().to_string();
@@ -374,43 +393,7 @@ mod tests {
}
fn write_package(name: &str, component: &[u8]) -> Result<PathBuf, Box<dyn Error>> {
let package = temp_root()?.join(format!("{name}.rplug"));
fs::create_dir_all(package.join("signatures"))?;
fs::write(package.join("component.wasm"), component)?;
let checksum = sha256_bytes(component);
let signature = "b".repeat(128);
fs::write(package.join("signatures").join("ed25519.sig"), &signature)?;
fs::write(
package.join("plugin.toml"),
manifest_toml(name, checksum.as_str(), signature.as_str()),
)?;
Ok(package)
}
fn manifest_toml(name: &str, checksum: &str, signature: &str) -> String {
format!(
r#"
id = "com.elydora.{name}"
name = "Verified Plugin"
description = "Exports verified content."
author = "Elydora"
homepage = "https://elydora.com/plugins/{name}"
permissions = ["page:metadata"]
contributes = ["command-bar-command"]
min_ely_build = "0.1.0"
checksum = "{checksum}"
[signature]
algorithm = "ed25519"
value = "{signature}"
"#
)
}
fn sha256_bytes(bytes: &[u8]) -> String {
format!("{:x}", Sha256::digest(bytes))
write_signed_package(temp_root()?.as_path(), name, component)
}
fn temp_root() -> Result<PathBuf, Box<dyn Error>> {
@@ -0,0 +1,306 @@
use std::{
ffi::OsStr,
fs,
io::{self, Read},
path::{Component, Path, PathBuf},
};
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use ely_domain::PluginManifest;
use sha2::{Digest, Sha256};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PluginSignatureVerificationError {
#[error("plugin signature public key is invalid for key {key_id}")]
InvalidPublicKey { key_id: String },
#[error("plugin signature value is invalid for key {key_id}")]
InvalidSignature { key_id: String },
#[error("plugin signature verification failed for key {key_id}")]
VerificationFailed { key_id: String },
#[error("failed to read plugin package entry {entry}: {path}")]
ReadFailed { entry: &'static str, path: PathBuf, source: io::Error },
#[error("plugin package entry name must be valid utf-8: {path}")]
InvalidEntryName { path: PathBuf },
#[error("plugin package entry cannot be signed through a symlink: {path}")]
SymlinkEntry { path: PathBuf },
#[error("plugin package entry type is unsupported: {path}")]
UnsupportedEntry { path: PathBuf },
}
pub struct PluginSignatureVerifier;
impl PluginSignatureVerifier {
pub fn verify(
package_root: &Path,
manifest: &PluginManifest,
) -> Result<(), PluginSignatureVerificationError> {
let key_id = manifest.signature().key_id().to_string();
let public_key =
decode_hex_array::<32>(manifest.signature().public_key()).ok_or_else(|| {
PluginSignatureVerificationError::InvalidPublicKey { key_id: key_id.clone() }
})?;
let signature = decode_hex_array::<64>(manifest.signature().value()).ok_or_else(|| {
PluginSignatureVerificationError::InvalidSignature { key_id: key_id.clone() }
})?;
let key = VerifyingKey::from_bytes(&public_key).map_err(|_| {
PluginSignatureVerificationError::InvalidPublicKey { key_id: key_id.clone() }
})?;
let signature = Signature::from_bytes(&signature);
let payload = signing_payload(package_root, manifest)?;
key.verify(payload.as_slice(), &signature)
.map_err(|_| PluginSignatureVerificationError::VerificationFailed { key_id })
}
}
pub(crate) fn signing_payload(
package_root: &Path,
manifest: &PluginManifest,
) -> Result<Vec<u8>, PluginSignatureVerificationError> {
let mut payload = Vec::new();
append_field(&mut payload, "format", "ely.rplug.signature.v1");
append_field(&mut payload, "id", manifest.id().as_str());
append_field(&mut payload, "name", manifest.name());
append_field(&mut payload, "description", manifest.description());
append_field(&mut payload, "author", manifest.author());
append_field(&mut payload, "homepage", manifest.homepage());
append_list(
&mut payload,
"permissions",
manifest.permissions().iter().map(|permission| permission.as_str()),
);
append_list(
&mut payload,
"contributes",
manifest.contributes().iter().map(|contribution| contribution.as_str()),
);
append_field(&mut payload, "min_ely_build", manifest.min_ely_build().to_string().as_str());
append_field(&mut payload, "checksum", manifest.checksum());
append_field(&mut payload, "signature_algorithm", manifest.signature().algorithm().as_str());
append_field(&mut payload, "signature_key_id", manifest.signature().key_id());
append_field(&mut payload, "signature_public_key", manifest.signature().public_key());
append_field(&mut payload, "package_files_sha256", package_files_hash(package_root)?.as_str());
Ok(payload)
}
fn package_files_hash(package_root: &Path) -> Result<String, PluginSignatureVerificationError> {
let mut entries = Vec::new();
collect_signed_entries(package_root, Path::new(""), &mut entries)?;
entries.sort_by(|left, right| left.relative_path.cmp(&right.relative_path));
let mut hasher = Sha256::new();
for entry in entries {
let relative_path = canonical_relative_path(entry.relative_path.as_path())?;
match entry.kind {
SignedEntryKind::Directory => {
hasher.update(b"dir\0");
hasher.update(relative_path.as_bytes());
hasher.update(b"\0");
}
SignedEntryKind::File => {
hasher.update(b"file\0");
hasher.update(relative_path.as_bytes());
hasher.update(b"\0");
hash_file_content(entry.absolute_path.as_path(), &mut hasher)?;
}
}
}
Ok(format!("{:x}", hasher.finalize()))
}
fn collect_signed_entries(
root: &Path,
relative_dir: &Path,
entries: &mut Vec<SignedEntry>,
) -> Result<(), PluginSignatureVerificationError> {
let absolute_dir = root.join(relative_dir);
for entry in fs::read_dir(absolute_dir.as_path()).map_err(|source| {
PluginSignatureVerificationError::ReadFailed {
entry: "package",
path: absolute_dir.clone(),
source,
}
})? {
let entry = entry.map_err(|source| PluginSignatureVerificationError::ReadFailed {
entry: "package",
path: absolute_dir.clone(),
source,
})?;
let absolute_path = entry.path();
let relative_path = relative_dir.join(entry.file_name());
if excluded_signature_entry(relative_path.as_path()) {
continue;
}
let metadata = fs::symlink_metadata(absolute_path.as_path()).map_err(|source| {
PluginSignatureVerificationError::ReadFailed {
entry: "package",
path: absolute_path.clone(),
source,
}
})?;
let file_type = metadata.file_type();
if file_type.is_symlink() {
return Err(PluginSignatureVerificationError::SymlinkEntry { path: absolute_path });
}
if file_type.is_dir() {
entries.push(SignedEntry::new(
relative_path.clone(),
absolute_path,
SignedEntryKind::Directory,
));
collect_signed_entries(root, relative_path.as_path(), entries)?;
continue;
}
if file_type.is_file() {
entries.push(SignedEntry::new(relative_path, absolute_path, SignedEntryKind::File));
continue;
}
return Err(PluginSignatureVerificationError::UnsupportedEntry { path: absolute_path });
}
Ok(())
}
fn excluded_signature_entry(path: &Path) -> bool {
let mut components = path.components();
match components.next() {
Some(Component::Normal(value)) if value == OsStr::new("plugin.toml") => true,
Some(Component::Normal(value)) if value == OsStr::new("signatures") => true,
_ => false,
}
}
fn canonical_relative_path(path: &Path) -> Result<String, PluginSignatureVerificationError> {
let mut parts = Vec::new();
for component in path.components() {
let Component::Normal(value) = component else {
return Err(PluginSignatureVerificationError::InvalidEntryName {
path: path.to_path_buf(),
});
};
parts.push(utf8_entry_name(value, path)?);
}
Ok(parts.join("/"))
}
fn utf8_entry_name<'a>(
value: &'a OsStr,
path: &Path,
) -> Result<&'a str, PluginSignatureVerificationError> {
value.to_str().ok_or_else(|| PluginSignatureVerificationError::InvalidEntryName {
path: path.to_path_buf(),
})
}
fn hash_file_content(
path: &Path,
hasher: &mut Sha256,
) -> Result<(), PluginSignatureVerificationError> {
let mut file =
fs::File::open(path).map_err(|source| PluginSignatureVerificationError::ReadFailed {
entry: "package",
path: path.to_path_buf(),
source,
})?;
let metadata =
file.metadata().map_err(|source| PluginSignatureVerificationError::ReadFailed {
entry: "package",
path: path.to_path_buf(),
source,
})?;
hasher.update(metadata.len().to_le_bytes());
hasher.update(b"\0");
let mut buffer = [0_u8; 8192];
loop {
let read = file.read(&mut buffer).map_err(|source| {
PluginSignatureVerificationError::ReadFailed {
entry: "package",
path: path.to_path_buf(),
source,
}
})?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
Ok(())
}
fn append_field(payload: &mut Vec<u8>, name: &str, value: &str) {
payload.extend_from_slice(name.as_bytes());
payload.extend_from_slice(b"\0");
payload.extend_from_slice(value.len().to_string().as_bytes());
payload.extend_from_slice(b"\0");
payload.extend_from_slice(value.as_bytes());
payload.extend_from_slice(b"\0");
}
fn append_list<'a>(payload: &mut Vec<u8>, name: &str, values: impl Iterator<Item = &'a str>) {
let values = values.collect::<Vec<_>>();
payload.extend_from_slice(name.as_bytes());
payload.extend_from_slice(b"\0");
payload.extend_from_slice(values.len().to_string().as_bytes());
payload.extend_from_slice(b"\0");
for value in values {
payload.extend_from_slice(value.len().to_string().as_bytes());
payload.extend_from_slice(b"\0");
payload.extend_from_slice(value.as_bytes());
payload.extend_from_slice(b"\0");
}
}
fn decode_hex_array<const N: usize>(value: &str) -> Option<[u8; N]> {
if value.len() != N * 2 {
return None;
}
let mut output = [0_u8; N];
for (index, chunk) in value.as_bytes().chunks_exact(2).enumerate() {
let high = hex_nibble(chunk[0])?;
let low = hex_nibble(chunk[1])?;
output[index] = (high << 4) | low;
}
Some(output)
}
fn hex_nibble(value: u8) -> Option<u8> {
match value {
b'0'..=b'9' => Some(value - b'0'),
b'a'..=b'f' => Some(value - b'a' + 10),
b'A'..=b'F' => Some(value - b'A' + 10),
_ => None,
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct SignedEntry {
relative_path: PathBuf,
absolute_path: PathBuf,
kind: SignedEntryKind,
}
impl SignedEntry {
fn new(relative_path: PathBuf, absolute_path: PathBuf, kind: SignedEntryKind) -> Self {
Self { relative_path, absolute_path, kind }
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum SignedEntryKind {
Directory,
File,
}
+2
View File
@@ -181,6 +181,8 @@ checksum = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
[signature]
algorithm = "ed25519"
key_id = "elydora-alpha-plugins"
public_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
value = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
"#
)
+6
View File
@@ -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 },
}
+5 -66
View File
@@ -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| {
+105
View File
@@ -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())
}
+37
View File
@@ -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()