Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/scheduled.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ jobs:
#
# No `-fork`: in fork mode libFuzzer counts an out-of-memory input or a timeout and keeps
# going, so the run stays green, and those are the inputs this job exists to find.
# `-timeout` is set because libFuzzer's default (1,200 s per input) never fires in a 60 s
# run; `-rss_limit_mb` states its default.
fuzz:
runs-on: ubuntu-26.04
# Every target × a 60 s bounded run + `cmin`, plus a from-source cargo-fuzz install and
Expand Down Expand Up @@ -96,7 +98,7 @@ jobs:
echo "fuzzing ${#targets[@]} targets: ${targets[*]}"
for target in "${targets[@]}"; do
echo "::group::fuzz $target"
cargo +nightly fuzz run "$target" -- -max_total_time=60
cargo +nightly fuzz run "$target" -- -max_total_time=60 -timeout=20 -rss_limit_mb=2048
# Minimize the (possibly grown) corpus in place so the checked-in seed
# set stays small and non-redundant for the next run.
cargo +nightly fuzz cmin "$target"
Expand Down
13 changes: 6 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -580,10 +580,8 @@ inside the `release` meta-leg are marked.
RSS/open-fd/thread plateau; `scripts/soak/crash-loop.sh` `kill -9`s the node and asserts
the store converges on restart. Local: run those scripts directly.
- **`advisories`** — re-runs the RustSec scan against the shipped lockfile and the
isolated fuzz lockfile; fails on a freshly-disclosed advisory. Local: `ci-local.sh deny`
covers the workspace lock. The fuzz lock is a local gap: scan it by hand with
`cargo deny --locked --manifest-path crates/core/fuzz/Cargo.toml check advisories` after
refreshing it.
isolated fuzz lockfile; fails on a freshly-disclosed advisory. Local: `ci-local.sh
supply-chain` scans both (`deny` the workspace lock, `deny_fuzz` the fuzz lock).
- **`vendored-sync`** — two questions, one tree. `scripts/vendored.sh check` asserts every
vendored conformance file is byte-identical to its pinned commit. Because that commit is
an immutable SHA it stays green however far upstream moves, so `scripts/vendored.sh
Expand Down Expand Up @@ -938,9 +936,10 @@ Then:
tag. A `v0.x` tag and any suffixed tag publish as pre-releases and do not move
`:latest`; only a stable tag does.

Update the version strings in the prose too. `README.md`, `docs/deployment.md`,
`docs/gdi-dataset-tool.md`, `docs/operating.md` and `docs/testing.md` name the current
release, and nothing checks them: `git grep 'v1\.0\.0-rc\.1'` finds the lot.
Update the version strings in the prose too. `README.md` and several docs name the
current release, with and without the `v`, and nothing checks them. With `old` set to
the version step 1 replaced, `git grep -nF "$old" -- ':!*Cargo.lock' ':!THIRD-PARTY-LICENSES.md'`
finds the lot.
5. **What the tag enforces.** `release.yml` gates `publish` and `image` behind `gate`
(`ci-local.sh rust supply-chain`), `version-guard`, and the changelog assembly, and
`ci.yml` also runs on the tag. If anything is red, nothing publishes: fix and re-tag.
Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,8 @@ arrow-select = { version = "=60.0.0", default-features = false }
sha2 = "=0.11.0"
# crypt4gh codec: the pure-Rust RustCrypto stack, so nothing here needs `unsafe` or C.
x25519-dalek = { version = "=3.0.0", default-features = false, features = ["static_secrets", "zeroize", "getrandom"] }
chacha20poly1305 = { version = "=0.11.0", default-features = false, features = ["alloc"] }
blake2 = { version = "=0.11.0", default-features = false }
chacha20poly1305 = { version = "=0.11.0", default-features = false, features = ["alloc", "zeroize"] }
blake2 = { version = "=0.11.0", default-features = false, features = ["zeroize"] }
getrandom = { version = "=0.4.3", default-features = false }
zeroize = { version = "=1.9.0", default-features = false, features = ["alloc"] }
tempfile = "=3.27.0"
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,10 @@ R=https://raw.githubusercontent.com/GenomicDataInfrastructure/gdi-node-standalon
mkdir -p ~/gdi-demo && cd ~/gdi-demo
N=gdi-node-standalone-$V-x86_64-unknown-linux-gnu
T=gdi-dataset-tool-$V-x86_64-unknown-linux-gnu
curl -fsSLO "$U/$N" -O "$U/$T" -O "$U/SHA256SUMS"
curl --fail-early -fsSLO "$U/$N" -O "$U/$T" -O "$U/SHA256SUMS"
sha256sum -c SHA256SUMS --ignore-missing # both must print OK
mv "$N" gdi-node-standalone && mv "$T" gdi-dataset-tool
curl -fsSLO "$R/covid-package.yaml" -O "$R/COVID.monogneic.aggregate.AFs.GRCh38.vcf"
curl --fail-early -fsSLO "$R/covid-package.yaml" -O "$R/COVID.monogneic.aggregate.AFs.GRCh38.vcf"
chmod +x gdi-node-standalone gdi-dataset-tool
export PATH="$PWD:$PATH"
```
Expand Down Expand Up @@ -227,7 +227,7 @@ gdi-dataset-tool build covid-package.yaml --cc EE -o build # in a checkout: cr
ID=$(ls build) # build/ was empty; every build mints a new id
gdi-dataset-tool deploy build/$ID --inbox ~/gdi-demo/inbox --wait --management-url http://127.0.0.1:9090
gdi-dataset-tool publish $ID --inbox ~/gdi-demo/inbox # writes {id}.state.json; the node applies it on its next scan
until curl -fsS http://127.0.0.1:9090/datasets/$ID/state | grep -q '"visible"'; do sleep 1; done
for _ in $(seq 60); do curl -fsS http://127.0.0.1:9090/datasets/$ID/state | grep -q '"visible"' && break; sleep 1; done # up to a minute

curl -s -X POST http://localhost:8080/aggregated/beacon/v2/g_variants \
-H 'content-type: application/json' \
Expand All @@ -242,7 +242,7 @@ build the realistic sample instead and repeat the `deploy` and `publish` lines w
```bash
V=v1.0.0-rc.2
S=https://raw.githubusercontent.com/GenomicDataInfrastructure/gdi-node-standalone/$V/crates/test-util/tests/fixtures/sample
curl -fsSLO "$S/gdi-sample.package.yaml" -O "$S/gdi-sample.GRCh38.vcf.gz"
curl --fail-early -fsSLO "$S/gdi-sample.package.yaml" -O "$S/gdi-sample.GRCh38.vcf.gz"
gdi-dataset-tool build gdi-sample.package.yaml --cc EE -o build-sample # checkout: crates/test-util/tests/fixtures/sample/
ID=$(ls build-sample)
```
Expand Down
5 changes: 5 additions & 0 deletions conformance/fixtures/negative/bad-email-pattern.package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ metadata:
- "http://data.gdi.eu/core/p2/1MGCompliant"
legalBasis:
- "https://w3id.org/dpv#Consent"
minTypicalAge: 18
maxTypicalAge: 90
provenance: "Whole-genome sequencing of the cohort's participants, aggregated per population."
healthTheme:
- "https://hdeu-dcat.data.health.europa.eu/resource/authority/health-theme/HEALTH_PRODUCTS"
contactPoint:
fn: "Data team"
hasEmail: "data@example.org"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ metadata:
- "http://data.gdi.eu/core/p2/1MGCompliant"
legalBasis:
- "https://w3id.org/dpv#Consent"
minTypicalAge: 18
maxTypicalAge: 90
provenance: "Whole-genome sequencing of the cohort's participants, aggregated per population."
healthTheme:
- "https://hdeu-dcat.data.health.europa.eu/resource/authority/health-theme/HEALTH_PRODUCTS"
contactPoint:
fn: "Data team"
hasEmail: "mailto:data@example.org"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ metadata:
- "http://data.gdi.eu/core/p2/1MGCompliant"
legalBasis:
- "https://w3id.org/dpv#Consent"
minTypicalAge: 18
maxTypicalAge: 90
provenance: "Whole-genome sequencing of the cohort's participants, aggregated per population."
healthTheme:
- "https://hdeu-dcat.data.health.europa.eu/resource/authority/health-theme/HEALTH_PRODUCTS"
contactPoint:
fn: "Data team"
hasEmail: "mailto:data@example.org"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ metadata:
- "http://data.gdi.eu/core/p2/1MGCompliant"
legalBasis:
- "https://w3id.org/dpv#Consent"
minTypicalAge: 18
maxTypicalAge: 90
provenance: "Whole-genome sequencing of the cohort's participants, aggregated per population."
healthTheme:
- "https://hdeu-dcat.data.health.europa.eu/resource/authority/health-theme/HEALTH_PRODUCTS"
contactPoint:
fn: "Data team"
hasEmail: "mailto:data@example.org"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ metadata:
- "http://data.gdi.eu/core/p2/1MGCompliant"
legalBasis:
- "https://w3id.org/dpv#Consent"
minTypicalAge: 18
maxTypicalAge: 90
provenance: "Whole-genome sequencing of the cohort's participants, aggregated per population."
healthTheme:
- "https://hdeu-dcat.data.health.europa.eu/resource/authority/health-theme/HEALTH_PRODUCTS"
contactPoint:
fn: "Data team"
hasEmail: "mailto:data@example.org"
Expand Down
5 changes: 5 additions & 0 deletions conformance/fixtures/valid/baseline.package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ metadata:
- "http://data.gdi.eu/core/p2/1MGCompliant"
legalBasis:
- "https://w3id.org/dpv#Consent"
minTypicalAge: 18
maxTypicalAge: 90
provenance: "Whole-genome sequencing of the cohort's participants, aggregated per population."
healthTheme:
- "https://hdeu-dcat.data.health.europa.eu/resource/authority/health-theme/HEALTH_PRODUCTS"
contactPoint:
fn: "Data team"
hasEmail: "mailto:data@example.org"
Expand Down
4 changes: 4 additions & 0 deletions crates/core/fuzz/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions crates/core/src/config/secrets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,9 @@ impl Secrets {
for name in self.profiles.keys() {
if config.profiles.get(name).is_some_and(|p| p.s3.is_none()) {
return Err(format!(
"{}: credentials for profile '{name}', but [profiles.{name}.s3] in the tool \
config sets nothing (commented-out keys don't count); add its settings or \
remove the credentials",
"{}: credentials for profile '{name}', but the tool config has no \
[profiles.{name}.s3] settings (the block is absent, or holds only \
commented-out keys); add them or remove the credentials",
path.display()
));
}
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/config/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3278,7 +3278,7 @@ impl ServiceConfig {
/// non-empty, `email` an address, with or without `mailto:`, `url` a valid URL if
/// set);
/// - at least one `theme` and one `applicable_legislation` entry; these, every
/// `theme_taxonomy` entry, and the publisher's and HDAB's `homepage` and
/// `theme_taxonomy` entry, the publisher's and HDAB's `homepage`, and
/// `endpoint_description` are valid IRIs;
/// - the distribution has a title and an access URL, and its IRIs are valid;
/// - `[fairdp.publish]`: kinds only on datasets and distributions, namespace IRIs;
Expand Down
8 changes: 3 additions & 5 deletions crates/core/src/crypt4gh/body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,9 @@ pub(crate) fn decrypt_body<R: Read, W: Write>(
.map(|k| ChaCha20Poly1305::new(k.into()))
.collect();

// Reusable scratch for the multi-key retry path. An in-place trial decrypt XORs the
// keystream into the buffer before the tag is checked, so a wrong-key attempt scrambles
// it; with more than one candidate key the trial therefore runs against this copy and a
// failure never consumes the original ciphertext. Allocated once, and empty in the
// single-key case, which decrypts in place.
// Scratch for the multi-key retry path: each trial decrypts this copy, so a failed
// attempt leaves the ciphertext alone without relying on the AEAD checking its tag
// first. Allocated once; empty in the single-key case, which decrypts in place.
//
// `Zeroizing` because both buffers hold plaintext after decryption, which on the
// identity-restore path is the node's own secret key material. A plain `Vec` would leave
Expand Down
4 changes: 2 additions & 2 deletions crates/core/src/crypt4gh/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ fn derive_shared_key(
sender_pk: &PublicKey,
recipient_pk: &PublicKey,
) -> Option<Zeroizing<[u8; 32]>> {
// `StaticSecret` stores the clamped scalar. Expose its raw bytes to compute the bare DH
// point, matching libsodium's `crypto_scalarmult`.
// `x25519` clamps the raw secret bytes as libsodium's `crypto_scalarmult` does, so the
// bare DH point matches the reference implementation.
let scalar = Zeroizing::new(local_sk.0.to_bytes());
let shared_point = Zeroizing::new(x25519(*scalar, *peer_pk.as_bytes()));

Expand Down
40 changes: 36 additions & 4 deletions crates/core/src/crypt4gh/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,8 +499,8 @@ pub fn recover_writer_keys<R: Read>(
/// [`CoreError::DecryptFailed`] if the magic/version is wrong, the header is
/// malformed, or no identity in `identities` can decrypt any header packet,
/// [`CoreError::InvalidManifest`] if the stream carries a non-data-encryption header packet,
/// such as an edit list, that rewrap cannot faithfully re-encode, or [`CoreError::Io`] on a
/// read or write failure.
/// such as an edit list, that rewrap cannot faithfully re-encode, [`CoreError::Io`] on a
/// read or write failure, or [`CoreError::InternalError`] if the system random source fails.
pub fn rewrap_header<R: Read, W: Write>(
reader: &mut R,
writer: &mut W,
Expand All @@ -525,7 +525,7 @@ pub fn rewrap_header<R: Read, W: Write>(
/// # Errors
///
/// As [`rewrap_header`]: empty `new_recipients`, an unreadable/undecryptable header, a
/// non-data-encryption header packet, or a write failure.
/// non-data-encryption header packet, a write failure, or a failed random source.
pub fn rewrap_header_as<R: Read, W: Write>(
reader: &mut R,
writer: &mut W,
Expand Down Expand Up @@ -596,7 +596,10 @@ pub fn rewrap_header_as<R: Read, W: Write>(
let sender_sk = if let Some(sk) = writer_sk {
sk
} else {
ephemeral_sk = generate_keypair().0;
// Drawn like the session key, so a failed random source is an error, not a panic.
let mut seed = Zeroizing::new([0u8; 32]);
fill_random(seed.as_mut())?;
ephemeral_sk = SecretKey::from_bytes(*seed);
&ephemeral_sk
};
let mut packets: Vec<Vec<u8>> = Vec::with_capacity(session_keys.len() * new_recipients.len());
Expand Down Expand Up @@ -662,6 +665,19 @@ mod tests {
reason = "sender/recipient sk/pk are the standard, clearest crypto naming"
)]

/// Every key-holding type here wipes itself on drop. RustCrypto makes that an opt-in
/// `zeroize` feature, so a bump that loses it fails to compile here rather than leaving
/// keys in freed memory. `Blake2b` has no marker of its own, so its state and block
/// buffer, which hold the shared point, are named instead.
#[test]
fn key_holders_wipe_themselves_on_drop() {
fn wipes_on_drop<T: zeroize::ZeroizeOnDrop>() {}
wipes_on_drop::<x25519_dalek::StaticSecret>();
wipes_on_drop::<chacha20poly1305::ChaCha20Poly1305>();
wipes_on_drop::<blake2::Blake2bVarCore>();
wipes_on_drop::<blake2::digest::block_api::Buffer<blake2::Blake2bVarCore>>();
}

#[test]
fn the_header_trial_decrypt_budget_bounds_packets_times_identities() {
// `MAX_HEADER_PACKETS` bounds the packet count, not the asymmetric work: every
Expand Down Expand Up @@ -1171,6 +1187,22 @@ mod tests {
provider_pk.as_bytes(),
"the plain rewrap must NOT coincidentally stamp the provider key"
);

// And a fresh one on every rewrap, not a constant key.
let mut again = Vec::new();
rewrap_header(
&mut &to_a[..],
&mut again,
std::slice::from_ref(&sk_a),
std::slice::from_ref(&pk_b),
)
.unwrap();
let eph_again = recover_writer_keys(&mut &again[..], std::slice::from_ref(&sk_b)).unwrap();
assert_ne!(
eph[0].as_bytes(),
eph_again[0].as_bytes(),
"each plain rewrap mints its own sender"
);
}

#[test]
Expand Down
Loading