use super::*;
use crate::registry_core::declaration::PluginSource;
use std::fmt;
#[derive(Clone, Debug)]
pub struct PluginArtifact {
pub registration: crate::RegistrationInfo,
pub bytes: Vec<u8>,
pub key_fingerprint: Option<String>,
}
#[derive(Clone, Debug)]
pub struct VerifiedPluginArtifact {
registration: crate::RegistrationInfo,
bytes: Vec<u8>,
assurance: PluginAssurance,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum PluginAssurance {
Digest,
Signature,
}
impl VerifiedPluginArtifact {
pub const fn registration(&self) -> crate::RegistrationInfo {
self.registration
}
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
pub const fn assurance(&self) -> PluginAssurance {
self.assurance
}
pub fn into_parts(self) -> (crate::RegistrationInfo, Vec<u8>) {
(self.registration, self.bytes)
}
}
impl PluginArtifact {
pub fn verify_artifact(
self,
policy: PluginTrustPolicy,
) -> Result<VerifiedPluginArtifact, PluginTrustError> {
let manifest = self
.registration
.plugin
.ok_or(PluginTrustError::MissingManifest)?;
policy.verify(manifest, &self.bytes, self.key_fingerprint.as_deref())?;
Ok(VerifiedPluginArtifact {
registration: self.registration,
bytes: self.bytes,
assurance: PluginAssurance::Digest,
})
}
pub fn verify_signed(
self,
policy: PluginTrustPolicy,
verifier: &impl PluginSignatureVerifier,
) -> Result<VerifiedPluginArtifact, PluginTrustError> {
let manifest = self
.registration
.plugin
.ok_or(PluginTrustError::MissingManifest)?;
if manifest.source != PluginSource::Official {
return Err(PluginTrustError::SignatureLaneRequired);
}
policy.verify_with(
manifest,
&self.bytes,
self.key_fingerprint.as_deref(),
verifier,
)?;
Ok(VerifiedPluginArtifact {
registration: self.registration,
bytes: self.bytes,
assurance: PluginAssurance::Signature,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::declaration::{LocalizedText, ObjectContract, SourceLocation};
use crate::plugin::{PluginChannel, validate_artifact};
use crate::{
Admission, FlowContract, FrameworkId, NodeId, PluginManifest, PluginMode, PluginSource,
RegistrationInfo, RegistrationRule,
};
const KEY: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
const BYTES: &[u8] = b"abc";
const CHECKSUM: &str = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
struct AcceptingVerifier;
impl PluginSignatureVerifier for AcceptingVerifier {
fn verify(&self, manifest: PluginManifest, bytes: &[u8], key_fingerprint: &str) -> bool {
manifest.signature == Some("adapter-verified-signature")
&& bytes == BYTES
&& key_fingerprint.len() == 64
}
}
struct RejectingVerifier;
impl PluginSignatureVerifier for RejectingVerifier {
fn verify(&self, _: PluginManifest, _: &[u8], _: &str) -> bool {
false
}
}
fn official_artifact() -> PluginArtifact {
PluginArtifact {
registration: RegistrationInfo {
namespace: "plugin-test",
id: NodeId::from_path("plugin.rs", "Plugin"),
parent: crate::ROOT_NODE_ID,
kind: "Plugin",
preset: "",
parts: "",
params: "",
handle: "PluginHandle",
stable_name: None,
name: LocalizedText {
zh: "插件",
en: "Plugin",
},
summary: LocalizedText { zh: "", en: "" },
exports: &[],
needs_registry: false,
registry_name: "plugin",
getting_from_other_registry: None,
registry_rule_path: "<test>",
registry_rule: RegistrationRule::ANY,
admission: Admission::ANY,
requires: &[],
provides: &[],
contract: ObjectContract {
required_parts: &[],
provided_parts: &[],
},
flow: FlowContract::NONE,
flow_provider: None,
handle_traits: &[],
part_traits: &[],
runtime_checks: &[],
plugin: Some(PluginManifest {
name: "official.canvas",
crate_name: "official_canvas",
version: "1.0.0",
framework: FrameworkId::new("nichlink.test"),
source: PluginSource::Official,
mode: PluginMode::Extension,
checksum: CHECKSUM,
signature: Some("adapter-verified-signature"),
public_key_fingerprint: Some(KEY),
revocation_list: Some("official-2026"),
}),
source: SourceLocation {
file: "plugin.rs",
line: 1,
column: 1,
function: "plugin",
},
},
bytes: BYTES.to_vec(),
key_fingerprint: Some(KEY.to_owned()),
}
}
fn official_channel(artifact: &VerifiedPluginArtifact) -> Result<(), SlotValidationError> {
validate_artifact(
"canvas",
FrameworkId::new("nichlink.test"),
PluginMode::Extension,
FlowContract::NONE,
&[PluginChannel::Official],
PluginChannel::Official,
artifact,
)
}
#[test]
fn a_signed_artifact_is_the_only_way_into_the_official_channel() {
let policy = PluginTrustPolicy::official(&[KEY], &[]);
let signed = official_artifact()
.verify_signed(policy, &AcceptingVerifier)
.expect("a trusted signature verifies");
assert_eq!(signed.assurance(), PluginAssurance::Signature);
let digest_only = official_artifact()
.verify_artifact(policy)
.expect("the checksum path still works");
assert_eq!(digest_only.assurance(), PluginAssurance::Digest);
assert!(
official_channel(&signed).is_ok(),
"a verified signature must open the official lane"
);
assert!(
official_channel(&digest_only).is_err(),
"a checksum alone must not"
);
}
#[test]
fn a_revoked_version_is_refused_even_with_a_good_signature() {
let policy = PluginTrustPolicy::official(
&[KEY],
&[PluginRevocation {
package: "official.canvas",
version: "1.0.0",
}],
);
assert!(matches!(
official_artifact().verify_signed(policy, &AcceptingVerifier),
Err(PluginTrustError::Revoked)
));
}
#[test]
fn signature_assurance_is_refused_for_a_user_artifact() {
let mut artifact = official_artifact();
let mut manifest = artifact.registration.plugin.expect("manifest");
manifest.source = PluginSource::User;
manifest.signature = None;
manifest.public_key_fingerprint = None;
artifact.registration.plugin = Some(manifest);
artifact.key_fingerprint = None;
assert!(
matches!(
artifact
.clone()
.verify_signed(PluginTrustPolicy::open(), &AcceptingVerifier),
Err(PluginTrustError::SignatureLaneRequired)
),
"a lane that never calls a verifier cannot record a verified signature"
);
let digest = artifact
.verify_artifact(PluginTrustPolicy::open())
.expect("the checksum path accepts a user artifact");
assert_eq!(digest.assurance(), PluginAssurance::Digest);
}
#[test]
fn a_refused_signature_never_earns_the_stronger_assurance() {
assert!(matches!(
official_artifact()
.verify_signed(PluginTrustPolicy::official(&[KEY], &[]), &RejectingVerifier),
Err(PluginTrustError::SignatureNotVerified)
));
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PluginDecision {
Accepted,
Rejected(PluginRejectReason),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PluginRejectReason {
FrameworkMismatch,
SourceDisabled,
InvalidDigest,
MissingSignature,
LockMismatch,
}
impl fmt::Display for PluginRejectReason {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::FrameworkMismatch => "framework mismatch",
Self::SourceDisabled => "plugin source disabled",
Self::InvalidDigest => "checksum is not a SHA-256 digest",
Self::MissingSignature => "official plugin signature is missing",
Self::LockMismatch => "official lock record mismatch",
})
}
}
pub(super) fn is_sha256(value: &str) -> bool {
let value = value.strip_prefix("sha256:").unwrap_or(value);
value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
}