use std::collections::BTreeMap;
use std::fmt::Debug;
use std::sync::Mutex;
use async_trait::async_trait;
use crate::core::{DOMAIN_MANIFEST, Digest, KeyId, KeySignature, Signer, Verifier, signing_hash};
use super::{Manifest, ManifestError};
#[derive(Debug, thiserror::Error)]
pub enum RegistryError {
#[error("no manifest '{name}' at version '{version}'")]
NotFound { name: String, version: String },
#[error(
"manifest '{name}' version '{version}' is already published as {existing} \
and cannot be replaced by {offered} — publish a new version"
)]
Immutable {
name: String,
version: String,
existing: String,
offered: String,
},
#[error(
"manifest '{name}' version '{version}' resolved to {actual}, not the \
pinned {expected} — the registry served content this caller did not review"
)]
PinBroken {
name: String,
version: String,
expected: String,
actual: String,
},
#[error("stored manifest '{name}' at '{version}' is unusable: {source}")]
Corrupt {
name: String,
version: String,
#[source]
source: ManifestError,
},
#[error(
"manifest '{name}' version '{version}' is unsigned, and this resolve required a signature"
)]
Unsigned { name: String, version: String },
#[error(
"manifest '{name}' version '{version}' carries a signature that '{key_id}' did not \
make — the content is intact, so it was republished by somebody who could compute \
a digest but not sign it"
)]
BadSignature {
name: String,
version: String,
key_id: String,
},
#[error(
"manifest '{name}' version '{version}' is already attributed to '{existing}', not \
'{offered}' — publisher evidence is immutable; publish a new version"
)]
PublisherChanged {
name: String,
version: String,
existing: String,
offered: String,
},
#[error("registry storage: {0}")]
Backend(String),
}
#[async_trait]
pub trait Registry: Send + Sync + Debug {
async fn publish(&self, manifest: &Manifest) -> Result<Digest, RegistryError>;
async fn publish_signed(
&self,
manifest: &Manifest,
signer: &dyn Signer,
) -> Result<Digest, RegistryError>;
async fn resolve_verified(
&self,
name: &str,
version: &str,
verifier: &dyn Verifier,
) -> Result<(Manifest, KeyId), RegistryError>;
async fn resolve(&self, name: &str, version: &str) -> Result<Manifest, RegistryError>;
async fn resolve_pinned(
&self,
name: &str,
version: &str,
expected: Digest,
) -> Result<Manifest, RegistryError> {
let m = self.resolve(name, version).await?;
let actual = m.digest().map_err(|e| RegistryError::Corrupt {
name: name.to_owned(),
version: version.to_owned(),
source: e,
})?;
if actual == expected {
Ok(m)
} else {
Err(RegistryError::PinBroken {
name: name.to_owned(),
version: version.to_owned(),
expected: expected.to_hex(),
actual: actual.to_hex(),
})
}
}
async fn versions(&self, name: &str) -> Result<Vec<String>, RegistryError>;
async fn names(&self) -> Result<Vec<String>, RegistryError>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PublishVerdict {
Insert,
AdoptSignature,
Unchanged,
}
pub fn decide_publish(
name: &str,
version: &str,
offered: Digest,
offered_signature: Option<&KeySignature>,
stored: Option<(Digest, Option<&KeySignature>)>,
) -> Result<PublishVerdict, RegistryError> {
let Some((existing, recorded)) = stored else {
return Ok(PublishVerdict::Insert);
};
if existing != offered {
return Err(RegistryError::Immutable {
name: name.to_owned(),
version: version.to_owned(),
existing: existing.to_hex(),
offered: offered.to_hex(),
});
}
match (recorded, offered_signature) {
(None, Some(_)) => Ok(PublishVerdict::AdoptSignature),
(Some(recorded), Some(offered)) if recorded.key_id != offered.key_id => {
Err(RegistryError::PublisherChanged {
name: name.to_owned(),
version: version.to_owned(),
existing: recorded.key_id.clone(),
offered: offered.key_id.clone(),
})
}
_ => Ok(PublishVerdict::Unchanged),
}
}
pub fn sign_manifest(
manifest: &Manifest,
signer: &dyn Signer,
) -> Result<(Digest, KeySignature), RegistryError> {
let digest = manifest.digest().map_err(|e| RegistryError::Corrupt {
name: manifest.metadata.name.clone(),
version: manifest.metadata.version.clone(),
source: e,
})?;
Ok((
digest,
signer.signature_over(&signing_hash(DOMAIN_MANIFEST, &digest)),
))
}
pub fn check_signature(
name: &str,
version: &str,
manifest: &Manifest,
signature: Option<&KeySignature>,
verifier: &dyn Verifier,
) -> Result<KeyId, RegistryError> {
let Some(a) = signature else {
return Err(RegistryError::Unsigned {
name: name.to_owned(),
version: version.to_owned(),
});
};
let digest = manifest.digest().map_err(|e| RegistryError::Corrupt {
name: name.to_owned(),
version: version.to_owned(),
source: e,
})?;
if verifier.verify(
&a.key_id,
&signing_hash(DOMAIN_MANIFEST, &digest),
&a.signature,
) {
Ok(a.key_id.clone())
} else {
Err(RegistryError::BadSignature {
name: name.to_owned(),
version: version.to_owned(),
key_id: a.key_id.clone(),
})
}
}
pub fn reparse(name: &str, version: &str, yaml: &str) -> Result<Manifest, RegistryError> {
Manifest::parse(yaml).map_err(|source| RegistryError::Corrupt {
name: name.to_owned(),
version: version.to_owned(),
source,
})
}
pub fn to_yaml(manifest: &Manifest) -> Result<String, RegistryError> {
serde_yaml_ng::to_string(manifest).map_err(|e| RegistryError::Backend(e.to_string()))
}
#[derive(Debug, Clone)]
struct Entry {
digest: Digest,
yaml: String,
signature: Option<KeySignature>,
}
#[derive(Debug, Default)]
pub struct MemoryRegistry {
entries: Mutex<BTreeMap<(String, String), Entry>>,
}
impl MemoryRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
fn insert(
&self,
manifest: &Manifest,
signature: Option<KeySignature>,
) -> Result<Digest, RegistryError> {
let (name, version) = (
manifest.metadata.name.clone(),
manifest.metadata.version.clone(),
);
let digest = manifest.digest().map_err(|e| RegistryError::Corrupt {
name: name.clone(),
version: version.clone(),
source: e,
})?;
let yaml = to_yaml(manifest)?;
let mut entries = self
.entries
.lock()
.map_err(|_| RegistryError::Backend("registry mutex poisoned".into()))?;
let key = (name.clone(), version.clone());
let stored = entries.get(&key).map(|e| (e.digest, e.signature.as_ref()));
match decide_publish(&name, &version, digest, signature.as_ref(), stored)? {
PublishVerdict::Insert => {
entries.insert(
key,
Entry {
digest,
yaml,
signature,
},
);
}
PublishVerdict::AdoptSignature => {
if let Some(entry) = entries.get_mut(&key) {
entry.signature = signature;
}
}
PublishVerdict::Unchanged => {}
}
Ok(digest)
}
}
#[async_trait]
impl Registry for MemoryRegistry {
async fn publish(&self, manifest: &Manifest) -> Result<Digest, RegistryError> {
self.insert(manifest, None)
}
async fn publish_signed(
&self,
manifest: &Manifest,
signer: &dyn Signer,
) -> Result<Digest, RegistryError> {
let (_, signature) = sign_manifest(manifest, signer)?;
self.insert(manifest, Some(signature))
}
async fn resolve_verified(
&self,
name: &str,
version: &str,
verifier: &dyn Verifier,
) -> Result<(Manifest, KeyId), RegistryError> {
let manifest = self.resolve(name, version).await?;
let signature = {
let entries = self
.entries
.lock()
.map_err(|_| RegistryError::Backend("registry mutex poisoned".into()))?;
entries
.get(&(name.to_owned(), version.to_owned()))
.and_then(|e| e.signature.clone())
};
let key = check_signature(name, version, &manifest, signature.as_ref(), verifier)?;
Ok((manifest, key))
}
async fn resolve(&self, name: &str, version: &str) -> Result<Manifest, RegistryError> {
let yaml = {
let entries = self
.entries
.lock()
.map_err(|_| RegistryError::Backend("registry mutex poisoned".into()))?;
entries
.get(&(name.to_owned(), version.to_owned()))
.map(|e| e.yaml.clone())
.ok_or_else(|| RegistryError::NotFound {
name: name.to_owned(),
version: version.to_owned(),
})?
};
reparse(name, version, &yaml)
}
async fn versions(&self, name: &str) -> Result<Vec<String>, RegistryError> {
let entries = self
.entries
.lock()
.map_err(|_| RegistryError::Backend("registry mutex poisoned".into()))?;
Ok(entries
.range((name.to_owned(), String::new())..)
.take_while(|((n, _), _)| n == name)
.map(|((_, v), _)| v.clone())
.collect())
}
async fn names(&self) -> Result<Vec<String>, RegistryError> {
let entries = self
.entries
.lock()
.map_err(|_| RegistryError::Backend("registry mutex poisoned".into()))?;
let mut names: Vec<String> = entries.keys().map(|(n, _)| n.clone()).collect();
names.dedup();
Ok(names)
}
}