Initial Dever 0.0.1

This commit is contained in:
Codex committed 2026-11-03 03:32:07 +00:00
commit 81233d098c
1479 files changed
+163782

No files matched your search

+220
View File
@@ -0,0 +1,220 @@
//! 真实签名测试包通过 production prepare 编译与执行,并验证源码移除后的行为。
use super::*;
use crate::compiler::{build, project::Project};
use std::{fs, path::Path, process::Command};
#[path = "build_fixture.rs"]
mod fixture;
struct EmptyStore;
impl Store for EmptyStore {
fn get(&self, _: &str, _: u64) -> Result<Option<Vec<u8>>, String> {
Ok(None)
}
fn put(&self, _: &[u8]) -> Result<(), String> {
panic!("Worker prepare must not write cache")
}
}
fn project(root: &Path) {
for (path, source) in [
("app", "value() (value: Int) { value = port.native() }\n"),
("port", "native() (value: Int) fails dever.api.Error\n"),
("adapter", "external go \"worker/main.go\" {}\n"),
("api", "cmd value = app.value\n"),
] {
fixture::write(root, &format!("module/demo/value/{path}.dever"), source);
}
fixture::write(
root,
"module/demo/value/worker/main.go",
"package main\nimport (\"context\"; _ \"embed\"; \"dever-worker/helper\")\n//go:embed note.txt\nvar note string\nfunc Native(_ context.Context, _ any, _ any) (any, error) { return map[string]any{\"value\":helper.Value()+Target()+int64(len(note))}, nil }\n",
);
fixture::write(
root,
"module/demo/value/worker/helper/value.go",
"package helper\nimport \"dever-worker/helper/internal/value\"\nfunc Value() int64 { return value.Get() }\n",
);
fixture::write(
root,
"module/demo/value/worker/helper/internal/value/value.go",
"package value\nfunc Get() int64 { return 40 }\n",
);
fixture::write(
root,
"module/demo/value/worker/target_linux.go",
"//go:build linux\npackage main\nfunc Target() int64 { return 1 }\n",
);
fixture::write(
root,
"module/demo/value/worker/target_windows.go",
"//go:build windows\npackage main\nfunc Target() int64 { return 99 }\n",
);
fixture::write(root, "module/demo/value/worker/note.txt", "x");
}
#[test]
#[ignore = "explicit dev-Pod Go authoring fixture; no product tool fallback"]
fn signed_go_pack_compiles_reproducibly_and_runs_native_after_source_removal() {
let fixture = fixture::Fixture::new();
let root = fixture.root.join("project");
project(&root);
let project = Project::load(&root).unwrap();
let program = project.check().unwrap();
let external = program.external_workers().next().unwrap();
let target = BuildTarget::host().unwrap();
let metadata = library::runtime::metadata(&fixture.signed, "go", target).unwrap();
let mut worker = Worker::from_external(external).unwrap();
worker.runtime = Some(metadata.pack.clone());
let mut lock = Lock::new(vec![]);
lock.workers.push(worker);
fixture::write(&root, "dever.lock", lock.encode().unwrap());
let prepare = || {
crate::compiler::project::dependency::worker::prepare_with_store(
&root,
&program,
target,
&fixture.signed,
&EmptyStore,
)
};
let files = prepare().unwrap();
let repeated = prepare().unwrap();
assert_eq!(
files
.iter()
.map(|file| (&file.path, &file.bytes, file.executable))
.collect::<Vec<_>>(),
repeated
.iter()
.map(|file| (&file.path, &file.bytes, file.executable))
.collect::<Vec<_>>()
);
assert!(!files.iter().any(|file| file.path.contains("/source/")
|| file.path.contains("/stdlib/")
|| file.path.ends_with("component.go")));
let binary = build::build_with_resources(
&program,
&project.sources,
"main",
&root.join("app"),
target,
&files,
)
.unwrap();
// 工具与 SDK 篡改、闭包缺失和目标被排除都必须在交付 Worker 前失败。
let installation = fixture.root.join("installation");
let release = installation.join("versions/0.0.1");
let signature = release.join("manifest.sig");
let original = fs::read(&signature).unwrap();
fs::write(&signature, "00".repeat(64)).unwrap();
assert!(SignedResources::load(&installation, "0.0.1").is_err());
fs::write(&signature, original).unwrap();
for relative in [
"sdk/go/component.go".into(),
format!("runtime/go/{}/runtime.pack", target.name()),
format!("runtime/go/{}/manifest.json", target.name()),
] {
let path = release.join(relative);
let original = fs::read(&path).unwrap();
let mut changed = original.clone();
let middle = changed.len() / 2;
changed[middle] ^= 1;
fs::write(&path, changed).unwrap();
assert!(
prepare()
.err()
.expect("tampered signed asset was accepted")
.contains("signed resource bytes differ")
);
fs::write(&path, original).unwrap();
let saved = fixture.root.join("missing-asset");
fs::rename(&path, &saved).unwrap();
assert!(prepare().is_err(), "missing asset used a host fallback");
fs::rename(saved, path).unwrap();
}
let main = root.join("module/demo/value/worker/main.go");
let source = fs::read_to_string(&main).unwrap();
fs::write(
&main,
source.replace("dever-worker/helper", "example.com/unlocked"),
)
.unwrap();
assert!(
prepare()
.err()
.expect("unlocked import was accepted")
.contains("locked module graph")
);
fs::write(&main, format!("//go:build windows\n{source}")).unwrap();
assert!(
prepare()
.err()
.expect("excluded entry was accepted")
.contains("excluded by target")
);
fs::write(&main, &source).unwrap();
for import in ["internal/abi", "dever-worker/helper/internal/value"] {
fs::write(
&main,
source.replace("_ \"embed\"", &format!("_ \"embed\"; _ {import:?}")),
)
.unwrap();
assert!(
prepare()
.err()
.expect("private import was accepted")
.contains("Go internal import")
);
}
fs::write(&main, &source).unwrap();
fixture::write(
&root,
"module/demo/value/worker/cgo.go",
"package main\nimport \"C\"\n",
);
assert!(prepare().is_err());
fs::remove_file(root.join("module/demo/value/worker/cgo.go")).unwrap();
let descriptor: Descriptor = serde_json::from_value(fixture.descriptor.clone()).unwrap();
let mut missing = fixture.files.clone();
missing.remove("stdlib/runtime.a");
assert!(descriptor.validate(&missing, target).is_err());
let mut missing = fixture.files.clone();
missing.remove("bin/compile");
assert!(descriptor.validate(&missing, target).is_err());
let mut version = descriptor.clone();
let actual = version.go_version.split_whitespace().nth(2).unwrap();
version.go_version = version.go_version.replace(actual, "go0.0.0");
assert!(
version
.validate(&fixture.files, target)
.unwrap_err()
.contains("archive target/compiler version differs")
);
let mut wrong = fixture.descriptor.clone();
wrong["goarch"] = "unsupported".into();
assert!(
serde_json::from_value::<Descriptor>(wrong)
.unwrap()
.validate(&fixture.files, target)
.is_err()
);
fs::remove_dir_all(root.join("module")).unwrap();
fs::remove_dir_all(fixture.root.join("installation")).unwrap();
for _ in 0..2 {
let output = Command::new(&binary)
.env_clear()
.args(["demo.value.value", "{}"])
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
output.stdout,
b"{\"code\":0,\"message\":\"ok\",\"data\":42}\n"
);
}
}
+220
View File
@@ -0,0 +1,220 @@
//! 只在测试自己的目录中,用 dev Pod 已安装的绝对路径 Go 工具制作签名 fixture。
use super::*;
use ring::signature::{Ed25519KeyPair, KeyPair};
use std::{
fs,
os::unix::fs::{DirBuilderExt, PermissionsExt},
path::{Path, PathBuf},
process::Command,
};
pub struct Fixture {
pub root: PathBuf,
pub signed: SignedResources,
pub files: BTreeMap<String, Vec<u8>>,
pub descriptor: serde_json::Value,
}
pub fn write(root: &Path, path: &str, bytes: impl AsRef<[u8]>) {
let path = root.join(path);
fs::create_dir_all(path.parent().unwrap()).unwrap();
for directory in path
.parent()
.unwrap()
.ancestors()
.take_while(|path| path.starts_with(root))
{
fs::set_permissions(directory, fs::Permissions::from_mode(0o700)).unwrap();
}
fs::write(&path, bytes).unwrap();
fs::set_permissions(path, fs::Permissions::from_mode(0o600)).unwrap();
}
fn go(root: &Path, arguments: &[&str]) -> String {
let result = Command::new("/usr/local/go/bin/go")
.args(arguments)
.current_dir(root)
.env_clear()
.env("GOTOOLCHAIN", "local")
.env("GOPROXY", "off")
.env("GOSUMDB", "off")
.env("CGO_ENABLED", "0")
.env("GO111MODULE", "off")
.env("GOCACHE", root.join("go-cache"))
.output()
.unwrap();
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
String::from_utf8(result.stdout).unwrap()
}
impl Fixture {
pub fn new() -> Self {
let root = std::env::temp_dir().join(format!(
"dever-go-acceptance-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
fs::DirBuilder::new().mode(0o700).create(&root).unwrap();
let version = go(&root, &["version"]);
let target = BuildTarget::host().unwrap();
let arch = if target == BuildTarget::LinuxX86_64 {
"amd64"
} else {
"arm64"
};
let version_number = version
.split_whitespace()
.nth(2)
.unwrap()
.strip_prefix("go")
.unwrap();
write(&root, "selector.go", include_bytes!("selector.go"));
go(
&root,
&["build", "-trimpath", "-o", "selector", "selector.go"],
);
let mut files = BTreeMap::new();
files.insert(
"bin/analyze".into(),
fs::read(root.join("selector")).unwrap(),
);
for tool in ["compile", "link"] {
files.insert(
format!("bin/{tool}"),
fs::read(format!("/usr/local/go/pkg/tool/linux_{arch}/{tool}")).unwrap(),
);
}
let exports = go(
&root,
&[
"list",
"-deps",
"-export",
"-f",
"{{if .Export}}{{.ImportPath}}={{.Export}}{{end}}",
"bytes",
"context",
"encoding/binary",
"encoding/json",
"errors",
"fmt",
"io",
"math",
"os",
"regexp",
"sort",
"strconv",
"strings",
"time",
"unicode/utf8",
"embed",
],
);
let mut cfg = String::new();
for line in exports.lines().filter(|line| !line.is_empty()) {
let (name, source) = line.split_once('=').unwrap();
let path = format!("stdlib/{name}.a");
cfg.push_str(&format!("packagefile {name}={path}\n"));
files.insert(path, fs::read(source).unwrap());
}
files.insert("stdlib/importcfg".into(), cfg.into_bytes());
let descriptor = serde_json::json!({"host":target.name(), "compiler":"bin/compile", "linker":"bin/link", "analyzer":"bin/analyze",
"stdlib_importcfg":"stdlib/importcfg", "goos":"linux", "goarch":arch, "go_version":version.trim()});
files.insert("dever-runtime.json".into(), serde_json::to_vec(&serde_json::json!({"format":"dever-worker-runtime-v1", "ecosystem":"go", "target":target.name(),
"executable":null, "arguments":[], "build":descriptor, "python_markers":null, "python_wheel_tags":[]})).unwrap());
let archive = archive(&files);
let metadata = serde_json::to_vec(&serde_json::json!({"format":"dever-registry-runtime-v1", "runtime":{"pack":{"name":"go", "version":version_number,
"sha256":crate::compiler::project::dependency::digest(&archive)}, "target":target.name(), "python_markers":null, "python_wheel_tags":[]}})).unwrap();
let loader = if arch == "amd64" {
"ld-linux-x86-64.so.2"
} else {
"ld-linux-aarch64.so.1"
};
let loader_source = if arch == "amd64" {
"/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2"
} else {
"/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1"
};
let artifacts = BTreeMap::from([
(
format!("runtime/go/{}/runtime.pack", target.name()),
archive,
),
(
format!("runtime/go/{}/manifest.json", target.name()),
metadata,
),
(
format!("process-runtime/{}/lib/{loader}", target.name()),
fs::read(loader_source).unwrap(),
),
(
"sdk/go/component.go".into(),
fs::read(Path::new(env!("CARGO_MANIFEST_DIR")).join("../sdk/go/component.go"))
.unwrap(),
),
]);
let signed = sign(&root.join("installation"), &artifacts);
Self {
root,
signed,
files,
descriptor,
}
}
}
pub fn archive(files: &BTreeMap<String, Vec<u8>>) -> Vec<u8> {
let gzip = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
let mut tar = tar::Builder::new(gzip);
for (name, bytes) in files {
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar.append_data(&mut header, name, bytes.as_slice())
.unwrap();
}
tar.into_inner().unwrap().finish().unwrap()
}
pub fn sign(root: &Path, artifacts: &BTreeMap<String, Vec<u8>>) -> SignedResources {
let key = Ed25519KeyPair::from_seed_unchecked(&[93; 32]).unwrap();
let hex = |bytes: &[u8]| {
bytes
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()
};
write(
root,
"state/trusted-release-key",
hex(key.public_key().as_ref()),
);
let entries = artifacts.iter().map(|(path, bytes)| {
write(root, &format!("versions/0.0.1/{path}"), bytes);
serde_json::json!({"path":path, "bytes":bytes.len(), "sha256":crate::compiler::project::dependency::digest(bytes)})
}).collect::<Vec<_>>();
let manifest = serde_json::to_vec(&serde_json::json!({"format":"dever-release-v2", "version":"0.0.1", "platform":BuildTarget::host().unwrap().name(), "artifacts":entries, "extensions":[]})).unwrap();
write(root, "versions/0.0.1/manifest.json", &manifest);
write(
root,
"versions/0.0.1/manifest.sig",
hex(key.sign(&manifest).as_ref()),
);
SignedResources::load(root, "0.0.1").unwrap()
}
impl Drop for Fixture {
fn drop(&mut self) {
fs::remove_dir_all(&self.root).unwrap();
}
}
+189
View File
@@ -0,0 +1,189 @@
//! 测试 key 只用于显式注入的 sumdb 对端,不进入产品公钥或项目设置。
use super::*;
use base64::{Engine, engine::general_purpose::STANDARD};
use ring::signature::{Ed25519KeyPair, KeyPair};
use sha2::{Digest, Sha256};
use std::{
cell::RefCell,
io::{Cursor, Write},
};
#[derive(Default)]
pub(super) struct Cache(pub RefCell<BTreeMap<String, Vec<u8>>>);
impl Store for Cache {
fn get(&self, hash: &str, _: u64) -> Result<Option<Vec<u8>>, String> {
Ok(self.0.borrow().get(hash).cloned())
}
fn put(&self, bytes: &[u8]) -> Result<(), String> {
self.0.borrow_mut().insert(
crate::compiler::project::dependency::digest(bytes),
bytes.to_vec(),
);
Ok(())
}
}
#[derive(Clone, Default)]
pub(super) struct Registry(pub BTreeMap<String, Vec<u8>>);
impl Transport for Registry {
fn get(&self, url: &str, limit: usize) -> Result<Vec<u8>, String> {
let bytes = self
.0
.get(url)
.ok_or_else(|| format!("missing owned registry response {url}"))?;
assert!(bytes.len() <= limit);
Ok(bytes.clone())
}
}
pub(super) fn runtime() -> Metadata {
serde_json::from_value(serde_json::json!({"pack":{"name":"go", "version":"1.25.0", "sha256":"0".repeat(64)},
"target":BuildTarget::host().unwrap().name(), "python_markers":null, "python_wheel_tags":[]})).unwrap()
}
pub(super) fn zip(module: &str, version: &str, contents: &str) -> Vec<u8> {
let mut archive = zip::ZipWriter::new(Cursor::new(Vec::new()));
archive
.start_file(
format!("{module}@v{version}/value.go"),
zip::write::SimpleFileOptions::default(),
)
.unwrap();
archive.write_all(contents.as_bytes()).unwrap();
archive.finish().unwrap().into_inner()
}
fn key() -> Ed25519KeyPair {
Ed25519KeyPair::from_seed_unchecked(&[47; 32]).unwrap()
}
fn key_id() -> Vec<u8> {
let mut hash = Sha256::new();
hash.update(b"dever-sumdb-test\n");
hash.update([1]);
hash.update(key().public_key().as_ref());
hash.finalize()[..4].to_vec()
}
pub(super) fn verifier() -> Verifier {
let mut bytes = vec![1];
bytes.extend_from_slice(key().public_key().as_ref());
Verifier::new(&format!(
"dever-sumdb-test+{}+{}",
key_id()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>(),
STANDARD.encode(bytes)
))
.unwrap()
}
fn root(hashes: &[[u8; 32]]) -> [u8; 32] {
if hashes.is_empty() {
return Sha256::digest([]).into();
}
if hashes.len() == 1 {
return hashes[0];
}
let split = hashes.len().next_power_of_two() / 2;
let mut hash = Sha256::new();
hash.update([1]);
hash.update(root(&hashes[..split]));
hash.update(root(&hashes[split..]));
hash.finalize().into()
}
impl Registry {
pub fn module(&mut self, name: &str, version: &str, source: &str) {
let prefix = format!(
"https://proxy.golang.org/{}/@v/v{version}",
module::escape(name)
);
self.0
.insert(format!("{prefix}.mod"), source.as_bytes().to_vec());
self.0.insert(
format!("{prefix}.zip"),
zip(name, version, "package value\n"),
);
}
pub fn seal(&mut self) {
self.0
.retain(|path, _| !path.starts_with("https://sum.golang.org/"));
let records = self
.0
.iter()
.filter_map(|(path, source)| {
let path = path.strip_prefix("https://proxy.golang.org/")?;
let (name, version) = path.split_once("/@v/v")?;
let version = version.strip_suffix(".mod")?;
let archive =
&self.0[&format!("https://proxy.golang.org/{name}/@v/v{version}.zip")];
let text = format!(
"{name} v{version} {}\n{name} v{version}/go.mod {}\n",
sumdb::zip_hash(name, version, archive).unwrap(),
sumdb::mod_hash(source)
);
Some((
format!("https://sum.golang.org/lookup/{name}@v{version}"),
text,
))
})
.collect::<Vec<_>>();
let hashes = records
.iter()
.map(|(_, text)| {
let mut hash = Sha256::new();
hash.update([0]);
hash.update(text);
hash.finalize().into()
})
.collect::<Vec<[u8; 32]>>();
let text = format!(
"go.sum database tree\n{}\n{}\n",
hashes.len(),
STANDARD.encode(root(&hashes))
);
let mut signature = key_id();
signature.extend_from_slice(key().sign(text.as_bytes()).as_ref());
let note = format!(
"{text}\n— dever-sumdb-test {}\n",
STANDARD.encode(signature)
);
self.0.insert(
"https://sum.golang.org/latest".into(),
note.as_bytes().to_vec(),
);
for (id, (path, text)) in records.into_iter().enumerate() {
self.0
.insert(path, format!("{id}\n{text}\n{note}").into_bytes());
}
assert!(hashes.len() < 256);
self.0.insert(
format!("https://sum.golang.org/tile/8/0/000.p/{}", hashes.len()),
hashes.into_iter().flatten().collect(),
);
}
}
pub(super) fn graph() -> Registry {
let mut registry = Registry::default();
for (name, version, source) in [
(
"a",
"1.0.0",
"module example.com/a\nrequire (\nexample.com/b v1.0.0\nexample.com/c v1.0.0\n)\n",
),
("b", "1.0.0", "module example.com/b\n"),
("b", "1.2.0", "module example.com/b\n"),
(
"c",
"1.0.0",
"module example.com/c\nrequire example.com/b v1.2.0\n",
),
] {
registry.module(&format!("example.com/{name}"), version, source);
}
registry.seal();
registry
}
+333
View File
@@ -0,0 +1,333 @@
//! Go 模块协议验收只使用拥有的传输和临时摘要缓存。
use super::*;
mod fixture;
mod proofs;
#[test]
fn independent_worker_graphs_merge_without_erasing_previous_modules() {
let mut registry = fixture::graph();
registry.module(
"example.com/z",
"1.0.0",
"module example.com/z\nrequire example.com/b v1.0.0\n",
);
registry.seal();
let cache = fixture::Cache::default();
let mut lock = Lock::new(vec![]);
for root in ["go:example.com/a@1.0.0", "go:example.com/z@1.0.0"] {
resolve_with(
&[root.parse().unwrap()],
BuildTarget::host().unwrap(),
&fixture::runtime(),
&registry,
&cache,
&mut lock,
&fixture::verifier(),
)
.unwrap();
}
assert_eq!(lock.libs.len(), 5);
assert_eq!(
lock.libs
.iter()
.filter(|lib| lib.spec.name == "example.com/b")
.count(),
2
);
validate_with(&lock, &fixture::verifier()).unwrap();
for (name, version) in [("a", "1.2.0"), ("z", "1.0.0")] {
let closure = selected(
&lock.libs,
&[format!("go:example.com/{name}@1.0.0").parse().unwrap()],
)
.unwrap();
assert_eq!(
closure
.iter()
.find(|lib| lib.spec.name == "example.com/b")
.unwrap()
.spec
.version,
version
);
}
}
#[test]
fn resolves_authenticated_mvs_graph_and_replays_locked_hashes() {
let registry = fixture::graph();
let cache = fixture::Cache::default();
let mut lock = Lock::new(vec![]);
let runtime = fixture::runtime();
resolve_with(
&["go:example.com/a@1.0.0".parse().unwrap()],
BuildTarget::host().unwrap(),
&runtime,
&registry,
&cache,
&mut lock,
&fixture::verifier(),
)
.unwrap();
assert_eq!(lock.libs.len(), 4);
assert_eq!(
selected(&lock.libs, &["go:example.com/a@1.0.0".parse().unwrap()])
.unwrap()
.into_iter()
.find(|lib| lib.spec.name == "example.com/b")
.unwrap()
.spec
.version,
"1.2.0"
);
let records = evidence(&lock).unwrap();
assert!(
records[0]
.records
.iter()
.any(|record| record.module == "example.com/b" && record.version == "1.0.0")
);
let checked = validate_with(&lock, &fixture::verifier()).unwrap();
for lib in &lock.libs {
let artifact = &lib.artifacts[0];
let bytes = cache
.get(&artifact.sha256, artifact.bytes)
.unwrap()
.unwrap();
checked
.verify_zip(&lib.spec.name, &lib.spec.version, &bytes)
.unwrap();
assert!(
checked
.verify_zip(&lib.spec.name, &lib.spec.version, &bytes[..bytes.len() / 2])
.is_err()
);
}
assert!(
validate_lock(&lock).is_err(),
"fixture signing key must never become a product trust root"
);
lock.libs[0].dependencies.clear();
assert!(validate_with(&lock, &fixture::verifier()).is_err());
}
#[test]
fn mvs_keeps_requirements_from_unselected_versions() {
let mut registry = fixture::Registry::default();
for (name, version, requires) in [
(
"a",
"1.0.0",
"require example.com/b v1.0.0\nrequire example.com/c v1.0.0\n",
),
("b", "1.0.0", "require example.com/d v1.2.0\n"),
("b", "1.1.0", "require example.com/d v1.0.0\n"),
(
"c",
"1.0.0",
"require example.com/b v1.1.0\nrequire example.com/d v1.0.0\n",
),
("d", "1.0.0", ""),
("d", "1.2.0", ""),
] {
registry.module(
&format!("example.com/{name}"),
version,
&format!("module example.com/{name}\ngo 1.16\n{requires}"),
);
}
registry.seal();
let roots = ["go:example.com/a@1.0.0".parse().unwrap()];
let mut lock = Lock::new(vec![]);
resolve_with(
&roots,
BuildTarget::host().unwrap(),
&fixture::runtime(),
&registry,
&fixture::Cache::default(),
&mut lock,
&fixture::verifier(),
)
.unwrap();
assert_eq!(lock.libs.len(), 6);
let versions = selected(&lock.libs, &roots)
.unwrap()
.into_iter()
.map(|lib| (lib.spec.name.as_str(), lib.spec.version.as_str()))
.collect::<BTreeMap<_, _>>();
assert_eq!(versions["example.com/b"], "1.1.0");
assert_eq!(versions["example.com/d"], "1.2.0");
let older = lock
.libs
.iter_mut()
.find(|lib| lib.spec.name == "example.com/b" && lib.spec.version == "1.0.0")
.unwrap();
older.dependencies[0].spec.version = "1.0.0".into();
assert!(validate_with(&lock, &fixture::verifier()).is_err());
}
#[test]
fn shared_module_keeps_distinct_worker_selections() {
let mut registry = fixture::Registry::default();
for (name, version, requires) in [
("x", "1.0.0", "require example.com/d v1.0.0\n"),
("y", "1.0.0", "require example.com/d v1.1.0\n"),
("d", "1.0.0", ""),
("d", "1.1.0", ""),
] {
registry.module(
&format!("example.com/{name}"),
version,
&format!("module example.com/{name}\n{requires}"),
);
}
registry.seal();
let first = vec!["go:example.com/x@1.0.0".parse().unwrap()];
let second = vec![
"go:example.com/x@1.0.0".parse().unwrap(),
"go:example.com/y@1.0.0".parse().unwrap(),
];
let mut lock = Lock::new(vec![]);
for roots in [&first, &second] {
resolve_with(
roots,
BuildTarget::host().unwrap(),
&fixture::runtime(),
&registry,
&fixture::Cache::default(),
&mut lock,
&fixture::verifier(),
)
.unwrap();
}
let restored: Lock = serde_json::from_str(&lock.encode().unwrap()).unwrap();
validate_with(&restored, &fixture::verifier()).unwrap();
assert_eq!(restored.libs.len(), 4);
for (roots, expected) in [(&first, "1.0.0"), (&second, "1.1.0")] {
let closure = selected(&restored.libs, roots).unwrap();
assert_eq!(
closure
.iter()
.filter(|lib| lib.spec.name == "example.com/d")
.count(),
1
);
assert_eq!(
closure
.iter()
.find(|lib| lib.spec.name == "example.com/d")
.unwrap()
.spec
.version,
expected
);
}
}
#[test]
fn rejected_metadata_archive_or_rollback_never_publishes_lock() {
let registry = fixture::graph();
let runtime = fixture::runtime();
for suffix in ["mod", "zip"] {
let mut changed = registry.clone();
changed
.0
.get_mut(&format!(
"https://proxy.golang.org/example.com/b/@v/v1.0.0.{suffix}"
))
.unwrap()
.extend(b"changed");
if suffix == "zip" {
changed.0.insert(
"https://proxy.golang.org/example.com/b/@v/v1.2.0.zip".into(),
fixture::zip("example.com/b", "1.2.0", "changed"),
);
}
let cache = fixture::Cache::default();
let mut lock = Lock::new(vec![]);
let before = lock.encode().unwrap();
assert!(
resolve_with(
&["go:example.com/a@1.0.0".parse().unwrap()],
BuildTarget::host().unwrap(),
&runtime,
&changed,
&cache,
&mut lock,
&fixture::verifier()
)
.is_err()
);
assert_eq!(lock.encode().unwrap(), before);
assert!(cache.0.borrow().is_empty());
}
let cache = fixture::Cache::default();
let mut lock = Lock::new(vec![]);
let mut first = fixture::Registry::default();
first.module("example.com/a", "1.0.0", "module example.com/a\n");
first.seal();
let roots = ["go:example.com/a@1.0.0".parse().unwrap()];
resolve_with(
&roots,
BuildTarget::host().unwrap(),
&runtime,
&first,
&cache,
&mut lock,
&fixture::verifier(),
)
.unwrap();
let mut next = first.clone();
next.module("example.com/z", "1.0.0", "module example.com/z\n");
next.seal();
resolve_with(
&roots,
BuildTarget::host().unwrap(),
&runtime,
&next,
&cache,
&mut lock,
&fixture::verifier(),
)
.unwrap();
assert_eq!(lock.go_sumdb.len(), 2);
let before = lock.encode().unwrap();
let error = resolve_with(
&roots,
BuildTarget::host().unwrap(),
&runtime,
&first,
&cache,
&mut lock,
&fixture::verifier(),
)
.unwrap_err();
assert!(error.contains("rollback"), "{error}");
assert_eq!(lock.encode().unwrap(), before);
}
#[test]
fn module_directives_check_identity_toolchain_and_requirements() {
let source = b"module example.com/a\ngo 1.25\nrequire (\nexample.com/b v1.2.0 // indirect\n)\n";
let parsed = module::requirements(source, "example.com/a", "1.25.0").unwrap();
assert_eq!(parsed["example.com/b"], "1.2.0");
assert!(module::requirements(source, "example.com/other", "1.25.0").is_err());
assert!(module::requirements(source, "example.com/a", "1.24.0").is_err());
for directive in [
"replace example.com/b => ../local",
"exclude example.com/b v1.0.0",
"require (\nexample.com/b v1.0.0",
"module example.com/a",
] {
assert!(
module::requirements(
format!("module example.com/a\n{directive}\n").as_bytes(),
"example.com/a",
"1.25.0"
)
.is_err()
);
}
assert_eq!(module::escape("example.com/Mixed"), "example.com/!mixed");
}
+74
View File
@@ -0,0 +1,74 @@
//! 中间归档只检查文件属性;读取计数与异常文件覆盖不启动 Go 工具。
use super::*;
use std::os::unix::fs::symlink;
fn stage() -> Stage {
let root = std::env::temp_dir().join(format!(
"dever-go-output-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
fs::DirBuilder::new().mode(0o700).create(&root).unwrap();
Stage {
root,
resources: BTreeMap::new(),
written: 0,
started: Instant::now(),
}
}
#[test]
fn go_intermediate_output_checks_metadata_without_reading_archive() {
let stage = stage();
let path = stage.root.join("package.a");
let length = 8 * 1024 * 1024;
fs::File::create(&path).unwrap().set_len(length).unwrap();
let read_chars = || {
fs::read_to_string("/proc/thread-self/io")
.unwrap()
.lines()
.find_map(|line| line.strip_prefix("rchar: "))
.unwrap()
.parse::<u64>()
.unwrap()
};
let before = read_chars();
drop(stage.output(&path).unwrap());
let consumed = read_chars() - before;
eprintln!("go intermediate: fixture=sparse-zero bytes={length} rchar={consumed}");
assert!(
consumed < 4096,
"read {consumed} bytes for a metadata check"
);
assert_eq!(fs::metadata(path).unwrap().len(), length);
}
#[test]
fn go_output_rejects_links_empty_special_and_oversized_files() {
let stage = stage();
let path = stage.root.join("output");
fs::write(&path, b"archive").unwrap();
drop(stage.output(&path).unwrap());
let link = stage.root.join("symbolic");
symlink(&path, &link).unwrap();
assert!(stage.output(&link).is_err());
fs::remove_file(link).unwrap();
let link = stage.root.join("hard");
fs::hard_link(&path, &link).unwrap();
assert!(stage.output(&path).is_err());
fs::remove_file(link).unwrap();
assert!(stage.output(&stage.root).is_err());
fs::File::create(&path).unwrap();
assert!(stage.output(&path).is_err());
fs::File::create(&path)
.unwrap()
.set_len(128 * 1024 * 1024 + 1)
.unwrap();
assert!(stage.output(&path).is_err());
fs::remove_file(&path).unwrap();
assert!(stage.output(&path).is_err());
}
+74
View File
@@ -0,0 +1,74 @@
//! 官方历史签名向量与目录哈希检查,独立于自签测试对端。
use super::*;
use base64::{Engine, engine::general_purpose::STANDARD};
use std::io::{Cursor, Write};
#[test]
fn official_inclusion_and_consistency_vectors_reject_each_tampered_hash() {
let evidence: Evidence = serde_json::from_str(include_str!("sumdb_uuid.json")).unwrap();
let verified = evidence.verify(&Verifier::official()).unwrap();
assert!(verified.contains("github.com/google/uuid", "1.6.0"));
assert!(evidence.verify(&fixture::verifier()).is_err());
assert_eq!(
sumdb::mod_hash(b"module github.com/google/uuid\n"),
"h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo="
);
for index in 0..evidence.records[0].inclusion.len() {
let mut changed = evidence.clone();
changed.records[0].inclusion[index] = STANDARD.encode([0; 32]);
assert!(changed.verify(&Verifier::official()).is_err());
}
for index in 0..evidence.consistency.len() {
let mut changed = evidence.clone();
changed.consistency[index] = STANDARD.encode([0; 32]);
assert!(changed.verify(&Verifier::official()).is_err());
}
let mut changed = evidence.clone();
changed.records[0].go_mod.push_str("// tampered\n");
assert!(changed.verify(&Verifier::official()).is_err());
let mut changed = evidence.clone();
changed.checkpoint = changed.checkpoint.replace("go.sum", "no.sum");
assert!(changed.verify(&Verifier::official()).is_err());
}
#[test]
fn hashzip_tracks_directory_names_but_not_compression_or_order() {
let archive = |reverse: bool, directory: bool| {
let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
let mut names = vec!["value.go", "LICENSE"];
if reverse {
names.reverse();
}
for name in names {
zip.start_file(
format!("example.com/Mixed@v1.0.0/{name}"),
zip::write::SimpleFileOptions::default().compression_method(if reverse {
zip::CompressionMethod::Deflated
} else {
zip::CompressionMethod::Stored
}),
)
.unwrap();
zip.write_all(b"same bytes").unwrap();
}
if directory {
zip.add_directory(
"example.com/Mixed@v1.0.0/empty/",
zip::write::SimpleFileOptions::default(),
)
.unwrap();
}
zip.finish().unwrap().into_inner()
};
let first = sumdb::zip_hash("example.com/Mixed", "1.0.0", &archive(false, false)).unwrap();
assert_eq!(
first,
sumdb::zip_hash("example.com/Mixed", "1.0.0", &archive(true, false)).unwrap()
);
assert_ne!(
first,
sumdb::zip_hash("example.com/Mixed", "1.0.0", &archive(false, true)).unwrap()
);
assert!(sumdb::zip_hash("example.com/mixed", "1.0.0", &archive(false, false)).is_err());
}
+54
View File
@@ -0,0 +1,54 @@
// 测试 pack 的固定目标源码选择器;产品只运行签名 pack 提供的对应工具。
package main
import (
"encoding/json"
"fmt"
"go/build"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
)
func main() {
if err := selectPackage(); err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
}
func selectPackage() error {
if len(os.Args) != 4 { return fmt.Errorf("expected OS architecture directory") }
context := build.Default
context.GOOS, context.GOARCH = os.Args[1], os.Args[2]
context.GOROOT, context.GOPATH = "", ""
context.CgoEnabled = true
directory := os.Args[3]
selected, err := context.ImportDir(directory, 0)
if err != nil { return err }
if len(selected.CgoFiles)+len(selected.CFiles)+len(selected.CXXFiles)+len(selected.SFiles)+len(selected.SysoFiles) != 0 {
return fmt.Errorf("cgo/assembly/native objects are unsupported")
}
embed := map[string][]string{}
for _, pattern := range selected.EmbedPatterns {
glob := strings.TrimPrefix(pattern, "all:")
if filepath.IsAbs(glob) || strings.Contains(glob, "..") { return fmt.Errorf("invalid embed pattern") }
matches, err := filepath.Glob(filepath.Join(directory, glob))
if err != nil || len(matches) == 0 { return fmt.Errorf("unmatched embed pattern") }
for _, match := range matches {
err = filepath.WalkDir(match, func(path string, entry fs.DirEntry, failure error) error {
if failure != nil { return failure }
if entry.Type()&os.ModeSymlink != 0 { return fmt.Errorf("embed link") }
if entry.IsDir() { return nil }
relative, err := filepath.Rel(directory, path)
if err != nil { return err }
embed[pattern] = append(embed[pattern], filepath.ToSlash(relative))
return nil
})
if err != nil { return err }
}
sort.Strings(embed[pattern])
}
imports := selected.Imports
if imports == nil { imports = []string{} }
return json.NewEncoder(os.Stdout).Encode(map[string]any{"name":selected.Name,"files":selected.GoFiles,"imports":imports,"embed":embed})
}
+68
View File
@@ -0,0 +1,68 @@
{
"previous": "go.sum database tree\n63512409\nF7eHRPqN/cqPtqti14NKdGGaI1Giw8coWP3KxihFZbg=\n\n— sum.golang.org Az3gri1OBHKT87Xj8UXU8zjoHGy/X+7Nl94f8TZkELVaasolmGApUsWqwVG9vE6hgqYb/ph/w2vdkLIVRH7bMB/FPQ4=\n",
"checkpoint": "go.sum database tree\n66321505\nnL/ZK4zIq2TWUhMi/cDJQRq6hXj67qFBbyEqxks3epY=\n\n— sum.golang.org Az3gruvaP9rIy6foXnttmnViaXM55mqUcP66AeP7N5r+tpFo2ll40wDr7sN0guXk6rXp0fL63fe30pchMCTqmbRo+Ak=\n",
"consistency": [
"3OyjS9XS6r41pxaFWGnW/WSmX7b1ik3nupquDNipdZ8=",
"zX5IsB02+IutT35ka/MwY7gFiPSAlihfnt2cu6smUjw=",
"Qc0CqO89A+/onGz1u4ZsDoMOfRIgPZpayRGTETOv8kk=",
"JhdrRO/y8Iu+MDeUupFG0uM4+odAADMjVH0mOgLOA/c=",
"lOgywJvTdrBWol0uphM6ZulvAebwJGNlekm8MIwiwKM=",
"FRf0N51Fw+8eSLHEa/e/AsnDway0EkKo4jI0E1Ne7dk=",
"Ak0gyC+RS6/ahVBCBuSBDA1wFN9mgq3acIbYm9uJHQ4=",
"FFJ8WrKTTQgCjTuAl5CukBdjDaQ7opSdHBpNSolRYWw=",
"rdNcd+MUjUxzlX/nlVGeQP5BkkZyTT89sn/yfg0AXQ0=",
"gJzOh5y5rP1BILUrFJw+aGjod840PErDnjy4T0BoUE4=",
"7I8BPI+ADxeJmu19mQxO+xt6bNhg3Ueq1j2dIfOppwA=",
"urhcUF9QOyTRAYRjJjsRHjHdwIc+BxfiQ6a+NuYeC6E=",
"p9eGEUHXeKLFnrIPB7VXHHMuwtaBnvHG4CYwgC0wq+w=",
"Gcry0ScaNfFmQujPMT95RQ3rbzKDSynQsL546C8CkfU=",
"dIpym+JRT/+z9/tlhpuIRyv+uoWGqgND/MHw0f1MtqA=",
"cQrKLJIBbPoWDnRnbGfAEMfpsau0h+Aw3aqDOJ5LAVU=",
"C46761Ui7jL+lf4vRXBlveoxuFS6tykAd6mMEfNttPE=",
"45Kz4HcZ9iZ84ySMHCy21d4vd2MvibvfNMzYm0hKYRs=",
"4JZTJ+4VUtLMTEs11jn4GmCxoz56Lm5HDwkYmkmAWlw=",
"2hJV4h1jZ2YujjyXUw+7YYCwLXGYZy+ondhRUgxKmXI=",
"lrn/kRfgYdYSDxgPdSQ8/GnZw6u/joKu9++AR+Saa5M=",
"hnm9+kaHQdhHWrAcyrvY5fbv8QB+GwVjRMFeobtTg8A=",
"rPFmKL/vD6Edsk/0DmpvDa/nMOqWgjfpPGRjBUL9yKs=",
"2IzbyezmwwGZZPJ276ejAQZiAnar+RcjAUU3MEMtbIA=",
"k3kfr9AECmkV+UA3/D6EMqaBDr67aubL5hXGsvJ+Nng=",
"/WnQtddEqG03mbcU1XID1iws1nlKHVkJcaREWknCOiE=",
"xJ20xXM4QItt5vxFOtolZ5l18p7ej1+RimNkgWmoRFM="
],
"records": [{
"module": "github.com/google/uuid",
"version": "1.6.0",
"go_mod": "module github.com/google/uuid\n",
"lookup": "22152757\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\n\ngo.sum database tree\n66321505\nnL/ZK4zIq2TWUhMi/cDJQRq6hXj67qFBbyEqxks3epY=\n\n— sum.golang.org Az3gruvaP9rIy6foXnttmnViaXM55mqUcP66AeP7N5r+tpFo2ll40wDr7sN0guXk6rXp0fL63fe30pchMCTqmbRo+Ak=\n",
"inclusion": [
"vYU6vRfhcMyd5/U04n+AYg52mm3BQrjwyN+27saiSnU=",
"syWiyNbj6gybIdLKoAFd7xF3qNltS7OhQjtp4y8rCFg=",
"Zqpa9zJsOg/sI/mv0UkXO9d7gvPHLLjMAKnZCPzeiNM=",
"tGbcXa1AQlYBIm5gM2KDy8JvdAFDizj4lUop18Eehxs=",
"/w9FG1Sae/omKxZdOvj719sT1rCow3tlP8VNo0fDQtE=",
"dR6Bk+gh/hyJx7bu65HfhQdfuvT/OdVv/G7RUsOc26Y=",
"fRC86/2mWWtFWNPz/Vou6f7hdyvRLHFPo9KaOlVOVmc=",
"jJS2b3XVP2sst4CS5srpnF34agHpiluU63h6UQyFPDc=",
"cOL2+zAXTEOaAiyvmCrL3Ned6soAg08CJJJ2VW3+a18=",
"In5NzPUzIAZfV6bnUL11Bkm4Aqoit/ZG7gJ8nxOdB2w=",
"r+8IH0m7Ig3BS+uwCVVG1CZT0I/Rro3bbSUVYcSrN2Q=",
"wOgTamt00bdokE1Nbv3aHYu2um06V1xdLFEmOBjt0GI=",
"21woobbbCvogV/ZXACdAozfVAFjRv5xMID/iLcryHzw=",
"5+o2gn9/zVod+MYr4UuoeMzuz68vt/hA5+9H4YZaVME=",
"040B+RrLQeFCJhPU69kzoKr4BTqhSwdrnYEVhknZ+I0=",
"ioI6UByPBEr2lEZB0dI6pCXgcNss6UkKxm+TYAADujg=",
"HOJ0dLK4hnl0tFeu1tmT7TRrNKRIbi/T8KI/2s4DXDs=",
"BBcHtB4mS19jHff/LQTNI/+WCVBZ1Up52gNqyoln71U=",
"Oz/OlF1Lj7wkqqE15p324ZSX1XHcnLzDSMKNtUx6rwY=",
"ofpmTAcvFvNzlwjZnsrMqeG4iIBCP2lt5wIusvZ2gG4=",
"IRpVS0aap6n0qb02jyOy6ikkOPxdFIQtWwckaGF2MAQ=",
"liRusDUG3TanNZLGkfjb58ppX/6d40M7X2s3wptjxMw=",
"ROjUzjXHDN0Yo/I6EeqHhpszFgIs7FCABVeQniliPTk=",
"NTjIv8CJqIIGrOHhNQWSP9FV6XEPL6uGA4lmMfNKGJg=",
"bwqUQG2wJXGxo1QLA2CFb3chH17xvlqXe6eQy43p0HY=",
"bqa+4xGvzGcI/PYne2fXfUvdULgy8o3zQmD13q/gu1Y="
],
"consistency": []
}]
}