Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ chrono = { version = "0.4", features = ["serde"] }
ciborium = "0.2"
clap = { version = "4", features = ["derive", "env", "wrap_help"] }
crypto-bigint = "0.5"
dice-mfg-msgs = { git = "https://github.com/oxidecomputer/dice-util", rev = "4a39ef08d81e5177edee0bddb1146032aa21074d", features = ["std"] }
dropshot = "0.17"
ed25519-dalek = { version = "2", features = ["pkcs8", "rand_core"] }
function_name = "0.3"
Expand Down Expand Up @@ -64,8 +65,8 @@ signature = "2"
sled-hardware-types = { git = "https://github.com/oxidecomputer/omicron", tag = "rel/v21/rc1" }
slog = "2"
slog-term = "2"
sprockets-tls = { git = "https://github.com/oxidecomputer/sprockets.git", rev = "98615b86d51f74ce220198eb16c57932fd24e7cb", default-features = false }
sprockets-tls-test-utils = { git = "https://github.com/oxidecomputer/sprockets.git", rev = "98615b86d51f74ce220198eb16c57932fd24e7cb" }
sprockets-tls = { git = "https://github.com/oxidecomputer/sprockets.git", rev = "02c7e5414d97c0c85d629cb76d1f6beab73d960e", default-features = false }
sprockets-tls-test-utils = { git = "https://github.com/oxidecomputer/sprockets.git", rev = "02c7e5414d97c0c85d629cb76d1f6beab73d960e" }
ssh-key = { version = "0.6", features = ["ed25519", "p256", "serde"] }
strum = { version = "0.27", features = ["derive"] }
tempfile = "3"
Expand Down
40 changes: 37 additions & 3 deletions api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use serde::{Deserialize, Serialize};
use sled_hardware_types::BaseboardId;
use strum::{Display, EnumString};

use sush_common::authn::Identity;
use sush_common::authn::{Identity, LoginReceipt};
use sush_common::jobs::{
Access, JobId, JobLimits, JobOutputStream, JobStatus, JsonJobStatusMap, Session,
SessionClientNonce, SessionId, SessionSeq, SessionServerNonce, SessionSignerNonce, SignedJob,
Expand Down Expand Up @@ -54,8 +54,8 @@ pub trait SushApi {
ctx: RequestContext<Self::Context>,
headers: Header<Authorization>,
query: QueryParams<RoutingParam>,
body: TypedBody<Option<SshPublicKey>>,
) -> Result<HttpResponseOk<Identity>, HttpError>;
body: TypedBody<IamRequest>,
) -> Result<HttpResponseOk<IamResponse>, HttpError>;

/// List cached identities.
///
Expand Down Expand Up @@ -281,6 +281,40 @@ pub struct JobTargetParams {
pub target: String,
}

/// Login evidence that accompanies initial credentials.
#[derive(Clone, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub struct IamRequest {
/// The key that signed the credentials.
pub public_key: Option<SshPublicKey>,

/// Another server's receipt for the credentials.
pub receipt: Option<LoginReceipt>,
}

impl IamRequest {
pub fn new(public_key: SshPublicKey, receipt: Option<LoginReceipt>) -> Self {
Self {
public_key: Some(public_key),
receipt,
}
}
}

impl From<SshPublicKey> for IamRequest {
fn from(public_key: SshPublicKey) -> Self {
Self::new(public_key, None)
}
}

/// The result of a successful login.
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
pub struct IamResponse {
pub identity: Identity,

/// A receipt for the login, present when the server issues receipts.
pub receipt: Option<LoginReceipt>,
}

#[derive(Deserialize, JsonSchema)]
pub struct SessionStartBody {
pub client_nonce: SessionClientNonce,
Expand Down
3 changes: 1 addition & 2 deletions client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ anstream.workspace = true
anstyle.workspace = true
anyhow = { workspace = true, optional = true }
async-recursion.workspace = true
attest-data.workspace = true
atomicwrites.workspace = true
base64.workspace = true
bytes.workspace = true
Expand Down Expand Up @@ -56,7 +55,7 @@ signature.workspace = true
sled-hardware-types.workspace = true
ssh-key.workspace = true
sush-api = { path = "../api" }
sush-common = { path = "../common" }
sush-common = { path = "../common", features = ["verify-vouchers"] }
thiserror.workspace = true
tokio.workspace = true
tokio-fd.workspace = true
Expand Down
53 changes: 39 additions & 14 deletions client/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ use tokio_tungstenite::tungstenite::protocol::Role;
use x509_cert::Certificate;
use x509_cert::der::DecodePem as _;

use sush_api::{ErrorCode, JobWait};
use sush_api::{ErrorCode, IamRequest, IamResponse, JobWait};
use sush_common::authn::{
AuthnError, Challenge, ChallengeResponse, Credentials, Identity, RequestKey,
};
Expand Down Expand Up @@ -918,19 +918,37 @@ async fn authenticate<E>(
let mut request = client
.iam()
.authorization(credentials.to_string())
.body(public_key.to_string());
.body(IamRequest::from(public_key.clone()));
if let Some(via) = via {
request = request.via(via.to_string());
}
let identity = match request.send().await {
Ok(identity) => identity.into_inner(),
let IamResponse { identity, receipt } = match request.send().await {
Ok(response) => response.into_inner(),
Err(error) if error.status() == Some(StatusCode::UNAUTHORIZED) => {
let server = client.version().send().await.ok().map(|v| v.into_inner());
return Err(login_refused(server, error.into()));
}
Err(error) => return Err(error.into()),
};
Ok((identity, Authz::new(credentials, key)))
let mut authz = Authz::new(credentials, key);
authz.receipt = receipt.map(|receipt| IamRequest::new(public_key, Some(receipt)));
Ok((identity, authz))
}

/// Present the receipt in `authz` to sled `via`, and return whether
/// the sled accepted it.
async fn present_receipt(client: &Client, via: Option<&BaseboardId>, authz: Authz) -> bool {
let Some(receipt) = authz.receipt else {
return false;
};
let mut request = client
.iam()
.authorization(authz.credentials.to_string())
.body(receipt);
if let Some(via) = via {
request = request.via(via.to_string());
}
request.send().await.is_ok()
}

/// A refused login usually indicates build skew rather than a bad key.
Expand Down Expand Up @@ -960,8 +978,8 @@ where
}

/// Like [`with_login`], for a request that the proxy routes to sled
/// `via`. A sled that refuses a request answers with a login challenge
/// that only it can verify, so the login goes to `via` as well.
/// `via`. If `via` refuses the request, present the current login's
/// receipt to it, and if it refuses that too, log in at `via`.
async fn with_login_via<T, E, Req>(
ctx: &mut impl CommandContext,
client: &Client,
Expand All @@ -974,16 +992,24 @@ where
{
let signer = ctx.authz_signer();
let mut generation = signer.generation();
let mut presented = false;
let mut logged_in = false;
loop {
match make_request().await {
Err(ClientError::ErrorResponse(err))
if err.status() == StatusCode::UNAUTHORIZED && !logged_in =>
{
if let Some(_login) = signer.login_needed(generation).await {
let (_identity, authz) = authenticate(ctx, client, via, err).await?;
ctx.set_credentials(Some(authz));
logged_in = true;
if !presented
&& let Some(authz) = signer.get()
&& present_receipt(client, via, authz).await
{
presented = true;
} else {
let (_identity, authz) = authenticate(ctx, client, via, err).await?;
ctx.set_credentials(Some(authz));
logged_in = true;
}
}
generation = signer.generation();
}
Expand Down Expand Up @@ -1029,10 +1055,9 @@ async fn iam(
}

IdentityCommand::Login => {
let identity = with_login(ctx, client, async || client.iam().body(None).send().await)
.await?
.into_inner();
ctx.iam(&identity)
let request = async || client.iam().body(IamRequest::default()).send().await;
let response = with_login(ctx, client, request).await?.into_inner();
ctx.iam(&response.identity)
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions client/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use clap::ValueEnum;
use sled_hardware_types::BaseboardId;
use x509_cert::Certificate;

use sush_api::IamRequest;
use sush_common::authn::{BoundRequest, Credentials, Identity, RequestKey};
use sush_common::jobs::{
Access, JobId, JobOutputStream, JobStartRequest, JobStatusMap, Session, SessionClientNonce,
Expand All @@ -30,6 +31,7 @@ use crate::commands::{CommandError, GlobalArgs};
#[derive(Clone, Debug)]
pub struct Authz {
pub credentials: Credentials,
pub receipt: Option<IamRequest>,
key: RequestKey,
seq: Arc<AtomicU64>,
}
Expand All @@ -38,6 +40,7 @@ impl Authz {
pub fn new(credentials: Credentials, key: RequestKey) -> Self {
Self {
credentials,
receipt: None,
key,
seq: Arc::new(AtomicU64::new(1)),
}
Expand Down
2 changes: 2 additions & 0 deletions client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ progenitor::generate_api!(
replace = {
Access = sush_common::jobs::Access,
BaseboardId = sled_hardware_types::BaseboardId,
IamRequest = sush_api::IamRequest,
IamResponse = sush_api::IamResponse,
Identity = sush_common::authn::Identity,
Session = sush_common::jobs::Session,
SessionId = sush_common::jobs::SessionId,
Expand Down
Loading