Verify plugin signatures
This commit is contained in:
Generated
+26
@@ -1816,6 +1816,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
"curve25519-dalek-derive",
|
"curve25519-dalek-derive",
|
||||||
|
"digest",
|
||||||
"fiat-crypto",
|
"fiat-crypto",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"subtle",
|
"subtle",
|
||||||
@@ -2186,6 +2187,30 @@ dependencies = [
|
|||||||
"spki",
|
"spki",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ed25519"
|
||||||
|
version = "2.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||||
|
dependencies = [
|
||||||
|
"pkcs8",
|
||||||
|
"signature",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ed25519-dalek"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||||
|
dependencies = [
|
||||||
|
"curve25519-dalek",
|
||||||
|
"ed25519",
|
||||||
|
"serde",
|
||||||
|
"sha2",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
@@ -2218,6 +2243,7 @@ name = "ely_app"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"directories",
|
"directories",
|
||||||
|
"ed25519-dalek",
|
||||||
"ely_browser_core",
|
"ely_browser_core",
|
||||||
"ely_design_system",
|
"ely_design_system",
|
||||||
"ely_domain",
|
"ely_domain",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ rust-version = "1.95"
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
directories = "6.0.0"
|
directories = "6.0.0"
|
||||||
dpi = "0.1"
|
dpi = "0.1"
|
||||||
|
ed25519-dalek = "2.2.0"
|
||||||
gpui = "0.2.2"
|
gpui = "0.2.2"
|
||||||
gpui-component = "0.5.1"
|
gpui-component = "0.5.1"
|
||||||
gpui-component-assets = "0.5.1"
|
gpui-component-assets = "0.5.1"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ rust-version.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
directories.workspace = true
|
directories.workspace = true
|
||||||
|
ed25519-dalek.workspace = true
|
||||||
ely_browser_core = { path = "../ely_browser_core" }
|
ely_browser_core = { path = "../ely_browser_core" }
|
||||||
ely_design_system = { path = "../ely_design_system" }
|
ely_design_system = { path = "../ely_design_system" }
|
||||||
ely_domain = { path = "../ely_domain" }
|
ely_domain = { path = "../ely_domain" }
|
||||||
|
|||||||
@@ -2,3 +2,7 @@ pub mod download_checksums;
|
|||||||
pub mod download_files;
|
pub mod download_files;
|
||||||
pub mod plugin_package_store;
|
pub mod plugin_package_store;
|
||||||
pub mod plugin_packages;
|
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},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
|
|
||||||
use super::{PluginPackageStore, PluginPackageStoreError};
|
use super::{PluginPackageStore, PluginPackageStoreError};
|
||||||
|
use crate::services::plugin_package_fixtures::write_signed_package;
|
||||||
use crate::services::plugin_packages::PluginPackageReader;
|
use crate::services::plugin_packages::PluginPackageReader;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -324,43 +323,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn write_package(root: &Path, name: &str, component: &[u8]) -> Result<PathBuf, Box<dyn Error>> {
|
fn write_package(root: &Path, name: &str, component: &[u8]) -> Result<PathBuf, Box<dyn Error>> {
|
||||||
let package = root.join(format!("{name}.rplug"));
|
write_signed_package(root, name, component)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TempTree {
|
struct TempTree {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ use ely_domain::PluginManifest;
|
|||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use super::plugin_signatures::{PluginSignatureVerificationError, PluginSignatureVerifier};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct VerifiedPluginPackage {
|
pub struct VerifiedPluginPackage {
|
||||||
source_path: PathBuf,
|
source_path: PathBuf,
|
||||||
@@ -48,6 +50,9 @@ pub enum PluginPackageError {
|
|||||||
#[error("plugin package signature mismatch for signatures/ed25519.sig")]
|
#[error("plugin package signature mismatch for signatures/ed25519.sig")]
|
||||||
SignatureMismatch,
|
SignatureMismatch,
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
SignatureVerification(#[from] PluginSignatureVerificationError),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Manifest(#[from] ely_domain::DomainError),
|
Manifest(#[from] ely_domain::DomainError),
|
||||||
}
|
}
|
||||||
@@ -78,6 +83,7 @@ impl PluginPackageReader {
|
|||||||
if signature.trim().to_ascii_lowercase() != manifest.signature().value() {
|
if signature.trim().to_ascii_lowercase() != manifest.signature().value() {
|
||||||
return Err(PluginPackageError::SignatureMismatch);
|
return Err(PluginPackageError::SignatureMismatch);
|
||||||
}
|
}
|
||||||
|
PluginSignatureVerifier::verify(path, &manifest)?;
|
||||||
|
|
||||||
let package_hash = sha256_directory(path)?;
|
let package_hash = sha256_directory(path)?;
|
||||||
Ok(VerifiedPluginPackage::new(path.to_path_buf(), manifest, package_hash))
|
Ok(VerifiedPluginPackage::new(path.to_path_buf(), manifest, package_hash))
|
||||||
@@ -304,9 +310,8 @@ mod tests {
|
|||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
|
|
||||||
use super::PluginPackageReader;
|
use super::PluginPackageReader;
|
||||||
|
use crate::services::plugin_package_fixtures::{sign_package_in_place, write_signed_package};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reads_verified_directory_package() -> Result<(), Box<dyn Error>> {
|
fn reads_verified_directory_package() -> Result<(), Box<dyn Error>> {
|
||||||
@@ -346,6 +351,19 @@ mod tests {
|
|||||||
Ok(())
|
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]
|
#[test]
|
||||||
fn rejects_non_package_directory() -> Result<(), Box<dyn Error>> {
|
fn rejects_non_package_directory() -> Result<(), Box<dyn Error>> {
|
||||||
let package = temp_root()?.join("invalid");
|
let package = temp_root()?.join("invalid");
|
||||||
@@ -365,6 +383,7 @@ mod tests {
|
|||||||
let first_hash =
|
let first_hash =
|
||||||
PluginPackageReader::read_directory_package(&package)?.package_hash().to_string();
|
PluginPackageReader::read_directory_package(&package)?.package_hash().to_string();
|
||||||
fs::write(package.join("README.md"), "updated package metadata")?;
|
fs::write(package.join("README.md"), "updated package metadata")?;
|
||||||
|
sign_package_in_place(package.as_path(), "package-hash")?;
|
||||||
|
|
||||||
let second_hash =
|
let second_hash =
|
||||||
PluginPackageReader::read_directory_package(&package)?.package_hash().to_string();
|
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>> {
|
fn write_package(name: &str, component: &[u8]) -> Result<PathBuf, Box<dyn Error>> {
|
||||||
let package = temp_root()?.join(format!("{name}.rplug"));
|
write_signed_package(temp_root()?.as_path(), name, component)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn temp_root() -> Result<PathBuf, Box<dyn Error>> {
|
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,
|
||||||
|
}
|
||||||
@@ -181,6 +181,8 @@ checksum = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|||||||
|
|
||||||
[signature]
|
[signature]
|
||||||
algorithm = "ed25519"
|
algorithm = "ed25519"
|
||||||
|
key_id = "elydora-alpha-plugins"
|
||||||
|
public_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||||
value = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
|
value = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
|
||||||
"#
|
"#
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -56,6 +56,12 @@ pub enum DomainError {
|
|||||||
#[error("invalid plugin signature algorithm: {value}")]
|
#[error("invalid plugin signature algorithm: {value}")]
|
||||||
InvalidPluginSignatureAlgorithm { value: String },
|
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}")]
|
#[error("invalid plugin signature: {value}")]
|
||||||
InvalidPluginSignature { value: String },
|
InvalidPluginSignature { value: String },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ use url::Url;
|
|||||||
|
|
||||||
use crate::DomainError;
|
use crate::DomainError;
|
||||||
|
|
||||||
|
mod signature;
|
||||||
|
|
||||||
|
use signature::RawPluginSignature;
|
||||||
|
pub use signature::{PluginSignature, PluginSignatureAlgorithm};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct PluginId(String);
|
pub struct PluginId(String);
|
||||||
|
|
||||||
@@ -69,17 +74,6 @@ pub enum PluginContributionPoint {
|
|||||||
ReadingModeExporter,
|
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)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct RawPluginManifest {
|
struct RawPluginManifest {
|
||||||
@@ -95,13 +89,6 @@ struct RawPluginManifest {
|
|||||||
signature: RawPluginSignature,
|
signature: RawPluginSignature,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
struct RawPluginSignature {
|
|
||||||
algorithm: String,
|
|
||||||
value: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PluginId {
|
impl PluginId {
|
||||||
pub fn parse(value: impl Into<String>) -> Result<Self, DomainError> {
|
pub fn parse(value: impl Into<String>) -> Result<Self, DomainError> {
|
||||||
let value = value.into();
|
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> {
|
fn parse_unique_permissions(values: &[String]) -> Result<Vec<PluginPermission>, DomainError> {
|
||||||
let mut seen = BTreeSet::new();
|
let mut seen = BTreeSet::new();
|
||||||
let mut permissions = Vec::with_capacity(values.len());
|
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())
|
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 {
|
fn is_valid_plugin_id(value: &str) -> bool {
|
||||||
(3..=128).contains(&value.len())
|
(3..=128).contains(&value.len())
|
||||||
&& value.split('.').all(|segment| {
|
&& 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"
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
);
|
);
|
||||||
assert_eq!(manifest.signature().algorithm(), &PluginSignatureAlgorithm::Ed25519);
|
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!(
|
assert_eq!(
|
||||||
manifest.signature().value(),
|
manifest.signature().value(),
|
||||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
@@ -100,6 +105,36 @@ fn rejects_unsigned_plugin_manifest() -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
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]
|
#[test]
|
||||||
fn rejects_mismatched_plugin_checksum() -> Result<(), Box<dyn Error>> {
|
fn rejects_mismatched_plugin_checksum() -> Result<(), Box<dyn Error>> {
|
||||||
let manifest = valid_manifest().replace(
|
let manifest = valid_manifest().replace(
|
||||||
@@ -163,6 +198,8 @@ checksum = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|||||||
|
|
||||||
[signature]
|
[signature]
|
||||||
algorithm = "ed25519"
|
algorithm = "ed25519"
|
||||||
|
key_id = "elydora-alpha-plugins"
|
||||||
|
public_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||||
value = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
|
value = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
|
||||||
"#
|
"#
|
||||||
.to_string()
|
.to_string()
|
||||||
|
|||||||
Reference in New Issue
Block a user