Share owned host API state between HTTP hooks and native pages
This commit is contained in:
+4
-4
@@ -15,7 +15,7 @@ fn caller() -> Caller {
|
||||
conversations: vec!["conversation".into()],
|
||||
}
|
||||
}
|
||||
async fn send(api: &Api<'_>, method: &str, path: &str, body: Value) -> Result<(u16, Value)> {
|
||||
async fn send(api: &Api, method: &str, path: &str, body: Value) -> Result<(u16, Value)> {
|
||||
let request = http::Request::builder()
|
||||
.method(method)
|
||||
.uri(path)
|
||||
@@ -31,7 +31,7 @@ async fn send(api: &Api<'_>, method: &str, path: &str, body: Value) -> Result<(u
|
||||
serde_json::from_slice(response.body())?,
|
||||
))
|
||||
}
|
||||
async fn ok(api: &Api<'_>, method: &str, path: &str, body: Value) -> Result<Value> {
|
||||
async fn ok(api: &Api, method: &str, path: &str, body: Value) -> Result<Value> {
|
||||
let (status, value) = send(api, method, path, body).await?;
|
||||
assert_eq!(status, 200, "unexpected API status");
|
||||
Ok(value)
|
||||
@@ -55,7 +55,7 @@ async fn public_routes_and_ssh() -> Result<()> {
|
||||
)?;
|
||||
let api = Api::new(
|
||||
cred,
|
||||
Passkeys::new(&db, "localhost", "http://localhost:8080")?,
|
||||
Passkeys::new(db, "localhost", "http://localhost:8080")?,
|
||||
EntryKey::open(&dir.path().join("entry.key"))?,
|
||||
);
|
||||
let who = ok(
|
||||
@@ -221,7 +221,7 @@ async fn ssh_rejects_ambiguous_items() -> Result<()> {
|
||||
item(&cred, "two")?;
|
||||
let api = Api::new(
|
||||
cred,
|
||||
Passkeys::new(&db, "localhost", "http://localhost:8080")?,
|
||||
Passkeys::new(db, "localhost", "http://localhost:8080")?,
|
||||
EntryKey::open(&dir.path().join("entry.key"))?,
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
+10
-9
@@ -21,7 +21,7 @@ fn caller() -> Caller {
|
||||
conversations: vec!["conversation".into()],
|
||||
}
|
||||
}
|
||||
async fn send(api: &Api<'_>, method: &str, path: &str, body: Value) -> Result<(u16, Value)> {
|
||||
async fn send(api: &Api, method: &str, path: &str, body: Value) -> Result<(u16, Value)> {
|
||||
let response = handle(
|
||||
api,
|
||||
caller(),
|
||||
@@ -37,12 +37,12 @@ async fn send(api: &Api<'_>, method: &str, path: &str, body: Value) -> Result<(u
|
||||
serde_json::from_slice(response.body())?,
|
||||
))
|
||||
}
|
||||
async fn ok(api: &Api<'_>, method: &str, path: &str, body: Value) -> Result<Value> {
|
||||
async fn ok(api: &Api, method: &str, path: &str, body: Value) -> Result<Value> {
|
||||
let (status, value) = send(api, method, path, body).await?;
|
||||
assert_eq!(status, 200, "unexpected API status");
|
||||
Ok(value)
|
||||
}
|
||||
async fn page(api: &Api<'_>, path: &str) -> Result<String> {
|
||||
async fn page(api: &Api, path: &str) -> Result<String> {
|
||||
let response = handle(
|
||||
api,
|
||||
caller(),
|
||||
@@ -62,7 +62,7 @@ async fn page(api: &Api<'_>, path: &str) -> Result<String> {
|
||||
fn item(cred: &Cred, name: &str) -> Result<cred::items::Item> {
|
||||
Ok(cred.create_item(serde_json::from_value(json!({"name":name,"rules":[],"transform_js":"","fields":[{"name":"first","field_type":"text"},{"name":"second","field_type":"text"}]}))?,&caller(),cred.unlocked()?)?)
|
||||
}
|
||||
async fn file(api: &Api<'_>, item: &str, key: &str) -> Result<String> {
|
||||
async fn file(api: &Api, item: &str, key: &str) -> Result<String> {
|
||||
let result = ok(
|
||||
api,
|
||||
"POST",
|
||||
@@ -72,7 +72,7 @@ async fn file(api: &Api<'_>, item: &str, key: &str) -> Result<String> {
|
||||
.await?;
|
||||
Ok(result["request"].as_str().unwrap().into())
|
||||
}
|
||||
async fn register(api: &Api<'_>) -> Result<WebauthnAuthenticator<SoftPasskey>> {
|
||||
async fn register(api: &Api) -> Result<WebauthnAuthenticator<SoftPasskey>> {
|
||||
let start: Challenge<CreationChallengeResponse> = serde_json::from_value(
|
||||
ok(
|
||||
api,
|
||||
@@ -94,7 +94,7 @@ async fn register(api: &Api<'_>) -> Result<WebauthnAuthenticator<SoftPasskey>> {
|
||||
Ok(auth)
|
||||
}
|
||||
async fn assertion(
|
||||
api: &Api<'_>,
|
||||
api: &Api,
|
||||
auth: &mut WebauthnAuthenticator<SoftPasskey>,
|
||||
path: &str,
|
||||
body: Value,
|
||||
@@ -114,7 +114,7 @@ async fn approvals_registration_and_revocation() -> Result<()> {
|
||||
let second = item(&cred, "review <script>not markup</script>")?;
|
||||
let api = Api::new(
|
||||
cred,
|
||||
Passkeys::new(&db, "localhost", ORIGIN)?,
|
||||
Passkeys::new(db, "localhost", ORIGIN)?,
|
||||
EntryKey::open(&dir.path().join("entry.key"))?,
|
||||
);
|
||||
page(&api, "/register").await?;
|
||||
@@ -314,7 +314,7 @@ async fn approvals_registration_and_revocation() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn encrypt(api: &Api<'_>, public_key: Value) -> Result<Envelope> {
|
||||
async fn encrypt(api: &Api, public_key: Value) -> Result<Envelope> {
|
||||
use deno_core::{JsRuntime, RuntimeOptions};
|
||||
let script = handle(
|
||||
api,
|
||||
@@ -358,7 +358,7 @@ async fn encrypted_entry_uses_the_host_page_code() -> Result<()> {
|
||||
let item = item(&cred, "secret")?;
|
||||
let api = Api::new(
|
||||
cred,
|
||||
Passkeys::new(&db, "localhost", ORIGIN)?,
|
||||
Passkeys::new(db, "localhost", ORIGIN)?,
|
||||
EntryKey::open(&dir.path().join("entry.key"))?,
|
||||
);
|
||||
let mut auth = register(&api).await?;
|
||||
@@ -408,6 +408,7 @@ async fn encrypted_entry_uses_the_host_page_code() -> Result<()> {
|
||||
assert!(!api.cred.item_get(&item.id)?.fields[0].needs_value);
|
||||
ok(&api, "GET", &format!("/cred/items/{}", item.id), json!({})).await?;
|
||||
let master = std::fs::read(dir.path().join("cred.key"))?;
|
||||
let db = rusqlite::Connection::open(dir.path().join("cred.sqlite"))?;
|
||||
let cipher: Vec<u8> = db.query_row(
|
||||
"SELECT ciphertext FROM fields WHERE item=?",
|
||||
[&item.id],
|
||||
|
||||
@@ -42,7 +42,10 @@ fn fixture() -> (tempfile::TempDir, Cred, Caller, Item) {
|
||||
.unwrap();
|
||||
(dir, c, caller, item)
|
||||
}
|
||||
fn register(host: &mut Passkeys<'_>, auth: &mut WebauthnAuthenticator<SoftPasskey>) {
|
||||
fn register(
|
||||
host: &mut Passkeys<&rusqlite::Connection>,
|
||||
auth: &mut WebauthnAuthenticator<SoftPasskey>,
|
||||
) {
|
||||
let start = host.start_registration("test".into(), None).unwrap();
|
||||
let response = auth
|
||||
.do_registration(Url::parse("http://localhost:8000").unwrap(), start.options)
|
||||
@@ -50,7 +53,7 @@ fn register(host: &mut Passkeys<'_>, auth: &mut WebauthnAuthenticator<SoftPasske
|
||||
host.finish_registration(&start.id, response).unwrap();
|
||||
}
|
||||
fn proof(
|
||||
host: &mut Passkeys<'_>,
|
||||
host: &mut Passkeys<&rusqlite::Connection>,
|
||||
auth: &mut WebauthnAuthenticator<SoftPasskey>,
|
||||
binding: Binding,
|
||||
) -> webauthn::Verified {
|
||||
|
||||
@@ -126,6 +126,8 @@ async fn identity_injection_denial_and_ambiguity() {
|
||||
assert_eq!(code, 0);
|
||||
let alias_out = String::from_utf8(alias_out).unwrap();
|
||||
assert!(alias_out.contains("/bin/guest.wasm") && !alias_out.contains("/bin/alias"));
|
||||
let items = run(&runtime, "http://host.sausage/cred/items".into(), vec![]).await;
|
||||
assert!(items.contains("test item") && !items.contains(&token));
|
||||
let server_token = token.clone();
|
||||
let server = tokio::spawn(async move {
|
||||
let (mut socket, _) = listener.accept().await.unwrap();
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
//! The hook and native listener share ceremony state without exposing the native listener to guests.
|
||||
use sausage::{
|
||||
api::{Api, Listener, Options as ApiOptions},
|
||||
cred::{Caller, Cred},
|
||||
exec::{Command, Exec, Options},
|
||||
process::Runtime,
|
||||
world::World,
|
||||
};
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
sync::Arc,
|
||||
};
|
||||
#[path = "hook_guest.rs"]
|
||||
mod hook_guest;
|
||||
|
||||
#[tokio::test]
|
||||
async fn one_api_for_pages_and_guest_requests() -> anyhow::Result<()> {
|
||||
use webauthn_authenticator_rs::{WebauthnAuthenticator, softpasskey::SoftPasskey};
|
||||
let dir = tempfile::tempdir()?;
|
||||
let world = World::open(dir.path())?;
|
||||
std::fs::create_dir_all(dir.path().join("world/bin"))?;
|
||||
std::fs::copy(
|
||||
hook_guest::component(),
|
||||
dir.path().join("world/bin/guest.wasm"),
|
||||
)?;
|
||||
let cred = Arc::new(Cred::open(&world.host_dir)?);
|
||||
let origin = "http://localhost:8081";
|
||||
let api = Arc::new(Api::open(
|
||||
cred.clone(),
|
||||
&world.host_dir,
|
||||
"localhost",
|
||||
origin,
|
||||
)?);
|
||||
let runtime = Runtime::new_with_hooks(
|
||||
world,
|
||||
cred,
|
||||
sausage::process::default_script(),
|
||||
Some(api.clone()),
|
||||
)?;
|
||||
let listener = Listener::start_with_api(
|
||||
api.clone(),
|
||||
ApiOptions {
|
||||
api_listen: "127.0.0.1:0".parse()?,
|
||||
api_rp_id: "localhost".into(),
|
||||
api_origin: origin.into(),
|
||||
},
|
||||
)?;
|
||||
runtime
|
||||
.network_policy()
|
||||
.deny_host_port(listener.address().port());
|
||||
let response = sausage::api::handle(
|
||||
&api,
|
||||
Caller {
|
||||
program: "/bin/guest.wasm".into(),
|
||||
pid: 1,
|
||||
conversations: vec![],
|
||||
},
|
||||
http::Request::post("http://host.sausage/cred/passkeys/register/start")
|
||||
.body(bytes::Bytes::from_static(b"{\"label\":\"shared\"}"))?,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response.status(), 200);
|
||||
let challenge: serde_json::Value = serde_json::from_slice(response.body())?;
|
||||
let mut authenticator = WebauthnAuthenticator::new(SoftPasskey::new(true));
|
||||
let registration = authenticator.do_registration(
|
||||
url::Url::parse(origin)?,
|
||||
serde_json::from_value(challenge["options"].clone())?,
|
||||
)?;
|
||||
let body = serde_json::to_vec(
|
||||
&serde_json::json!({"challenge_id":challenge["id"], "response": registration}),
|
||||
)?;
|
||||
let mut connection = std::net::TcpStream::connect(listener.address())?;
|
||||
write!(
|
||||
connection,
|
||||
"POST /cred/passkeys/register/finish HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nContent-Length: {}\r\n\r\n",
|
||||
body.len()
|
||||
)?;
|
||||
connection.write_all(&body)?;
|
||||
let mut reply = String::new();
|
||||
connection.read_to_string(&mut reply)?;
|
||||
assert!(reply.starts_with("HTTP/1.1 200"));
|
||||
for (uri, expected) in [
|
||||
("http://host.sausage/cred/passkeys".to_owned(), "shared"),
|
||||
(format!("http://{}/whoami", listener.address()), "error"),
|
||||
] {
|
||||
let mut out = Vec::new();
|
||||
let code = runtime
|
||||
.exec(
|
||||
Command::Argv(vec!["/bin/guest.wasm".into(), uri, "GET".into()]),
|
||||
Options {
|
||||
cwd: "/".into(),
|
||||
timeout: Some(std::time::Duration::from_secs(30)),
|
||||
..Default::default()
|
||||
},
|
||||
&mut |_, b| out.extend_from_slice(b),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(code, 0);
|
||||
assert!(String::from_utf8(out)?.contains(expected));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user