mirror of
https://github.com/usestrix/firewall.git
synced 2026-11-09 05:19:35 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dc9c84b29 | ||
|
|
0ca30e444e |
No files matched your search
Generated
+73
-109
@@ -935,7 +935,7 @@ checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1331,9 +1331,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.105"
|
||||
version = "0.3.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
|
||||
checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
@@ -1500,7 +1500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -1630,6 +1630,24 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-system-configuration"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396"
|
||||
dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oid-registry"
|
||||
version = "0.8.1"
|
||||
@@ -3111,7 +3129,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"which",
|
||||
"windows",
|
||||
"whoami",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3589,6 +3607,15 @@ version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.7+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
|
||||
dependencies = [
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.4+wasi-0.2.12"
|
||||
@@ -3599,10 +3626,19 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.128"
|
||||
name = "wasite"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
|
||||
checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42"
|
||||
dependencies = [
|
||||
"wasi 0.14.7+wasi-0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -3613,9 +3649,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.128"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
|
||||
checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -3623,9 +3659,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.128"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
|
||||
checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
@@ -3636,13 +3672,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.128"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
|
||||
checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.9"
|
||||
@@ -3670,6 +3716,19 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libredox",
|
||||
"objc2-system-configuration",
|
||||
"wasite",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wildcard"
|
||||
version = "0.3.0"
|
||||
@@ -3710,89 +3769,12 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
@@ -3802,15 +3784,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@@ -3845,15 +3818,6 @@ dependencies = [
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
|
||||
+1
-7
@@ -52,10 +52,6 @@ path = "tests/age.rs"
|
||||
name = "mitm"
|
||||
path = "tests/mitm.rs"
|
||||
|
||||
[[test]]
|
||||
name = "tpm"
|
||||
path = "tests/tpm.rs"
|
||||
|
||||
[[test]]
|
||||
name = "managers"
|
||||
path = "tests/managers.rs"
|
||||
@@ -76,6 +72,7 @@ tokio = { version = "1.53.1", features = ["io-util", "rt", "sync", "time"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||
which = "8"
|
||||
whoami = "2.1.3"
|
||||
|
||||
[target.'cfg(target_vendor = "apple")'.dependencies]
|
||||
core-foundation = "0.10.1"
|
||||
@@ -83,9 +80,6 @@ rama = { version = "0.4.0", features = ["net-apple-networkextension"] }
|
||||
security-framework = "3.7.0"
|
||||
security-framework-sys = "2.17.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.62", features = ["Win32_Foundation", "Win32_Security_Cryptography"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.53.1", features = ["macros", "rt-multi-thread"] }
|
||||
|
||||
|
||||
+21
-2
@@ -18,21 +18,35 @@ pub struct FirewallConfig {
|
||||
pub min_age_hours: u64,
|
||||
pub denylist: DenylistSource,
|
||||
/// Packages never screened, by ecosystem: `{"npm": [{"name": "left-pad"}]}`.
|
||||
pub allow: HashMap<String, Vec<AllowedPackage>>,
|
||||
pub allow: PackageList,
|
||||
pub telemetry: bool,
|
||||
/// Reports installs to, and takes blocks from, a Strix workspace.
|
||||
pub platform: Option<PlatformConfig>,
|
||||
/// Not describable in the file yet, so this is always the built-in table.
|
||||
#[serde(skip)]
|
||||
pub registries: Vec<&'static Registry>,
|
||||
}
|
||||
|
||||
/// Packages by ecosystem, as the allow list and organization blocks name them.
|
||||
pub type PackageList = HashMap<String, Vec<ListedPackage>>;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AllowedPackage {
|
||||
pub struct ListedPackage {
|
||||
pub name: String,
|
||||
/// Every version when absent.
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PlatformConfig {
|
||||
pub api_key: String,
|
||||
/// Where a self-hosted Strix lives.
|
||||
#[serde(default = "hosted")]
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct DenylistSource {
|
||||
@@ -52,6 +66,7 @@ impl Default for FirewallConfig {
|
||||
denylist: DenylistSource::default(),
|
||||
allow: HashMap::new(),
|
||||
telemetry: true,
|
||||
platform: None,
|
||||
registries: BUILT_IN.to_vec(),
|
||||
}
|
||||
}
|
||||
@@ -113,6 +128,10 @@ impl FirewallConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn hosted() -> String {
|
||||
"https://app.usestrix.com".into()
|
||||
}
|
||||
|
||||
pub fn path() -> Option<PathBuf> {
|
||||
let dirs = ProjectDirs::from("com", "usestrix", "firewall")?;
|
||||
Some(dirs.config_dir().join("config.json"))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::config::FirewallConfig;
|
||||
use crate::policies::filters::age::Age;
|
||||
use crate::policies::filters::denylist::Denylist;
|
||||
use crate::policies::filters::organization::OrganizationBlocks;
|
||||
use crate::policies::filters::osv::Osv;
|
||||
use crate::policies::filters::Filter;
|
||||
use crate::policies::registries::*;
|
||||
@@ -28,7 +29,12 @@ impl Firewall {
|
||||
}
|
||||
let ecosystems = config.registries.iter().map(|r| r.osv_name);
|
||||
let denylist = Denylist::new(config.denylist.clone(), ecosystems);
|
||||
let mut filters: Vec<Box<dyn Filter>> = vec![Box::new(denylist), Box::new(Osv)];
|
||||
let mut filters: Vec<Box<dyn Filter>> = Vec::new();
|
||||
if let Some(platform) = &config.platform {
|
||||
filters.push(Box::new(OrganizationBlocks::new(platform.clone())));
|
||||
}
|
||||
filters.push(Box::new(denylist));
|
||||
filters.push(Box::new(Osv));
|
||||
if config.min_age_hours > 0 {
|
||||
filters.push(Box::new(Age::new(config.min_age_hours)));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ pub mod firewall;
|
||||
pub mod http;
|
||||
pub mod mitm;
|
||||
pub mod os;
|
||||
mod platform;
|
||||
pub mod policies;
|
||||
pub mod proxy;
|
||||
pub mod telemetry;
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ use rcgen::{
|
||||
use std::sync::Arc;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
/// Held in the Secure Enclave on macOS, the TPM on Windows, in memory elsewhere.
|
||||
/// Held in the Secure Enclave on macOS, in memory elsewhere.
|
||||
pub type CaSigner = Arc<dyn SigningKey + Send + Sync>;
|
||||
|
||||
pub struct CertificateAuthority {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
#[cfg(windows)]
|
||||
pub mod windows;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use macos::{system_config, uninstall};
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
mod tpm;
|
||||
|
||||
pub use tpm::CaKey;
|
||||
@@ -1,153 +0,0 @@
|
||||
use rama::error::BoxError;
|
||||
use rama::tls::boring::core::bn::BigNum;
|
||||
use rama::tls::boring::core::ecdsa::EcdsaSig;
|
||||
use rama::tls::boring::core::sha::sha256;
|
||||
use rcgen::{Error, PublicKeyData, SignatureAlgorithm, SigningKey, PKCS_ECDSA_P256_SHA256};
|
||||
use windows::core::{Owned, Result as WinResult, HSTRING};
|
||||
use windows::Win32::Foundation::{NTE_BAD_KEY, NTE_BAD_KEYSET};
|
||||
use windows::Win32::Security::Cryptography::*;
|
||||
|
||||
const KEY_NAME: &str = "Strix Firewall CA";
|
||||
|
||||
/// A P-256 key held by the TPM. Signing asks the TPM to sign, so the private key never leaves it.
|
||||
pub struct CaKey {
|
||||
// Fields drop in order, so the key handle closes before its provider's.
|
||||
key_handle: Owned<NCRYPT_KEY_HANDLE>,
|
||||
_provider_handle: Owned<NCRYPT_PROV_HANDLE>,
|
||||
/// 0x04 || X || Y, the form rcgen writes into a certificate's SubjectPublicKeyInfo.
|
||||
public_point: Vec<u8>,
|
||||
}
|
||||
|
||||
impl CaKey {
|
||||
/// A machine key, so the service and an elevated installer share it.
|
||||
pub fn load_or_create() -> Result<Self, BoxError> {
|
||||
match open_key(KEY_NAME, NCRYPT_MACHINE_KEY_FLAG)? {
|
||||
Some(key) => Ok(key),
|
||||
None => Ok(create_key(KEY_NAME, NCRYPT_MACHINE_KEY_FLAG)?),
|
||||
}
|
||||
}
|
||||
|
||||
/// A per-user key under a random name, which needs no elevation; `delete_from_tpm` it after.
|
||||
pub fn create_test_key() -> WinResult<Self> {
|
||||
let name = format!("Strix Firewall test {:016x}", rand::random::<u64>());
|
||||
create_key(&name, NCRYPT_FLAGS(0))
|
||||
}
|
||||
|
||||
/// Deletes the key from the TPM, not just this handle to it.
|
||||
pub fn delete_from_tpm(mut self) -> WinResult<()> {
|
||||
// NCryptDeleteKey also frees the handle, so the Owned mustn't free it again.
|
||||
let key_handle = std::mem::take(&mut *self.key_handle);
|
||||
unsafe { NCryptDeleteKey(key_handle, 0) }
|
||||
}
|
||||
|
||||
/// Takes both handles and reads the key's public point once, for rcgen.
|
||||
fn from_handles(
|
||||
provider_handle: Owned<NCRYPT_PROV_HANDLE>,
|
||||
key_handle: Owned<NCRYPT_KEY_HANDLE>,
|
||||
) -> WinResult<Self> {
|
||||
// A BCRYPT_ECCKEY_BLOB header, then X and Y.
|
||||
const HEADER_LEN: usize = size_of::<BCRYPT_ECCKEY_BLOB>();
|
||||
let mut blob = [0u8; HEADER_LEN + 64];
|
||||
let mut blob_len = 0;
|
||||
unsafe {
|
||||
NCryptExportKey(
|
||||
*key_handle,
|
||||
None,
|
||||
BCRYPT_ECCPUBLIC_BLOB,
|
||||
None,
|
||||
Some(&mut blob),
|
||||
&mut blob_len,
|
||||
NCRYPT_FLAGS(0),
|
||||
)?
|
||||
};
|
||||
let (header, x_and_y) = blob.split_at(HEADER_LEN);
|
||||
let is_p256 = header[..4] == BCRYPT_ECDSA_PUBLIC_P256_MAGIC.to_le_bytes();
|
||||
if !is_p256 || blob_len as usize != blob.len() {
|
||||
return Err(windows::core::Error::new(NTE_BAD_KEY, "key is not an ECDSA P-256 key"));
|
||||
}
|
||||
let public_point = [&[0x04], x_and_y].concat();
|
||||
Ok(CaKey { key_handle, _provider_handle: provider_handle, public_point })
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicKeyData for CaKey {
|
||||
fn der_bytes(&self) -> &[u8] {
|
||||
&self.public_point
|
||||
}
|
||||
|
||||
fn algorithm(&self) -> &'static SignatureAlgorithm {
|
||||
&PKCS_ECDSA_P256_SHA256
|
||||
}
|
||||
}
|
||||
|
||||
impl SigningKey for CaKey {
|
||||
fn sign(&self, msg: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
let mut r_and_s = [0u8; 64];
|
||||
let mut signature_len = 0;
|
||||
let signed = unsafe {
|
||||
NCryptSignHash(
|
||||
*self.key_handle,
|
||||
None,
|
||||
&sha256(msg),
|
||||
Some(&mut r_and_s),
|
||||
&mut signature_len,
|
||||
NCRYPT_SILENT_FLAG,
|
||||
)
|
||||
};
|
||||
signed.map_err(|_| Error::RemoteKeyError)?;
|
||||
// NCrypt returns r || s, each half the length; X.509 wants DER SEQUENCE { r, s }.
|
||||
let (r, s) = r_and_s[..signature_len as usize].split_at(signature_len as usize / 2);
|
||||
let der = BigNum::from_slice(r)
|
||||
.and_then(|r| EcdsaSig::from_private_components(r, BigNum::from_slice(s)?))
|
||||
.and_then(|signature| signature.to_der());
|
||||
der.map_err(|_| Error::RemoteKeyError)
|
||||
}
|
||||
}
|
||||
|
||||
/// `None` if the TPM holds no key by that name.
|
||||
/// `scope` is `NCRYPT_MACHINE_KEY_FLAG`, or empty for the current user's keys.
|
||||
fn open_key(name: &str, scope: NCRYPT_FLAGS) -> WinResult<Option<CaKey>> {
|
||||
let provider_handle = open_tpm_provider()?;
|
||||
let mut key_handle = NCRYPT_KEY_HANDLE::default();
|
||||
let opened = unsafe {
|
||||
NCryptOpenKey(
|
||||
*provider_handle,
|
||||
&mut key_handle,
|
||||
&HSTRING::from(name),
|
||||
CERT_KEY_SPEC::default(), // legacy, unused by the TPM provider
|
||||
scope | NCRYPT_SILENT_FLAG,
|
||||
)
|
||||
};
|
||||
match opened {
|
||||
Err(error) if error.code() == NTE_BAD_KEYSET => Ok(None),
|
||||
Err(error) => Err(error),
|
||||
Ok(()) => CaKey::from_handles(provider_handle, unsafe { Owned::new(key_handle) }).map(Some),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates the key inside the TPM, which never lets it out.
|
||||
fn create_key(name: &str, scope: NCRYPT_FLAGS) -> WinResult<CaKey> {
|
||||
let provider_handle = open_tpm_provider()?;
|
||||
let mut key_handle = NCRYPT_KEY_HANDLE::default();
|
||||
unsafe {
|
||||
NCryptCreatePersistedKey(
|
||||
*provider_handle,
|
||||
&mut key_handle,
|
||||
NCRYPT_ECDSA_P256_ALGORITHM,
|
||||
&HSTRING::from(name),
|
||||
CERT_KEY_SPEC::default(), // legacy, unused by the TPM provider
|
||||
scope,
|
||||
)?
|
||||
};
|
||||
let key_handle = unsafe { Owned::new(key_handle) };
|
||||
// Until finalized the key is only a handle; this generates and stores it.
|
||||
unsafe { NCryptFinalizeKey(*key_handle, NCRYPT_SILENT_FLAG)? };
|
||||
CaKey::from_handles(provider_handle, key_handle)
|
||||
}
|
||||
|
||||
/// The Microsoft Platform Crypto Provider, which keeps its keys in the TPM.
|
||||
fn open_tpm_provider() -> WinResult<Owned<NCRYPT_PROV_HANDLE>> {
|
||||
let mut provider_handle = NCRYPT_PROV_HANDLE::default();
|
||||
unsafe { NCryptOpenStorageProvider(&mut provider_handle, MS_PLATFORM_CRYPTO_PROVIDER, 0)? };
|
||||
Ok(unsafe { Owned::new(provider_handle) })
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use crate::config::PlatformConfig;
|
||||
use crate::policies::Package;
|
||||
use crate::FirewallConfig;
|
||||
use rama::http::client::EasyHttpWebClient;
|
||||
use rama::http::header::{AUTHORIZATION, CONTENT_TYPE};
|
||||
use rama::http::service::client::HttpClientExt;
|
||||
use serde_json::{json, Value};
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
use std::time::Duration;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
const TIMEOUT: Duration = Duration::from_secs(10);
|
||||
/// The most events the platform takes in one request.
|
||||
const BATCH: usize = 500;
|
||||
|
||||
/// On when a readable config names a Strix API key.
|
||||
static PLATFORM: LazyLock<Option<Platform>> = LazyLock::new(|| {
|
||||
let config = FirewallConfig::load().ok()?.platform?;
|
||||
Some(Platform { config, events: Mutex::default() })
|
||||
});
|
||||
|
||||
struct Platform {
|
||||
config: PlatformConfig,
|
||||
events: Mutex<Vec<Value>>,
|
||||
}
|
||||
|
||||
pub fn on() -> bool {
|
||||
PLATFORM.is_some()
|
||||
}
|
||||
|
||||
pub fn record(eco: &str, pkg: &Package, reason: Option<&str>) {
|
||||
let Some(platform) = &*PLATFORM else { return };
|
||||
let event = json!({
|
||||
"ecosystem": eco,
|
||||
"name": pkg.name,
|
||||
"version": pkg.version,
|
||||
"blocked": reason.is_some(),
|
||||
"reason": reason,
|
||||
"occurred_at": OffsetDateTime::now_utc().format(&Rfc3339).ok(),
|
||||
});
|
||||
platform.events.lock().expect("platform lock").push(event);
|
||||
}
|
||||
|
||||
/// Events in a batch the platform did not take are dropped, not retried.
|
||||
pub async fn send() {
|
||||
let Some(platform) = &*PLATFORM else { return };
|
||||
let events = std::mem::take(&mut *platform.events.lock().expect("platform lock"));
|
||||
let url = format!("{}/api/v1/firewall/events", platform.config.url);
|
||||
let client = EasyHttpWebClient::default();
|
||||
for batch in events.chunks(BATCH) {
|
||||
let body = json!({
|
||||
"device": whoami::hostname().ok(),
|
||||
"user": whoami::username().ok(),
|
||||
"os": std::env::consts::OS,
|
||||
"firewall_version": env!("CARGO_PKG_VERSION"),
|
||||
"events": batch,
|
||||
});
|
||||
let request = client
|
||||
.post(&url)
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.header(AUTHORIZATION, format!("Bearer {}", platform.config.api_key));
|
||||
match tokio::time::timeout(TIMEOUT, request.body(body.to_string()).send()).await {
|
||||
Ok(Ok(response)) if response.status().is_success() => {
|
||||
debug!(events = batch.len(), "installs reported")
|
||||
}
|
||||
Ok(Ok(response)) => warn!(status = %response.status(), %url, "platform refused installs"),
|
||||
Ok(Err(error)) => warn!(%error, %url, "could not report installs"),
|
||||
Err(_) => warn!(%url, "reporting installs timed out"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
pub mod age;
|
||||
pub mod denylist;
|
||||
pub mod organization;
|
||||
pub mod osv;
|
||||
|
||||
use super::registries::{Package, Registry};
|
||||
use crate::config::PackageList;
|
||||
use crate::Firewall;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
@@ -30,7 +32,7 @@ pub async fn consult(
|
||||
registry: &'static Registry,
|
||||
pkg: &Package,
|
||||
) -> Option<(&'static str, Verdict)> {
|
||||
if allowed(firewall, registry, pkg) {
|
||||
if listed(&firewall.config.allow, registry, pkg) {
|
||||
return None;
|
||||
}
|
||||
let mut unsure = None;
|
||||
@@ -45,8 +47,9 @@ pub async fn consult(
|
||||
unsure
|
||||
}
|
||||
|
||||
fn allowed(firewall: &Firewall, registry: &Registry, pkg: &Package) -> bool {
|
||||
let Some(entries) = firewall.config.allow.get(registry.eco) else {
|
||||
/// Whether `list` names `pkg`.
|
||||
fn listed(list: &PackageList, registry: &Registry, pkg: &Package) -> bool {
|
||||
let Some(entries) = list.get(registry.eco) else {
|
||||
return false;
|
||||
};
|
||||
entries.iter().any(|entry| {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
use super::{listed, Filter, Verdict, VerdictFuture, AGENT};
|
||||
use crate::config::{PackageList, PlatformConfig};
|
||||
use crate::policies::registries::Registry;
|
||||
use crate::policies::Package;
|
||||
use rama::http::client::EasyHttpWebClient;
|
||||
use rama::http::header::{AUTHORIZATION, USER_AGENT};
|
||||
use rama::http::service::client::HttpClientExt;
|
||||
use rama::http::{BodyExtractExt, StatusCode};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::OnceCell;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{info, warn};
|
||||
|
||||
const POLL: Duration = Duration::from_secs(60);
|
||||
|
||||
type Blocks = Arc<OnceCell<RwLock<Arc<Result<PackageList, String>>>>>;
|
||||
|
||||
/// Packages a Strix admin blocked, downloaded when the first package is screened.
|
||||
pub struct OrganizationBlocks {
|
||||
platform: PlatformConfig,
|
||||
blocks: Blocks,
|
||||
watch: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl OrganizationBlocks {
|
||||
pub fn new(platform: PlatformConfig) -> Self {
|
||||
let blocks = Arc::new(OnceCell::new());
|
||||
let watch = tokio::spawn(watch(platform.clone(), Arc::clone(&blocks)));
|
||||
OrganizationBlocks { platform, blocks, watch }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for OrganizationBlocks {
|
||||
fn drop(&mut self) {
|
||||
self.watch.abort();
|
||||
}
|
||||
}
|
||||
|
||||
impl Filter for OrganizationBlocks {
|
||||
fn name(&self) -> &'static str {
|
||||
"organization"
|
||||
}
|
||||
|
||||
fn check<'a>(&'a self, registry: &'static Registry, pkg: &'a Package) -> VerdictFuture<'a> {
|
||||
Box::pin(async move {
|
||||
let load = || async { RwLock::new(Arc::new(load(&self.platform).await)) };
|
||||
let blocks = self.blocks.get_or_init(load).await;
|
||||
let blocks = Arc::clone(&blocks.read().expect("blocks lock"));
|
||||
match &*blocks {
|
||||
Err(e) => Verdict::Unknown(e.clone()),
|
||||
Ok(blocks) if listed(blocks, registry, pkg) => {
|
||||
Verdict::Deny("is blocked by your organization".into())
|
||||
}
|
||||
Ok(_) => Verdict::Allow,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A failed fetch leaves the last good list in place.
|
||||
async fn watch(platform: PlatformConfig, blocks: Blocks) {
|
||||
loop {
|
||||
tokio::time::sleep(POLL).await;
|
||||
let Some(current) = blocks.get() else { continue };
|
||||
if let Ok(fresh) = load(&platform).await {
|
||||
*current.write().expect("blocks lock") = Arc::new(Ok(fresh));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn load(platform: &PlatformConfig) -> Result<PackageList, String> {
|
||||
let blocks = fetch(platform).await;
|
||||
match &blocks {
|
||||
Ok(_) => info!("organization blocks loaded"),
|
||||
Err(error) => warn!(%error, "organization blocks unavailable"),
|
||||
}
|
||||
blocks
|
||||
}
|
||||
|
||||
async fn fetch(platform: &PlatformConfig) -> Result<PackageList, String> {
|
||||
let url = format!("{}/api/v1/firewall/blocklist", platform.url);
|
||||
let client = EasyHttpWebClient::default();
|
||||
let request = client
|
||||
.get(&url)
|
||||
.header(USER_AGENT, AGENT)
|
||||
.header(AUTHORIZATION, format!("Bearer {}", platform.api_key));
|
||||
let response = request.send().await.map_err(|e| e.to_string())?;
|
||||
if response.status() != StatusCode::OK {
|
||||
return Err(format!("{url} replied {}", response.status()));
|
||||
}
|
||||
let text = response.try_into_string().await.map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(&text).map_err(|e| e.to_string())
|
||||
}
|
||||
+11
-3
@@ -1,3 +1,4 @@
|
||||
use crate::platform;
|
||||
use crate::policies::Package;
|
||||
use crate::{Firewall, FirewallConfig};
|
||||
use rama::http::client::EasyHttpWebClient;
|
||||
@@ -40,7 +41,7 @@ struct Telemetry {
|
||||
}
|
||||
|
||||
pub fn screened(firewall: &Firewall, url: &str, reason: Option<&str>) {
|
||||
if TELEMETRY.is_none() {
|
||||
if !on() {
|
||||
return;
|
||||
}
|
||||
let Some((registry, rest)) = firewall.match_on(url, |r| r.upstream) else {
|
||||
@@ -52,6 +53,7 @@ pub fn screened(firewall: &Firewall, url: &str, reason: Option<&str>) {
|
||||
}
|
||||
|
||||
pub fn screened_package(eco: &str, pkg: &Package, reason: Option<&str>) {
|
||||
platform::record(eco, pkg, reason);
|
||||
let mut properties = json!({ "eco": eco, "blocked": reason.is_some() });
|
||||
if let Some(reason) = reason {
|
||||
properties["package"] = pkg.label().into();
|
||||
@@ -69,7 +71,7 @@ pub fn error(kind: &'static str) {
|
||||
}
|
||||
|
||||
pub fn heartbeat() {
|
||||
if TELEMETRY.is_none() {
|
||||
if !on() {
|
||||
return;
|
||||
}
|
||||
tokio::spawn(async {
|
||||
@@ -81,7 +83,7 @@ pub fn heartbeat() {
|
||||
}
|
||||
|
||||
pub fn flush() {
|
||||
if TELEMETRY.is_none() {
|
||||
if !on() {
|
||||
return;
|
||||
}
|
||||
match Runtime::try_new() {
|
||||
@@ -90,6 +92,11 @@ pub fn flush() {
|
||||
}
|
||||
}
|
||||
|
||||
/// PostHog, or the Strix platform when the config names one.
|
||||
fn on() -> bool {
|
||||
TELEMETRY.is_some() || platform::on()
|
||||
}
|
||||
|
||||
fn record(event: &str, mut properties: Value) {
|
||||
let Some(telemetry) = &*TELEMETRY else { return };
|
||||
properties["distinct_id"] = telemetry.session.clone().into();
|
||||
@@ -103,6 +110,7 @@ fn record(event: &str, mut properties: Value) {
|
||||
}
|
||||
|
||||
pub async fn send() {
|
||||
platform::send().await;
|
||||
let Some(telemetry) = &*TELEMETRY else { return };
|
||||
let batch = std::mem::take(&mut *telemetry.events.lock().expect("telemetry lock"));
|
||||
if batch.is_empty() {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
#![cfg(windows)]
|
||||
|
||||
use firewall::mitm::CertificateAuthority;
|
||||
use firewall::os::windows::CaKey;
|
||||
use rcgen::PublicKeyData;
|
||||
|
||||
/// Skipped on machines without a TPM.
|
||||
#[test]
|
||||
fn key_signs_a_verifiable_ca() {
|
||||
let key = match CaKey::create_test_key() {
|
||||
Ok(key) => key,
|
||||
Err(error) => return eprintln!("skipping, no TPM key: {error}"),
|
||||
};
|
||||
let ca = CertificateAuthority::generate(&["registry.npmjs.org"], &key);
|
||||
let spki = key.subject_public_key_info();
|
||||
key.delete_from_tpm().expect("delete the test key");
|
||||
let ca = ca.expect("mint a ca");
|
||||
let public = ca.crt.public_key().expect("ca public key");
|
||||
assert!(ca.crt.verify(&public).expect("verify the ca"));
|
||||
assert_eq!(public.public_key_to_der().unwrap(), spki);
|
||||
}
|
||||
Reference in new issue
Block a user