use std::net::SocketAddr;
use anyhow::{Context, Result};
use cli_shared::{ClientConfig, UserConfig, credentials};
use crypto::{Ed25519Signer, Signer};
use wire::{AuthToken, ProtocolError};
use super::{
HostedClient, RenewableAuthorityCredential, credential::server_keys_match,
resolve_hosted_credential, resolver::resolve_and_verify_endpoint_descriptor,
};
pub enum HostedAuthMode {
Unauthenticated,
CredentialFallback,
}
pub struct HostedSession {
config: ClientConfig,
renewable_authority_credential: Option<RenewableAuthorityCredential>,
}
impl HostedSession {
pub fn build_stored_credential(user_config: &UserConfig, server_key: &str) -> Result<Self> {
let credential = credentials::get_server_credential(server_key)?.ok_or_else(|| {
anyhow::anyhow!(weft_client_shim::HostedRecoveryAdvice::auth_required(
server_key
))
})?;
let proof_key = validated_stored_proof_key(&credential, server_key)?;
let authenticated_principal = validated_authenticated_principal(&credential)?;
let renewable_authority_credential = RenewableAuthorityCredential::from_stored(&credential);
let token = AuthToken::new(credential.token, "credential-store");
let config = user_config
.hosted_runtime_config(Some(token))?
.with_server_key(server_key.to_string())
.with_auth_proof_key_pem(proof_key)
.with_authenticated_principal(authenticated_principal);
Ok(Self {
config,
renewable_authority_credential,
})
}
pub fn build(
user_config: &UserConfig,
server_key: Option<String>,
mode: HostedAuthMode,
) -> Result<Self> {
let (
token,
mut credential_proof_key,
renewable_authority_credential,
stored_credential_subject,
) = match mode {
HostedAuthMode::Unauthenticated => (None, None, None, None),
HostedAuthMode::CredentialFallback => {
let resolved = resolve_hosted_credential(server_key.as_deref())?;
(
resolved.token,
resolved.proof_key_pem,
resolved.renewable,
resolved.subject,
)
}
};
if credential_proof_key.is_none()
&& let Some(ref key) = server_key
&& let Some(token) = token.as_ref()
{
credential_proof_key = shared_device_proof_key(key, &token.id)?;
}
let mut config = user_config.hosted_runtime_config(token)?;
if let Some(key) = server_key {
config = config.with_server_key(key);
}
if let Some(pem) = credential_proof_key
&& config.auth_proof_key_pem.is_none()
{
config = config.with_auth_proof_key_pem(pem);
}
if config.auth_proof_key_pem.is_some() {
let token = config.token.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"hosted request signing has a proof key but no authenticated bearer token"
)
})?;
let subject = crate::hosted_runtime::device_flow::authenticated_subject(&token.id)
.context("reading the hosted bearer token's authenticated principal")?;
if stored_credential_subject
.as_deref()
.is_some_and(|stored| stored != subject.as_str())
{
anyhow::bail!(
"stored credential subject does not match the bearer token's authenticated principal"
);
}
config = config.with_authenticated_principal(format!("principal:{subject}"));
}
Ok(Self {
config,
renewable_authority_credential,
})
}
pub fn with_allow_insecure(mut self, allow: bool) -> Self {
if allow {
self.config.allow_insecure = true;
}
self
}
pub async fn discover_endpoint(
&self,
fallback_addr: SocketAddr,
) -> super::Result<super::VerifiedEndpointDescriptor> {
let server = self
.config
.server_key
.as_deref()
.map(str::to_string)
.unwrap_or_else(|| fallback_addr.to_string());
resolve_and_verify_endpoint_descriptor(&server, &self.config).await
}
pub async fn connect(&self, fallback_addr: SocketAddr) -> Result<HostedClient, ProtocolError> {
let descriptor = self
.discover_endpoint(fallback_addr)
.await
.map_err(|error| ProtocolError::Remote(error.to_string()))?;
let mut client = HostedClient::connect_with_config(&descriptor, &self.config)
.await
.map_err(|error| ProtocolError::Remote(error.to_string()))?;
client
.auto_rotate_if_needed(self.renewable_authority_credential.as_ref())
.await;
Ok(client)
}
}
impl HostedClient {
pub async fn connect_server(server: &str, config: &ClientConfig) -> Result<Self> {
let descriptor = resolve_and_verify_endpoint_descriptor(server, config).await?;
Ok(Self::connect_with_config(&descriptor, config).await?)
}
pub async fn open_session(
addr: SocketAddr,
user_config: &UserConfig,
server_key: Option<String>,
mode: HostedAuthMode,
) -> Result<Self> {
Self::open_session_with_insecure(addr, user_config, server_key, mode, false).await
}
pub async fn open_session_with_insecure(
addr: SocketAddr,
user_config: &UserConfig,
server_key: Option<String>,
mode: HostedAuthMode,
allow_insecure: bool,
) -> Result<Self> {
Ok(HostedSession::build(user_config, server_key, mode)?
.with_allow_insecure(allow_insecure)
.connect(addr)
.await?)
}
}
fn validated_authenticated_principal(credential: &credentials::ServerCredential) -> Result<String> {
let subject = crate::hosted_runtime::device_flow::authenticated_subject(&credential.token)
.context("reading the stored credential's authenticated principal")?;
if subject != credential.subject {
anyhow::bail!(
"stored credential subject does not match the bearer token's authenticated principal"
);
}
Ok(format!("principal:{subject}"))
}
fn validated_stored_proof_key(
credential: &credentials::ServerCredential,
server_key: &str,
) -> Result<String> {
let pem = credential.private_key_pem.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"stored credential for {server_key} has no device proof key; run `heddle auth login --server {server_key}` first"
)
})?;
let signer = Ed25519Signer::from_pem(pem)
.map_err(|error| anyhow::anyhow!("stored device proof key is invalid: {error}"))?;
let token_key =
crate::hosted_runtime::device_flow::effective_pop_public_key_hex(&credential.token)
.context("reading the stored credential's effective proof key")?;
if !token_key.eq_ignore_ascii_case(&hex::encode(signer.public_key())) {
anyhow::bail!("stored device proof key does not match the credential Biscuit");
}
Ok(pem.to_string())
}
fn shared_device_proof_key(server_key: &str, token: &str) -> Result<Option<String>> {
let identity = repo::identity::load_device(&repo::identity::device_identity_path())
.context("loading this host's shared device identity")?;
let Some(identity) = identity else {
return Ok(None);
};
if !server_keys_match(&identity.server, server_key)
|| !crate::hosted_runtime::device_flow::effective_pop_public_key_hex(token)
.is_ok_and(|key| key.eq_ignore_ascii_case(&identity.public_key))
{
return Ok(None);
}
Ok(Some(identity.private_key_pem))
}