crates/monetize-server/tests/control_sshsig.rs
//! **The control API from outside the estate: SSHSIG per call** (owner
//! 2026-09-28). The shared operator console holds an SSH key; this monetize
//! knows its PUBLIC half as the admin principal `monetize-ui`
//! (`control_principals`). Every call is signed for its own method, once.
//!
//! Proven over a real gRPC listener: the principal's signed call is served
//! WITHOUT the bearer; the same credential replayed, or presented at another
//! method, is refused; a key monetize does not know is refused; a principal
//! added to the config later is taken in live; the bearer still opens the API
//! (the console inside the estate, on loopback).
use std::collections::BTreeMap;
use std::sync::Arc;
use monetize_proto::control_auth::{self, sshsig, METADATA_KEY};
use monetize_proto::products_client::ProductsClient;
use monetize_proto::ListProductsRequest;
use monetize_server::registry::Registry;
use monetize_server::state::AppState;
use monetize_server::testing::TestServer;
use tonic::metadata::MetadataValue;
use tonic::transport::Channel;
use tonic::Request;
const BEARER: &str = "the-estates-own-bearer-0123456789";
fn key(seed: u8) -> sshsig::ssh_key::PrivateKey {
sshsig::ssh_key::PrivateKey::from(sshsig::ssh_key::private::Ed25519Keypair::from_seed(&[seed; 32]))
}
fn line(k: &sshsig::ssh_key::PrivateKey) -> String {
k.public_key().to_openssh().unwrap()
}
fn now_ms() -> i64 {
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as i64
}
fn signed(k: &sshsig::ssh_key::PrivateKey, nonce: &str, method: &str) -> Request<ListProductsRequest> {
let c = control_auth::sign(k, now_ms(), nonce, method).unwrap();
let mut r = Request::new(ListProductsRequest {});
r.metadata_mut().insert(METADATA_KEY, MetadataValue::try_from(c.to_header_value()).unwrap());
r
}
#[tokio::test(flavor = "multi_thread")]
async fn a_control_principal_signs_each_call_and_nothing_else_opens_the_api_without_the_bearer() {
let console = key(7);
let stranger = key(8);
let state = Arc::new(AppState::in_memory(Registry::new()));
state.control.set(&BTreeMap::from([("monetize-ui".to_string(), line(&console))]));
let server = TestServer::spawn(state.clone(), BEARER).await;
let ch = Channel::from_shared(server.endpoint()).unwrap().connect().await.unwrap();
let mut client = ProductsClient::new(ch);
// The principal, signed for THIS method, with no bearer at all.
client.list(signed(&console, "nonce-one-0000000001", "Products.List")).await.expect("the console's signed call is served");
// Nobody else, and nothing twice.
let c = control_auth::sign(&console, now_ms(), "nonce-two-0000000002", "Products.List").unwrap();
let with = |c: &control_auth::Credential| {
let mut r = Request::new(ListProductsRequest {});
r.metadata_mut().insert(METADATA_KEY, MetadataValue::try_from(c.to_header_value()).unwrap());
r
};
client.list(with(&c)).await.expect("first use");
assert_eq!(client.list(with(&c)).await.unwrap_err().code(), tonic::Code::Unauthenticated, "a replayed credential");
let other = client.list(signed(&console, "nonce-three-00000003", "Purchase.Start")).await.unwrap_err();
assert_eq!(other.code(), tonic::Code::Unauthenticated, "signed for another method");
assert!(other.message().contains("Purchase.Start"), "{}", other.message());
assert_eq!(client.list(signed(&stranger, "nonce-four-000000004", "Products.List")).await.unwrap_err().code(), tonic::Code::Unauthenticated, "a key monetize does not know");
assert_eq!(client.list(Request::new(ListProductsRequest {})).await.unwrap_err().code(), tonic::Code::Unauthenticated, "no credential, no bearer");
// A principal fed later is taken in live; a removed one is out.
state.control.set(&BTreeMap::from([("monetize-ui".to_string(), line(&console)), ("second-console".to_string(), line(&stranger))]));
client.list(signed(&stranger, "nonce-five-000000005", "Products.List")).await.expect("added live");
state.control.set(&BTreeMap::new());
assert_eq!(client.list(signed(&console, "nonce-six-0000000006", "Products.List")).await.unwrap_err().code(), tonic::Code::Unauthenticated, "revoked by removing the line");
// The bearer still opens it (the console inside the estate).
let mut r = Request::new(ListProductsRequest {});
r.metadata_mut().insert("authorization", MetadataValue::try_from(format!("Bearer {BEARER}")).unwrap());
client.list(r).await.expect("the bearer is unchanged");
}
/// A gunnar credential (another product's scheme) never opens monetize.
#[tokio::test(flavor = "multi_thread")]
async fn another_products_credential_is_not_a_monetize_credential() {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
struct Gunnarish;
impl sshsig::Scheme for Gunnarish {
const METADATA_KEY: &'static str = METADATA_KEY;
const SCHEME: &'static str = "monetize-sshsig-v1";
const SCHEME_UNBOUND: Option<&'static str> = None;
const NAMESPACE: &'static str = "gunnar-grpc";
const PREAMBLE: &'static str = "monetize-grpc-auth-v1";
const PREAMBLE_UNBOUND: &'static str = "";
type Error = sshsig::Refusal;
}
let console = key(9);
let state = Arc::new(AppState::in_memory(Registry::new()));
state.control.set(&BTreeMap::from([("monetize-ui".to_string(), line(&console))]));
let server = TestServer::spawn(state, BEARER).await;
let ch = Channel::from_shared(server.endpoint()).unwrap().connect().await.unwrap();
let c = sshsig::sign::<Gunnarish>(&console, now_ms(), "nonce-gunnar-0000001", Some("Products.List")).unwrap();
let mut r = Request::new(ListProductsRequest {});
r.metadata_mut().insert(METADATA_KEY, MetadataValue::try_from(c.to_header_value()).unwrap());
assert_eq!(ProductsClient::new(ch).list(r).await.unwrap_err().code(), tonic::Code::Unauthenticated, "signed under gunnar's namespace");
}