test(doubles): land the wave-1 in-memory seams + unified ed25519 fixtures

The `w1-test-doubles` task: the in-memory seams every later wave's
contract tests run against, behind their production seams.

- tests/common/mod.rs — the doubles, test-only (nothing reachable from
  lib.rs, no feature): MemIdentityProvider (registered fingerprints ->
  alkcall Identity resolutions over the IdentityProvider trait; token
  path resolves none per ADR-004), TestVault (a wrapper over alkvault
  0.1's shipped in-memory construction — resolving the flagged
  vault-double question: wrap, not re-impl), MemHostKeyPolicy
  (exact (host, fingerprint) rules + explicit default verdict,
  fail-closed by default, inspectable decision log), and the fixture
  table. Module docs record the location/vault/fixture decisions and
  the row/stamp discipline (greppable Contract stamps).
- tests/vectors/ — one unified fixture set: four ssh-keygen-minted
  ed25519 keypairs (committed .key + .pub + fingerprints.txt recorded
  from ssh-keygen -lf, OpenSSH_9.6p1). The previous inline PEM
  constants in src/key_source.rs were rejected by OpenSSH itself
  (russh-only decodable); all keys were re-minted and the overlapping
  vector set retired — one fixture set now serves the parity tests,
  the custody round-trip, and every later auth/mapping test
  (src-unit tests share it via include_str!).
- tests/doubles.rs — the end-to-end rows: register -> resolve,
  seal -> source -> decode (+ locked/absent/tampered postures),
  host-key set in order, policy verdicts honored-and-logged.
- tests/fingerprint_parity.rs and src/key_source.rs tests moved onto
  the shared fixture files; src changes are #cfg(test) only.

Verification: cargo test (33 + 9 + 5 green; --all-features also green),
cargo clippy --all-targets -- -D warnings clean, cargo fmt --check
clean, taskgraph validate ok (12 tasks).
This commit is contained in:
glm-5.3-flash committed 2026-10-28 07:09:17 +00:00
1 parent 949d0bbbe4
commit 214690b9c2
14 files changed
+933 -113

No files matched your search

+32 -33
View File
@@ -235,39 +235,18 @@ mod tests {
use crate::error::KeySourceError;
use crate::fingerprint::fingerprint;
/// Throwaway test fixtures generated at vector time with
/// `ssh-keygen -t ed25519` (OpenSSH 9.6p1); recorded here as
/// committed test constants — throwaway secrets, never deployed,
/// never registered anywhere real (the w1-test-doubles posture).
/// `ALPHA` is the client-dial fixture; `BRAVO`/`CHARLIE` are two
/// independent host-key fixtures.
const ALPHA_PEM: &str = concat!(
"-----BEGIN OPENSSH PRIVATE KEY-----\n",
"b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\n",
"QyNTUxOQAAACA7vQRGU0CqQMrqRUJef/2aZK/bR2mxnzBWWZHMUkq8/wAAAJgexTRBHsU0\n",
"QQAAAAtzc2gtZWQyNTUxOQAAACA7vQRGU0CqQMrqRUJef/2aZK/bR2mxnzBWWZHMUkq8/w\n",
"AAAEAjImC/Ay2J2FejKJYNoRllIF9+ed5Qmdd7erLCisK03Du9BEZTQKpAyupFQl5//Zpk\n",
"r9tHabGfMFZZkcxSSrz/AAAAEmFsa3NzaC13MS1rcy1hbHBoYQECAw==\n",
"-----END OPENSSH PRIVATE KEY-----\n",
);
const BRAVO_PEM: &str = concat!(
"-----BEGIN OPENSSH PRIVATE KEY-----\n",
"b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\n",
"QyNTUxOQAAACDqobhDdf1GvNXSd8tK6HHp9tIlhWooOWSp+MS8jRTlTAAAAJjsjCqp7Iwq\n",
"qQAAAAtzc2gtZWQyNTUxOQAAACDqobhDdf1GvNXSd8tK6HHp9tIlhWooOWSp+MS8jRTlTA\n",
"AAAEDe5J9K933yhWNUQmQ6VdT1zBN9eu3/39IoR0Q+4hXpheqhuEN1/Ua81dJ3y0rocen2\n",
"0iWFaig5ZKn4xLyNFOVMAAAAEmFsa3NzaC13MS1rcy1icmF2bwECAw==\n",
"-----END OPENSSH PRIVATE KEY-----\n",
);
const CHARLIE_PEM: &str = concat!(
"-----BEGIN OPENSSH PRIVATE KEY-----\n",
"b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\n",
"QyNTUxOQAAACAnkAMvpNU6zs25fONkvgqt5E0V8eR8ySnFzv5OOdU2OQAAAJi/2ytvv9sr\n",
"bwAAAAtzc2gtZWQyNTUxOQAAACAnkAMvpNU6zs25fONkvgqt5E0V8eR8ySnFzv5OOdU2OQ\n",
"AAAEBqCsZ6AAe7S+AedrAYPjMBIcW10vHlaEWdIGH7jJQefCeQAy+k1TrOzbl842S+Cq3k\n",
"TRXx5HzJKcXO/k451TY5AAAAFGFsa3NzaC13MS1rcy1jaGFybGllAQ==\n",
"-----END OPENSSH PRIVATE KEY-----\n",
);
/// The committed test-key fixtures (`tests/vectors/`) — the
/// wave-1 fixture set, read by this `#[cfg(test)]` module through
/// `include_str!` (the `tests/common/mod.rs` double module is the
/// tests/-side owner; src-unit tests never own fixture copies).
/// Throwaway `ssh-keygen`-minted keys, never registered anywhere
/// real (`ALPHA` is the client-dial fixture; `BRAVO`/`CHARLIE`
/// the host-key pair). Their `ssh-keygen -lf` fingerprints are
/// recorded in `tests/vectors/fingerprints.txt` — the fixture
/// parity row below asserts this module sees the same values.
const ALPHA_PEM: &str = include_str!("../tests/vectors/alpha.key");
const BRAVO_PEM: &str = include_str!("../tests/vectors/bravo.key");
const CHARLIE_PEM: &str = include_str!("../tests/vectors/charlie.key");
fn unlocked_vault() -> alkvault::VaultServiceHandle {
let vault = alkvault::VaultServiceHandle::new();
@@ -275,6 +254,26 @@ mod tests {
vault
}
/// Fixture parity for this module's `include_str!` reads: the
/// computed fingerprints match the independent-issuer record
/// (`ssh-keygen -lf`, `tests/vectors/fingerprints.txt`).
#[test]
fn fixture_keys_match_their_ssh_keygen_recorded_fingerprints() {
let manifest = include_str!("../tests/vectors/fingerprints.txt");
for (name, pem) in [
("alpha", ALPHA_PEM),
("bravo", BRAVO_PEM),
("charlie", CHARLIE_PEM),
] {
let key = russh::keys::decode_secret_key(pem, None).expect("fixture PEM decodes");
let fp = fingerprint(key.public_key());
assert!(
manifest.contains(&format!("{name}.pub {fp}")),
"{name}'s computed fingerprint is not the recorded value"
);
}
}
fn encrypt(vault: &alkvault::VaultServiceHandle, pem: &str) -> alkvault::EncryptedData {
vault
.encrypt(pem, CURRENT_KEY_VERSION)
+67 -3
View File
@@ -1,7 +1,7 @@
---
id: w1-test-doubles
name: Wave 1 — test doubles (the in-memory seams)
status: pending
status: completed
depends_on: [w1-error-model, w1-key-sources, w1-host-key-policy]
scope: narrow
risk: low
@@ -64,8 +64,72 @@ non-test code.
## Notes
> To be filled by implementation agent
- **Vault-double shape (recorded, flagged for the wave-1 gate):** alkvault
0.1 ships an in-memory construction — `VaultServiceHandle::new()` is an
in-process vault that `unlock_new` arms in-memory. The double is a
**wrapper** (`TestVault` in `tests/common/mod.rs`) around that
construction, not a local re-impl behind the source trait: the real
decrypt taxonomy, GCM integrity behavior, and rotation semantics are
exercised verbatim through the production `VaultKeySource` /
`VaultHostKeySource` seams, and the POC-2 §3 postures (locked /
absent / tampered / wrong seed) ride the real seams.
- **Fixture decision (recorded):** committed static OpenSSH-PEM test
keys, minted by `ssh-keygen` at vector-generation time — not generated
at test time (determinism, no tooling dependency). Committed private
halves are throwaway fixtures by the task's own posture, documented as
such in `tests/vectors/fingerprints.txt` and in the common module docs.
- **Location decision (recorded):** the doubles live only under
`tests/common/mod.rs`, reached with `mod common;` — the alkgit
convention, adapted. Nothing doubles-related is reachable from
`lib.rs`; no feature gates them. `src/`-unit tests that need the
fixture material read the committed *files* via `include_str!`
(`src/key_source.rs` tests), never a copy — the single fixture set is
shared by the src-side and tests/-side suites. dead_code is allowed
module-wide in `tests/common/` (each test binary compiles the whole
shared module and never exercises all of it).
- **The fixture set was unified and re-minted (flagged for the gate):**
the previous state had two overlapping throwaway sets — public-only
vectors under `tests/vectors/` (fingerprint parity) and inline
private-PEM constants in `src/key_source.rs` — and the inline PEM
constants, while decodable by russh, were **rejected by OpenSSH
itself** (`ssh-keygen` → "error in libcrypto"): unusable as
cross-issuer fixtures. All four keys (`alpha`/`bravo`/`charlie`/
`delta`) were freshly minted with OpenSSH_9.6p1's ssh-keygen, their
fingerprints recorded byte-verbatim in `tests/vectors/fingerprints.txt`,
and the two sets unified into one fixture table (`FIXTURE_KEYS`) —
committed `.key` + `.pub` files, served to the fingerprint parity
tests, the vault round-trip, and every later auth/mapping test. The
old vector-only set was retired (its keys had no private halves).
- The row/stamp discipline is established in `tests/common/mod.rs`'s
module docs (adapted from the sibling crates): rows state properties
and carry greppable `Contract stamp:` items citing the pinning
ADR § — `grep -rn "Contract stamp:" tests/` is the inventory (11
stamps across the 11 landed rows).
- No `src/` non-test changes: `src/key_source.rs` changed only inside
its `#[cfg(test)]` module (fixture `include_str!`s + one parity row);
`tests/common/mod.rs` and `tests/doubles.rs` are the new surfaces.
## Summary
> To be filled on completion
The wave-1 test-double convention landed: `tests/common/mod.rs` carries
`MemIdentityProvider` (registered fingerprints → alkcall `Identity`
resolutions; token path resolves none — ADR-004's fingerprint-only
disposition), `TestVault` (a wrapper over alkvault 0.1's shipped
in-memory construction — the pinned resolution of the flagged
vault-double question), `MemHostKeyPolicy` (exact `(host, fingerprint)`
rules + explicit default verdict — fail-closed by default, permissive
only by explicit choice — with a fully inspectable decision log), and
the unified ed25519 fixture table (`FIXTURE_KEYS`: four committed
`ssh-keygen`-minted OpenSSH-PEM key pairs with recorded
`ssh-keygen -lf` fingerprints in `tests/vectors/fingerprints.txt`). The
end-to-end rows in `tests/doubles.rs` run the seams through the
production surfaces: register → resolve, seal → source → decode
(round-trip and the locked/absent/tampered postures), verdict →
honored-and-logged. `src/key_source.rs` and
`tests/fingerprint_parity.rs` were moved onto the shared fixture files
(one fixture set, no copies; the previous inline PEMs were discovered
to be OpenSSH-rejected and were replaced by freshly minted keys). All
doubles live in test-only compilation (`tests/`), with nothing
doubles-related reachable from the public API. Verified: `cargo test`
(33 + 9 + 5 green; also `--all-features`), `cargo clippy --all-targets
-- -D warnings`, `cargo fmt --check` clean.
+369
View File
@@ -0,0 +1,369 @@
//! alkssh's wave-1 test doubles — the in-memory seams every later
//! wave's contract tests run against (the alkgit `tests/common/`
//! convention, adapted): an in-memory `IdentityProvider`
//! ([`MemIdentityProvider`]), the in-memory vault seam ([`TestVault`]),
//! an in-memory `HostKeyPolicy` ([`MemHostKeyPolicy`]), and the
//! ed25519 test-key fixtures ([`FIXTURE_KEYS`]).
//!
//! **Location decision (recorded, the OQ-03 freeze-inventory
//! discipline):** the doubles live *only* here — a shared module under
//! `tests/` reached with `mod common;` — and never in the public API:
//! no re-export from `lib.rs`, no feature gate, nothing doubles-related
//! reachable from the crate root. The doubles' shapes are test
//! surface, test-versioned freely; the only `src/`-side consumers are
//! `#[cfg(test)]` modules reading the fixture *files* through
//! `include_str!`, never a public export.
//!
//! **Vault double decision (recorded, flagged for the wave-1 gate):**
//! alkvault 0.1 ships an in-memory construction —
//! `VaultServiceHandle::new()` is an in-process vault that
//! `unlock_new` arms in-memory. The double is therefore a *wrapper*
//! ([`TestVault`]) around that construction, not a local re-impl
//! behind the source trait: the same decrypt error taxonomy, GCM
//! integrity behavior, and rotation semantics the production source
//! seam sees are exercised verbatim, and the POC-2 §3 failure postures
//! (locked / absent / tampered / wrong seed) ride the real seams.
//!
//! **Fixture decision (recorded):** committed static OpenSSH-PEM test
//! keys, minted by `ssh-keygen` at vector-generation time (the
//! independent issuer), never generated at test time — determinism and
//! no tooling dependency. The committed private halves are throwaway
//! fixtures, not secrets: minted for this suite only, never registered
//! anywhere real, never deployed. Each key's fingerprint is recorded
//! byte-verbatim from `ssh-keygen -lf` in
//! `tests/vectors/fingerprints.txt`; [`FIXTURE_KEYS`] carries the
//! recorded values and every fingerprint comparison in the suite
//! asserts parity against them.
//!
//! Each test binary's `mod common;` compiles the whole shared module
//! and no single binary exercises every double — dead_code is allowed
//! module-wide so one binary's suite never fails the next's.
//!
//! The row/stamp discipline (adapted from the sibling crates): test
//! functions are *rows* — the name states the property, not the
//! caller — and each carries a doc-comment list item beginning
//! **`Contract stamp:`** citing the pinning spec §/ADR § (never a bare
//! wave or date). The marker is greppable:
//! `grep -rn "Contract stamp:" tests/` is the inventory.
#![allow(dead_code)]
use std::collections::HashMap;
use alkcall::core::auth::{AuthToken, Identity, IdentityProvider};
use alkvault::{EncryptedData, VaultServiceHandle};
use async_trait::async_trait;
use russh::keys::{PrivateKey, PublicKey, decode_secret_key, parse_public_key_base64};
use alkssh::{HostKeyPolicy, PresentedHostKey};
/// One committed ed25519 test-key fixture: the private OpenSSH-PEM
/// (`tests/vectors/<name>.key`), the public one-line blob
/// (`tests/vectors/<name>.pub`), and the fingerprint recorded
/// byte-verbatim from `ssh-keygen -lf` (`tests/vectors/fingerprints.txt`).
///
/// The `pem`/`blob`/`fingerprint` values are committed constants — throwaway
/// fixture material, never registered or deployed anywhere real.
#[derive(Debug)]
pub struct FixtureKey {
/// The fixture's name (`alpha` … `delta`).
pub name: &'static str,
/// The OpenSSH private-key PEM, included verbatim from the
/// committed fixture file.
pub pem: &'static str,
/// The public key's base64 blob — the OpenSSH one-line encoding,
/// `ssh-ed25519 <blob>` stripped of the key-type prefix and comment.
pub blob: &'static str,
/// The fingerprint recorded from `ssh-keygen -lf` — the
/// independent-issuer value every comparison in the suite asserts
/// parity against (ADR-004 decision 1's contract invariant).
pub fingerprint: &'static str,
}
impl FixtureKey {
/// The decoded public key (parity input; ADR-004 decision 1).
pub fn public_key(&self) -> PublicKey {
parse_public_key_base64(self.blob).expect("committed fixture blob parses")
}
/// The decoded private key — `decode_secret_key(pem, None)`, the
/// ADR-005 decision 2 conversion (the fixtures' inner format is
/// exactly the at-rest contract: unencrypted OpenSSH PEM).
pub fn private_key(&self) -> PrivateKey {
decode_secret_key(self.pem, None).expect("committed fixture PEM decodes")
}
}
/// The wave-1 fixture set: four minted ed25519 keys. `alpha` is the
/// canonical client-dial fixture; `bravo`/`charlie` are the host-key
/// pair (rotation/multi-key rows); `delta` is the independent extra
/// (unregistered-peer, mismatch, and wrong-key rows).
pub const FIXTURE_KEYS: &[FixtureKey] = &[
FixtureKey {
name: "alpha",
pem: include_str!("../vectors/alpha.key"),
blob: "AAAAC3NzaC1lZDI1NTE5AAAAIM28sP5UAl3npJJ8trZgPt2kE+Af6477/Rx6J2VbM14z",
fingerprint: "SHA256:YyaiL42Xl8tcRRUhNCiqQBsiiRWVHHDR9hpXC45EQ6A",
},
FixtureKey {
name: "bravo",
pem: include_str!("../vectors/bravo.key"),
blob: "AAAAC3NzaC1lZDI1NTE5AAAAIMvu9S+hH9Dug7fdiHLeW+B9kee+i495jPAdBH8YaOvQ",
fingerprint: "SHA256:yQf/w2wYBoZ9adDlRGbcNh7AHE3mbqWRCqoGHra1ib0",
},
FixtureKey {
name: "charlie",
pem: include_str!("../vectors/charlie.key"),
blob: "AAAAC3NzaC1lZDI1NTE5AAAAIKL8zAzLkR0jKFY2l+QrBjC3Cv4Q3OBUUCX7rQV1jXQe",
fingerprint: "SHA256:b5jdwosbZUx5nsY5zfr/ZCt+4sJYehVBq7Xl/wstJ9U",
},
FixtureKey {
name: "delta",
pem: include_str!("../vectors/delta.key"),
blob: "AAAAC3NzaC1lZDI1NTE5AAAAIIcMK+njeiGqh8THEBPU9MTYrSJ3WHvxv3ZH11JTS/SU",
fingerprint: "SHA256:4MiQ0KGoTEaklHBTlxPDuQkeu0FTyqEbne10K6Z8CKs",
},
];
/// Looks a fixture up by name — `fixture("alpha")` is the canonical
/// client-dial key.
pub fn fixture(name: &str) -> &'static FixtureKey {
FIXTURE_KEYS
.iter()
.find(|k| k.name == name)
.unwrap_or_else(|| panic!("no fixture named {name}"))
}
/// Builds an [`Identity`] for registration (`id` + scopes; the
/// `resources` map is empty — the wire layer only needs the resolved
/// identity, richer ACL shapes are the assembly's).
pub fn identity(id: &str, scopes: &[&str]) -> Identity {
Identity {
id: id.into(),
scopes: scopes.iter().map(|s| s.to_string()).collect(),
resources: HashMap::new(),
}
}
/// The in-memory [`IdentityProvider`] — registered fingerprints →
/// [`Identity`] resolutions over alkcall's trait verbatim.
///
/// The double the wave-2 auth bridge and dispatch tests resolve
/// against, and the analog of serving `Arc<dyn IdentityProvider>` in
/// [`alkssh::ServeConfig`]: the fingerprint → identity path is ADR-004
/// decision 1's only resolution shape.
pub struct MemIdentityProvider {
identities: parking_lot::RwLock<HashMap<String, Identity>>,
}
impl MemIdentityProvider {
/// An empty registry — nothing resolves until registered
/// (fail-closed: unregistered fingerprints decline).
pub fn new() -> Self {
Self {
identities: parking_lot::RwLock::new(HashMap::new()),
}
}
/// Registers (or replaces) one fingerprint → identity mapping.
pub fn register(&self, fingerprint: &str, identity: Identity) {
self.identities
.write()
.insert(fingerprint.to_string(), identity);
}
/// Removes a registration — the post-resolution-revoke shape.
pub fn revoke(&self, fingerprint: &str) {
self.identities.write().remove(fingerprint);
}
/// The registered fingerprints — inspection for test assertions.
pub fn registered_fingerprints(&self) -> Vec<String> {
let mut keys = self
.identities
.read()
.keys()
.cloned()
.collect::<Vec<String>>();
keys.sort();
keys
}
}
impl Default for MemIdentityProvider {
fn default() -> Self {
Self::new()
}
}
impl IdentityProvider for MemIdentityProvider {
/// The fingerprint path — ADR-004 decision 1's single resolution
/// shape: exact string equality against registered values.
fn resolve_from_fingerprint(&self, fingerprint: &str) -> Option<Identity> {
self.identities.read().get(fingerprint).cloned()
}
/// The token path resolves nothing in the SSH direction —
/// ADR-004's dispositions are fingerprint-only on this surface
/// (`auth-bridge.md`); the double never invents a token shape.
fn resolve_from_token(&self, _: &AuthToken) -> Option<Identity> {
None
}
}
/// The in-memory vault seam — a wrapper over alkvault 0.1's own
/// in-memory construction ([`VaultServiceHandle`], unlocked at
/// construction via `unlock_new`).
///
/// The locked/absent/tampered/mismatch failure postures (POC-2 §3) are
/// exercised *through the real production seams* the source traits
/// call, against this handle — the double exists so tests never touch
/// vault lifecycle decisions themselves: [`TestVault::unlocked`] arms
/// it, `lock` closes it, and the sources keep refusing, never
/// retrying (the crate never unlocks — ADR-005's separation).
#[derive(Clone)]
pub struct TestVault {
handle: VaultServiceHandle,
}
impl TestVault {
/// A freshly armed in-memory vault — unlocked, ready to encrypt.
pub fn unlocked() -> Self {
let handle = VaultServiceHandle::new();
handle
.unlock_new(24)
.expect("in-memory vault arms unlocked");
Self { handle }
}
/// The vault handle the production sources consume — clones share
/// the same vault state (locking through one is visible to all).
pub fn handle(&self) -> VaultServiceHandle {
self.handle.clone()
}
/// Seals one fixture key's PEM into an at-rest record — the
/// ADR-005 inner-format contract (unencrypted OpenSSH PEM in an
/// `EncryptedData` record).
pub fn seal(&self, key: &FixtureKey) -> EncryptedData {
self.encrypt(key.pem)
}
/// Encrypts an arbitrary plaintext (the non-key and tamper rows
/// need it; ADR-005 decision 4's decode-refusal shape).
pub fn encrypt(&self, plaintext: &str) -> EncryptedData {
self.handle
.encrypt(plaintext, alkvault::CURRENT_KEY_VERSION)
.expect("in-memory vault encrypts the plaintext")
}
/// Locks the vault — the locked-vault posture (POC-2 §3, the
/// `KeySourceError::Locked` refusal class).
pub fn lock(&self) {
self.handle.lock();
}
/// The vault's lifecycle state, for posture assertions.
pub fn is_unlocked(&self) -> bool {
self.handle.is_unlocked()
}
}
/// The in-memory [`HostKeyPolicy`] — a configurable verdict double with
/// an inspectable decision log, for the wave-4 dial tests (and the
/// case-1/case-2 rows of every host-key contract test).
///
/// Verdicts: a rules list of exact `(host, fingerprint)` matches —
/// last matching rule wins — falling back to a configurable
/// default verdict for unlisted pairs. The default of the
/// **default-constructed double is reject** (the crate's fail-closed
/// posture, `OQ-05`); permissive verdicts are the test's explicit
/// choice, mirroring "permissive policy is the assembly's to write".
/// Every decision is recorded with the full [`PresentedHostKey`]
/// context; the log is the inspectable record.
pub struct MemHostKeyPolicy {
default_verdict: bool,
rules: parking_lot::RwLock<Vec<Rule>>,
log: parking_lot::Mutex<Vec<Decision>>,
}
struct Rule {
host: String,
fingerprint: String,
accept: bool,
}
/// One recorded host-key decision — the presented key and the verdict
/// the policy returned.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Decision {
/// The presented key that was decided.
pub presented: PresentedHostKey,
/// The verdict: `true` = accept, `false` = decline.
pub accepted: bool,
}
impl MemHostKeyPolicy {
/// A decision double with the fail-closed default (reject
/// everything without a matching accept rule).
pub fn new() -> Self {
Self::with_default(false)
}
/// A decision double with an explicit default verdict — the
/// case-2 permissive posture (`accepting` doubles) is the test's
/// explicit choice, never a default.
pub fn with_default(default_verdict: bool) -> Self {
Self {
default_verdict,
rules: parking_lot::RwLock::new(Vec::new()),
log: parking_lot::Mutex::new(Vec::new()),
}
}
/// Adds a rule: exact `(host, fingerprint)` → verdict. Last match
/// wins, so later rules override earlier ones.
pub fn rule(&self, host: &str, fingerprint: &str, accept: bool) {
self.rules.write().push(Rule {
host: host.into(),
fingerprint: fingerprint.into(),
accept,
});
}
/// Pins an accept rule for one `(host, fingerprint)` — the case-1
/// trust-anchor shape (hard accept on exact match).
pub fn pin(&self, host: &str, fingerprint: &str) {
self.rule(host, fingerprint, true);
}
/// The decision log — every [`Decision`] in call order.
pub fn decisions(&self) -> Vec<Decision> {
self.log.lock().clone()
}
}
impl Default for MemHostKeyPolicy {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl HostKeyPolicy for MemHostKeyPolicy {
async fn decide(&self, presented: &PresentedHostKey) -> bool {
let rules = self.rules.read();
let verdict = rules
.iter()
.rev()
.find(|r| r.host == presented.host && r.fingerprint == presented.fingerprint)
.map(|r| r.accept)
.unwrap_or(self.default_verdict);
drop(rules);
self.log.lock().push(Decision {
presented: presented.clone(),
accepted: verdict,
});
verdict
}
}
+304
View File
@@ -0,0 +1,304 @@
//! The wave-1 doubles' end-to-end rows — the seams exercised through
//! the doubles exactly as the later waves' contract tests will run
//! against them: fingerprint registration resolution, the vault
//! custody round-trip and its failure postures, and the host-key
//! policy verdict path.
//!
//! The row/stamp discipline (see `tests/common/mod.rs`): assertions
//! are named rows stating the property; every row carries its
//! `Contract stamp:` citing the pinning §/ADR.
mod common;
use alkcall::core::auth::{AuthToken, IdentityProvider};
use alkssh::{
HostKeyPolicy, HostKeySource, KeySource, KeySourceError, PresentedHostKey, VaultHostKeySource,
VaultKeySource, fingerprint,
};
use russh::keys::signature::Signer;
use common::{FIXTURE_KEYS, MemHostKeyPolicy, MemIdentityProvider, TestVault, fixture, identity};
/// The registered fingerprint resolves with the registered identity;
/// unregistered fingerprints decline (fail-closed empty registry).
///
/// - *Contract stamp:* ADR-004 decision 1 (fingerprint →
/// `resolve_from_fingerprint` → `Some` = accept; `None` = reject).
/// - *Contract stamp:* ADR-005 context (client fingerprints never
/// involve the vault — the `IdentityProvider` path only).
#[test]
fn mem_identity_provider_registered_fingerprint_resolves() {
let alpha = fixture("alpha");
let registered_fp = fingerprint(&alpha.public_key());
let provider = MemIdentityProvider::new();
provider.register(
&registered_fp,
identity("fixture-alpha", &["relay:connect"]),
);
let resolved = provider
.resolve_from_fingerprint(&registered_fp)
.expect("registered fingerprint resolves");
assert_eq!(resolved.id, "fixture-alpha");
assert_eq!(resolved.scopes, vec!["relay:connect".to_string()]);
assert_eq!(provider.registered_fingerprints(), vec![registered_fp]);
assert!(
provider
.resolve_from_fingerprint(&fingerprint(&fixture("delta").public_key()))
.is_none(),
"unregistered fingerprint declines"
);
}
/// A revocation makes the fingerprint resolve `None` again — the
/// resolution set is inspectable and mutable.
///
/// - *Contract stamp:* ADR-004 decision 1 (`None` → reject is the
/// only disposition for unresolvable fingerprints).
#[test]
fn mem_identity_provider_revocation_resolves_none() {
let alpha = fixture("alpha");
let provider = MemIdentityProvider::new();
provider.register(
alpha.fingerprint,
identity("fixture-alpha", &["relay:connect"]),
);
provider.revoke(alpha.fingerprint);
assert!(
provider
.resolve_from_fingerprint(alpha.fingerprint)
.is_none(),
"revoked fingerprint declines (fail-closed)"
);
assert!(provider.registered_fingerprints().is_empty());
}
/// The token path resolves nothing on the SSH surface — the double
/// never invents a token shape.
///
/// - *Contract stamp:* ADR-004 decision 1 (the fingerprint is the
/// SSH-shaped resolution path); `auth-bridge.md` (`IdentityProvider`
/// is the one SSH-adjacent bridge, fingerprint-shaped).
#[test]
fn mem_identity_provider_token_path_resolves_none() {
let provider = MemIdentityProvider::new();
let token = AuthToken {
raw: b"tok".to_vec(),
};
assert!(provider.resolve_from_token(&token).is_none());
}
/// Every fixture key's fingerprint — whether derived via the private
/// PEM or the committed public blob — equals its `ssh-keygen -lf`
/// recorded value. This row keeps the fixture set honest for every
/// consumer; `tests/fingerprint_parity.rs` is the deep parity check.
///
/// - *Contract stamp:* ADR-004 decision 1 (the fingerprint string
/// format is a contract invariant across issuers).
/// - *Contract stamp:* wave-1 test-double convention (fixtures carry
/// recorded `ssh-keygen -lf` fingerprints).
#[test]
fn fixture_keys_carry_ssh_keygen_recorded_fingerprints() {
for key in FIXTURE_KEYS {
let from_private = fingerprint(key.private_key().public_key());
let from_blob = fingerprint(&key.public_key());
assert_eq!(from_private, key.fingerprint, "fixture {}", key.name);
assert_eq!(from_blob, key.fingerprint, "fixture {}", key.name);
assert_eq!(from_private.len(), 50, "SHA256: + 43 unpadded base64 chars");
}
}
/// The full custody round-trip: fixture PEM sealed into an at-rest
/// record through the vault seam, loaded through the production
/// [`VaultKeySource`], decoded by the ADR-005 chain, and verified to
/// be the *same* fixture key (fingerprint parity) that is usable by
/// russh (sign-capable).
///
/// - *Contract stamp:* ADR-005 decision 1 (inner format: unencrypted
/// OpenSSH-PEM in the record).
/// - *Contract stamp:* ADR-005 decision 2 (decrypt-at-use →
/// `Zeroizing` wrap → one-time `decode_secret_key`).
#[tokio::test]
async fn test_vault_seal_then_key_source_decode_round_trips() {
let alpha = fixture("alpha");
let vault = TestVault::unlocked();
let blob = vault.seal(alpha);
let source = VaultKeySource::new(vault.handle(), Some(blob));
let loaded = source.load().await.expect("sealed dial key loads");
assert_eq!(
fingerprint(loaded.public_key()),
alpha.fingerprint,
"the loaded key is the fixture key"
);
assert!(
loaded
.try_sign(b"alkssh w1 doubles usability probe")
.is_ok(),
"the decoded key is sign-capable (russh-usable)"
);
}
/// The POC-2 §3 failure postures ride the real seams: locked vault →
/// `Locked`, absent record → `Absent`, tampered ciphertext →
/// `Decryption` — each a distinct variant, each a refusal, no
/// fallback key shape.
///
/// - *Contract stamp:* ADR-005 decision 4 (failure modes fail closed
/// with distinct error shapes).
/// - *Contract stamp:* `docs/research/poc-2-vault-keys.md` §3 (the
/// failure matrix: locked / absent / tampered / wrong seed).
#[tokio::test]
async fn test_vault_postures_refuse_fail_closed_through_the_source() {
let alpha = fixture("alpha");
let locked = TestVault::unlocked();
let blob = locked.seal(alpha);
locked.lock();
let err = VaultKeySource::new(locked.handle(), Some(blob))
.load()
.await
.expect_err("locked vault refuses");
assert!(matches!(err, KeySourceError::Locked), "got {err:?}");
let absent = TestVault::unlocked();
let err = VaultKeySource::new(absent.handle(), None)
.load()
.await
.expect_err("absent record refuses");
assert!(matches!(err, KeySourceError::Absent), "got {err:?}");
let tampering = TestVault::unlocked();
let mut blob = tampering.seal(alpha);
blob.data = tamper_one_byte(&blob.data);
let err = VaultKeySource::new(tampering.handle(), Some(blob))
.load()
.await
.expect_err("tampered ciphertext refuses");
assert!(matches!(err, KeySourceError::Decryption(_)), "got {err:?}");
}
/// The host-key set loads from the vault seam in registration order —
/// one custody model, both directions (the decrypt-at-construction
/// load path).
///
/// - *Contract stamp:* ADR-005 decision 6 (host keys ride the same
/// model; the client's host *pin* is public config, never vault
/// material).
/// - *Contract stamp:* ADR-005 decision 2 (the same decrypt-and-decode
/// chain as the dial key).
#[tokio::test]
async fn host_key_source_set_loads_from_the_vault_seam_in_order() {
let (bravo, charlie) = (fixture("bravo"), fixture("charlie"));
let vault = TestVault::unlocked();
let blobs = vec![vault.seal(bravo), vault.seal(charlie)];
let source: std::sync::Arc<dyn HostKeySource> =
std::sync::Arc::new(VaultHostKeySource::new(vault.handle(), blobs));
let loaded = source.load().await.expect("host-key set loads");
let fps = loaded
.iter()
.map(|k| fingerprint(k.public_key()))
.collect::<Vec<String>>();
assert_eq!(
fps,
vec![
bravo.fingerprint.to_string(),
charlie.fingerprint.to_string()
]
);
}
/// The policy double's verdicts are honored and its decision log is
/// the inspectable record: pinned exact `(host, fingerprint)` pairs
/// accept, unmatched pairs fall to the default (reject — fail-closed),
/// and every decision is logged with the full presented context.
///
/// - *Contract stamp:* OQ-05 resolution (`host_key.rs` module docs —
/// the crate is policy-blind; verdicts are the policy's).
/// - *Contract stamp:* ADR-005 decision 6 (the host *pin* is public
/// config; exact-string comparison by contract).
#[tokio::test]
async fn mem_host_key_policy_verdicts_are_recorded_and_honored() {
let alpha = fixture("alpha");
let presented = PresentedHostKey {
host: "runner.example".into(),
port: 2222,
fingerprint: alpha.fingerprint.to_string(),
};
let pinned = MemHostKeyPolicy::new();
pinned.pin("runner.example", alpha.fingerprint);
assert!(pinned.decide(&presented).await, "pinned pair accepts");
let delta_fp = PresentedHostKey {
host: "runner.example".into(),
port: 2222,
fingerprint: fixture("delta").fingerprint.to_string(),
};
pinned.pin("runner.example", fixture("delta").fingerprint);
assert!(pinned.decide(&delta_fp).await, "any pinned pair accepts");
let stranger = PresentedHostKey {
host: "stranger.example".into(),
port: 2222,
fingerprint: alpha.fingerprint.to_string(),
};
assert!(
!pinned.decide(&stranger).await,
"default verdict rejects without a matching rule (fail-closed)"
);
let decisions = pinned.decisions();
assert_eq!(decisions.len(), 3);
assert_eq!(decisions[0].presented, presented);
assert!(decisions[0].accepted);
assert!(decisions[1].accepted);
assert_eq!(decisions[2].presented, stranger);
assert!(!decisions[2].accepted);
}
/// A permissive default is the test's explicit choice (never a
/// default), and an explicit rule overrides it — the case-2
/// ephemeral-fleet shape versus a pin-style correction.
///
/// - *Contract stamp:* OQ-05 resolution (`host_key.rs` — there is no
/// accept-all built-in crate option; case-2 policies are the
/// assembly's to write).
/// - *Contract stamp:* ADR-004 decision 6 posture analog
/// (fail-closed by default, permissive only by explicit choice).
#[tokio::test]
async fn mem_host_key_policy_accept_default_is_explicit_and_overrideable() {
let alpha = fixture("alpha");
let presented = PresentedHostKey {
host: "fleet.example".into(),
port: 2200,
fingerprint: alpha.fingerprint.to_string(),
};
let permissive = MemHostKeyPolicy::with_default(true);
assert!(
permissive.decide(&presented).await,
"case-2 permissive posture is the explicit choice"
);
let denying = MemHostKeyPolicy::with_default(true);
denying.rule("fleet.example", alpha.fingerprint, false);
assert!(
!denying.decide(&presented).await,
"an explicit rule overrides the default"
);
}
fn tamper_one_byte(text: &str) -> String {
let mut bytes = text.as_bytes().to_vec();
if bytes[4] == b'A' {
bytes[4] = b'B';
} else {
bytes[4] = b'A';
}
String::from_utf8(bytes).expect("base64 mutation stays text")
}
+115 -63
View File
@@ -1,93 +1,145 @@
//! Fingerprint-format parity — ADR-004 decision 1's contract
//! invariant proven against independent-issuer vectors (POC-1 §1).
//!
//! Each committed fixture carries its fingerprint byte-verbatim from
//! `ssh-keygen -lf` (OpenSSH, recorded at vector-generation time; see
//! `tests/vectors/fingerprints.txt`). The crate's single fingerprint
//! path must reproduce those strings exactly: registration values
//! from any standard issuer must compare equal with what this crate
//! computes, without cross-issuer drift.
//! The vectors are the wave-1 test-key fixture set
//! (`tests/common/mod.rs`'s [`FIXTURE_KEYS`]; the committed files
//! under `tests/vectors/`), each key's fingerprint recorded
//! byte-verbatim from `ssh-keygen -lf` (OpenSSH 9.6p1, at
//! vector-generation time; see `tests/vectors/fingerprints.txt`). The
//! crate's single fingerprint path must reproduce those strings
//! exactly from every issuer shape — the committed public blob, the
//! private key's derived public half, and the committed `.pub` file —
//! so registration values from any standard issuer compare equal,
//! without cross-issuer drift.
mod common;
use std::fs;
use common::FIXTURE_KEYS;
const VECTOR_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/vectors");
struct Vector {
name: &'static str,
blob: &'static str,
fingerprint: &'static str,
}
const VECTORS: &[Vector] = &[
Vector {
name: "alpha.pub",
blob: "AAAAC3NzaC1lZDI1NTE5AAAAICS7Ub4A073+G450Nt2DUOwWyniGVU/lrlUoQZAUg1sd",
fingerprint: "SHA256:yM/XYR4VM1XX3gfQqOh+wOjOCokfTSu4QqqQIeUTa+c",
},
Vector {
name: "bravo.pub",
blob: "AAAAC3NzaC1lZDI1NTE5AAAAIMNPMofKbGRA5CXtRnGsdXK/Jbj4DDOk7OutS6cOawqn",
fingerprint: "SHA256:Ruj1hCyzfsM8rjZhP4c4I+lVGEaEG33/srVLOnVpB3s",
},
Vector {
name: "charlie.pub",
blob: "AAAAC3NzaC1lZDI1NTE5AAAAIAGJveAmhmFyBN635c/xhaveGRzafpYobIqg9J6/b8cF",
fingerprint: "SHA256:fIcjLEaAm4aMpWEtwHm73TiIqGCX1X75HG9hGiJZ6TQ",
},
Vector {
name: "delta.pub",
blob: "AAAAC3NzaC1lZDI1NTE5AAAAIDg+r/at1NqJqwN5bUEqswoHlYAUMkJxJtKNJyWXH3UX",
fingerprint: "SHA256:D+qcroJ1ly9fVWpDQ4Zn+ig9e0Q7e0xOrQr47ruEZWs",
},
];
/// The crate's single fingerprint path reproduces the recorded value
/// from the committed public blob alone — the public-key-only issuer
/// shape (a registry that only ever sees public material).
///
/// - *Contract stamp:* ADR-004 decision 1 (fingerprint parity with
/// `ssh-keygen -lf`; POC-1 §1's parity checkpoint shape).
#[test]
fn fingerprint_parity_with_ssh_keygen_recorded_vectors() {
for v in VECTORS {
let key = russh::keys::parse_public_key_base64(v.blob)
.expect("committed fixture parses as OpenSSH key material");
fn computed_fingerprints_match_recorded_values_for_public_blobs() {
for key in FIXTURE_KEYS {
let parsed = russh::keys::parse_public_key_base64(key.blob)
.expect("committed fixture blob parses as OpenSSH key material");
assert_eq!(
alkssh::fingerprint(&key),
v.fingerprint,
alkssh::fingerprint(&parsed),
key.fingerprint,
"parity drift on {name}",
name = v.name
name = key.name
);
}
}
/// The private-key path — the shape every auth/mapping test uses
/// (`decode_secret_key` → sign/serve) — yields a public half whose
/// fingerprint is the same recorded value.
///
/// - *Contract stamp:* ADR-004 decision 1 (the fingerprint string
/// format is a contract invariant across issuers).
#[test]
fn committed_fixture_files_match_the_recorded_blobs() {
for v in VECTORS {
let path = format!("{VECTOR_DIR}/{}", v.name);
let file = fs::read_to_string(&path).expect("committed fixture file readable");
let expected_line = format!("ssh-ed25519 {blob} ", blob = v.blob);
fn private_derived_fingerprints_match_recorded_values() {
for key in FIXTURE_KEYS {
let decoded = key.private_key();
assert_eq!(
alkssh::fingerprint(decoded.public_key()),
key.fingerprint,
"private-derived parity drift on {name}",
name = key.name
);
}
}
/// The committed fixture files are the recorded keys: each `.pub`
/// starts with the recorded blob line, and each `.key` is an
/// unencrypted OpenSSH private-key PEM (the ADR-005 decision 1 inner
/// format) whose decoded public half fingerprints to the recorded
/// value.
///
/// - *Contract stamp:* ADR-004 decision 1 (independent-issuer vectors
/// are the parity proof — the commitment must be the recorded key).
/// - *Contract stamp:* ADR-005 decision 1 (the fixtures' inner format
/// is exactly the at-rest contract: unencrypted OpenSSH PEM).
#[test]
fn committed_fixture_files_match_the_recorded_keys() {
for key in FIXTURE_KEYS {
let pub_path = format!("{VECTOR_DIR}/{}.pub", key.name);
let pub_file = fs::read_to_string(&pub_path).expect("committed .pub readable");
let expected_line = format!("ssh-ed25519 {blob} ", blob = key.blob);
assert!(
file.starts_with(&expected_line),
pub_file.starts_with(&expected_line),
"fixture file {name} does not start with the recorded blob",
name = v.name
name = key.name
);
}
}
#[test]
fn recorded_vector_manifest_matches_the_inlined_vectors() {
let manifest = fs::read_to_string(format!("{VECTOR_DIR}/fingerprints.txt"))
.expect("fingerprint manifest readable");
for v in VECTORS {
let key_path = format!("{VECTOR_DIR}/{}.key", key.name);
let key_file = fs::read_to_string(&key_path).expect("committed .key readable");
assert!(
manifest.contains(&format!("{} {fp}", v.name, fp = v.fingerprint)),
"manifest does not record {name} at {fp}",
name = v.name,
fp = v.fingerprint
key_file.starts_with("-----BEGIN OPENSSH PRIVATE KEY-----"),
"fixture {name} is not an OpenSSH private-key PEM",
name = key.name
);
assert_eq!(key_file, key.pem, "committed .key is the fixture PEM");
assert!(
!key_file.contains("ENCRYPTED"),
"fixture {name} is unencrypted (ADR-005 decision 1)",
name = key.name
);
let decoded = key.private_key();
let from_file = decoded.public_key();
assert_eq!(
alkssh::fingerprint(from_file),
key.fingerprint,
"committed .key public half drifts on {name}",
name = key.name
);
}
}
/// The recorded manifest (`tests/vectors/fingerprints.txt`) records
/// every fixture — no fixture ships without its independent issuer's
/// fingerprint.
///
/// - *Contract stamp:* wave-1 test-double convention (recorded
/// `ssh-keygen -lf` fingerprints, the independent-issuer record).
#[test]
fn recorded_manifest_covers_every_fixture() {
let manifest =
fs::read_to_string(format!("{VECTOR_DIR}/fingerprints.txt")).expect("manifest readable");
for key in FIXTURE_KEYS {
assert!(
manifest.contains(&format!("{}.pub {fp}", key.name, fp = key.fingerprint)),
"manifest does not record {name} at {fp}",
name = key.name,
fp = key.fingerprint
);
}
}
/// The registered-string comparison is byte-identical: the computed
/// value meets a registered `ssh-keygen` string with no padding or
/// url-safe-alphabet drift (`+`/`/`, no `=`).
///
/// - *Contract stamp:* ADR-004 decision 1 (`SHA256:` + unpadded
/// base64, standard alphabet — byte-identical comparison by
/// contract).
#[test]
fn registered_string_compare_is_byte_identical() {
let key = russh::keys::parse_public_key_base64(VECTORS[0].blob).expect("fixture parses");
let computed = alkssh::fingerprint(&key);
let registered: &str = VECTORS[0].fingerprint;
let alpha = FIXTURE_KEYS
.iter()
.find(|k| k.name == "alpha")
.expect("alpha fixture present");
let computed = alkssh::fingerprint(&alpha.public_key());
let registered: &str = alpha.fingerprint;
assert_eq!(computed, registered);
assert_eq!(registered.len(), 50, "SHA256: + 43 unpadded base64 chars");
assert!(!registered.contains('=') && !registered.contains('!'));
+7
View File
@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDNvLD+VAJd56SSfLa2YD7dpBPgH+uO+/0ceidlWzNeMwAAAKDJQe/myUHv
5gAAAAtzc2gtZWQyNTUxOQAAACDNvLD+VAJd56SSfLa2YD7dpBPgH+uO+/0ceidlWzNeMw
AAAEATzyiI7Cwpy3CJBDKDepykbHiNzVpZ38kiDqa1S1p5Us28sP5UAl3npJJ8trZgPt2k
E+Af6477/Rx6J2VbM14zAAAAF2Fsa3NzaC13MS1maXh0dXJlLWFscGhhAQIDBAUG
-----END OPENSSH PRIVATE KEY-----
+1 -1
View File
@@ -1 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICS7Ub4A073+G450Nt2DUOwWyniGVU/lrlUoQZAUg1sd alkssh-w1-vectors-alpha
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM28sP5UAl3npJJ8trZgPt2kE+Af6477/Rx6J2VbM14z alkssh-w1-fixture-alpha
+7
View File
@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDL7vUvoR/Q7oO33Yhy3lvgfZHnvouPeYzwHQR/GGjr0AAAAKAUKLAWFCiw
FgAAAAtzc2gtZWQyNTUxOQAAACDL7vUvoR/Q7oO33Yhy3lvgfZHnvouPeYzwHQR/GGjr0A
AAAEC9aPqnyC37OKCL/VksdonIl6ICy+I8X4So6TDmnSObEMvu9S+hH9Dug7fdiHLeW+B9
kee+i495jPAdBH8YaOvQAAAAF2Fsa3NzaC13MS1maXh0dXJlLWJyYXZvAQIDBAUG
-----END OPENSSH PRIVATE KEY-----
+1 -1
View File
@@ -1 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMNPMofKbGRA5CXtRnGsdXK/Jbj4DDOk7OutS6cOawqn alkssh-w1-vectors-bravo
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMvu9S+hH9Dug7fdiHLeW+B9kee+i495jPAdBH8YaOvQ alkssh-w1-fixture-bravo
+7
View File
@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCi/MwMy5EdIyhWNpfkKwYwtwr+ENzgVFAl+60FdY10HgAAAKBk/WqJZP1q
iQAAAAtzc2gtZWQyNTUxOQAAACCi/MwMy5EdIyhWNpfkKwYwtwr+ENzgVFAl+60FdY10Hg
AAAEBebYYCpFNzgy2jbKpQ3YPC0mKcrm6iDIYFCyKWl2D5Z6L8zAzLkR0jKFY2l+QrBjC3
Cv4Q3OBUUCX7rQV1jXQeAAAAGWFsa3NzaC13MS1maXh0dXJlLWNoYXJsaWUBAgME
-----END OPENSSH PRIVATE KEY-----
+1 -1
View File
@@ -1 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAGJveAmhmFyBN635c/xhaveGRzafpYobIqg9J6/b8cF alkssh-w1-vectors-charlie
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKL8zAzLkR0jKFY2l+QrBjC3Cv4Q3OBUUCX7rQV1jXQe alkssh-w1-fixture-charlie
+7
View File
@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCHDCvp43ohqofExxAT1PTE2K0id1h78b92R9dSU0v0lAAAAKACHXI/Ah1y
PwAAAAtzc2gtZWQyNTUxOQAAACCHDCvp43ohqofExxAT1PTE2K0id1h78b92R9dSU0v0lA
AAAEDfzKwFxEpqXnfPvMr/GpLI5RTtwyOm5GT2nFrZKr/TG4cMK+njeiGqh8THEBPU9MTY
rSJ3WHvxv3ZH11JTS/SUAAAAF2Fsa3NzaC13MS1maXh0dXJlLWRlbHRhAQIDBAUG
-----END OPENSSH PRIVATE KEY-----
+1 -1
View File
@@ -1 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDg+r/at1NqJqwN5bUEqswoHlYAUMkJxJtKNJyWXH3UX alkssh-w1-vectors-delta
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIcMK+njeiGqh8THEBPU9MTYrSJ3WHvxv3ZH11JTS/SU alkssh-w1-fixture-delta
+14 -10
View File
@@ -1,12 +1,16 @@
# Independent-issuer fingerprint vectors for the POC-1 §1 parity
# checkpoint (ADR-004 decision 1). Each entry pairs a committed OpenSSH
# ed25519 public key fixture (the .pub files in this directory) with the
# fingerprint byte-verbatim from `ssh-keygen -lf` — recorded from
# OpenSSH 9.6p1 (Ubuntu-3ubuntu13.20) at vector-generation time. The
# keys are throwaway test fixtures; no secret material is committed.
# Independent-issuer fingerprint vectors: the alkssh wave-1 test-key
# fixture set. Each entry pairs a committed OpenSSH ed25519 keypair (the
# .key/.pub files in this directory) with its fingerprint byte-verbatim
# from `ssh-keygen -lf` — recorded from OpenSSH_9.6p1
# (Ubuntu-3ubuntu13.20) at vector-generation time.
#
# The keys are throwaway test fixtures minted for the test suite only;
# their private halves are committed on purpose (committed keys are
# throwaway fixtures, not secrets — they are never registered anywhere
# real and never deployed).
#
# <fixture> <expected fingerprint>
alpha.pub SHA256:yM/XYR4VM1XX3gfQqOh+wOjOCokfTSu4QqqQIeUTa+c
bravo.pub SHA256:Ruj1hCyzfsM8rjZhP4c4I+lVGEaEG33/srVLOnVpB3s
charlie.pub SHA256:fIcjLEaAm4aMpWEtwHm73TiIqGCX1X75HG9hGiJZ6TQ
delta.pub SHA256:D+qcroJ1ly9fVWpDQ4Zn+ig9e0Q7e0xOrQr47ruEZWs
alpha.pub SHA256:YyaiL42Xl8tcRRUhNCiqQBsiiRWVHHDR9hpXC45EQ6A
bravo.pub SHA256:yQf/w2wYBoZ9adDlRGbcNh7AHE3mbqWRCqoGHra1ib0
charlie.pub SHA256:b5jdwosbZUx5nsY5zfr/ZCt+4sJYehVBq7Xl/wstJ9U
delta.pub SHA256:4MiQ0KGoTEaklHBTlxPDuQkeu0FTyqEbne10K6Z8CKs