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)
135 lines
5.3 KiB
Rust
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);
|
|
}
|
|
}
|