open-standards/wai/wai-quantum-cli/src/keys.rs
Transaction Science 605ea19aa2 wai-quantum-cli: the command line on the job service's engine, installable
A new crate, wai-quantum-cli, whose binary is wai-quantum. It depends on the
wai-quantum library alone, so `cargo install wai-quantum-cli` can install it.

- run: an OpenQASM program planned and run as the service runs it
  (wai_quantum::engine), signed with this user's key; --json prints the job
  as the service returns it. Shots, seed, method, observables, amplitudes,
  noise and method options; an unknown flag or noise channel is an error.
- plan, lint (the line of the first problem), draw (the circuit as text,
  gates packed into columns, conditions in a legend).
- submit: the same job on the service (feature `service`, on by default),
  waiting for a long one.
- verify: a job's receipt from run, submit or the service, and every
  receipt the research tools sign; a changed result is INVALID.
- key: this user's did:key; --key FILE and --demo-key as before.
- methods: each method's reach in a job's memory here.
- mcp: run, plan, lint, draw, verify and the method list for an agent,
  over stdio; runs stay on this machine.
- lab: the earlier wai-quantum command, moved here from wai-rs unchanged
  (wai:: paths now wai_quantum::), its key handling shared with run.
- completions for bash, zsh, fish, elvish and PowerShell.
- Feature `metal`: the GPU state vector on macOS. With no default features
  it builds for wasm32-wasip2.

wai-rs loses the wai-quantum bin and its cli_keys feature. The wai README's
quickstart now uses the new crate. smoke.sh exercises every command, and
.github/workflows/wai-quantum-cli.yml runs the tests, clippy with and
without submit, smoke.sh, and the WASI component against the native build,
byte for byte; on macOS also a run on the GPU.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit d2459835d8)
2026-10-20 17:34:16 -04:00

135 lines
5.3 KiB
Rust

//! The key a receipt is signed with.
//!
//! - `--demo-key`: the published demo key, for receipts that repeat byte for
//! byte everywhere. Anyone can sign with it, so it proves nothing about who
//! signed.
//! - `--key FILE`, or `$WAI_QUANTUM_KEY` naming a file: that key.
//! - Otherwise this user's key, `$XDG_CONFIG_HOME/wai/quantum-key` (or
//! `~/.config/wai/quantum-key`), made from the operating system's random
//! source on first use and readable by its owner only.
//!
//! A key file holds the 32-byte Ed25519 seed as 64 hex digits.
use std::path::{Path, PathBuf};
use wai_quantum::ed25519_dalek::SigningKey;
/// The published demo key's seed.
pub const DEMO_SEED: [u8; 32] = [42; 32];
/// The key to sign with, and where it came from.
pub struct Key {
pub key: SigningKey,
/// The file it was read from or made in; `None` for the demo key.
pub path: Option<PathBuf>,
/// Whether this call made the file.
pub made: bool,
}
/// This user's key file: `$XDG_CONFIG_HOME/wai/quantum-key`, or
/// `~/.config/wai/quantum-key`.
pub fn user_path() -> Option<PathBuf> {
let base = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| Path::new(&h).join(".config")))?;
Some(base.join("wai").join("quantum-key"))
}
fn parse(text: &str) -> Option<[u8; 32]> {
let t = text.trim();
if t.len() != 64 {
return None;
}
let mut out = [0u8; 32];
for (i, b) in out.iter_mut().enumerate() {
*b = u8::from_str_radix(t.get(2 * i..2 * i + 2)?, 16).ok()?;
}
Some(out)
}
/// The key `demo` and `given` (`--demo-key`, `--key FILE`) name: a named file
/// must exist; this user's own is made when there is none.
pub fn load(given: Option<&Path>, demo: bool) -> Result<Key, String> {
if demo {
return Ok(Key { key: SigningKey::from_bytes(&DEMO_SEED), path: None, made: false });
}
let named = given.map(Path::to_path_buf).or_else(|| std::env::var_os("WAI_QUANTUM_KEY").map(PathBuf::from));
let path = match &named {
Some(p) => p.clone(),
None => user_path().ok_or("no HOME to keep a key in: pass --key FILE")?,
};
match std::fs::read_to_string(&path) {
Ok(text) => {
let seed = parse(&text).ok_or_else(|| format!("{}: a key file holds 64 hex digits", path.display()))?;
Ok(Key { key: SigningKey::from_bytes(&seed), path: Some(path), made: false })
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound && named.is_none() => {
let mut seed = [0u8; 32];
getrandom::fill(&mut seed).map_err(|e| format!("no random source for a new key: {e}"))?;
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
}
let text: String = seed.iter().map(|b| format!("{b:02x}")).collect();
write_private(&path, &format!("{text}\n")).map_err(|e| format!("{}: {e}", path.display()))?;
Ok(Key { key: SigningKey::from_bytes(&seed), path: Some(path), made: true })
}
Err(e) => Err(format!("{}: {e}", path.display())),
}
}
fn write_private(path: &Path, text: &str) -> std::io::Result<()> {
use std::io::Write as _;
let mut f = std::fs::OpenOptions::new();
f.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
f.mode(0o600);
}
f.open(path)?.write_all(text.as_bytes())
}
/// What `--demo-key` and a new key say on standard error.
pub fn notes(k: &Key) {
match &k.path {
None => eprintln!("note: signed with the published demo key; the receipt shows the result is unaltered, not who signed it"),
Some(p) if k.made => eprintln!("note: made a signing key at {}", p.display()),
Some(_) => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_key_is_made_once_private_then_read_and_a_named_file_must_exist() {
let dir = std::env::temp_dir().join(format!("wai-quantum-cli-key-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let path = dir.join("k");
// A named file that is not there is refused, never made.
assert!(load(Some(&path), false).is_err());
assert!(!path.exists());
// The demo key is the published one.
let demo = load(None, true).unwrap();
assert_eq!(demo.key.to_bytes(), DEMO_SEED);
assert!(demo.path.is_none());
// A written key reads back; a malformed one is refused.
std::fs::create_dir_all(&dir).unwrap();
let seed: String = (0..32).map(|i| format!("{:02x}", i * 7)).collect();
std::fs::write(&path, format!("{seed}\n")).unwrap();
let k = load(Some(&path), false).unwrap();
assert_eq!(k.key.to_bytes()[1], 7);
assert!(!k.made);
std::fs::write(&path, "not a key").unwrap();
assert!(load(Some(&path), false).is_err());
// A made key is the owner's alone.
let fresh = dir.join("fresh");
write_private(&fresh, "x").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(std::fs::metadata(&fresh).unwrap().permissions().mode() & 0o777, 0o600);
}
let _ = std::fs::remove_dir_all(&dir);
}
}