From da7b0965d643f703a09d30bbb2112a5ce70af0a1 Mon Sep 17 00:00:00 2001 From: Alex Plotnick Date: Sat, 29 Aug 2026 17:31:16 -0600 Subject: [PATCH 1/4] Authenticate to permslip through the SSH agent The signer logs in by the sshauth challenge with the same agent key that identifies the user to the rack, and caches the token so a hardware key is touched once per ten minutes, not per command. Co-Authored-By: Claude Mythos 5 --- Cargo.lock | 4 +- Cargo.toml | 4 +- client/src/cli.rs | 95 ++++++++++++++++++++++++++++++++++-------- client/src/commands.rs | 72 +++++++++++++++++++++++--------- client/src/context.rs | 7 ++++ client/src/permslip.rs | 33 ++++++++++++--- client/src/repl.rs | 8 ++++ common/src/keys.rs | 8 +++- 8 files changed, 183 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 078598d6..60f85944 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3122,7 +3122,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "permission-slip-client" version = "1.1.0" -source = "git+https://github.com/oxidecomputer/permission-slip?rev=2a8adcadde01518f1d39993a1cc45ec3b78b3204#2a8adcadde01518f1d39993a1cc45ec3b78b3204" +source = "git+https://github.com/oxidecomputer/permission-slip?rev=07b5953145cd55bcf14b2461c40639eaced3bd77#07b5953145cd55bcf14b2461c40639eaced3bd77" dependencies = [ "anyhow", "base64 0.22.1", @@ -3158,7 +3158,7 @@ dependencies = [ [[package]] name = "permission-slip-common" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/permission-slip?rev=2a8adcadde01518f1d39993a1cc45ec3b78b3204#2a8adcadde01518f1d39993a1cc45ec3b78b3204" +source = "git+https://github.com/oxidecomputer/permission-slip?rev=07b5953145cd55bcf14b2461c40639eaced3bd77#07b5953145cd55bcf14b2461c40639eaced3bd77" dependencies = [ "aws-types", "blake3", diff --git a/Cargo.toml b/Cargo.toml index 37766b1b..3d31ce25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,8 +38,8 @@ memmap2 = "0.9" p256 = { version = "0.13", features = ["ecdsa", "pkcs8"] } pem-rfc7468 = { version = "0.7", features = ["std"] } percent-encoding = "2" -permission-slip-client = { git = "https://github.com/oxidecomputer/permission-slip", rev = "2a8adcadde01518f1d39993a1cc45ec3b78b3204" } -permission-slip-common = { git = "https://github.com/oxidecomputer/permission-slip", rev = "2a8adcadde01518f1d39993a1cc45ec3b78b3204" } +permission-slip-client = { git = "https://github.com/oxidecomputer/permission-slip", rev = "07b5953145cd55bcf14b2461c40639eaced3bd77" } +permission-slip-common = { git = "https://github.com/oxidecomputer/permission-slip", rev = "07b5953145cd55bcf14b2461c40639eaced3bd77" } progenitor = "0.14" progenitor-client = "0.14" pwd = "1" diff --git a/client/src/cli.rs b/client/src/cli.rs index 158b3708..995dba66 100644 --- a/client/src/cli.rs +++ b/client/src/cli.rs @@ -17,7 +17,7 @@ use anstream::print; use anstyle::{AnsiColor, Style}; use atomicwrites::{AtomicFile, OverwriteBehavior}; use bytesize::ByteSize; -use chrono::TimeDelta; +use chrono::{DateTime, TimeDelta, Utc}; use humantime::format_duration; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use rustix::io::ioctl_fionread; @@ -43,8 +43,15 @@ use crate::context::{CommandContext, OutputFormat, StatusDisplayStyle}; use crate::types::SessionStartNonce; pub(crate) const PREFIX: &str = "sush"; -const SESSION_FILE: &str = "session.json"; +const SESSION_FILE_NAME: &str = "session.json"; const SESSION_FILE_VERSION: u32 = 1; +const TOKEN_FILE_NAME: &str = "permslip-token.json"; +const TOKEN_FILE_VERSION: u32 = 1; + +/// How long a permslip token is trusted for reuse. Staying well +/// under the server's 15 minute TTL spares a command from expiring +/// mid-flight. +const TOKEN_REUSE: TimeDelta = TimeDelta::minutes(10); /// The persisted session, versioned for future migrations. #[derive(Deserialize, Serialize)] @@ -53,6 +60,17 @@ struct SavedSession { session: Session, } +/// A persisted permslip token, versioned for future migrations. The +/// url and fingerprint pin it to one signing server and one identity. +#[derive(Deserialize, Serialize)] +struct SavedToken { + version: u32, + url: String, + fingerprint: String, + token: String, + created: DateTime, +} + #[derive(Clone, Debug, Default)] pub struct Cli { globals: Arc>, @@ -61,6 +79,7 @@ pub struct Cli { watch: Arc>>, session: Arc>>, session_file: Option, + token_file: Option, credentials: AuthzSigner, } @@ -69,7 +88,7 @@ impl Cli { /// Without persistence, every one-shot command would need a fresh /// `session attach`. pub fn load_session(&mut self) { - let path = match BaseDirectories::with_prefix(PREFIX).place_state_file(SESSION_FILE) { + let path = match BaseDirectories::with_prefix(PREFIX).place_state_file(SESSION_FILE_NAME) { Ok(path) => path, Err(error) => { eprintln!("⚠️ The session will not persist: {error}"); @@ -91,6 +110,10 @@ impl Cli { Err(error) => eprintln!("⚠️ Ignoring the saved session: {error}"), } self.session_file = Some(path); + match BaseDirectories::with_prefix(PREFIX).place_state_file(TOKEN_FILE_NAME) { + Ok(path) => self.token_file = Some(path), + Err(error) => eprintln!("⚠️ Signing tokens will not persist: {error}"), + } } /// Adopt `session` unless one with the same ID is already @@ -119,18 +142,7 @@ impl Cli { session: session.clone(), }) .map_err(io::Error::other) - .and_then(|json| { - AtomicFile::new(path, OverwriteBehavior::AllowOverwrite) - .write(|file| { - file.set_permissions(Permissions::from_mode(0o600))?; - file.write_all(&json) - }) - .map_err(|error| match error { - atomicwrites::Error::Internal(error) | atomicwrites::Error::User(error) => { - error - } - }) - }), + .and_then(|json| write_private(path, &json)), None => match fs::remove_file(path) { Err(error) if error.kind() != ErrorKind::NotFound => Err(error), _ => Ok(()), @@ -142,6 +154,18 @@ impl Cli { } } +/// Atomically write a file only the user may read. +fn write_private(path: &Path, bytes: &[u8]) -> io::Result<()> { + AtomicFile::new(path, OverwriteBehavior::AllowOverwrite) + .write(|file| { + file.set_permissions(Permissions::from_mode(0o600))?; + file.write_all(bytes) + }) + .map_err(|error| match error { + atomicwrites::Error::Internal(error) | atomicwrites::Error::User(error) => error, + }) +} + fn byte_size(len: u64) -> bytesize::Display { ByteSize::b(len).display().si() } @@ -247,6 +271,43 @@ impl CommandContext for Cli { self.credentials.clone() } + fn permslip_token(&self, url: &str, fingerprint: &str) -> Option { + let json = fs::read(self.token_file.as_ref()?).ok()?; + match serde_json::from_slice::(&json) { + Ok(SavedToken { + version: TOKEN_FILE_VERSION, + url: saved_url, + fingerprint: saved_fingerprint, + token, + created, + }) if saved_url == url + && saved_fingerprint == fingerprint + && Utc::now() - created < TOKEN_REUSE => + { + Some(token) + } + _ => None, + } + } + + fn save_permslip_token(&self, url: &str, fingerprint: &str, token: &str) { + let Some(path) = &self.token_file else { + return; + }; + let result = serde_json::to_vec_pretty(&SavedToken { + version: TOKEN_FILE_VERSION, + url: url.to_owned(), + fingerprint: fingerprint.to_owned(), + token: token.to_owned(), + created: Utc::now(), + }) + .map_err(io::Error::other) + .and_then(|json| write_private(path, &json)); + if let Err(error) = result { + eprintln!("⚠️ The token was not saved: {error}"); + } + } + fn session_id(&self) -> Option { self.session .lock() @@ -866,7 +927,7 @@ impl CommandContext for Cli { time_authenticated, time_revoked, } = identity; - let fingerprint = public_key.fingerprint(Default::default()).to_string(); + let fingerprint = public_key.fingerprint(); let algorithm = public_key.algorithm(); let comment = public_key.comment(); println!( @@ -895,7 +956,7 @@ impl CommandContext for Cli { } else { for key in keys { let key_id = key.key_id()?; - let fingerprint = key.fingerprint(Default::default()).to_string(); + let fingerprint = key.fingerprint(); let algorithm = key.algorithm(); let comment = key.comment(); match self.get_output_format() { diff --git a/client/src/commands.rs b/client/src/commands.rs index 4af83637..b7df19b0 100644 --- a/client/src/commands.rs +++ b/client/src/commands.rs @@ -64,7 +64,7 @@ use crate::context::{Authz, CommandContext, OutputFormat, StatusDisplayStyle}; use crate::identity::{IdentityError, SshAgentConnection}; use crate::interactive::interactive_job; #[cfg(feature = "permslip")] -use crate::permslip::{PermslipError, PermslipSigner}; +use crate::permslip::{PermslipError, PermslipSigner, fresh_token}; use crate::repl::Repl; use crate::tls; use crate::tunnel::{Tunnel, TunnelError}; @@ -1153,22 +1153,23 @@ async fn session( ) => { // Creation already announces the session; don't echo it // when the start succeeds. - let (session_id, nonce, show) = - if let (Some(session_id), Some(nonce)) = (session_id, nonce) { - (session_id, nonce, true) - } else { - let (baseboard_id, nonce) = with_login(ctx, client, async || { - Ok(( - client.target().send().await?.into_inner(), - client.session_start_nonce().send().await?.into_inner(), - )) - }) - .await?; - let (session_id, nonce) = - session_create(permslip, permslip_url, &baseboard_id, nonce.nonce).await?; - ctx.session_created(Session::new(session_id), nonce); - (session_id, nonce, false) - }; + let (session_id, nonce, show) = if let (Some(session_id), Some(nonce)) = + (session_id, nonce) + { + (session_id, nonce, true) + } else { + let (baseboard_id, nonce) = with_login(ctx, client, async || { + Ok(( + client.target().send().await?.into_inner(), + client.session_start_nonce().send().await?.into_inner(), + )) + }) + .await?; + let (session_id, nonce) = + session_create(ctx, permslip, permslip_url, &baseboard_id, nonce.nonce).await?; + ctx.session_created(Session::new(session_id), nonce); + (session_id, nonce, false) + }; session_start(ctx, client, session_id, nonce, wait, show).await } @@ -1183,7 +1184,7 @@ async fn session( _, ) => { let (session_id, nonce) = - session_create(permslip, permslip_url, &baseboard_id, nonce).await?; + session_create(ctx, permslip, permslip_url, &baseboard_id, nonce).await?; ctx.session_created(Session::new(session_id), nonce); Ok(()) } @@ -1357,7 +1358,7 @@ async fn job( } else { JobMode::Batch }; - let signer = PermslipSigner::new(key_name, permslip_url).await?; + let signer = permslip_signer(ctx, key_name, permslip_url).await?; let mut interval = interval(SIGNING_UPDATE_INTERVAL); interval.set_missed_tick_behavior(MissedTickBehavior::Skip); let sign = signer.sign_job_request(JobStartRequest::new( @@ -1732,9 +1733,40 @@ fn parse_baseboard_id(value: &str) -> Result { value.parse::().map_err(|e| e.to_string()) } +/// A permslip signer authenticated as the agent key `-s`/`SUSH_KEY_ID` +/// names, or the agent's first. We choose the key rather than letting +/// permslip choose, so that one identity serves both the rack and the +/// signing service. +#[cfg(feature = "permslip")] +async fn permslip_signer( + ctx: &mut impl CommandContext, + key_name: &str, + permslip_url: &str, +) -> Result { + let globals = ctx.get_globals(); + let Some(sock) = globals.ssh_auth_sock.clone() else { + return Err(CommandError::MissingSshAuthSock); + }; + let key_id = globals.ssh_key_id.clone(); + let mut agent = SshAgentConnection::connect(&sock).await?; + let key = agent.identity(key_id.as_ref()).await?; + let fingerprint = key.fingerprint(); + let token = match ctx.permslip_token(permslip_url, &fingerprint) { + Some(token) => token, + None => { + ctx.please_touch(&key)?; + let token = fresh_token(permslip_url, sock, fingerprint.clone()).await?; + ctx.save_permslip_token(permslip_url, &fingerprint, &token); + token + } + }; + Ok(PermslipSigner::new(key_name, permslip_url, &token)?) +} + /// Ask the online signing service to create a session. #[cfg(feature = "permslip")] async fn session_create( + ctx: &mut impl CommandContext, permslip: Option, permslip_url: Option, baseboard_id: &BaseboardId, @@ -1747,7 +1779,7 @@ async fn session_create( return Err(CommandError::MissingKeyName); }; let invalid = |e: InvalidCodephrase| CommandError::UnsupportedPermslipResponse(e.to_string()); - let signer = PermslipSigner::new(permslip_key, &permslip_url).await?; + let signer = permslip_signer(ctx, &permslip_key, &permslip_url).await?; let created = signer.create_session(baseboard_id, nonce).await?; Ok(( created.session_id.to_string().parse().map_err(invalid)?, diff --git a/client/src/context.rs b/client/src/context.rs index adaf1933..a3687bf0 100644 --- a/client/src/context.rs +++ b/client/src/context.rs @@ -114,6 +114,13 @@ pub trait CommandContext: Clone + Send + Sync { fn set_credentials(&mut self, credentials: Option) { self.authz_signer().set(credentials) } + /// The cached permslip token for `url` and the key `fingerprint` + /// names, if it is still fresh. The default caches nothing. + fn permslip_token(&self, _url: &str, _fingerprint: &str) -> Option { + None + } + /// Remember a permslip token for reuse. + fn save_permslip_token(&self, _url: &str, _fingerprint: &str, _token: &str) {} fn session_id(&self) -> Option; fn next_job_id(&self) -> Result; fn session_start_params(&self, baseboard_id: BaseboardId, nonce: SessionStartNonce); diff --git a/client/src/permslip.rs b/client/src/permslip.rs index b3891f6f..dd7024b3 100644 --- a/client/src/permslip.rs +++ b/client/src/permslip.rs @@ -4,7 +4,8 @@ //! Use Permission Slip to sign Support Shell job requests. -use permslip_client_lib::login::{IdentityProvider, TokenProvider}; +use http::HeaderValue; +use permslip_client_lib::login::TokenProvider; use permslip_client_lib::types::{CreateSushSessionBody, CreatedSushSession, Error as ApiError}; use permslip_client_lib::{Client, ClientRequestBuilder, Error as ClientError}; use sled_hardware_types::BaseboardId; @@ -20,12 +21,32 @@ pub struct PermslipSigner { key_name: String, } +/// Get a bearer token by the sshauth challenge, signed with the agent +/// key named by `fingerprint`. +pub async fn fresh_token( + url: &str, + agent_sock: String, + fingerprint: String, +) -> Result { + let tokens = TokenProvider::SshAuth { + fingerprint: Some(fingerprint), + server_url: url.to_owned(), + agent_sock, + }; + let token = tokens.token().await.map_err(PermslipError::token)?; + let value = token.into_header_value().map_err(PermslipError::token)?; + value + .to_str() + .map(str::to_owned) + .map_err(PermslipError::token) +} + impl PermslipSigner { - pub async fn new>(key_name: N, url: &str) -> Result { - let tokens = TokenProvider::IdP(IdentityProvider::Google); - let mut builder = ClientRequestBuilder::new(); - let token = tokens.token().await.map_err(PermslipError::token)?; - builder = builder.token(token.into_header_value().map_err(PermslipError::token)?); + /// A signer authenticated with `token`. + pub fn new>(key_name: N, url: &str, token: &str) -> Result { + let mut token = HeaderValue::from_str(token).map_err(PermslipError::token)?; + token.set_sensitive(true); + let builder = ClientRequestBuilder::new().token(token); Ok(Self { client: Client::new_with_client( url, diff --git a/client/src/repl.rs b/client/src/repl.rs index 578a5627..57d91a09 100644 --- a/client/src/repl.rs +++ b/client/src/repl.rs @@ -228,6 +228,14 @@ impl CommandContext for Repl { self.cli.authz_signer() } + fn permslip_token(&self, url: &str, fingerprint: &str) -> Option { + self.cli.permslip_token(url, fingerprint) + } + + fn save_permslip_token(&self, url: &str, fingerprint: &str, token: &str) { + self.cli.save_permslip_token(url, fingerprint, token) + } + fn session_id(&self) -> Option { self.cli.session_id() } diff --git a/common/src/keys.rs b/common/src/keys.rs index 84fb1f66..6d627991 100644 --- a/common/src/keys.rs +++ b/common/src/keys.rs @@ -29,7 +29,8 @@ use schemars::{JsonSchema, SchemaGenerator}; use serde::{Deserialize, Serialize}; use signature::Verifier; use ssh_key::{ - Algorithm as SshAlgorithm, EcdsaCurve, Error as SshKeyError, Mpint, Signature as SshSignature, + Algorithm as SshAlgorithm, EcdsaCurve, Error as SshKeyError, HashAlg, Mpint, + Signature as SshSignature, }; use thiserror::Error; use x509_cert::der::Encode as _; @@ -130,6 +131,11 @@ impl SshPublicKey { matches!(self.algorithm(), SkEcdsaSha2NistP256 | SkEd25519) } + /// The OpenSSH `SHA256:...` fingerprint. + pub fn fingerprint(&self) -> String { + self.0.fingerprint(HashAlg::Sha256).to_string() + } + pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<(), KeyError> { signature.verify_with_ssh_public_key(message, self) } From 2866d777d1ce630950f0f1059b849e5149efeeba Mon Sep 17 00:00:00 2001 From: Alex Plotnick Date: Sat, 29 Aug 2026 18:46:31 -0600 Subject: [PATCH 2/4] Advance permission-slip to security key support With sshauth speaking the sk- algorithms, a hardware key in the agent now serves the whole flow. Co-Authored-By: Claude Mythos 5 --- Cargo.lock | 6 +++--- Cargo.toml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60f85944..115e55a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3122,7 +3122,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "permission-slip-client" version = "1.1.0" -source = "git+https://github.com/oxidecomputer/permission-slip?rev=07b5953145cd55bcf14b2461c40639eaced3bd77#07b5953145cd55bcf14b2461c40639eaced3bd77" +source = "git+https://github.com/oxidecomputer/permission-slip?rev=c9d302ef44576e075e35f4f28f8e7ac8345bcd4b#c9d302ef44576e075e35f4f28f8e7ac8345bcd4b" dependencies = [ "anyhow", "base64 0.22.1", @@ -3158,7 +3158,7 @@ dependencies = [ [[package]] name = "permission-slip-common" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/permission-slip?rev=07b5953145cd55bcf14b2461c40639eaced3bd77#07b5953145cd55bcf14b2461c40639eaced3bd77" +source = "git+https://github.com/oxidecomputer/permission-slip?rev=c9d302ef44576e075e35f4f28f8e7ac8345bcd4b#c9d302ef44576e075e35f4f28f8e7ac8345bcd4b" dependencies = [ "aws-types", "blake3", @@ -4701,7 +4701,7 @@ dependencies = [ [[package]] name = "sshauth" version = "0.1.2" -source = "git+https://github.com/oxidecomputer/sshauth#6233c1b25d9e3b03d51ab95b19ed7903bfadfd8f" +source = "git+https://github.com/oxidecomputer/sshauth?rev=afd9b4e549d0e9229c2910c94aa7bca76623dfdb#afd9b4e549d0e9229c2910c94aa7bca76623dfdb" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index 3d31ce25..07505a5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,8 +38,8 @@ memmap2 = "0.9" p256 = { version = "0.13", features = ["ecdsa", "pkcs8"] } pem-rfc7468 = { version = "0.7", features = ["std"] } percent-encoding = "2" -permission-slip-client = { git = "https://github.com/oxidecomputer/permission-slip", rev = "07b5953145cd55bcf14b2461c40639eaced3bd77" } -permission-slip-common = { git = "https://github.com/oxidecomputer/permission-slip", rev = "07b5953145cd55bcf14b2461c40639eaced3bd77" } +permission-slip-client = { git = "https://github.com/oxidecomputer/permission-slip", rev = "c9d302ef44576e075e35f4f28f8e7ac8345bcd4b" } +permission-slip-common = { git = "https://github.com/oxidecomputer/permission-slip", rev = "c9d302ef44576e075e35f4f28f8e7ac8345bcd4b" } progenitor = "0.14" progenitor-client = "0.14" pwd = "1" From 063dbeed8293a6ce8e53daff1adb13fa2196b010 Mon Sep 17 00:00:00 2001 From: Alex Plotnick Date: Sat, 29 Aug 2026 20:03:32 -0600 Subject: [PATCH 3/4] Tighten the token cache A backward clock step no longer leaves a future-stamped token trusted forever, and the cache file is born 0600 instead of being chmodded after open. Co-Authored-By: Claude Mythos 5 --- client/src/cli.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/src/cli.rs b/client/src/cli.rs index 995dba66..6f86bd40 100644 --- a/client/src/cli.rs +++ b/client/src/cli.rs @@ -6,9 +6,9 @@ use std::collections::BTreeMap; use std::fmt; -use std::fs::{self, Permissions}; +use std::fs; use std::io::{self, BufRead as _, ErrorKind, Read as _, Write as _, stderr, stdin, stdout}; -use std::os::unix::fs::PermissionsExt as _; +use std::os::unix::fs::OpenOptionsExt as _; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; @@ -154,13 +154,13 @@ impl Cli { } } -/// Atomically write a file only the user may read. +/// Atomically write a file only the user may read, born that way +/// rather than chmodded after opening. fn write_private(path: &Path, bytes: &[u8]) -> io::Result<()> { + let mut options = fs::OpenOptions::new(); + options.write(true).create(true).truncate(true).mode(0o600); AtomicFile::new(path, OverwriteBehavior::AllowOverwrite) - .write(|file| { - file.set_permissions(Permissions::from_mode(0o600))?; - file.write_all(bytes) - }) + .write_with_options(|file| file.write_all(bytes), options) .map_err(|error| match error { atomicwrites::Error::Internal(error) | atomicwrites::Error::User(error) => error, }) @@ -282,7 +282,7 @@ impl CommandContext for Cli { created, }) if saved_url == url && saved_fingerprint == fingerprint - && Utc::now() - created < TOKEN_REUSE => + && (TimeDelta::zero()..TOKEN_REUSE).contains(&(Utc::now() - created)) => { Some(token) } From d7f832138efc4a2c203d0e229853d87086058a41 Mon Sep 17 00:00:00 2001 From: Alex Plotnick Date: Tue, 1 Sep 2026 10:04:33 -0600 Subject: [PATCH 4/4] Show permslip API errors instead of raw blobs Co-Authored-By: Claude Mythos 5 --- Cargo.lock | 1 + Cargo.toml | 1 + client/Cargo.toml | 3 ++- client/src/permslip.rs | 10 +++++++++- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 115e55a3..b00d7e59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4841,6 +4841,7 @@ version = "0.1.0" dependencies = [ "anstream", "anstyle", + "anyhow", "async-recursion", "atomicwrites", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index 07505a5f..16b2b161 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ version = "0.1.0" [workspace.dependencies] anstream = "1" +anyhow = "1" anstyle = "1" async-recursion = "1" attest-mock = { git = "https://github.com/oxidecomputer/dice-util", rev = "10952e8d9599b735b85d480af3560a11700e5b64" } diff --git a/client/Cargo.toml b/client/Cargo.toml index 3c1a479d..83293386 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -14,11 +14,12 @@ path = "src/main.rs" [features] # Sign job requests through Permission Slip. Off by default because the # permission-slip repositories are private; Oxide builds turn it on. -permslip = ["dep:permission-slip-client", "dep:permission-slip-common"] +permslip = ["dep:anyhow", "dep:permission-slip-client", "dep:permission-slip-common"] [dependencies] anstream.workspace = true anstyle.workspace = true +anyhow = { workspace = true, optional = true } async-recursion.workspace = true atomicwrites.workspace = true base64.workspace = true diff --git a/client/src/permslip.rs b/client/src/permslip.rs index dd7024b3..fca7477c 100644 --- a/client/src/permslip.rs +++ b/client/src/permslip.rs @@ -33,7 +33,7 @@ pub async fn fresh_token( server_url: url.to_owned(), agent_sock, }; - let token = tokens.token().await.map_err(PermslipError::token)?; + let token = tokens.token().await.map_err(PermslipError::token_flow)?; let value = token.into_header_value().map_err(PermslipError::token)?; value .to_str() @@ -121,6 +121,14 @@ impl PermslipError { fn token(error: E) -> Self { Self::Token(error.to_string()) } + + /// Recover the API error from the permslip token flow. + fn token_flow(error: anyhow::Error) -> Self { + match error.downcast::>() { + Ok(client) => client.into(), + Err(error) => Self::token(error), + } + } } impl From> for PermslipError {