use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::install::VerifyError;
use crate::layer::Line;
use crate::verify::{dsse_sign_typed, dsse_verify_typed};
pub const LINE_INDEX_PAYLOAD_TYPE: &str = "application/vnd.pulseengine.varve.line-index.v1+json";
pub const LINE_INDEX_ARTIFACT_TYPE: &str = LINE_INDEX_PAYLOAD_TYPE;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IndexedLayer {
pub layer: String,
pub digest: String,
pub channel: String,
pub counter: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LineIndex {
pub line: String,
pub counter: u64,
#[serde(rename = "issued-at")]
pub issued_at: String,
pub layers: Vec<IndexedLayer>,
}
#[derive(Debug, thiserror::Error)]
pub enum IndexError {
#[error(transparent)]
Verify(#[from] VerifyError),
#[error("line-index payload is not valid: {0}")]
Payload(String),
#[error(
"refusing stale line-index for {line}: presented counter {presented}, cached {cached} — \
a withdrawn or superseded index cannot be replayed over a newer one"
)]
Stale {
line: String,
presented: u64,
cached: u64,
},
#[error(
"the realm's signed index for {line} names layer {layer} ({digest}), which this source \
does not serve. A source that hides a layer is either compromised or stale; every \
layer it DOES serve still verifies, which is exactly why this check exists. Use a \
different source, or obtain a newer signed index."
)]
Omitted {
line: String,
layer: String,
digest: String,
},
#[error(
"realm '{realm}' declares that it publishes a signed line index, but none was found for \
{line}. Either the source is not serving it, or the realm's declaration is wrong — \
varve will not fall back to an unauthenticated listing for a realm that promised one."
)]
Missing { realm: String, line: String },
#[error("line-index document is for line {document}, not {expected}")]
WrongLine { document: String, expected: String },
#[error("io error at {path}: {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
}
impl LineIndex {
pub fn verify_and_parse(envelope: &[u8], root_public_key: &[u8]) -> Result<Self, IndexError> {
let payload = dsse_verify_typed(envelope, LINE_INDEX_PAYLOAD_TYPE, root_public_key)?;
serde_json::from_slice(&payload).map_err(|e| IndexError::Payload(e.to_string()))
}
pub fn sign(&self, secret_key: &[u8], key_id: &str) -> Result<String, IndexError> {
let payload = serde_json::to_vec_pretty(self).expect("index serializes");
Ok(dsse_sign_typed(
&payload,
LINE_INDEX_PAYLOAD_TYPE,
secret_key,
key_id,
)?)
}
pub fn line(&self) -> Result<Line, IndexError> {
self.line
.parse()
.map_err(|e: crate::layer::LayerIdError| IndexError::Payload(e.to_string()))
}
pub fn refuse_omission(&self, served: &[String]) -> Result<(), IndexError> {
for entry in &self.layers {
if !served.iter().any(|s| s == &entry.layer) {
return Err(IndexError::Omitted {
line: self.line.clone(),
layer: entry.layer.clone(),
digest: entry.digest.clone(),
});
}
}
Ok(())
}
pub fn high_water(&self) -> Option<u64> {
self.layers.iter().map(|e| e.counter).max()
}
pub fn refuse_regression(&self, cached: Option<&LineIndex>) -> Result<(), IndexError> {
if let Some(prev) = cached
&& prev.line == self.line
&& self.counter < prev.counter
{
return Err(IndexError::Stale {
line: self.line.clone(),
presented: self.counter,
cached: prev.counter,
});
}
Ok(())
}
pub fn by_layer(&self) -> BTreeMap<&str, &str> {
self.layers
.iter()
.map(|e| (e.layer.as_str(), e.digest.as_str()))
.collect()
}
}
#[derive(Debug, Clone, Copy)]
pub struct IndexPolicy<'a> {
pub realm: &'a str,
pub root_public_key: &'a [u8],
pub required: bool,
}
pub fn check(
line: &str,
envelope: Option<&[u8]>,
served: Option<&[String]>,
cached: Option<&LineIndex>,
policy: &IndexPolicy<'_>,
) -> Result<Option<LineIndex>, IndexError> {
let Some(bytes) = envelope else {
if policy.required {
return Err(IndexError::Missing {
realm: policy.realm.to_string(),
line: line.to_string(),
});
}
return Ok(None);
};
let index = LineIndex::verify_and_parse(bytes, policy.root_public_key)?;
if index.line != line {
return Err(IndexError::WrongLine {
document: index.line.clone(),
expected: line.to_string(),
});
}
index.refuse_regression(cached)?;
if let Some(served) = served {
index.refuse_omission(served)?;
}
Ok(Some(index))
}
pub const ANN_INDEX_LINE: &str = "eu.pulseengine.varve.index-line";
pub const LINE_INDEX_TAG_PREFIX: &str = "line-index-";
pub fn index_tag(line: &str) -> String {
format!("{LINE_INDEX_TAG_PREFIX}{line}")
}
pub fn attach_to_layout(layout: &Path, line: &str, envelope: &[u8]) -> Result<(), IndexError> {
let io = |path: &Path, source: std::io::Error| IndexError::Io {
path: path.display().to_string(),
source,
};
let digest = crate::store::manifest_digest(envelope);
let hex = digest.strip_prefix("sha256:").expect("digest shape");
let blob_dir = layout.join("blobs").join("sha256");
std::fs::create_dir_all(&blob_dir).map_err(|e| io(&blob_dir, e))?;
let blob_path = blob_dir.join(hex);
std::fs::write(&blob_path, envelope).map_err(|e| io(&blob_path, e))?;
let index_path = layout.join("index.json");
let mut index: serde_json::Value =
serde_json::from_slice(&std::fs::read(&index_path).map_err(|e| io(&index_path, e))?)
.map_err(|e| IndexError::Payload(format!("index.json: {e}")))?;
let entries = index["manifests"]
.as_array_mut()
.ok_or_else(|| IndexError::Payload("index.json has no manifests array".into()))?;
entries.retain(|e| {
!(e["artifactType"] == LINE_INDEX_ARTIFACT_TYPE
&& e["annotations"][ANN_INDEX_LINE] == *line)
});
entries.push(serde_json::json!({
"mediaType": "application/json",
"artifactType": LINE_INDEX_ARTIFACT_TYPE,
"digest": digest,
"size": envelope.len(),
"annotations": { ANN_INDEX_LINE: line }
}));
std::fs::write(
&index_path,
serde_json::to_vec_pretty(&index).expect("index serializes"),
)
.map_err(|e| io(&index_path, e))?;
Ok(())
}
pub fn attach_envelope_to_layout(
layout: &Path,
envelope: &[u8],
) -> Result<(String, u64), IndexError> {
let doc = parse_unverified(envelope)?;
let line: Line = doc.line.parse().map_err(|e: crate::layer::LayerIdError| {
IndexError::Payload(format!("index line '{}': {e}", doc.line))
})?;
let line = line.to_string();
if let Some(existing) = read_from_layout(layout, &line)? {
let prev = parse_unverified(&existing)?;
doc.refuse_regression(Some(&prev))?;
}
if let Some(layout_line) = crate::linestatus::layout_line(layout)
&& layout_line != line
{
return Err(IndexError::WrongLine {
document: line,
expected: layout_line,
});
}
attach_to_layout(layout, &line, envelope)?;
Ok((line, doc.counter))
}
pub fn read_from_layout(layout: &Path, line: &str) -> Result<Option<Vec<u8>>, IndexError> {
let index_path = layout.join("index.json");
let bytes = match std::fs::read(&index_path) {
Ok(bytes) => bytes,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(source) => {
return Err(IndexError::Io {
path: index_path.display().to_string(),
source,
});
}
};
let index: serde_json::Value = serde_json::from_slice(&bytes)
.map_err(|e| IndexError::Payload(format!("index.json: {e}")))?;
let Some(entry) = index["manifests"].as_array().and_then(|entries| {
entries.iter().find(|e| {
e["artifactType"] == LINE_INDEX_ARTIFACT_TYPE
&& e["annotations"][ANN_INDEX_LINE] == *line
})
}) else {
return Ok(None);
};
let digest = entry["digest"]
.as_str()
.ok_or_else(|| IndexError::Payload("index entry has no digest".into()))?;
let hex = digest.strip_prefix("sha256:").unwrap_or(digest);
let blob_path = layout.join("blobs").join("sha256").join(hex);
std::fs::read(&blob_path)
.map(Some)
.map_err(|source| IndexError::Io {
path: blob_path.display().to_string(),
source,
})
}
fn parse_unverified(envelope: &[u8]) -> Result<LineIndex, IndexError> {
let text = std::str::from_utf8(envelope)
.map_err(|e| IndexError::Payload(format!("envelope is not utf-8: {e}")))?;
let env = wsc::dsse::DsseEnvelope::from_json(text)
.map_err(|e| IndexError::Payload(format!("not a DSSE envelope: {e}")))?;
let payload = env
.payload_bytes()
.map_err(|e| IndexError::Payload(format!("envelope payload: {e}")))?;
serde_json::from_slice(&payload)
.map_err(|e| IndexError::Payload(format!("index document: {e}")))
}
#[derive(Debug)]
pub struct IndexCache {
dir: PathBuf,
}
impl IndexCache {
pub fn at_root(root: &Path) -> Self {
IndexCache {
dir: root.join("state").join("line-index"),
}
}
pub fn load(&self, line: &str) -> Result<Option<LineIndex>, IndexError> {
let path = self.path(line);
match std::fs::read(&path) {
Ok(bytes) => Ok(Some(parse_unverified(&bytes)?)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(source) => Err(IndexError::Io {
path: path.display().to_string(),
source,
}),
}
}
pub fn update(
&self,
line: &str,
envelope: &[u8],
parsed: &LineIndex,
) -> Result<(), IndexError> {
parsed.refuse_regression(self.load(line)?.as_ref())?;
let io = |path: &Path, source: std::io::Error| IndexError::Io {
path: path.display().to_string(),
source,
};
std::fs::create_dir_all(&self.dir).map_err(|e| io(&self.dir, e))?;
let path = self.path(line);
std::fs::write(&path, envelope).map_err(|e| io(&path, e))?;
Ok(())
}
fn path(&self, line: &str) -> PathBuf {
self.dir.join(format!("{line}.dsse.json"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::verify::generate_root_keypair;
fn index(counter: u64, layers: &[(&str, &str)]) -> LineIndex {
LineIndex {
line: "2026.08".into(),
counter,
issued_at: "2026-08-18T00:00:00Z".into(),
layers: layers
.iter()
.enumerate()
.map(|(i, (l, d))| IndexedLayer {
layer: (*l).into(),
digest: (*d).into(),
channel: "qualified".into(),
counter: (i as u64) + 1,
})
.collect(),
}
}
#[test]
fn an_index_verifies_only_against_the_realm_that_signed_it() {
let (sk, pk) = generate_root_keypair();
let (_other_sk, other_pk) = generate_root_keypair();
let doc = index(1, &[("2026.08.0", "sha256:aa"), ("2026.08.1", "sha256:bb")]);
let envelope = doc.sign(&sk, "root-1").unwrap();
assert_eq!(
LineIndex::verify_and_parse(envelope.as_bytes(), &pk).unwrap(),
doc
);
assert!(LineIndex::verify_and_parse(envelope.as_bytes(), &other_pk).is_err());
}
#[test]
fn a_signed_line_status_cannot_be_replayed_as_an_index() {
let (sk, pk) = generate_root_keypair();
let status = crate::linestatus::LineStatus {
line: "2026.08".into(),
counter: 9,
issued_at: "2026-08-18T00:00:00Z".into(),
support_until: None,
yanked: Default::default(),
known_problems: Vec::new(),
};
let envelope = status.sign(&sk, "root-1").unwrap();
match LineIndex::verify_and_parse(envelope.as_bytes(), &pk) {
Err(IndexError::Verify(_)) => {}
Err(IndexError::Payload(p)) => panic!(
"rejected by the SCHEMA ({p}), not by the payload type — the type is the \
defence against cross-document replay and must be what fails"
),
Ok(_) => panic!("a line-status must not verify as a line-index"),
Err(other) => panic!("expected a payload-type rejection, got {other}"),
}
let idx = index(1, &[("2026.08.0", "sha256:aa")]);
let idx_env = idx.sign(&sk, "root-1").unwrap();
assert!(
crate::linestatus::LineStatus::verify_and_parse(idx_env.as_bytes(), &pk).is_err(),
"a line-index must not verify as a line-status"
);
}
#[test]
fn a_source_that_hides_a_layer_the_index_names_is_refused() {
let doc = index(1, &[("2026.08.0", "sha256:aa"), ("2026.08.1", "sha256:bb")]);
let err = doc
.refuse_omission(&["2026.08.0".to_string()])
.expect_err("hiding a layer must be refused");
match &err {
IndexError::Omitted { layer, digest, .. } => {
assert_eq!(layer, "2026.08.1");
assert_eq!(digest, "sha256:bb", "name the digest, so it can be sought");
}
other => panic!("expected Omitted, got {other}"),
}
let msg = err.to_string();
assert!(msg.contains("2026.08.1"), "names the hidden layer: {msg}");
assert!(
msg.contains("still verifies"),
"says WHY per-artifact verification did not catch this: {msg}"
);
assert!(
doc.refuse_omission(&[
"2026.08.0".to_string(),
"2026.08.1".to_string(),
"2026.08.2".to_string(),
])
.is_ok()
);
}
#[test]
fn the_high_water_mark_comes_from_the_index_not_from_what_was_served() {
let doc = index(
1,
&[
("2026.08.0", "sha256:aa"),
("2026.08.2", "sha256:cc"),
("2026.08.1", "sha256:bb"),
],
);
assert_eq!(
doc.high_water(),
Some(3),
"the greatest counter the REALM asserts, regardless of the order \
entries appear in the document or of what any source served"
);
assert_eq!(index(1, &[]).high_water(), None);
}
#[test]
fn a_stale_index_cannot_replace_a_newer_one() {
let newer = index(7, &[("2026.08.1", "sha256:bb")]);
let older = index(3, &[("2026.08.0", "sha256:aa")]);
let err = older
.refuse_regression(Some(&newer))
.expect_err("a lower counter must be refused");
assert!(matches!(
err,
IndexError::Stale {
presented: 3,
cached: 7,
..
}
));
let msg = err.to_string();
assert!(msg.contains('3') && msg.contains('7'), "names both: {msg}");
assert!(newer.refuse_regression(Some(&newer)).is_ok());
assert!(newer.refuse_regression(Some(&older)).is_ok());
assert!(older.refuse_regression(None).is_ok());
}
#[test]
fn an_index_for_another_line_is_not_silently_accepted() {
let doc = index(1, &[("2026.08.0", "sha256:aa")]);
let other = LineIndex {
line: "2026.09".into(),
..index(1, &[("2026.09.0", "sha256:zz")])
};
assert!(doc.refuse_regression(Some(&other)).is_ok());
assert_eq!(doc.line().unwrap().to_string(), "2026.08");
}
#[test]
fn a_realm_that_promised_an_index_does_not_fall_back_to_an_unsigned_listing() {
let (_sk, pk) = generate_root_keypair();
let declaring = IndexPolicy {
realm: "acme",
root_public_key: &pk,
required: true,
};
let silent = IndexPolicy {
required: false,
..declaring
};
let err = check("2026.08", None, None, None, &declaring)
.expect_err("a declaring realm must not accept a missing index");
assert!(matches!(err, IndexError::Missing { .. }));
let msg = err.to_string();
assert!(msg.contains("acme"), "names the realm: {msg}");
assert!(
msg.contains("will not fall back"),
"says what it refused to do, not merely that something is absent: {msg}"
);
assert!(
check("2026.08", None, None, None, &silent)
.unwrap()
.is_none()
);
}
#[test]
fn an_index_for_a_different_line_cannot_satisfy_this_line() {
let (sk, pk) = generate_root_keypair();
let policy = IndexPolicy {
realm: "acme",
root_public_key: &pk,
required: true,
};
let quiet = LineIndex {
line: "2026.01".into(),
..index(1, &[])
};
let envelope = quiet.sign(&sk, "k").unwrap();
let err = check(
"2026.08",
Some(envelope.as_bytes()),
Some(&["2026.08.0".to_string()]),
None,
&policy,
)
.expect_err("an index for another line must not satisfy this one");
assert!(matches!(
err,
IndexError::WrongLine { ref document, ref expected }
if document == "2026.01" && expected == "2026.08"
));
}
#[test]
fn a_source_that_cannot_enumerate_is_not_treated_as_hiding_everything() {
let (sk, pk) = generate_root_keypair();
let policy = IndexPolicy {
realm: "acme",
root_public_key: &pk,
required: true,
};
let doc = index(1, &[("2026.08.0", "sha256:aa")]);
let envelope = doc.sign(&sk, "k").unwrap();
let ok = check("2026.08", Some(envelope.as_bytes()), None, None, &policy)
.expect("a source that cannot enumerate is not evidence of hiding");
assert_eq!(ok.unwrap().counter, 1);
assert!(matches!(
check(
"2026.08",
Some(envelope.as_bytes()),
Some(&[]),
None,
&policy
),
Err(IndexError::Omitted { .. })
));
}
#[test]
fn check_refuses_a_stale_index_not_only_refuse_regression_does() {
let (sk, pk) = generate_root_keypair();
let policy = IndexPolicy {
realm: "acme",
root_public_key: &pk,
required: true,
};
let cached = index(7, &[("2026.08.1", "sha256:bb")]);
let stale = index(3, &[("2026.08.0", "sha256:aa")]);
let envelope = stale.sign(&sk, "k").unwrap();
let err = check(
"2026.08",
Some(envelope.as_bytes()),
None,
Some(&cached),
&policy,
)
.expect_err("a replayed older index must be refused by the path install uses");
assert!(matches!(
err,
IndexError::Stale {
presented: 3,
cached: 7,
..
}
));
let fresher = index(8, &[("2026.08.1", "sha256:bb")]);
let ok = fresher.sign(&sk, "k").unwrap();
assert_eq!(
check("2026.08", Some(ok.as_bytes()), None, Some(&cached), &policy)
.unwrap()
.unwrap()
.counter,
8
);
}
fn layout_for(layer: &str, sk: &[u8]) -> (tempfile::TempDir, std::path::PathBuf) {
let tmp = tempfile::tempdir().unwrap();
let dest = tmp.path().join("layout");
let payload = crate::manifest::fixtures::manifest_with_tools(
layer,
"qualified",
1,
"2026-08-18T00:00:00Z",
&[],
);
let envelope = crate::verify::sign_layer_manifest(&payload, sk, "root-1").unwrap();
crate::archive::write_oci_layout(
&payload,
envelope.as_bytes(),
&[],
layer,
"qualified",
None,
&dest,
)
.unwrap();
(tmp, dest)
}
#[test]
fn an_index_attached_to_a_layout_is_what_an_offline_install_reads_back() {
use crate::source::LayerSource;
let (sk, _pk) = generate_root_keypair();
let (_tmp, layout) = layout_for("2026.08.0", &sk);
let envelope = index(4, &[("2026.08.0", "sha256:aa")])
.sign(&sk, "root-1")
.unwrap();
attach_to_layout(&layout, "2026.08", envelope.as_bytes()).unwrap();
let source = crate::archive::OciLayoutSource::at(&layout);
assert_eq!(
source.fetch_line_index("2026.08").unwrap().as_deref(),
Some(envelope.as_bytes()),
"the layout source must hand back the attached index verbatim"
);
assert_eq!(source.fetch_line_index("2026.09").unwrap(), None);
let (_t2, bare) = layout_for("2026.08.0", &sk);
assert_eq!(
crate::archive::OciLayoutSource::at(&bare)
.fetch_line_index("2026.08")
.unwrap(),
None
);
let status = crate::linestatus::LineStatus {
line: "2026.08".into(),
counter: 1,
issued_at: "2026-08-18T00:00:00Z".into(),
support_until: None,
yanked: Default::default(),
known_problems: Vec::new(),
}
.sign(&sk, "root-1")
.unwrap();
crate::linestatus::attach_to_layout(
&layout,
&"2026.08".parse().unwrap(),
status.as_bytes(),
)
.unwrap();
assert_eq!(
source.fetch_line_index("2026.08").unwrap().as_deref(),
Some(envelope.as_bytes()),
"attaching a status must not displace the index"
);
assert_eq!(
crate::linestatus::read_from_layout(&layout, &"2026.08".parse().unwrap())
.unwrap()
.as_deref(),
Some(status.as_bytes()),
"…and the index must not be handed back as the status either"
);
let newer = index(5, &[("2026.08.0", "sha256:aa")])
.sign(&sk, "root-1")
.unwrap();
attach_to_layout(&layout, "2026.08", newer.as_bytes()).unwrap();
let json: serde_json::Value =
serde_json::from_slice(&std::fs::read(layout.join("index.json")).unwrap()).unwrap();
assert_eq!(
json["manifests"]
.as_array()
.unwrap()
.iter()
.filter(|e| e["artifactType"] == LINE_INDEX_ARTIFACT_TYPE)
.count(),
1,
"one index per line, replaced in place"
);
assert_eq!(
source.fetch_line_index("2026.08").unwrap().as_deref(),
Some(newer.as_bytes())
);
}
#[test]
fn a_producer_cannot_downgrade_or_misfile_a_published_index() {
let (sk, _pk) = generate_root_keypair();
let (_tmp, layout) = layout_for("2026.08.0", &sk);
let newer = index(7, &[("2026.08.0", "sha256:aa")])
.sign(&sk, "root-1")
.unwrap();
let (line, counter) = attach_envelope_to_layout(&layout, newer.as_bytes()).unwrap();
assert_eq!((line.as_str(), counter), ("2026.08", 7));
let older = index(3, &[("2026.08.0", "sha256:aa")])
.sign(&sk, "root-1")
.unwrap();
let err = attach_envelope_to_layout(&layout, older.as_bytes())
.expect_err("a producer must not publish an index older than the layout's");
assert!(
matches!(
err,
IndexError::Stale {
presented: 3,
cached: 7,
..
}
),
"got: {err}"
);
assert_eq!(
read_from_layout(&layout, "2026.08").unwrap().as_deref(),
Some(newer.as_bytes())
);
let foreign = LineIndex {
line: "2099.01".into(),
..index(1, &[])
}
.sign(&sk, "root-1")
.unwrap();
let err = attach_envelope_to_layout(&layout, foreign.as_bytes())
.expect_err("a 2099.01 index does not belong on a 2026.08 layout");
assert!(
matches!(err, IndexError::WrongLine { ref document, ref expected }
if document == "2099.01" && expected == "2026.08"),
"got: {err}"
);
let nonsense = LineIndex {
line: "twenty-twenty-six".into(),
..index(1, &[])
}
.sign(&sk, "root-1")
.unwrap();
assert!(attach_envelope_to_layout(&layout, nonsense.as_bytes()).is_err());
}
#[test]
fn the_cache_is_what_gives_clause_two_something_to_compare_against() {
let tmp = tempfile::tempdir().unwrap();
let cache = IndexCache::at_root(tmp.path());
assert_eq!(
cache.load("2026.08").unwrap(),
None,
"nothing accepted yet is None, not an empty index — an empty index \
asserts that the line contains nothing"
);
let (sk, _pk) = generate_root_keypair();
let seven = index(7, &[("2026.08.1", "sha256:bb")]);
cache
.update("2026.08", seven.sign(&sk, "k").unwrap().as_bytes(), &seven)
.unwrap();
assert_eq!(cache.load("2026.08").unwrap().unwrap().counter, 7);
let three = index(3, &[("2026.08.0", "sha256:aa")]);
let err = cache
.update("2026.08", three.sign(&sk, "k").unwrap().as_bytes(), &three)
.expect_err("the cache must not accept a regression");
assert!(matches!(err, IndexError::Stale { .. }), "got: {err}");
assert_eq!(cache.load("2026.08").unwrap().unwrap().counter, 7);
cache
.update("2026.08", seven.sign(&sk, "k").unwrap().as_bytes(), &seven)
.unwrap();
let eight = index(8, &[("2026.08.1", "sha256:bb")]);
cache
.update("2026.08", eight.sign(&sk, "k").unwrap().as_bytes(), &eight)
.unwrap();
assert_eq!(cache.load("2026.08").unwrap().unwrap().counter, 8);
let other = LineIndex {
line: "2026.09".into(),
..index(1, &[])
};
cache
.update("2026.09", other.sign(&sk, "k").unwrap().as_bytes(), &other)
.expect("a low counter on a DIFFERENT line is not a regression");
assert_eq!(cache.load("2026.08").unwrap().unwrap().counter, 8);
}
#[test]
fn the_index_tag_cannot_be_mistaken_for_a_layer_of_the_line() {
assert_eq!(index_tag("2026.08"), "line-index-2026.08");
assert!(
index_tag("2026.08")
.parse::<crate::layer::LayerId>()
.is_err()
);
assert!(index_tag("2026.08").starts_with(LINE_INDEX_TAG_PREFIX));
}
#[test]
fn the_source_never_gets_to_vouch_for_its_own_index() {
let (_realm_sk, realm_pk) = generate_root_keypair();
let (impostor_sk, _impostor_pk) = generate_root_keypair();
let policy = IndexPolicy {
realm: "acme",
root_public_key: &realm_pk,
required: true,
};
let forged = index(99, &[("2026.08.0", "sha256:aa")])
.sign(&impostor_sk, "not-the-realm")
.unwrap();
assert!(matches!(
check("2026.08", Some(forged.as_bytes()), None, None, &policy),
Err(IndexError::Verify(_))
));
}
}