gunnar_server/
registry.rs1use anyhow::{Context, Result};
34
35use crate::stores::ServerStores;
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum Refusal {
40 NotAttested,
42 ProvenanceForged(String),
44 NotLogged,
46 NotUnderASignedHead { index: usize, size: usize },
48 LogRewritten { signed: String, actual: String },
50}
51
52impl std::fmt::Display for Refusal {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 Refusal::NotAttested => write!(
56 f,
57 "no build record claims to have produced this artifact. The registry does not \
58 take bytes on trust: build it through a builder that attests."
59 ),
60 Refusal::ProvenanceForged(why) => write!(
61 f,
62 "a build record names this artifact and does not verify: {why}"
63 ),
64 Refusal::NotLogged => write!(
65 f,
66 "the build record is not in the transparency log, so nobody outside this \
67 server could ever check it"
68 ),
69 Refusal::NotUnderASignedHead { index, size } => write!(
70 f,
71 "the build record is record {index} of {size} in the log and no signed tree \
72 head covers it. An inclusion proof with nothing signed over it is arithmetic \
73 this server did about itself. Run `gunnar attest head --builds`."
74 ),
75 Refusal::LogRewritten { signed, actual } => write!(
76 f,
77 "THE BUILD LOG HAS BEEN REWRITTEN: a signed head swore to {signed} and the \
78 records now hash to {actual}"
79 ),
80 }
81 }
82}
83
84#[derive(Debug, Clone)]
86pub struct Admitted {
87 pub record: gunnar_core::BuildRecord,
89 pub index: usize,
91 pub head: gunnar_core::TreeHead,
93}
94
95pub fn admit(stores: &ServerStores, artifact_sha256: &str) -> Result<Result<Admitted, Refusal>> {
102 let builds = stores.build_log();
103 let builder_key = stores.provenance_key()?;
104 let log_key = stores.transparency_key()?;
105
106 let signed: Vec<gunnar_core::SignedBuild> = builds
107 .lines_of()?
108 .iter()
109 .map(|line| serde_json::from_str(line).context("decoding a build record"))
110 .collect::<Result<_>>()?;
111
112 let Some(index) = signed
113 .iter()
114 .position(|build| build.record.produced(artifact_sha256))
115 else {
116 return Ok(Err(Refusal::NotAttested));
117 };
118 let build = &signed[index];
119
120 if let Err(problem) = gunnar_core::provenance::verify_build(build, builder_key.public_key()) {
121 return Ok(Err(Refusal::ProvenanceForged(problem.to_string())));
122 }
123
124 let Some(inclusion) = builds.prove(index)? else {
125 return Ok(Err(Refusal::NotLogged));
126 };
127 let heads = builds.heads()?;
128 let covering = heads.iter().rev().find(|head| {
129 head.head.size as usize >= inclusion.size
130 && gunnar_core::transparency::verify_tree_head(head, log_key.public_key()).is_ok()
131 });
132 let Some(head) = covering else {
133 return Ok(Err(Refusal::NotUnderASignedHead {
134 index: inclusion.index,
135 size: inclusion.size,
136 }));
137 };
138 if head.head.root != inclusion.root {
139 return Ok(Err(Refusal::LogRewritten {
140 signed: head.head.root.clone(),
141 actual: inclusion.root,
142 }));
143 }
144
145 Ok(Ok(Admitted {
146 record: build.record.clone(),
147 index,
148 head: head.head.clone(),
149 }))
150}
151
152#[cfg(feature = "http")]
155mod wire {
156 use super::*;
157
158 pub struct StoreRegistry {
166 stores: std::sync::Arc<ServerStores>,
167 registry: crate::oci::Registry,
168 }
169
170 impl StoreRegistry {
171 pub fn new(stores: std::sync::Arc<ServerStores>) -> StoreRegistry {
173 let registry = crate::oci::Registry::under(stores.root());
174 StoreRegistry { stores, registry }
175 }
176 }
177
178 impl gunnar_http::registry::RegistryBackend for StoreRegistry {
179 fn blob(&self, digest: &str) -> std::result::Result<Option<Vec<u8>>, String> {
180 self.registry
181 .get_blob(digest)
182 .map_err(|err| format!("{err:#}"))
183 }
184
185 fn manifest(
186 &self,
187 name: &str,
188 reference: &str,
189 ) -> std::result::Result<Option<(String, Vec<u8>)>, String> {
190 let Some(digest) = self
191 .registry
192 .resolve(name, reference)
193 .map_err(|err| format!("{err:#}"))?
194 else {
195 return Ok(None);
196 };
197 let Some(bytes) = self
198 .registry
199 .get_blob(&digest)
200 .map_err(|err| format!("{err:#}"))?
201 else {
202 tracing::warn!(
207 name,
208 reference,
209 %digest,
210 "a tag resolves to a manifest that is not in the blob store"
211 );
212 return Ok(None);
213 };
214 Ok(Some((digest, bytes)))
215 }
216
217 fn put_blob(&self, bytes: &[u8]) -> std::result::Result<String, String> {
218 self.registry
219 .put_blob(bytes)
220 .map_err(|err| format!("{err:#}"))
221 }
222
223 fn put_manifest(
224 &self,
225 name: &str,
226 reference: &str,
227 bytes: &[u8],
228 ) -> std::result::Result<std::result::Result<String, String>, String> {
229 match self
230 .registry
231 .put_manifest(&self.stores, name, reference, bytes)
232 .map_err(|err| format!("{err:#}"))?
233 {
234 Ok(landed) => Ok(Ok(landed.digest)),
235 Err(refusal) => Ok(Err(format!("{refusal}"))),
238 }
239 }
240 }
241}
242
243#[cfg(feature = "http")]
244pub use wire::StoreRegistry;