use std::collections::BTreeMap;
use std::fmt::Debug;
use std::sync::Mutex;
use async_trait::async_trait;
use crate::core::{Attestation, DOMAIN_MANIFEST, Digest, KeyId, 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>;
}
#[derive(Debug, Clone)]
struct Entry {
digest: Digest,
yaml: String,
attestation: Option<Attestation>,
}
#[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,
attestation: Option<Attestation>,
) -> 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 = serde_yaml_ng::to_string(manifest)
.map_err(|e| RegistryError::Backend(e.to_string()))?;
let mut entries = self
.entries
.lock()
.map_err(|_| RegistryError::Backend("registry mutex poisoned".into()))?;
match entries.get_mut(&(name.clone(), version.clone())) {
Some(existing) if existing.digest == digest => {
match (&existing.attestation, attestation) {
(None, Some(signed)) => existing.attestation = Some(signed),
(Some(recorded), Some(offered)) if recorded.key_id != offered.key_id => {
return Err(RegistryError::PublisherChanged {
name,
version,
existing: recorded.key_id.clone(),
offered: offered.key_id,
});
}
_ => {}
}
Ok(digest)
}
Some(existing) => Err(RegistryError::Immutable {
name,
version,
existing: existing.digest.to_hex(),
offered: digest.to_hex(),
}),
None => {
entries.insert(
(name, version),
Entry {
digest,
yaml,
attestation,
},
);
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 digest = manifest.digest().map_err(|e| RegistryError::Corrupt {
name: manifest.metadata.name.clone(),
version: manifest.metadata.version.clone(),
source: e,
})?;
let attestation = signer.attest(&signing_hash(DOMAIN_MANIFEST, &digest));
self.insert(manifest, Some(attestation))
}
async fn resolve_verified(
&self,
name: &str,
version: &str,
verifier: &dyn Verifier,
) -> Result<(Manifest, KeyId), RegistryError> {
let manifest = self.resolve(name, version).await?;
let attestation = {
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.attestation.clone())
};
let Some(a) = attestation 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((manifest, a.key_id))
} else {
Err(RegistryError::BadSignature {
name: name.to_owned(),
version: version.to_owned(),
key_id: a.key_id.clone(),
})
}
}
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(),
})?
};
Manifest::parse(&yaml).map_err(|e| RegistryError::Corrupt {
name: name.to_owned(),
version: version.to_owned(),
source: e,
})
}
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())
}
}