mirror of
https://github.com/freemkv/freemkv-keysources.git
synced 2026-10-07 18:13:11 +00:00
Compare commits
97
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1da51fdbf5 | ||
|
|
9f5116cc26 | ||
|
|
3c6ef54cf3 | ||
|
|
73c7f05b4b | ||
|
|
e6320d52c9 | ||
|
|
49cf396c07 | ||
|
|
940bb215ab | ||
|
|
18b590407c | ||
|
|
116b33739b | ||
|
|
d935fa6ffd | ||
|
|
b65959d3a1 | ||
|
|
3f08e980a0 | ||
|
|
6c8e2c5c46 | ||
|
|
ba357e46f5 | ||
|
|
a815a0a578 | ||
|
|
ba6f213be3 | ||
|
|
37568b1002 | ||
|
|
40389acd3f | ||
|
|
54508d8ec2 | ||
|
|
2c93420970 | ||
|
|
c48faa042b | ||
|
|
ab93818012 | ||
|
|
f3b66307cc | ||
|
|
ad59f3ce45 | ||
|
|
4e83b3c1f8 | ||
|
|
c5985d1c87 | ||
|
|
8f923fd5f9 | ||
|
|
18fdab6b77 | ||
|
|
dfa02adbe2 | ||
|
|
cbab284c85 | ||
|
|
0ffdb2a9f6 | ||
|
|
adbb1a12a0 | ||
|
|
05281249d1 | ||
|
|
cedbc4b1fa | ||
|
|
1606ff3ca3 | ||
|
|
10b0348db8 | ||
|
|
d0a3ab852b | ||
|
|
91b906fa61 | ||
|
|
e987506d96 | ||
|
|
60cff0ead6 | ||
|
|
c333c6000b | ||
|
|
adf2401c06 | ||
|
|
b89b7c9660 | ||
|
|
1121522534 | ||
|
|
146f53873d | ||
|
|
b925a4fa50 | ||
|
|
4f169a4881 | ||
|
|
e251e0bf6f | ||
|
|
6704c1e05b | ||
|
|
a34980970b | ||
|
|
948511853e | ||
|
|
373cdc4cf2 | ||
|
|
a60b021c35 | ||
|
|
fc8b6e3a05 | ||
|
|
bdbc805ba5 | ||
|
|
fa75d28441 | ||
|
|
5d2e11a4dd | ||
|
|
50bd0e4be2 | ||
|
|
148419c2bb | ||
|
|
1b97e6186d | ||
|
|
23988922eb | ||
|
|
e4ecc3cf83 | ||
|
|
55ef44e74e | ||
|
|
173a532a6e | ||
|
|
6879df55eb | ||
|
|
0a735498fb | ||
|
|
531e3a40bf | ||
|
|
ae8e5ade3c | ||
|
|
8b914e7121 | ||
|
|
62ebd61ff1 | ||
|
|
30c16bc415 | ||
|
|
8c3afbee67 | ||
|
|
6e4f05dec4 | ||
|
|
5bf6c4875b | ||
|
|
1c29377780 | ||
|
|
7c544e3780 | ||
|
|
4a241e04bc | ||
|
|
d19481ae59 | ||
|
|
707c8bdc57 | ||
|
|
599422ef22 | ||
|
|
ed5066af48 | ||
|
|
9b28a7c89d | ||
|
|
8a8397965b | ||
|
|
a52698b6bf | ||
|
|
89e58d716b | ||
|
|
bf18ec0cec | ||
|
|
7e9849ccbb | ||
|
|
94f082a43b | ||
|
|
1fbf3f6c63 | ||
|
|
b75599a5cb | ||
|
|
e83b7a3489 | ||
|
|
4ef4d5dfd6 | ||
|
|
febf4d0504 | ||
|
|
5e96e3e9d8 | ||
|
|
2415a58d5d | ||
|
|
edb14d3aea | ||
|
|
29d67e803d |
+31
-61
@@ -1,63 +1,33 @@
|
||||
# Version updates are monthly and grouped into one PR per ecosystem.
|
||||
# Security updates are separate and run whenever an advisory is published.
|
||||
version: 2
|
||||
|
||||
# Dependency updates land on `dev`, never on `main`.
|
||||
#
|
||||
# `main` here is a RELEASE POINTER that release.sh moves to each tag. A bot
|
||||
# commit on it would put work there that no tag contains, which is exactly the
|
||||
# state that aborted the 1.6.2 cascade at the last step -- so pointing
|
||||
# Dependabot at main would recreate that failure on a schedule.
|
||||
updates:
|
||||
- package-ecosystem: cargo
|
||||
directory: /
|
||||
target-branch: dev
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
# One PR per week for the routine bumps instead of one per crate. Eight
|
||||
# repos times a handful of crates is a volume nobody reads, and an
|
||||
# unread PR queue is indistinguishable from no updates at all.
|
||||
groups:
|
||||
minor-and-patch:
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
ignore:
|
||||
# The freemkv crates depend on each other by GIT TAG, re-pinned by
|
||||
# release.sh as part of the release commit. Dependabot cannot see that
|
||||
# cascade, so a PR bumping one of these would fight the release process
|
||||
# and could pin a version whose tag does not exist yet.
|
||||
- dependency-name: freemkv-unlock
|
||||
- dependency-name: libfreemkv
|
||||
- dependency-name: freemkv-keysources
|
||||
- dependency-name: freemkv-i18n
|
||||
- dependency-name: freemkv-engine
|
||||
|
||||
# The workflows are now real infrastructure -- the release cascade, the
|
||||
# cross-platform hash matrix, the disc gate -- so their actions need the same
|
||||
# attention as the crates.
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
target-branch: dev
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
actions:
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
ignore:
|
||||
# NOT a dependency: `dtolnay/rust-toolchain` is versioned by the RUST
|
||||
# release it installs, and the tag we pin is the toolchain CI is pinned
|
||||
# to on purpose -- precommit.sh runs the same one locally so a lint that
|
||||
# passes on a developer's newer default cannot pass CI by accident.
|
||||
#
|
||||
# Dependabot reads those tags as semver and proposed 1.97.0 -> 1.100.0,
|
||||
# a Rust version that does not exist. Every such PR 404s on toolchain
|
||||
# download across all eight repos, and they regenerate weekly -- eight
|
||||
# permanently-red PRs that promote.yml then has to special-case when it
|
||||
# decides whether dev is green.
|
||||
#
|
||||
# Bumping the toolchain is a deliberate, all-eight-repos change, made by
|
||||
# hand together with precommit.sh. There is nothing here for a bot.
|
||||
- dependency-name: dtolnay/rust-toolchain
|
||||
- package-ecosystem: cargo
|
||||
directory: /
|
||||
target-branch: dev
|
||||
schedule:
|
||||
interval: monthly
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
all:
|
||||
patterns:
|
||||
- '*'
|
||||
ignore:
|
||||
- dependency-name: freemkv-unlock
|
||||
- dependency-name: libfreemkv
|
||||
- dependency-name: freemkv-keysources
|
||||
- dependency-name: freemkv-i18n
|
||||
- dependency-name: freemkv-engine
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
target-branch: dev
|
||||
schedule:
|
||||
interval: monthly
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
all:
|
||||
patterns:
|
||||
- '*'
|
||||
ignore:
|
||||
- dependency-name: dtolnay/rust-toolchain
|
||||
- dependency-name: freemkv/.github*
|
||||
|
||||
+123
-88
@@ -10,16 +10,22 @@ on:
|
||||
branches: [main, dev, qa]
|
||||
pull_request:
|
||||
|
||||
# Rapid dev pushes supersede their own in-progress runs instead of stacking.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# WHY THE SIBLING CHECKOUTS — this is NOT a monorepo.
|
||||
#
|
||||
# The committed Cargo.toml resolves libfreemkv from a git TAG (it is off
|
||||
# crates.io). That means CI was building this branch against the LAST RELEASE
|
||||
# of libfreemkv, never against libfreemkv's dev tip — so a coordinated change
|
||||
# across the two repos could not be verified until release.sh cut the tags.
|
||||
# across the two repos could not be verified until release-orchestrate.yml cut the tags.
|
||||
# Green here meant "compiles against last month's library".
|
||||
#
|
||||
# The fix mirrors the developer's local layout. Every repo is checked out into
|
||||
# a subdirectory of $GITHUB_WORKSPACE, reproducing
|
||||
# The fix mirrors the developer's local layout via the shared composite
|
||||
# freemkv/.github//actions/checkout-siblings: every sibling repo is checked out
|
||||
# into a subdirectory of $GITHUB_WORKSPACE, reproducing
|
||||
# ~/Developer/freemkv/{freemkv-keysources,libfreemkv,freemkv-unlock}, and a
|
||||
# generated .cargo/config.toml redirects the git-tag patch to those paths.
|
||||
# Config-level [patch.crates-io] wins over the manifest one, which is the same
|
||||
@@ -32,12 +38,26 @@ on:
|
||||
# libfreemkv path-deps ../freemkv-unlock, which resolves to the workspace root
|
||||
# under this layout — hence the third checkout.
|
||||
#
|
||||
# `--locked` is deliberately absent: Cargo.lock is gitignored here (library
|
||||
# convention), so there is nothing to lock against and it fails rather than
|
||||
# resolves. That is what was breaking this workflow. The lock that has to hold
|
||||
# is the one release.sh regenerates in each consuming binary.
|
||||
# `--locked` is deliberately absent: Cargo.lock is now committed (issue #15) and
|
||||
# the `deny` job audits the resolved graph, but CI's sibling path-patch layout
|
||||
# resolves the freemkv crates as path packages, which `--locked` can't satisfy —
|
||||
# the same reason the binary crates omit it. The lock that has to hold is the one
|
||||
# release-orchestrate.yml regenerates in each consuming binary.
|
||||
|
||||
jobs:
|
||||
# Single source of truth for the MSRV — read rust-version from Cargo.toml once
|
||||
# and export it; MSRV-bound jobs consume needs.resolve-msrv.outputs.v. clippy
|
||||
# runs on stable instead (see CI.md's Toolchain policy).
|
||||
resolve-msrv:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
v: ${{ steps.msrv.outputs.v }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- id: msrv
|
||||
shell: bash
|
||||
run: echo "v=$(grep -m1 '^rust-version' Cargo.toml | sed -E 's/.*\"([^\"]+)\".*/\1/')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# CI owns the changelog: main may never carry a released version without a
|
||||
# CHANGELOG.md entry for it. The release orchestrator dates an existing
|
||||
# `## [X.Y.Z]` heading — it does NOT invent one — so a version bump that
|
||||
@@ -47,7 +67,7 @@ jobs:
|
||||
changelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v7
|
||||
- name: Require a CHANGELOG entry for the crate version on main
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -65,74 +85,78 @@ jobs:
|
||||
echo "::warning::${msg} (allowed on ${GITHUB_REF_NAME:-unknown}; required before it can reach main)."
|
||||
|
||||
lint:
|
||||
needs: resolve-msrv
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: freemkv-keysources
|
||||
- uses: actions/checkout@v7
|
||||
- uses: freemkv/.github/.github/actions/checkout-siblings@v1
|
||||
with:
|
||||
repository: freemkv/libfreemkv
|
||||
ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}"
|
||||
path: libfreemkv
|
||||
- uses: actions/checkout@v7
|
||||
checkout: libfreemkv freemkv-unlock
|
||||
patch: libfreemkv freemkv-unlock
|
||||
self: freemkv-keysources
|
||||
# fmt runs on the MSRV pin; clippy runs on stable so new-lint debt is a
|
||||
# blocking gate the pinned toolchain would hide. Stable is installed first
|
||||
# for clippy; the MSRV pin goes second so it stays fmt's default toolchain.
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
repository: freemkv/freemkv-unlock
|
||||
ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}"
|
||||
path: freemkv-unlock
|
||||
- name: Resolve siblings from their dev tips
|
||||
shell: bash
|
||||
working-directory: freemkv-keysources
|
||||
run: |
|
||||
mkdir -p .cargo
|
||||
cat > .cargo/config.toml <<'EOF'
|
||||
[patch.crates-io]
|
||||
libfreemkv = { path = "../libfreemkv" }
|
||||
EOF
|
||||
- uses: dtolnay/rust-toolchain@1.97.0
|
||||
components: clippy
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
components: clippy, rustfmt
|
||||
toolchain: ${{ needs.resolve-msrv.outputs.v }}
|
||||
components: rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: freemkv-keysources
|
||||
- run: cargo fmt --check
|
||||
working-directory: freemkv-keysources
|
||||
# --all-targets so the test code is linted too, not just the library.
|
||||
- run: cargo clippy --all-targets -- -D warnings
|
||||
- run: cargo +stable clippy --all-targets -- -D warnings
|
||||
working-directory: freemkv-keysources
|
||||
|
||||
test:
|
||||
needs: resolve-msrv
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: freemkv-keysources
|
||||
- uses: actions/checkout@v7
|
||||
- uses: freemkv/.github/.github/actions/checkout-siblings@v1
|
||||
with:
|
||||
repository: freemkv/libfreemkv
|
||||
ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}"
|
||||
path: libfreemkv
|
||||
- uses: actions/checkout@v7
|
||||
checkout: libfreemkv freemkv-unlock
|
||||
patch: libfreemkv freemkv-unlock
|
||||
self: freemkv-keysources
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
repository: freemkv/freemkv-unlock
|
||||
ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}"
|
||||
path: freemkv-unlock
|
||||
- name: Resolve siblings from their dev tips
|
||||
shell: bash
|
||||
working-directory: freemkv-keysources
|
||||
run: |
|
||||
mkdir -p .cargo
|
||||
cat > .cargo/config.toml <<'EOF'
|
||||
[patch.crates-io]
|
||||
libfreemkv = { path = "../libfreemkv" }
|
||||
EOF
|
||||
- uses: dtolnay/rust-toolchain@1.97.0
|
||||
toolchain: ${{ needs.resolve-msrv.outputs.v }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: freemkv-keysources
|
||||
- run: cargo test --tests
|
||||
working-directory: freemkv-keysources
|
||||
|
||||
# Dependency-audit gate. Mirrors the `test` job's sibling-checkout layout and
|
||||
# generated .cargo/config.toml so `cargo metadata` resolves the git-tag patch
|
||||
# to the local sibling paths; cargo-deny then audits advisories, bans,
|
||||
# licenses, and sources against the committed deny.toml.
|
||||
deny:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: freemkv-keysources
|
||||
- uses: freemkv/.github/.github/actions/checkout-siblings@v1
|
||||
with:
|
||||
checkout: libfreemkv freemkv-unlock
|
||||
patch: libfreemkv freemkv-unlock
|
||||
self: freemkv-keysources
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: taiki-e/install-action@cargo-deny
|
||||
# Run from the repo dir so the generated .cargo/config.toml path patches apply.
|
||||
- run: cargo deny --all-features check advisories bans licenses sources
|
||||
working-directory: freemkv-keysources
|
||||
|
||||
# This crate ships wherever freemkv and autorip ship, and it drags in all of
|
||||
# libfreemkv's platform code (SCSI transports included). Build the tests, not
|
||||
# `cargo check` — check skips codegen and never compiles test-only code.
|
||||
@@ -146,41 +170,64 @@ jobs:
|
||||
# to agree is what a release candidate is for.
|
||||
#
|
||||
# `if` SKIPS the job (it does not queue). A queued job would be far worse
|
||||
# than a slow one: release.sh's CI gate refuses while any run for the
|
||||
# commit is still in progress, so a never-scheduled job blocks releases
|
||||
# silently -- see the note on real-media in qa.yml.
|
||||
# than a slow one: a never-scheduled job leaves the run pending forever, so
|
||||
# the branch's required checks never report complete.
|
||||
needs: resolve-msrv
|
||||
if: github.ref_name == 'qa' || github.ref_name == 'main'
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: freemkv-keysources
|
||||
- uses: actions/checkout@v7
|
||||
- uses: freemkv/.github/.github/actions/checkout-siblings@v1
|
||||
with:
|
||||
repository: freemkv/libfreemkv
|
||||
ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}"
|
||||
path: libfreemkv
|
||||
- uses: actions/checkout@v7
|
||||
checkout: libfreemkv freemkv-unlock
|
||||
patch: libfreemkv freemkv-unlock
|
||||
self: freemkv-keysources
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
repository: freemkv/freemkv-unlock
|
||||
ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}"
|
||||
path: freemkv-unlock
|
||||
- name: Resolve siblings from their dev tips
|
||||
shell: bash
|
||||
working-directory: freemkv-keysources
|
||||
run: |
|
||||
mkdir -p .cargo
|
||||
cat > .cargo/config.toml <<'EOF'
|
||||
[patch.crates-io]
|
||||
libfreemkv = { path = "../libfreemkv" }
|
||||
EOF
|
||||
- uses: dtolnay/rust-toolchain@1.97.0
|
||||
toolchain: ${{ needs.resolve-msrv.outputs.v }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: freemkv-keysources
|
||||
- run: cargo build --tests
|
||||
working-directory: freemkv-keysources
|
||||
|
||||
# Additive, NON-REQUIRED coverage. Mirrors the `test` job's sibling-checkout
|
||||
# layout and pinned toolchain (nothing in the release cascade depends on it,
|
||||
# and codecov upload is tokenless with fail_ci_if_error:false, so a flaky
|
||||
# upload never reddens the run). `--locked` is deliberately absent here for
|
||||
# the same reason it is in `test`: CI's sibling path-patch layout resolves the
|
||||
# freemkv crates as path packages, which `--locked` can't satisfy.
|
||||
coverage:
|
||||
needs: resolve-msrv
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: freemkv-keysources
|
||||
- uses: freemkv/.github/.github/actions/checkout-siblings@v1
|
||||
with:
|
||||
checkout: libfreemkv freemkv-unlock
|
||||
patch: libfreemkv freemkv-unlock
|
||||
self: freemkv-keysources
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ needs.resolve-msrv.outputs.v }}
|
||||
components: llvm-tools-preview
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: freemkv-keysources
|
||||
- uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- run: cargo llvm-cov --workspace --ignore-run-fail --lcov --output-path lcov.info
|
||||
working-directory: freemkv-keysources
|
||||
- uses: codecov/codecov-action@v7
|
||||
with:
|
||||
files: freemkv-keysources/lcov.info
|
||||
slug: freemkv/freemkv-keysources
|
||||
fail_ci_if_error: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
check-windows:
|
||||
# dev is the FAST lane: this job still runs, but on the release-candidate
|
||||
# branches rather than on every push to dev. Nothing is deleted and no
|
||||
@@ -191,35 +238,23 @@ jobs:
|
||||
# to agree is what a release candidate is for.
|
||||
#
|
||||
# `if` SKIPS the job (it does not queue). A queued job would be far worse
|
||||
# than a slow one: release.sh's CI gate refuses while any run for the
|
||||
# commit is still in progress, so a never-scheduled job blocks releases
|
||||
# silently -- see the note on real-media in qa.yml.
|
||||
# than a slow one: a never-scheduled job leaves the run pending forever, so
|
||||
# the branch's required checks never report complete.
|
||||
needs: resolve-msrv
|
||||
if: github.ref_name == 'qa' || github.ref_name == 'main'
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: freemkv-keysources
|
||||
- uses: actions/checkout@v7
|
||||
- uses: freemkv/.github/.github/actions/checkout-siblings@v1
|
||||
with:
|
||||
repository: freemkv/libfreemkv
|
||||
ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}"
|
||||
path: libfreemkv
|
||||
- uses: actions/checkout@v7
|
||||
checkout: libfreemkv freemkv-unlock
|
||||
patch: libfreemkv freemkv-unlock
|
||||
self: freemkv-keysources
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
repository: freemkv/freemkv-unlock
|
||||
ref: "${{ github.ref_name == 'qa' && 'qa' || 'dev' }}"
|
||||
path: freemkv-unlock
|
||||
- name: Resolve siblings from their dev tips
|
||||
shell: bash
|
||||
working-directory: freemkv-keysources
|
||||
run: |
|
||||
mkdir -p .cargo
|
||||
cat > .cargo/config.toml <<'EOF'
|
||||
[patch.crates-io]
|
||||
libfreemkv = { path = "../libfreemkv" }
|
||||
EOF
|
||||
- uses: dtolnay/rust-toolchain@1.97.0
|
||||
toolchain: ${{ needs.resolve-msrv.outputs.v }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: freemkv-keysources
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
name: comment-guard
|
||||
|
||||
# Thin caller → the canonical reusable guard in freemkv/.github (one definition
|
||||
# for every repo). Fork PRs still run it; same-repo pushes aren't double-run.
|
||||
on: [push, pull_request]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
comment-guard:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
|
||||
uses: freemkv/.github/.github/workflows/guard-comment.yml@v2
|
||||
@@ -0,0 +1,14 @@
|
||||
name: identity-guard
|
||||
|
||||
# Thin caller → the canonical reusable guard in freemkv/.github.
|
||||
on:
|
||||
push:
|
||||
branches: [dev, qa, main]
|
||||
pull_request:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
identity:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
|
||||
uses: freemkv/.github/.github/workflows/guard-identity.yml@v1
|
||||
@@ -1,35 +1,11 @@
|
||||
name: leak-guard
|
||||
|
||||
# Self-contained public-repo leak gate. Public CI cannot reach the private
|
||||
# tooling, so this encodes only the generic net: internal-infra references,
|
||||
# tracked CLAUDE.md/.claude paths, and AI-attribution in commit messages.
|
||||
# No project-specific reverse-engineering vocabulary lives here.
|
||||
|
||||
# Thin caller → the canonical reusable guard in freemkv/.github.
|
||||
on: [push, pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
leak-guard:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Compute commit range
|
||||
id: range
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
base="${{ github.event.pull_request.base.sha }}"
|
||||
head="${{ github.event.pull_request.head.sha }}"
|
||||
echo "range=$base..$head" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
before="${{ github.event.before }}"
|
||||
after="${{ github.sha }}"
|
||||
# New branch / first push: github.event.before is all-zeros.
|
||||
if [ -z "$before" ] || [ "$before" = "0000000000000000000000000000000000000000" ]; then
|
||||
echo "range=$after" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "range=$before..$after" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
fi
|
||||
- name: Run leak-guard
|
||||
run: bash ci/leak-guard.sh "${{ steps.range.outputs.range }}"
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork
|
||||
uses: freemkv/.github/.github/workflows/guard-leak.yml@v1
|
||||
|
||||
+40
-58
@@ -13,17 +13,35 @@ name: qa
|
||||
# Sibling repos are checked out at `qa`, NOT `dev`. A qa run that resolved its
|
||||
# dependencies from dev tips would be validating a combination that is not the
|
||||
# one being released, which is the exact failure this branch exists to prevent.
|
||||
# The shared checkout-siblings composite resolves qa->qa from the caller branch.
|
||||
#
|
||||
# What this gate CANNOT cover: `disc://` and real `iso://` need physical media,
|
||||
# and no hosted runner has an optical drive or the image hoard. Those run on a
|
||||
# self-hosted runner (see the media job at the end) and are the one leg that
|
||||
# stays on hardware.
|
||||
# and no hosted runner has an optical drive or the image hoard. That coverage
|
||||
# lives in freemkv's qa.yml (the real-media EC2 matrix), not in this file.
|
||||
on:
|
||||
push:
|
||||
branches: [qa]
|
||||
workflow_dispatch:
|
||||
|
||||
# Supersede an in-progress qa run when a newer candidate lands on the branch.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Single source of truth for the MSRV — see ci.yml. Jobs that build/test AT
|
||||
# the MSRV read needs.resolve-msrv.outputs.v from Cargo.toml's rust-version;
|
||||
# clippy (cross-lint) runs on stable instead.
|
||||
resolve-msrv:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
v: ${{ steps.msrv.outputs.v }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- id: msrv
|
||||
shell: bash
|
||||
run: echo "v=$(grep -m1 '^rust-version' Cargo.toml | sed -E 's/.*\"([^\"]+)\".*/\1/')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# ── Name the candidate ────────────────────────────────────────────────
|
||||
#
|
||||
# Every push to `qa` is a release candidate, so every push gets a tag:
|
||||
@@ -65,6 +83,7 @@ jobs:
|
||||
# changes what the optimiser can prove. A test that only passes in debug is
|
||||
# a test that never guarded the binary anyone actually ships.
|
||||
release-tests:
|
||||
needs: resolve-msrv
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -74,26 +93,14 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: freemkv-keysources
|
||||
- uses: actions/checkout@v7
|
||||
- uses: freemkv/.github/.github/actions/checkout-siblings@v1
|
||||
with:
|
||||
repository: freemkv/libfreemkv
|
||||
ref: qa
|
||||
path: libfreemkv
|
||||
- uses: actions/checkout@v7
|
||||
checkout: libfreemkv freemkv-unlock
|
||||
patch: libfreemkv freemkv-unlock
|
||||
self: freemkv-keysources
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
repository: freemkv/freemkv-unlock
|
||||
ref: qa
|
||||
path: freemkv-unlock
|
||||
- name: Resolve siblings from their qa tips
|
||||
shell: bash
|
||||
working-directory: freemkv-keysources
|
||||
run: |
|
||||
mkdir -p .cargo
|
||||
cat > .cargo/config.toml <<'EOF'
|
||||
[patch.crates-io]
|
||||
libfreemkv = { path = "../libfreemkv" }
|
||||
EOF
|
||||
- uses: dtolnay/rust-toolchain@1.97.0
|
||||
toolchain: ${{ needs.resolve-msrv.outputs.v }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: freemkv-keysources
|
||||
@@ -109,26 +116,12 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: freemkv-keysources
|
||||
- uses: actions/checkout@v7
|
||||
- uses: freemkv/.github/.github/actions/checkout-siblings@v1
|
||||
with:
|
||||
repository: freemkv/libfreemkv
|
||||
ref: qa
|
||||
path: libfreemkv
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
repository: freemkv/freemkv-unlock
|
||||
ref: qa
|
||||
path: freemkv-unlock
|
||||
- name: Resolve siblings from their qa tips
|
||||
shell: bash
|
||||
working-directory: freemkv-keysources
|
||||
run: |
|
||||
mkdir -p .cargo
|
||||
cat > .cargo/config.toml <<'EOF'
|
||||
[patch.crates-io]
|
||||
libfreemkv = { path = "../libfreemkv" }
|
||||
EOF
|
||||
- uses: dtolnay/rust-toolchain@1.97.0
|
||||
checkout: libfreemkv freemkv-unlock
|
||||
patch: libfreemkv freemkv-unlock
|
||||
self: freemkv-keysources
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
@@ -143,31 +136,20 @@ jobs:
|
||||
# of the SCSI transport and platform layers compile on no other runner, so
|
||||
# without this they are first built at release time.
|
||||
windows-build:
|
||||
needs: resolve-msrv
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: freemkv-keysources
|
||||
- uses: actions/checkout@v7
|
||||
- uses: freemkv/.github/.github/actions/checkout-siblings@v1
|
||||
with:
|
||||
repository: freemkv/libfreemkv
|
||||
ref: qa
|
||||
path: libfreemkv
|
||||
- uses: actions/checkout@v7
|
||||
checkout: libfreemkv freemkv-unlock
|
||||
patch: libfreemkv freemkv-unlock
|
||||
self: freemkv-keysources
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
repository: freemkv/freemkv-unlock
|
||||
ref: qa
|
||||
path: freemkv-unlock
|
||||
- name: Resolve siblings from their qa tips
|
||||
shell: bash
|
||||
working-directory: freemkv-keysources
|
||||
run: |
|
||||
mkdir -p .cargo
|
||||
cat > .cargo/config.toml <<'EOF'
|
||||
[patch.crates-io]
|
||||
libfreemkv = { path = "../libfreemkv" }
|
||||
EOF
|
||||
- uses: dtolnay/rust-toolchain@1.97.0
|
||||
toolchain: ${{ needs.resolve-msrv.outputs.v }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: freemkv-keysources
|
||||
|
||||
@@ -29,28 +29,32 @@ jobs:
|
||||
|
||||
# Tests run as a PARALLEL TRIPWIRE: they fail the run if they fail, but the
|
||||
# release job does NOT `needs:` this job. The tag decision was already gated by
|
||||
# the local precommit (same Rust 1.86, same commit).
|
||||
# CI (ci.yml on dev, then a green qa.yml on the same commit).
|
||||
#
|
||||
# freemkv-keysources is a library — Cargo.lock isn't tracked, so --locked
|
||||
# would always fail (no lockfile to lock against on a fresh runner). Its
|
||||
# Cargo.lock is now committed (issue #15), but --locked is still omitted: on
|
||||
# the runner the libfreemkv dep resolves from crates.io, not the local path
|
||||
# patch, which --locked can't reconcile. Its
|
||||
# libfreemkv dep resolves from crates.io (the dev .cargo/config.toml patch is
|
||||
# gitignored and absent on the runner), so libfreemkv must already be published
|
||||
# at this version — the release script enforces that ordering before tagging.
|
||||
# at this version — release-orchestrate.yml enforces that ordering before tagging.
|
||||
test:
|
||||
needs: verify
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@1.97.0
|
||||
- id: msrv
|
||||
shell: bash
|
||||
run: echo "v=$(grep -m1 '^rust-version' Cargo.toml | sed -E 's/.*\"([^\"]+)\".*/\1/')" >> "$GITHUB_OUTPUT"
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ steps.msrv.outputs.v }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo test
|
||||
|
||||
# NOTE: the crates.io publish is driven by the maintainer's release tooling
|
||||
# from local credentials, not from CI — this repo has no CARGO_REGISTRY_TOKEN
|
||||
# secret. release.sh runs `cargo publish --no-verify` locally. CI here only
|
||||
# verifies the version/tag match, runs tests (tripwire), and cuts the GitHub
|
||||
# Release. The binaries git-tag-pin keysources, so they don't wait on the
|
||||
# crates.io publish — it serves external consumers in parallel.
|
||||
# NOTE: there is no crates.io publish — keysources is `publish = false` and
|
||||
# git-tag-only. Consumers git-tag-pin it via a committed [patch.crates-io], so
|
||||
# the git tag itself is the release artifact. CI here only verifies the
|
||||
# version/tag match, runs tests (tripwire), and cuts the GitHub Release.
|
||||
|
||||
release:
|
||||
# Only needs `verify`; the GitHub Release is cut as soon as the version
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
|
||||
# session scratch — never track (may contain RE breadcrumbs)
|
||||
scratch/
|
||||
|
||||
+104
@@ -1,5 +1,109 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- The online key service ignores `HTTP_PROXY`/`HTTPS_PROXY`/`ALL_PROXY`: with one set, every key lookup failed (the pinned resolver also answered the proxy's address, so the proxy request went to the key service).
|
||||
|
||||
### Changed
|
||||
|
||||
- `KeydbSource` hands out host certs best first, with no disc input: certs with no `Revoked in MKBv<N>` note in keydb order, then revoked ones by latest revocation. The inherent `host_certs()` and the `KeySource::host_certs` trait method both return this order; the trait ignores its `mkb` argument.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- `KeyDb::host_certs(mkb)`: use `KeyDb::host_certs_ranked`.
|
||||
|
||||
### Added
|
||||
|
||||
- `KeyDb::host_certs_ranked`.
|
||||
- `check_keyserver_url` with `KeyserverUrlRejection`/`KeyserverUrlFault`: a typed Permanent vs Temporary verdict, so callers need not match `validate_keyserver_url`'s text.
|
||||
- `test-hooks` feature exposing `set_last_decode_reachability`, to test decode-verdict handling without a network.
|
||||
|
||||
## [1.7.7] — 2026-09-26
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Replace comment-overflow documentation with concise source contracts and README instructions; enforce the shared comment policy in CI.
|
||||
|
||||
## [1.7.6] — 2026-09-26
|
||||
|
||||
### Changed
|
||||
|
||||
- Version aligned to 1.7.6 for the unified release. No functional changes to this crate; the release is driven by the freemkv 1.7.6 Linux desktop shell (GTK4 + libadwaita) and the rip-finished desktop notification (issue #56).
|
||||
|
||||
## [1.7.5] — 2026-09-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Version aligned to 1.7.5 for the unified release. No functional changes to this crate; the release is driven by freemkv-unlock mirroring the freemkv-firmware 0.9.0 ABI (the drive's `Ake` and `Bus` levers retired into a single `Encryption` lever).
|
||||
|
||||
## [1.7.4] — 2026-09-21
|
||||
### Fixed
|
||||
|
||||
- keydb lookups now report match state and entry shape, so a "no entry" result tells you whether the disc was truly absent or matched-but-underivable (no usable VID) (#46).
|
||||
- ureq 3.4.2 with a bounded, rolling-idle reply body, so a slow-but-progressing download is no longer killed by an absolute deadline.
|
||||
|
||||
### Maintenance
|
||||
|
||||
- CI moved to the central reusable workflows.
|
||||
|
||||
## [1.7.3] — 2026-09-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Unified release with freemkv 1.7.3 (`info --share` disc-structure capture for bug reports + the multi-angle UHD main-title fix in libfreemkv; see the libfreemkv/freemkv 1.7.3 notes). No functional changes to this crate.
|
||||
|
||||
## [1.7.2] — UNRELEASED
|
||||
|
||||
### Added
|
||||
|
||||
- `OnlineSource` now records the reachability outcome of its actual `/decode` POST (an HTTP `Status(code)` for any answer, including 200/404/422/429/5xx, or `Transport` for a connect/timeout/DNS/TLS failure) in a per-thread slot, readable via the new `take_last_decode_reachability()` / `DecodeReachability`. This lets a caller (autorip) classify a no-key result from the REAL decode's outcome instead of firing a second, redundant probe against the POST-only endpoint. No behavioural change to key resolution itself.
|
||||
|
||||
## [1.7.1] — UNRELEASED
|
||||
|
||||
### Changed
|
||||
|
||||
- Version aligned to 1.7.1 for the unified release (freemkv-unlock 1.7.1 LibreDrive unlock fix); no functional changes.
|
||||
|
||||
## [1.7.0] — 2026-09-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Version aligned to 1.7.0 for the unified release. Internal CI/lint hardening only (stable-clippy MSRV split, cargo-deny dependency-audit gate, audience-based comment-guard); no functional changes.
|
||||
|
||||
## [1.6.14] — 2026-08-31
|
||||
|
||||
### Changed
|
||||
|
||||
- Version aligned to 1.6.14 for the unified release.
|
||||
|
||||
## [1.6.13] — 2026-08-28
|
||||
|
||||
### Changed
|
||||
|
||||
- Version aligned to 1.6.13 for the unified release.
|
||||
|
||||
## [1.6.12] — 2026-08-27
|
||||
|
||||
### Changed
|
||||
|
||||
- Version aligned to 1.6.12 for the unified release; no functional changes.
|
||||
|
||||
## [1.6.11] — 2026-08-26
|
||||
|
||||
### Changed
|
||||
|
||||
- Version aligned to 1.6.11 for the unified release. No functional changes to
|
||||
this crate; the release was driven by the libfreemkv main-feature selection
|
||||
improvements and the autorip mux-quarantine fix (see the libfreemkv and
|
||||
autorip 1.6.11 notes).
|
||||
|
||||
### Added
|
||||
|
||||
- Codecov coverage reporting and badge.
|
||||
- Substantially expanded unit-test coverage.
|
||||
|
||||
## [1.6.10] — 2026-08-23
|
||||
|
||||
### Changed
|
||||
|
||||
Generated
+1311
File diff suppressed because it is too large
Load Diff
+17
-4
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "freemkv-keysources"
|
||||
version = "1.6.9"
|
||||
version = "1.7.7"
|
||||
edition = "2024"
|
||||
rust-version = "1.97"
|
||||
rust-version = "1.88"
|
||||
license = "MIT"
|
||||
description = "Pluggable AACS key sources (keydb, online key service) for libfreemkv. Each source looks a disc up and hands libfreemkv its terminal Unit Keys via get_unit_keys; the library does all derivation."
|
||||
repository = "https://github.com/freemkv/freemkv-keysources"
|
||||
@@ -17,7 +17,7 @@ publish = false
|
||||
# The crate provides the `KeySource` trait + `Key`/`DiscInputs` types these impls fill.
|
||||
# libfreemkv is git-only now; the committed [patch.crates-io] below redirects this
|
||||
# bare version req to the libfreemkv git tag (local dev overrides it to a path).
|
||||
libfreemkv = "1.6"
|
||||
libfreemkv = "1.7"
|
||||
# OnlineSource: POST disc inputs + samples to a key service over HTTP.
|
||||
ureq = { version = "3", features = ["json"] }
|
||||
serde_json = "1"
|
||||
@@ -30,6 +30,15 @@ zip = { version = "8", default-features = false, features = ["deflate"] }
|
||||
# key material).
|
||||
tracing = "0.1"
|
||||
|
||||
[features]
|
||||
# `set_last_decode_reachability`, for consumers' tests only (enable in dev-dependencies).
|
||||
test-hooks = []
|
||||
|
||||
[dev-dependencies]
|
||||
# `test_util` fixtures (`decrypt_unit`, …) for this crate's tests (keys-upfront
|
||||
# KU-P1). CI resolves it through the sibling path patch.
|
||||
libfreemkv = { version = "1.7", features = ["test-util"] }
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
@@ -40,4 +49,8 @@ codegen-units = 1
|
||||
# overrides it with a path patch via the gitignored .cargo/config.toml (a
|
||||
# config-level [patch.crates-io] wins over this manifest one for the same crate).
|
||||
[patch.crates-io]
|
||||
libfreemkv = { git = "https://github.com/freemkv/libfreemkv", tag = "v1.6.9" }
|
||||
libfreemkv = { git = "https://github.com/freemkv/libfreemkv", tag = "v1.7.7" }
|
||||
freemkv-unlock = { git = "https://github.com/freemkv/freemkv-unlock", tag = "v1.7.7" }
|
||||
|
||||
[lints.clippy]
|
||||
uninlined_format_args = "allow"
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
[](LICENSE)
|
||||
[](https://github.com/freemkv/freemkv-keysources/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/freemkv/freemkv-keysources)
|
||||
|
||||
# freemkv-keysources
|
||||
|
||||
Pluggable AACS key sources (keydb, online key service) for
|
||||
[libfreemkv](https://github.com/freemkv/libfreemkv). Each source looks a disc up
|
||||
and hands libfreemkv its terminal Unit Keys via `get_unit_keys`; the library
|
||||
does all derivation.
|
||||
|
||||
## Development
|
||||
|
||||
Build API documentation with `cargo doc --no-deps --open` for source construction,
|
||||
keydb paths and online-query contracts. Run `cargo test --tests` to validate
|
||||
parsing, caching and request guards. Tests use synthetic keys and local fixtures;
|
||||
never commit real key material or credentials.
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
@@ -1,120 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# leak-guard.sh — self-contained public-repo leak gate.
|
||||
#
|
||||
# This is the LAST line of defense in CI. It is intentionally self-contained:
|
||||
# public CI cannot reach the private tooling, so this script encodes ONLY the
|
||||
# generic net — internal infrastructure references, agent-context files, and
|
||||
# AI-attribution in commit messages. It deliberately contains NO project-
|
||||
# specific reverse-engineering vocabulary (those words would themselves be a
|
||||
# leak). The richer private scanner stays private.
|
||||
#
|
||||
# Fails (exit 1) if any of the following appear in the repo:
|
||||
# 1. a tracked CLAUDE.md or .claude/ path (agent context — never public),
|
||||
# 2. tracked file content matching the internal-infra net,
|
||||
# 3. a commit message (in the given range) with AI attribution.
|
||||
#
|
||||
# Usage:
|
||||
# leak-guard.sh [<commit-range>]
|
||||
# <commit-range> optional git rev-list range to scan commit messages
|
||||
# (e.g. "abc..def"). If omitted, commit-message scan is
|
||||
# skipped (path + content checks always run).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Absolute path to this script, resolved before any cd, so we can exclude it
|
||||
# from the content scan (it necessarily contains the detection patterns).
|
||||
SELF_ABS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
||||
|
||||
REPO="$(git rev-parse --show-toplevel)"
|
||||
cd "$REPO"
|
||||
|
||||
fail=0
|
||||
note() { printf ' ✗ %s\n' "$1"; fail=1; }
|
||||
|
||||
# Internal-infra net — GENERIC ONLY. This script ships in the public repo, so
|
||||
# the patterns themselves must not name any org-specific identifier (doing so
|
||||
# would itself leak the infra they guard). We catch the leak *class*:
|
||||
# - RFC1918 private IPv4 ranges (10/8, 172.16/12, 192.168/16),
|
||||
# - private/internal/non-routable TLDs (.internal/.local/.lan/.corp/.invalid),
|
||||
# - docker.internal.
|
||||
# The full org-specific net (literal hostnames, service names, repo paths,
|
||||
# vendor tooling, …) lives ONLY in the private scanner and never ships here.
|
||||
INFRA_RE='\b10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|\b172\.(1[6-9]|2[0-9]|3[01])\.[0-9]{1,3}\.[0-9]{1,3}|\b192\.168\.[0-9]{1,3}\.[0-9]{1,3}|\.internal\b|\.local\b|\.lan\b|\.corp\b|\.invalid\b|docker\.internal'
|
||||
# Home-path net — GENERIC ONLY. Catches an absolute developer home path
|
||||
# committed into a tracked file (a macOS /Users/<user>/… or Linux /home/<user>/…
|
||||
# path). This names NO specific user — it matches the leak *class* (any home
|
||||
# path), so the pattern itself reveals nothing org- or person-specific. A real
|
||||
# leak (e.g. /Users/alice/Developer/x slipping into a public RELEASE.md) trips
|
||||
# this regardless of whose machine it came from. The username segment is a
|
||||
# literal-username class ([A-Za-z0-9._-]) so dynamic/templated paths that build
|
||||
# the user at runtime — shell `/home/$USER/`, doc `/home/<rip>/`, Rust
|
||||
# `/home/{user}/` — do NOT false-positive; only a baked-in literal home leaks.
|
||||
HOMEPATH_RE='/Users/[A-Za-z0-9._-]+/|/home/[A-Za-z0-9._-]+/'
|
||||
# AI-attribution net (case-insensitive). "claude" matches only as a standalone
|
||||
# word — NOT preceded by a dot/slash/alnum and NOT followed by .md — so legit
|
||||
# mentions of CLAUDE.md / .claude/ in a commit message don't false-positive.
|
||||
ATTR_RE='co-authored-by|generated with|🤖|(?<![.\/A-Za-z0-9])claude(?!\.md)'
|
||||
|
||||
echo "── leak-guard: tracked agent-context paths ──"
|
||||
while IFS= read -r f; do
|
||||
case "$f" in
|
||||
CLAUDE.md|*/CLAUDE.md|.claude|.claude/*|*/.claude|*/.claude/*)
|
||||
note "tracked agent-context file: $f (CLAUDE.md/.claude must never be tracked in a public repo)" ;;
|
||||
esac
|
||||
done < <(git ls-files)
|
||||
|
||||
# Match a PCRE against a file, emitting "LINE: MATCH". The pattern is passed as
|
||||
# an argument (not interpolated into a //) so metacharacters like the "/" in a
|
||||
# path-style token can't break the regex. Reads raw bytes so non-UTF-8 blobs
|
||||
# don't abort the scan.
|
||||
pcre_matches() {
|
||||
perl -e '
|
||||
my ($file, $re) = @ARGV;
|
||||
open(my $fh, "<:raw", $file) or exit 0;
|
||||
my $rx; eval { $rx = qr/$re/i }; exit 0 if $@;
|
||||
while (my $l = <$fh>) { if ($l =~ /$rx/) { print "$.: $&\n"; } }
|
||||
' "$1" "$2" 2>/dev/null
|
||||
}
|
||||
|
||||
# This script's own source necessarily contains the detection patterns (e.g.
|
||||
# the regex tokens in INFRA_RE), so scanning it would always self-flag. Skip it.
|
||||
SELF="$(git ls-files --full-name -- "$SELF_ABS" 2>/dev/null | head -1)"
|
||||
|
||||
echo "── leak-guard: internal-infra references in tracked files ──"
|
||||
while IFS= read -r f; do
|
||||
case "$f" in *.png|*.jpg|*.jpeg|*.ico|*.gif|*.bin|*.crate|*.gz|*.zip|*.pdf) continue ;; esac
|
||||
[ -n "$SELF" ] && [ "$f" = "$SELF" ] && continue
|
||||
[ -f "$f" ] || continue
|
||||
while IFS= read -r hit; do
|
||||
[ -z "$hit" ] && continue
|
||||
note "internal-infra reference: $f:$hit"
|
||||
done < <(pcre_matches "$f" "$INFRA_RE")
|
||||
while IFS= read -r hit; do
|
||||
[ -z "$hit" ] && continue
|
||||
note "[HOME-PATH] absolute home path: $f:$hit (no local home path may be committed to a public repo)"
|
||||
done < <(pcre_matches "$f" "$HOMEPATH_RE")
|
||||
done < <(git ls-files)
|
||||
|
||||
RANGE="${1:-}"
|
||||
if [ -n "$RANGE" ]; then
|
||||
echo "── leak-guard: AI-attribution in commit messages ($RANGE) ──"
|
||||
while IFS= read -r sha; do
|
||||
[ -z "$sha" ] && continue
|
||||
msg="$(git log -1 --format='%B' "$sha" 2>/dev/null || true)"
|
||||
# Pass the pattern as an argument (not interpolated into a //) so the
|
||||
# lookbehind char class and "/" don't break the regex.
|
||||
hit="$(printf '%s' "$msg" | perl -e '
|
||||
my $re = $ARGV[0]; my $rx = qr/$re/i;
|
||||
while (my $l = <STDIN>) { if ($l =~ /($rx)/) { print "$1\n"; last; } }
|
||||
' "$ATTR_RE" | head -1 || true)"
|
||||
[ -n "$hit" ] && note "commit ${sha:0:12}: message contains \"$hit\" (owner rule: zero AI attribution, ever)"
|
||||
done < <(git rev-list "$RANGE" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$fail" -ne 0 ]; then
|
||||
echo "✗ leak-guard: blocking finding(s) above — DO NOT MERGE/PUBLISH"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ leak-guard: clean"
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Coverage is informational only: it never blocks a merge or a release. The
|
||||
# release cascade is gated by ci.yml / qa.yml and the local precommit, not by
|
||||
# a coverage threshold.
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
informational: true
|
||||
|
||||
# No PR comment noise; the badge and the Codecov UI are enough.
|
||||
comment: false
|
||||
|
||||
# Exclude non-product code from the coverage denominator.
|
||||
ignore:
|
||||
- "tests/**"
|
||||
- "ci/**"
|
||||
@@ -0,0 +1,42 @@
|
||||
# cargo-deny — gates the RESOLVED dependency graph (issue #15).
|
||||
# Runs in CI against the committed Cargo.lock: RUSTSEC advisories, a permissive
|
||||
# licence allow-list, and source/ban hygiene. Update the lock (cargo update)
|
||||
# to clear an advisory rather than adding an `ignore` entry without cause.
|
||||
|
||||
[advisories]
|
||||
yanked = "deny"
|
||||
# ignore = ["RUSTSEC-0000-0000"] # only with a written justification
|
||||
|
||||
[licenses]
|
||||
# The whole tree is permissive. r-efi's LGPL arm is satisfied by Apache-2.0
|
||||
# (its expression is "Apache-2.0 OR LGPL-2.1-or-later"), so no copyleft is
|
||||
# allowed here. Add a licence only when a real dependency introduces it.
|
||||
allow = [
|
||||
"MIT",
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"0BSD",
|
||||
"Unicode-3.0",
|
||||
"Unlicense",
|
||||
"Zlib",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"CDLA-Permissive-2.0",
|
||||
]
|
||||
confidence-threshold = 0.9
|
||||
|
||||
[bans]
|
||||
multiple-versions = "warn" # transitive dupes (digest 0.10/0.11 etc.) are noise, not a gate
|
||||
wildcards = "deny"
|
||||
# The freemkv siblings resolve as path deps under CI's path-patch layout, which
|
||||
# reads as a wildcard version req. That is internal, not a supply-chain wildcard.
|
||||
allow-wildcard-paths = true
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
# The freemkv siblings are first-party git-tag deps; allow git sources from
|
||||
# our own org only (CI resolves them as git, not the local path override).
|
||||
[sources.allow-org]
|
||||
github = ["freemkv"]
|
||||
+696
-448
File diff suppressed because it is too large
Load Diff
+605
-374
File diff suppressed because it is too large
Load Diff
+261
-66
@@ -1,24 +1,10 @@
|
||||
//! Pluggable AACS key sources for libfreemkv.
|
||||
//!
|
||||
//! libfreemkv owns the AACS crypto; this crate provides the published
|
||||
//! [`KeySource`] implementations that look a disc up and drive the boil-down
|
||||
//! primitives down to terminal Unit Keys:
|
||||
//!
|
||||
//! - [`KeydbSource`] — a local `keydb.cfg` (source #1).
|
||||
//! - [`OnlineSource`] — a remote key service (source #2).
|
||||
//!
|
||||
//! Applications (autorip, the `freemkv` CLI) choose and order the sources from
|
||||
//! their own config — the local-vs-online policy is just which impls they plug
|
||||
//! in — then resolve and hand the resulting key to `Disc::decrypt_with`.
|
||||
//!
|
||||
//! Each source resolves a disc's terminal **Unit Keys** in one shot via
|
||||
//! [`KeySource::get_unit_keys`], driving libfreemkv's boil-down crypto primitives for
|
||||
//! whatever level of material it holds. Compose several with [`MultiSource`] in
|
||||
//! the caller's chosen order. Reading the encrypted content-sample units a key
|
||||
//! server validates on, and applying the resolved keys against a disc, is
|
||||
//! decryption *mechanism* — it lives in the library
|
||||
//! (`libfreemkv::resolve_and_apply`, `libfreemkv::read_encrypted_units`), not
|
||||
//! here.
|
||||
//! libfreemkv owns the AACS crypto; this crate provides [`KeySource`] impls that look a disc up
|
||||
//! and drive the boil-down primitives to terminal Unit Keys: [`KeydbSource`] (local
|
||||
//! `keydb.cfg`) and [`OnlineSource`] (remote key service). Applications choose and order
|
||||
//! sources, then resolve and hand the key to `Disc::decrypt_with`; compose several with
|
||||
//! [`MultiSource`].
|
||||
|
||||
mod keydb;
|
||||
/// The `keydb.cfg` parser (`KeyDb`, `DiscEntry`, …). Public: parsing the keydb
|
||||
@@ -30,7 +16,13 @@ mod paths;
|
||||
|
||||
pub use keydb::{KeydbSource, UpdateResult};
|
||||
pub use keydb_format::{DiscEntry, KeyDb};
|
||||
pub use online::{MIN_SAMPLE_UNITS, OnlineSource, validate_keyserver_url};
|
||||
#[cfg(feature = "test-hooks")]
|
||||
pub use online::set_last_decode_reachability;
|
||||
pub use online::{
|
||||
DecodeReachability, KeyserverUrlFault, KeyserverUrlRejection, MIN_SAMPLE_UNITS, OnlineSource,
|
||||
check_keyserver_url, check_keyserver_url_static, take_last_decode_reachability,
|
||||
validate_keyserver_url,
|
||||
};
|
||||
pub use paths::{default_keydb_path, existing_keydb_path, keydb_search_paths};
|
||||
|
||||
// Re-exported for downstream convenience so apps need only depend on this crate
|
||||
@@ -39,10 +31,9 @@ pub use libfreemkv::aacs::types::UnitKey;
|
||||
pub use libfreemkv::keysource::ResolveCtx;
|
||||
pub use libfreemkv::{DiscInputs, KeySource};
|
||||
|
||||
/// VUK → the disc's terminal Unit Keys (positional index), one AES-ECB-decrypt
|
||||
/// per encrypted title key. Composes the raw `aacs::derive::decrypt_unit_key`
|
||||
/// primitive directly — replaces the removed libfreemkv `aacs::boil::uk_from_vuk`
|
||||
/// wrapper (that veneer is gone; libfreemkv owns only the AES).
|
||||
// VUK -> the disc's terminal Unit Keys (positional index), one AES-ECB-decrypt
|
||||
// per encrypted title key, via `aacs::derive::decrypt_unit_key`. Replaces the
|
||||
// removed libfreemkv `aacs::boil::uk_from_vuk` wrapper.
|
||||
pub(crate) fn uks_from_vuk(vuk: &[u8; 16], enc_title_keys: &[[u8; 16]]) -> Vec<UnitKey> {
|
||||
enc_title_keys
|
||||
.iter()
|
||||
@@ -52,24 +43,12 @@ pub(crate) fn uks_from_vuk(vuk: &[u8; 16], enc_title_keys: &[[u8; 16]]) -> Vec<U
|
||||
}
|
||||
|
||||
/// An ordered composition of key sources, driven as one.
|
||||
/// [`MultiSource::get_unit_keys`] tries each inner source in order and returns
|
||||
/// the first non-empty Unit Key set (and [`MultiSource::get_fmts_indexes`] does
|
||||
/// the same for the forensic set). **The caller supplies the list AND the
|
||||
/// order** — local-first `[Keydb,
|
||||
/// Online]`, online-first `[Online, Keydb]`, etc. —
|
||||
/// so the "which sources, in what order" policy lives entirely with the
|
||||
/// application, not the library. `MultiSource` is itself a [`KeySource`], so it
|
||||
/// nests and composes.
|
||||
///
|
||||
/// **A source that could not ANSWER is not a source that answered "no key".**
|
||||
/// When no inner source produces keys, the composition returns `Err` if any
|
||||
/// inner source failed, and `Ok(empty)` only when every source genuinely
|
||||
/// answered and none held a key. This mirrors libfreemkv's own resolver
|
||||
/// (`keysource::drive_unit_keys`, which tracks an `errored` flag, and
|
||||
/// `source_failure`, which stamps the first failure onto the disc) — a
|
||||
/// composition that swallowed the `Err` would hand the resolver a clean
|
||||
/// `Ok(empty)`, and an outage would once again be reported to the operator as
|
||||
/// `E7022 No key source has a decryption key for this disc`.
|
||||
/// [`MultiSource::get_unit_keys`] tries each inner source in order and returns the first
|
||||
/// non-empty Unit Key set (and [`MultiSource::get_fmts_indexes`] does the same for the forensic
|
||||
/// set). The caller supplies the list AND the order — local-first `[Keydb, Online]`,
|
||||
/// online-first `[Online, Keydb]`, etc. `MultiSource` is itself a [`KeySource`], so it nests
|
||||
/// and composes.
|
||||
pub struct MultiSource {
|
||||
sources: Vec<Box<dyn KeySource>>,
|
||||
}
|
||||
@@ -81,19 +60,9 @@ impl MultiSource {
|
||||
}
|
||||
}
|
||||
|
||||
/// Drive `sources` in order and return the first non-empty result — preserving
|
||||
/// the Ok/Err distinction when nothing resolves.
|
||||
///
|
||||
/// A source failure never BLOCKS the chain (a later source is still tried, and a
|
||||
/// later success still wins outright, error or no error), but it is not
|
||||
/// forgotten either: with no keys anywhere, the FIRST failure is returned. First
|
||||
/// (not last) matches libfreemkv's `source_failure` rule, so the most-preferred
|
||||
/// source's reason is the one the operator is told about, in the same order the
|
||||
/// caller expressed a preference for successes.
|
||||
///
|
||||
/// `get` selects which trait method to drive, so the base and forensic paths
|
||||
/// share ONE implementation of this rule and cannot drift apart — the forensic
|
||||
/// half carried the identical bug.
|
||||
// Drive `sources` in order, returning the first non-empty result and preserving Ok/Err when
|
||||
// nothing resolves. `get` selects the trait method so the base and forensic paths share one
|
||||
// implementation.
|
||||
fn first_non_empty(
|
||||
sources: &[Box<dyn KeySource>],
|
||||
get: impl Fn(&dyn KeySource, &dyn ResolveCtx) -> Result<Vec<UnitKey>, libfreemkv::Error>,
|
||||
@@ -121,26 +90,56 @@ fn first_non_empty(
|
||||
}
|
||||
|
||||
impl KeySource for MultiSource {
|
||||
/// Try each inner source in order; the FIRST to return a non-empty base Unit
|
||||
/// Key set wins, even if an earlier source failed. All sources exhausted →
|
||||
/// `Ok(empty)` if they all ANSWERED, or the first source failure if any could
|
||||
/// not (see [`MultiSource`]).
|
||||
// First inner source to return a non-empty base Unit Key set wins. All
|
||||
// exhausted -> `Ok(empty)` if all answered, else the first failure (see
|
||||
// [`MultiSource`]).
|
||||
fn get_unit_keys(&self, ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, libfreemkv::Error> {
|
||||
first_non_empty(&self.sources, |s, c| s.get_unit_keys(c), ctx)
|
||||
}
|
||||
|
||||
/// Forensic-index counterpart: try each inner source's `get_fmts_indexes` in
|
||||
/// the same order and return the first non-empty set, under the same
|
||||
/// failure-preserving rule. A source with no forensic material (the keydb,
|
||||
/// via the trait default) contributes empty and is skipped; on today's discs
|
||||
/// the online source answers.
|
||||
// De-conflated counterpart: first inner source with keys wins; else the
|
||||
// richest miss survives composition (a MATCHED inner source beats a plain
|
||||
// miss). Same Err/Ok failure contract as `get_unit_keys`.
|
||||
fn resolve_unit_keys(
|
||||
&self,
|
||||
ctx: &dyn ResolveCtx,
|
||||
) -> Result<libfreemkv::keysource::UnitKeyResolution, libfreemkv::Error> {
|
||||
let mut first_failure: Option<libfreemkv::Error> = None;
|
||||
let mut matched: Option<libfreemkv::keysource::UnitKeyResolution> = None;
|
||||
for s in &self.sources {
|
||||
match s.resolve_unit_keys(ctx) {
|
||||
Ok(r) if !r.keys.is_empty() => return Ok(r),
|
||||
Ok(r) => {
|
||||
if r.matched && matched.is_none() {
|
||||
matched = Some(r);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if first_failure.is_none() {
|
||||
first_failure = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(r) = matched {
|
||||
return Ok(r);
|
||||
}
|
||||
match first_failure {
|
||||
Some(e) => Err(e),
|
||||
None => Ok(libfreemkv::keysource::UnitKeyResolution::default()),
|
||||
}
|
||||
}
|
||||
|
||||
// Forensic-index counterpart to `get_unit_keys`, same failure-preserving
|
||||
// rule. A source with no forensic material (keydb, via the trait default)
|
||||
// contributes empty and is skipped; today the online source answers.
|
||||
fn get_fmts_indexes(&self, ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, libfreemkv::Error> {
|
||||
first_non_empty(&self.sources, |s, c| s.get_fmts_indexes(c), ctx)
|
||||
}
|
||||
|
||||
/// UNION every inner source's host certs (filtered at the given MKB
|
||||
/// generation). Without this a composed source would hide an inner source's
|
||||
/// cert from the OEM cert-auth route — the gap this fixes.
|
||||
/// UNION every inner source's host certs, in source order (`mkb` is passed
|
||||
/// through unchanged). Without this a composed source would hide an inner
|
||||
/// source's cert from the OEM cert-auth route — the gap this fixes.
|
||||
fn host_certs(&self, mkb: Option<u32>) -> Vec<libfreemkv::aacs::types::HostCert> {
|
||||
self.sources
|
||||
.iter()
|
||||
@@ -151,4 +150,200 @@ impl KeySource for MultiSource {
|
||||
fn label(&self) -> &'static str {
|
||||
"multi"
|
||||
}
|
||||
|
||||
// KU3-6: depends on samples if ANY inner source does, so a nested
|
||||
// sample-dependent source keeps getting the per-piece ask.
|
||||
fn answer_depends_on_samples(&self) -> bool {
|
||||
self.sources.iter().any(|s| s.answer_depends_on_samples())
|
||||
}
|
||||
|
||||
// Same `any(inner)` rule (KU-K1 review): `resolve` retries the composition
|
||||
// whenever any inner source's last failure was transport-class.
|
||||
fn last_failure_was_transport(&self) -> bool {
|
||||
self.sources.iter().any(|s| s.last_failure_was_transport())
|
||||
}
|
||||
|
||||
// KU J23: any inner source that consumes the VID makes the composition one.
|
||||
fn uses_vid(&self) -> bool {
|
||||
self.sources.iter().any(|s| s.uses_vid())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use libfreemkv::aacs::types::HostCert;
|
||||
|
||||
// A minimal KeySource that only supplies host certs — the sole MultiSource
|
||||
// behaviour under test. It never resolves a unit key.
|
||||
struct CertOnly {
|
||||
certs: Vec<HostCert>,
|
||||
}
|
||||
impl KeySource for CertOnly {
|
||||
fn get_unit_keys(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, libfreemkv::Error> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
fn host_certs(&self, _mkb: Option<u32>) -> Vec<HostCert> {
|
||||
self.certs.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// A host cert tagged with a distinct marker byte so a returned set can be
|
||||
// identified exactly. Synthetic bytes only; no real key material.
|
||||
fn cert(marker: u8) -> HostCert {
|
||||
HostCert {
|
||||
private_key: [marker; 20],
|
||||
certificate: vec![marker; 92],
|
||||
private_key_v2: None,
|
||||
certificate_v2: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn boxed(certs: Vec<HostCert>) -> Box<dyn KeySource> {
|
||||
Box::new(CertOnly { certs }) as Box<dyn KeySource>
|
||||
}
|
||||
|
||||
/// `MultiSource::host_certs` must UNION every inner source's certs. Without
|
||||
/// this a composed source hides an inner source's cert from the OEM
|
||||
/// cert-auth route — the gap the union closes.
|
||||
#[test]
|
||||
fn multi_source_host_certs_unions_every_inner_source() {
|
||||
let multi = MultiSource::new(vec![
|
||||
boxed(vec![cert(0x01)]),
|
||||
boxed(vec![cert(0x02), cert(0x03)]),
|
||||
]);
|
||||
let mut markers: Vec<u8> = multi
|
||||
.host_certs(Some(50))
|
||||
.iter()
|
||||
.map(|c| c.certificate[0])
|
||||
.collect();
|
||||
markers.sort_unstable();
|
||||
assert_eq!(
|
||||
markers,
|
||||
vec![0x01, 0x02, 0x03],
|
||||
"certs from every inner source must be unioned"
|
||||
);
|
||||
}
|
||||
|
||||
/// With no inner source holding a cert, the union is empty (not an error).
|
||||
#[test]
|
||||
fn multi_source_host_certs_empty_when_no_source_has_one() {
|
||||
let multi = MultiSource::new(vec![boxed(vec![]), boxed(vec![])]);
|
||||
assert!(multi.host_certs(None).is_empty());
|
||||
}
|
||||
|
||||
// A source whose `answer_depends_on_samples` is fixed to the given value —
|
||||
// the only behaviour KSK2 exercises.
|
||||
struct FixedDependency(bool);
|
||||
impl KeySource for FixedDependency {
|
||||
fn get_unit_keys(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, libfreemkv::Error> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
fn answer_depends_on_samples(&self) -> bool {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
// KSK2: `MultiSource::answer_depends_on_samples` is `any(inner)`, so nesting
|
||||
// a sample-dependent online source under a sample-independent keydb still
|
||||
// gets the per-piece ask (`resolve` must not treat the pair as keydb-only).
|
||||
#[test]
|
||||
fn multi_source_answer_depends_on_samples_is_any_inner() {
|
||||
let all_independent = MultiSource::new(vec![
|
||||
Box::new(FixedDependency(false)) as Box<dyn KeySource>,
|
||||
Box::new(FixedDependency(false)) as Box<dyn KeySource>,
|
||||
]);
|
||||
assert!(!all_independent.answer_depends_on_samples());
|
||||
|
||||
let keydb_then_online = MultiSource::new(vec![
|
||||
Box::new(FixedDependency(false)) as Box<dyn KeySource>,
|
||||
Box::new(FixedDependency(true)) as Box<dyn KeySource>,
|
||||
]);
|
||||
assert!(keydb_then_online.answer_depends_on_samples());
|
||||
}
|
||||
|
||||
// A source whose `uses_vid` is fixed to the given value.
|
||||
struct FixedVid(bool);
|
||||
impl KeySource for FixedVid {
|
||||
fn get_unit_keys(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, libfreemkv::Error> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
fn uses_vid(&self) -> bool {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
// KU J23: like `answer_depends_on_samples`, a composed source uses the VID if any
|
||||
// inner one does, so a nested online source still makes a Missing piece VID-derivable.
|
||||
#[test]
|
||||
fn multi_source_uses_vid_is_any_inner() {
|
||||
let neither = MultiSource::new(vec![
|
||||
Box::new(FixedVid(false)) as Box<dyn KeySource>,
|
||||
Box::new(FixedVid(false)) as Box<dyn KeySource>,
|
||||
]);
|
||||
assert!(!neither.uses_vid());
|
||||
let keydb_then_online = MultiSource::new(vec![
|
||||
Box::new(FixedVid(false)) as Box<dyn KeySource>,
|
||||
Box::new(FixedVid(true)) as Box<dyn KeySource>,
|
||||
]);
|
||||
assert!(keydb_then_online.uses_vid());
|
||||
}
|
||||
|
||||
// A source whose `last_failure_was_transport` is fixed to the given value.
|
||||
struct FixedTransport(bool);
|
||||
impl KeySource for FixedTransport {
|
||||
fn get_unit_keys(&self, _ctx: &dyn ResolveCtx) -> Result<Vec<UnitKey>, libfreemkv::Error> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
fn last_failure_was_transport(&self) -> bool {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
// For consistency with `answer_depends_on_samples` (KU3-6): a composed
|
||||
// source's last-failure verdict is `any(inner)` too, so `resolve` retries
|
||||
// the composition whenever ANY inner source's last failure was transport.
|
||||
#[test]
|
||||
fn multi_source_last_failure_was_transport_is_any_inner() {
|
||||
let none_transport = MultiSource::new(vec![
|
||||
Box::new(FixedTransport(false)) as Box<dyn KeySource>,
|
||||
Box::new(FixedTransport(false)) as Box<dyn KeySource>,
|
||||
]);
|
||||
assert!(!none_transport.last_failure_was_transport());
|
||||
|
||||
let one_transport = MultiSource::new(vec![
|
||||
Box::new(FixedTransport(false)) as Box<dyn KeySource>,
|
||||
Box::new(FixedTransport(true)) as Box<dyn KeySource>,
|
||||
]);
|
||||
assert!(one_transport.last_failure_was_transport());
|
||||
}
|
||||
|
||||
/// The `mkb` generation must reach each inner source so its OWN revocation
|
||||
/// filter can act; a source that drops everything at a given generation is
|
||||
/// honoured by the union.
|
||||
#[test]
|
||||
fn multi_source_host_certs_passes_mkb_through_to_inner_sources() {
|
||||
struct GenAware;
|
||||
impl KeySource for GenAware {
|
||||
fn get_unit_keys(
|
||||
&self,
|
||||
_c: &dyn ResolveCtx,
|
||||
) -> Result<Vec<UnitKey>, libfreemkv::Error> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
fn host_certs(&self, mkb: Option<u32>) -> Vec<HostCert> {
|
||||
match mkb {
|
||||
// Modelled on a cert revoked in MKBv72.
|
||||
Some(g) if g >= 72 => Vec::new(),
|
||||
_ => vec![cert(0xaa)],
|
||||
}
|
||||
}
|
||||
}
|
||||
let multi = MultiSource::new(vec![Box::new(GenAware) as Box<dyn KeySource>]);
|
||||
assert_eq!(multi.host_certs(Some(50)).len(), 1, "usable below gen 72");
|
||||
assert!(
|
||||
multi.host_certs(Some(72)).is_empty(),
|
||||
"the gen must reach the inner source's filter"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+2187
-626
File diff suppressed because it is too large
Load Diff
+5
-10
@@ -1,16 +1,11 @@
|
||||
//! Where the `keydb.cfg` lives: local to the executable, local ONLY.
|
||||
//!
|
||||
//! Key-path policy belongs with the key sources (this crate), not the library:
|
||||
//! libfreemkv is handed a path and reads it. The CLI/app asks here for the
|
||||
//! list of locations to *search* (first existing wins) and for the single
|
||||
//! *default* location to write to (e.g. `update-keys`/save).
|
||||
//! Key-path policy belongs here (key sources), not in libfreemkv, which
|
||||
//! just reads a handed-in path. This module gives the *search* list (first
|
||||
//! existing wins) and the single *default* write location.
|
||||
//!
|
||||
//! freemkv is a portable, standalone binary: the `keydb.cfg` lives *next to*
|
||||
//! the executable — `<dir of current exe>/keydb.cfg` — and nowhere else. There
|
||||
//! is no OS-specific config-dir lookup (`%APPDATA%`, `%USERPROFILE%\.config`,
|
||||
//! `$XDG_CONFIG_HOME`, `$HOME/.config`). Drop the exe and its `keydb.cfg` in
|
||||
//! the same folder and it works. Callers needing a custom location pass
|
||||
//! `--keydb PATH`, which bypasses this module entirely.
|
||||
//! freemkv is portable: `keydb.cfg` lives next to the exe — `<dir of current exe>/keydb.cfg` —
|
||||
//! with no OS config-dir fallback. `--keydb PATH` bypasses this module.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
+25
-29
@@ -1,15 +1,12 @@
|
||||
//! Fixture-based integration tests for the published key sources.
|
||||
//!
|
||||
//! These exercise the *public* surface of `freemkv-keysources` end-to-end —
|
||||
//! real files on disk, the real `KeyDb` parser from libfreemkv, and
|
||||
//! the `KeySource` trait (`get_unit_keys` over a `ResolveCtx`) the applications drive.
|
||||
//!
|
||||
//! Covered:
|
||||
//! - `KeydbSource`: terminal unit-key lookup by disc hash through a real
|
||||
//! `keydb.cfg`; the exe-local path helpers; MKB-aware host-cert serving.
|
||||
//! - `OnlineSource`: SSRF/scheme validation and the unconfigured no-op.
|
||||
//! - `MultiSource`: caller-supplied ordering / precedence, host-cert UNION,
|
||||
//! and nesting.
|
||||
//! real files on disk, the real `KeyDb` parser from libfreemkv, and the
|
||||
//! `KeySource` trait (`get_unit_keys` over a `ResolveCtx`) the applications
|
||||
//! drive. Covered: `KeydbSource` terminal unit-key lookup by disc hash
|
||||
//! (plus exe-local path helpers and MKB-aware host-cert serving);
|
||||
//! `OnlineSource` SSRF/scheme validation and the unconfigured no-op; and
|
||||
//! `MultiSource` ordering/precedence, host-cert UNION, and nesting.
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -141,7 +138,7 @@ fn keydb_source_label_is_keydb() {
|
||||
assert_eq!(KeydbSource::new("/nonexistent/keydb.cfg").label(), "keydb");
|
||||
}
|
||||
|
||||
// ── KeydbSource: MKB-aware host-cert serving from a file ────────────────────
|
||||
// ── KeydbSource: host-cert serving from a file ──────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn keydb_source_serves_host_cert_from_hc_row() {
|
||||
@@ -155,14 +152,13 @@ fn keydb_source_serves_host_cert_from_hc_row() {
|
||||
let path = s.write("keydb.cfg", &line);
|
||||
|
||||
let src = KeydbSource::new(&path);
|
||||
// Inherent (no-MKB, scan-options) form.
|
||||
// Inherent (scan-options) form.
|
||||
assert_eq!(
|
||||
src.host_certs().len(),
|
||||
1,
|
||||
"inherent host_certs sees the HC row"
|
||||
);
|
||||
// Trait form now wires the MKB generation through (no revocation annotation
|
||||
// → always returned).
|
||||
// Trait form: `mkb` is ignored; the ranked list is returned.
|
||||
let via_trait = KeySource::host_certs(&src, Some(70));
|
||||
assert_eq!(via_trait.len(), 1, "trait host_certs sees the HC row");
|
||||
assert_eq!(via_trait[0].certificate.len(), 92);
|
||||
@@ -232,13 +228,14 @@ fn online_source_metadata() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_keyserver_url_gates_scheme_and_ssrf() {
|
||||
fn validate_keyserver_url_gates_scheme_and_allows_lan() {
|
||||
assert!(validate_keyserver_url("https://8.8.8.8/keys").is_ok());
|
||||
// Home app: LAN/loopback/link-local hosts pass; cleartext http and invalid addresses do not.
|
||||
assert!(validate_keyserver_url("https://127.0.0.1/keys").is_ok());
|
||||
assert!(validate_keyserver_url("https://169.254.1.5/keys").is_ok());
|
||||
assert!(validate_keyserver_url("https://[::1]:9000/keys").is_ok());
|
||||
assert!(validate_keyserver_url("http://127.0.0.1/keys").is_err());
|
||||
// SSRF blocking is IP-based, not scheme-gated: https://<private-IP> is rejected too.
|
||||
assert!(validate_keyserver_url("https://127.0.0.1/keys").is_err());
|
||||
assert!(validate_keyserver_url("http://169.254.169.254/latest/meta-data/").is_err());
|
||||
assert!(validate_keyserver_url("http://[::1]:9000/keys").is_err());
|
||||
assert!(validate_keyserver_url("https://0.0.0.0/keys").is_err());
|
||||
assert!(validate_keyserver_url("ftp://example.com/keys").is_err());
|
||||
assert!(validate_keyserver_url("").is_err());
|
||||
}
|
||||
@@ -270,6 +267,15 @@ fn uk(b: u8) -> UnitKey {
|
||||
UnitKey::new(0, [b; 16])
|
||||
}
|
||||
|
||||
/// `MultiSource` itself reports a fixed label, distinct from any inner
|
||||
/// source's own label — this is what a rip's source-attribution log shows
|
||||
/// when the composition, not one of its members, answered.
|
||||
#[test]
|
||||
fn multi_source_label_is_multi() {
|
||||
let multi = MultiSource::new(vec![Box::new(ScriptedSource::new("A", Vec::new()))]);
|
||||
assert_eq!(multi.label(), "multi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_source_first_non_empty_wins_in_caller_order() {
|
||||
// Caller supplies [A, B]; A is non-empty so A's keys win.
|
||||
@@ -362,13 +368,7 @@ fn multi_source_real_keydb_resolves_through_chain() {
|
||||
);
|
||||
}
|
||||
|
||||
// ── MultiSource: a source that could not ANSWER is not a source that said no ─
|
||||
//
|
||||
// `MultiSource` re-collapsed the Ok/Err distinction `OnlineSource` was
|
||||
// engineered to draw: `if let Ok(uks) = s.get_unit_keys(ctx)` discarded every
|
||||
// `Err`, so a composition whose sources ALL failed returned `Ok(Vec::new())` —
|
||||
// which the resolver reports as `E7022 No key source has a decryption key for
|
||||
// this disc`. That is the seven-hour-502 incident reproduced one layer up.
|
||||
// ── MultiSource: a source that could not ANSWER is not a source that said no ─.
|
||||
|
||||
/// A source that always fails, with a chosen error — the key-service outage.
|
||||
struct FailingSource {
|
||||
@@ -396,10 +396,6 @@ fn unauthorized() -> libfreemkv::Error {
|
||||
libfreemkv::Error::KeyServiceUnauthorized
|
||||
}
|
||||
|
||||
/// THE regression, one layer up from `interpret_reply`: when no source holds a
|
||||
/// key AND a source could not answer, the composition must report the FAILURE,
|
||||
/// never a clean "no key". Catches the mutation that restores
|
||||
/// `if let Ok(uks) = ..` (which drops the `Err` and returns `Ok(empty)`).
|
||||
#[test]
|
||||
fn multi_source_reports_a_source_failure_instead_of_a_clean_no_key() {
|
||||
let multi = MultiSource::new(vec![
|
||||
|
||||
Reference in New Issue
Block a user