cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

test

dami etoile committed f52cb66
commit
f52cb66c48dc214321e71edbc1ea15e86136c811
parent
2a0cbdf0dec6c512a74a08636597150c7d341b66
author
dami etoile <dami.etoile@gmail.com>
1 file changed, +55 -27
Mspikes/007-titan-no-copy/src/main.rs82++++++++++++++++++++----------
M spikes/007-titan-no-copy/src/main.rs +55 -27
@@ -1,18 +1,23 @@
11 //! Spike 007: No-copy/paste — derive Iroh identity from Titan key22 //!33 //! Both host and client derive the SAME Iroh SecretKey from the Titan's4-//! hmac-secret extension. The host uses it as its endpoint identity.5-//! The client derives it only to compute the host's node ID, then dials6-//! via the N0 relay — no address JSON exchanged.4+//! hmac-secret extension. Uses resident keys (discoverable credentials)5+//! so the credential persists on the Titan — no credential ID needs to6+//! be shared between machines.7+//!8+//! First run on a Titan: creates a resident credential.9+//! Subsequent runs: reuses the existing credential via get_assertion10+//! without specifying a credential ID.711 //!812 //! Usage:9-//! cargo run -- host # Tap Titan twice, starts hosting10-//! cargo run -- client # Tap Titan twice, auto-connects to host13+//! cargo run -- host # Tap Titan, starts hosting14+//! cargo run -- client # Tap Titan, auto-connects to host1115 1216 use anyhow::{bail, Context, Result};1317 use ctap_hid_fido2::fidokey::{1418 GetAssertionArgsBuilder, MakeCredentialArgsBuilder,1519 get_assertion::Extension as Gext,20+ get_assertion::get_assertion_params::Assertion,1621 make_credential::Extension as Mext,1722 };1823 use ctap_hid_fido2::{verifier, Cfg, FidoKeyHidFactory};
@@ -22,7 +27,6 @@ use iroh::protocol::{ProtocolHandler, Router};
2227 use sha2::{Digest, Sha256};2328 use std::sync::Arc;2429 use std::time::Duration;25-use tokio::io::{AsyncReadExt, AsyncWriteExt};2630 2731 const ALPN: &[u8] = b"keyhome/titan-probe/0";2832 const RPID: &str = "keyhome";
@@ -48,6 +52,11 @@ fn read_pin() -> Result<String> {
4852 }4953 5054 /// Derive a 32-byte secret from the Titan using hmac-secret extension.55+///56+/// Strategy: try get_assertion WITHOUT a credential ID first (uses resident key).57+/// If that fails (no credential exists yet), create a resident credential,58+/// then get_assertion with it.59+///5160 /// Same Titan + same PIN + same salt = same 32 bytes every time.5261 fn derive_secret_from_titan() -> Result<[u8; 32]> {5362 let pin = read_pin()?;
@@ -58,8 +67,36 @@ fn derive_secret_from_titan() -> Result<[u8; 32]> {
5867 5968 eprintln!("[titan] FIDO2 device opened");6069 61- // Step 1: Create credential with hmac-secret extension62- eprintln!("[titan] Step 1: make_credential (TAP TITAN when it blinks)");70+ let salt: [u8; 32] = {71+ let mut hasher = Sha256::new();72+ hasher.update(SALT_MESSAGE.as_bytes());73+ let result = hasher.finalize();74+ let mut s = [0u8; 32];75+ s.copy_from_slice(&result);76+ s77+ };78+79+ // Step 1: Try get_assertion WITHOUT credential ID (uses resident key)80+ eprintln!("[titan] Trying get_assertion with resident key (TAP TITAN when it blinks)");81+82+ let challenge = verifier::create_challenge();83+ let get_args = GetAssertionArgsBuilder::new(RPID, &challenge)84+ .pin(&pin)85+ .extensions(&[Gext::HmacSecret(Some(salt))])86+ .build();87+88+ match device.get_assertion_with_args(&get_args) {89+ Ok(assertions) => {90+ eprintln!("[titan] resident key found, assertion received");91+ return extract_hmac_secret(&assertions);92+ }93+ Err(e) => {94+ eprintln!("[titan] no resident key found ({}), creating one...", e);95+ }96+ }97+98+ // Step 2: Create a resident credential with hmac-secret99+ eprintln!("[titan] make_credential (TAP TITAN when it blinks)");63100 64101 let challenge = verifier::create_challenge();65102 let make_args = MakeCredentialArgsBuilder::new(RPID, &challenge)
@@ -76,19 +113,10 @@ fn derive_secret_from_titan() -> Result<[u8; 32]> {
76113 bail!("Attestation verification failed");77114 }78115 let credential_id = verify_result.credential_id;79- eprintln!("[titan] credential registered, id_len={}", credential_id.len());116+ eprintln!("[titan] credential created, id_len={}", credential_id.len());80117 81- // Step 2: Get assertion with hmac-secret to derive the secret82- eprintln!("[titan] Step 2: get_assertion (TAP TITAN when it blinks)");83-84- let salt: [u8; 32] = {85- let mut hasher = Sha256::new();86- hasher.update(SALT_MESSAGE.as_bytes());87- let result = hasher.finalize();88- let mut s = [0u8; 32];89- s.copy_from_slice(&result);90- s91- };118+ // Step 3: Get assertion with the new credential119+ eprintln!("[titan] get_assertion (TAP TITAN when it blinks)");92120 93121 let challenge2 = verifier::create_challenge();94122 let get_args = GetAssertionArgsBuilder::new(RPID, &challenge2)
@@ -101,17 +129,19 @@ fn derive_secret_from_titan() -> Result<[u8; 32]> {
101129 .get_assertion_with_args(&get_args)102130 .context("get_assertion failed")?;103131 104- // Extract hmac-secret output (32 bytes)132+ extract_hmac_secret(&assertions)133+}134+135+fn extract_hmac_secret(assertions: &[Assertion]) -> Result<[u8; 32]> {105136 for ext in &assertions[0].extensions {106137 if let Gext::HmacSecret(Some(output)) = ext {107138 let mut secret = [0u8; 32];108- secret.copy_from_slice(output);139+ secret.copy_from_slice(&output[..]);109140 eprintln!("[titan] derived 32-byte secret from hmac-secret");110141 return Ok(secret);111142 }112143 }113-114- bail!("No hmac-secret in assertion response");144+ bail!("No hmac-secret in assertion response")115145 }116146 117147 /// Derive an Iroh SecretKey from the Titan.
@@ -180,7 +210,6 @@ async fn client() -> Result<()> {
180210 eprintln!("[client] Deriving host identity from Titan...");181211 let host_secret = derive_iroh_secret_from_titan()?;182212 183- // The host's node ID is derived from the same Titan184213 let host_node_id = host_secret.public();185214 println!("[client] derived host node ID: {}", host_node_id);186215
@@ -196,8 +225,7 @@ async fn client() -> Result<()> {
196225 197226 let _ = tokio::time::timeout(Duration::from_secs(10), endpoint.online()).await;198227 199- // Construct an EndpointAddr with just the node ID + N0 relay200- // No address JSON needed — relay routes by node ID228+ // Connect using just the node ID + N0 relay — no address JSON201229 let addr = iroh::EndpointAddr::new(host_node_id)202230 .with_relay_url("https://usw1-1.relay.n0.iroh.link./".parse()203231 .context("invalid relay URL")?);