use crate::registry_core::declaration::{PluginManifest, PluginSource, RegistrationInfo};
use std::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PluginTrustPolicy {
pub official_key_fingerprints: &'static [&'static str],
pub revoked: &'static [PluginRevocation],
pub require_digest: bool,
}
pub trait PluginSignatureVerifier {
fn verify(&self, manifest: PluginManifest, payload: &[u8], key_fingerprint: &str) -> bool;
}
impl<F> PluginSignatureVerifier for F
where
F: Fn(PluginManifest, &[u8], &str) -> bool,
{
fn verify(&self, manifest: PluginManifest, payload: &[u8], key_fingerprint: &str) -> bool {
self(manifest, payload, key_fingerprint)
}
}
impl PluginTrustPolicy {
pub const fn open() -> Self {
Self {
official_key_fingerprints: &[],
revoked: &[],
require_digest: false,
}
}
pub const fn official(
keys: &'static [&'static str],
revoked: &'static [PluginRevocation],
) -> Self {
Self {
official_key_fingerprints: keys,
revoked,
require_digest: true,
}
}
pub fn verify(
self,
manifest: PluginManifest,
bytes: &[u8],
key_fingerprint: Option<&str>,
) -> Result<(), PluginTrustError> {
if self.require_digest && !super::artifact::is_sha256(manifest.checksum) {
return Err(PluginTrustError::InvalidDigest);
}
if !manifest.verify_bytes(bytes) {
return Err(PluginTrustError::DigestMismatch);
}
if self
.revoked
.iter()
.any(|entry| entry.package == manifest.name && entry.version == manifest.version)
{
return Err(PluginTrustError::Revoked);
}
if manifest.source == PluginSource::Official && !self.official_key_fingerprints.is_empty() {
if manifest.revocation_list.is_none() {
return Err(PluginTrustError::MissingRevocationList);
}
if manifest
.signature
.is_none_or(|signature| signature.trim().is_empty())
{
return Err(PluginTrustError::MissingSignature);
}
let fingerprint = key_fingerprint
.or(manifest.public_key_fingerprint)
.ok_or(PluginTrustError::MissingOfficialKey)?;
if !super::artifact::is_sha256(fingerprint) {
return Err(PluginTrustError::UntrustedOfficialKey);
}
if !self
.official_key_fingerprints
.iter()
.any(|allowed| allowed.eq_ignore_ascii_case(fingerprint))
{
return Err(PluginTrustError::UntrustedOfficialKey);
}
}
Ok(())
}
pub fn verify_with<V: PluginSignatureVerifier>(
self,
manifest: PluginManifest,
registration: &RegistrationInfo,
bytes: &[u8],
key_fingerprint: Option<&str>,
verifier: &V,
) -> Result<(), PluginTrustError> {
self.verify(manifest, bytes, key_fingerprint)?;
if manifest.source != PluginSource::Official {
return Ok(());
}
if self.official_key_fingerprints.is_empty() {
return Err(PluginTrustError::MissingOfficialKey);
}
let fingerprint = key_fingerprint
.or(manifest.public_key_fingerprint)
.ok_or(PluginTrustError::MissingOfficialKey)?;
let payload = manifest.signing_payload(registration, bytes);
if !verifier.verify(manifest, &payload, fingerprint) {
return Err(PluginTrustError::SignatureNotVerified);
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PluginRevocation {
pub package: &'static str,
pub version: &'static str,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PluginTrustError {
MissingManifest,
InvalidDigest,
DigestMismatch,
MissingSignature,
MissingRevocationList,
MissingOfficialKey,
UntrustedOfficialKey,
SignatureNotVerified,
Revoked,
SignatureLaneRequired,
}
impl fmt::Display for PluginTrustError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::MissingManifest => "plugin artifact has no manifest",
Self::InvalidDigest => "plugin checksum is not a SHA-256 digest",
Self::DigestMismatch => "plugin bytes do not match the manifest checksum",
Self::MissingSignature => "official plugin has no signature payload",
Self::MissingRevocationList => "official plugin has no revocation-list snapshot",
Self::MissingOfficialKey => "official plugin has no signing-key fingerprint",
Self::UntrustedOfficialKey => "official plugin signing key is not trusted",
Self::SignatureNotVerified => "official plugin signature was not verified",
Self::SignatureLaneRequired => {
"signature assurance requires the official lane; use the digest path for other sources"
}
Self::Revoked => "plugin version has been revoked",
})
}
}
impl std::error::Error for PluginTrustError {}
#[cfg(test)]
mod tests {
use super::*;
use crate::registry_core::declaration::{FrameworkId, PluginMode};
#[test]
fn trust_policy_checks_bytes_key_and_revocation() {
const DIGEST: &str = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
const KEY: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
const KEYS: &[&str] = &[KEY];
const REVOKED: &[PluginRevocation] = &[];
let manifest = PluginManifest {
name: "official.canvas",
crate_name: "official_canvas",
version: "1.0.0",
framework: FrameworkId::new("nichlink.default"),
source: PluginSource::Official,
mode: PluginMode::Replacement,
checksum: DIGEST,
signature: Some("adapter-verified-signature"),
public_key_fingerprint: Some(KEY),
revocation_list: Some("official-2026-09"),
};
let policy = PluginTrustPolicy::official(KEYS, REVOKED);
assert_eq!(policy.verify(manifest, b"abc", Some(KEY)), Ok(()));
assert_eq!(
policy.verify(
PluginManifest {
public_key_fingerprint: None,
..manifest
},
b"abc",
None,
),
Err(PluginTrustError::MissingOfficialKey)
);
assert_eq!(
policy.verify(
PluginManifest {
revocation_list: None,
..manifest
},
b"abc",
Some(KEY),
),
Err(PluginTrustError::MissingRevocationList)
);
let revoked = PluginTrustPolicy::official(
KEYS,
&[PluginRevocation {
package: "official.canvas",
version: "1.0.0",
}],
);
assert_eq!(
revoked.verify(manifest, b"abc", Some(KEY)),
Err(PluginTrustError::Revoked)
);
}
struct AcceptingVerifier;
impl PluginSignatureVerifier for AcceptingVerifier {
fn verify(&self, manifest: PluginManifest, payload: &[u8], key_fingerprint: &str) -> bool {
manifest.signature == Some("adapter-verified-signature")
&& !payload.is_empty()
&& key_fingerprint.len() == 64
}
}
fn registration(manifest: PluginManifest) -> crate::RegistrationInfo {
use crate::registry_core::declaration::{
Admission, LocalizedText, ObjectContract, RegistrationRule, SourceLocation,
};
crate::RegistrationInfo {
namespace: "trust-test",
id: crate::NodeId::from_path("trust.rs", "Trust"),
parent: crate::ROOT_NODE_ID,
kind: "Trust",
preset: "",
parts: "",
params: "",
handle: "",
stable_name: None,
name: LocalizedText { zh: "", en: "" },
summary: LocalizedText { zh: "", en: "" },
exports: &[],
needs_registry: false,
registry_name: "",
getting_from_other_registry: None,
registry_rule_path: "",
registry_rule: RegistrationRule::ANY,
admission: Admission::ANY,
requires: &[],
provides: &[],
contract: ObjectContract {
required_parts: &[],
provided_parts: &[],
},
flow: crate::FlowContract::NONE,
flow_provider: None,
handle_traits: &[],
part_traits: &[],
runtime_checks: &[],
plugin: Some(manifest),
source: SourceLocation {
file: "trust.rs",
line: 1,
column: 1,
function: "registration",
},
}
}
#[test]
fn trust_policy_can_delegate_signature_verification() {
const KEY: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let manifest = PluginManifest {
name: "official.canvas",
crate_name: "official_canvas",
version: "1.0.0",
framework: FrameworkId::new("nichlink.default"),
source: PluginSource::Official,
mode: PluginMode::Extension,
checksum: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
signature: Some("adapter-verified-signature"),
public_key_fingerprint: Some(KEY),
revocation_list: Some("official-2026"),
};
let registration = registration(manifest);
let policy = PluginTrustPolicy::official(&[KEY], &[]);
assert_eq!(
policy.verify_with(
manifest,
®istration,
b"abc",
Some(KEY),
&AcceptingVerifier
),
Ok(())
);
assert_eq!(
policy.verify_with(
manifest,
®istration,
b"abc",
Some(KEY),
&RejectingVerifier
),
Err(PluginTrustError::SignatureNotVerified)
);
}
struct RejectingVerifier;
impl PluginSignatureVerifier for RejectingVerifier {
fn verify(&self, _: PluginManifest, _: &[u8], _: &str) -> bool {
false
}
}
}