mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-22 17:06:40 +00:00
fix(security): harden extension and engine trust
This commit is contained in:
+1
-1
Submodule Extensions/Firefox updated: b08c76517a...528810a190
@@ -0,0 +1,45 @@
|
|||||||
|
import crypto from 'node:crypto';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
export function sha256(file) {
|
||||||
|
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectRegularFiles(root, options = {}) {
|
||||||
|
const ignoredNames = new Set(options.ignoredNames || []);
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
const walk = directory => {
|
||||||
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||||
|
const file = path.join(directory, entry.name);
|
||||||
|
const relative = path.relative(root, file).split(path.sep).join('/');
|
||||||
|
if (ignoredNames.has(entry.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.isSymbolicLink()) {
|
||||||
|
throw new Error(`Unsupported symlink in engine payload: ${relative}`);
|
||||||
|
}
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walk(file);
|
||||||
|
} else if (entry.isFile()) {
|
||||||
|
files.push(file);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unsupported filesystem entry in engine payload: ${relative}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
walk(root);
|
||||||
|
return files.sort((left, right) => left.localeCompare(right));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function treeDigest(root) {
|
||||||
|
const files = collectRegularFiles(root);
|
||||||
|
const digest = crypto.createHash('sha256');
|
||||||
|
for (const file of files) {
|
||||||
|
const relative = path.relative(root, file).split(path.sep).join('/');
|
||||||
|
digest.update(`${relative}\0${sha256(file)}\n`);
|
||||||
|
}
|
||||||
|
return { files: files.length, sha256: digest.digest('hex') };
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { collectRegularFiles, treeDigest } from './engine-payload-integrity.js';
|
||||||
|
|
||||||
|
test('collectRegularFiles returns regular files and honors ignored names', () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'firelink-engine-payload-'));
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.join(root, 'nested'));
|
||||||
|
fs.writeFileSync(path.join(root, 'engine'), 'binary');
|
||||||
|
fs.writeFileSync(path.join(root, 'nested', 'runtime.dat'), 'runtime');
|
||||||
|
fs.writeFileSync(path.join(root, 'payload-manifest.json'), '{}');
|
||||||
|
|
||||||
|
const files = collectRegularFiles(root, {
|
||||||
|
ignoredNames: ['payload-manifest.json'],
|
||||||
|
}).map(file => path.relative(root, file).split(path.sep).join('/'));
|
||||||
|
|
||||||
|
assert.deepEqual(files, ['engine', 'nested/runtime.dat']);
|
||||||
|
const digest = treeDigest(path.join(root, 'nested'));
|
||||||
|
assert.equal(digest.files, 1);
|
||||||
|
assert.match(digest.sha256, /^[a-f0-9]{64}$/);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('collectRegularFiles rejects symlinks in engine payloads', { skip: process.platform === 'win32' }, () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'firelink-engine-payload-'));
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(path.join(root, 'target'), 'target');
|
||||||
|
fs.symlinkSync('target', path.join(root, 'link'));
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => collectRegularFiles(root),
|
||||||
|
/Unsupported symlink in engine payload: link/
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import crypto from 'node:crypto';
|
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { collectRegularFiles, sha256 } from './engine-payload-integrity.js';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const repoRoot = path.resolve(__dirname, '..');
|
const repoRoot = path.resolve(__dirname, '..');
|
||||||
@@ -36,10 +36,6 @@ const temporary = fs.mkdtempSync(path.join(os.tmpdir(), `firelink-engines-${targ
|
|||||||
const isWindows = target.includes('windows');
|
const isWindows = target.includes('windows');
|
||||||
const executableSuffix = isWindows ? '.exe' : '';
|
const executableSuffix = isWindows ? '.exe' : '';
|
||||||
|
|
||||||
function sha256(file) {
|
|
||||||
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function download(name, source) {
|
async function download(name, source) {
|
||||||
const sourcePath = new URL(source.url).pathname;
|
const sourcePath = new URL(source.url).pathname;
|
||||||
const archive = path.join(
|
const archive = path.join(
|
||||||
@@ -99,16 +95,9 @@ function copyExecutable(source, engine) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function writePayloadManifest() {
|
function writePayloadManifest() {
|
||||||
const files = [];
|
const files = collectRegularFiles(destination, {
|
||||||
const walk = directory => {
|
ignoredNames: ['payload-manifest.json'],
|
||||||
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
});
|
||||||
const file = path.join(directory, entry.name);
|
|
||||||
if (entry.isDirectory()) walk(file);
|
|
||||||
else if (entry.isFile() && entry.name !== 'payload-manifest.json') files.push(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
walk(destination);
|
|
||||||
files.sort((left, right) => left.localeCompare(right));
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
target,
|
target,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import crypto from 'node:crypto';
|
import { collectRegularFiles, sha256, treeDigest } from './engine-payload-integrity.js';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const repoRoot = path.resolve(__dirname, '..');
|
const repoRoot = path.resolve(__dirname, '..');
|
||||||
@@ -51,29 +51,6 @@ if (!source) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sha256(file) {
|
|
||||||
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
function treeDigest(root) {
|
|
||||||
const files = [];
|
|
||||||
const walk = directory => {
|
|
||||||
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
||||||
const file = path.join(directory, entry.name);
|
|
||||||
if (entry.isDirectory()) walk(file);
|
|
||||||
else if (entry.isFile()) files.push(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
walk(root);
|
|
||||||
files.sort((left, right) => left.localeCompare(right));
|
|
||||||
const digest = crypto.createHash('sha256');
|
|
||||||
for (const file of files) {
|
|
||||||
const relative = path.relative(root, file).split(path.sep).join('/');
|
|
||||||
digest.update(`${relative}\0${sha256(file)}\n`);
|
|
||||||
}
|
|
||||||
return { files: files.length, sha256: digest.digest('hex') };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetLock) {
|
if (targetLock) {
|
||||||
for (const engine of engines) {
|
for (const engine of engines) {
|
||||||
const name = `${engine}-${target}${suffix}`;
|
const name = `${engine}-${target}${suffix}`;
|
||||||
@@ -115,17 +92,9 @@ if (targetLock) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const actualFiles = [];
|
const actualFiles = collectRegularFiles(source, {
|
||||||
const walk = directory => {
|
ignoredNames: ['payload-manifest.json'],
|
||||||
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
}).map(file => path.relative(source, file).split(path.sep).join('/'));
|
||||||
const file = path.join(directory, entry.name);
|
|
||||||
if (entry.isDirectory()) walk(file);
|
|
||||||
else if (entry.isFile() && entry.name !== 'payload-manifest.json') {
|
|
||||||
actualFiles.push(path.relative(source, file).split(path.sep).join('/'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
walk(source);
|
|
||||||
const expectedFiles = Object.keys(manifest.files || {}).sort();
|
const expectedFiles = Object.keys(manifest.files || {}).sort();
|
||||||
actualFiles.sort();
|
actualFiles.sort();
|
||||||
if (JSON.stringify(actualFiles) !== JSON.stringify(expectedFiles)) {
|
if (JSON.stringify(actualFiles) !== JSON.stringify(expectedFiles)) {
|
||||||
|
|||||||
+31
-17
@@ -90,6 +90,31 @@ function ok(msg) {
|
|||||||
console.log(`[OK] ${msg}`);
|
console.log(`[OK] ${msg}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rejectSymlinks(root, label) {
|
||||||
|
if (!fs.existsSync(root)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let symlinkCount = 0;
|
||||||
|
const walk = directory => {
|
||||||
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||||
|
const file = path.join(directory, entry.name);
|
||||||
|
const relative = path.relative(root, file).split(path.sep).join('/');
|
||||||
|
if (entry.isSymbolicLink()) {
|
||||||
|
fail(`Unsupported symlink in ${label}: ${relative}`);
|
||||||
|
symlinkCount += 1;
|
||||||
|
} else if (entry.isDirectory()) {
|
||||||
|
walk(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
walk(root);
|
||||||
|
if (symlinkCount === 0) {
|
||||||
|
ok(`${label} contains no symlinks`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function binName(engine) {
|
function binName(engine) {
|
||||||
return `${engine}${suffix}`;
|
return `${engine}${suffix}`;
|
||||||
}
|
}
|
||||||
@@ -208,23 +233,7 @@ console.log('\n─── 6. yt-dlp packaging ───');
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
fail(`Cannot read _internal/: ${e.message}`);
|
fail(`Cannot read _internal/: ${e.message}`);
|
||||||
}
|
}
|
||||||
if (entries) {
|
rejectSymlinks(internalDir, '_internal/');
|
||||||
let broken = 0;
|
|
||||||
for (const entry of entries) {
|
|
||||||
const ep = path.join(internalDir, entry);
|
|
||||||
if (fs.lstatSync(ep).isSymbolicLink()) {
|
|
||||||
try {
|
|
||||||
fs.accessSync(ep);
|
|
||||||
} catch {
|
|
||||||
fail(`Broken symlink in _internal/: ${entry}`);
|
|
||||||
broken++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (broken === 0) {
|
|
||||||
ok('_internal/ symlinks valid');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const requiredRuntimeFiles = [
|
const requiredRuntimeFiles = [
|
||||||
path.join(internalDir, 'yt_dlp_ejs', 'yt', 'solver', 'core.min.js'),
|
path.join(internalDir, 'yt_dlp_ejs', 'yt', 'solver', 'core.min.js'),
|
||||||
@@ -253,6 +262,11 @@ console.log('\n─── 6. yt-dlp packaging ───');
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const aria2LibsDir = path.join(binariesDir, 'aria2-libs');
|
||||||
|
if (fs.existsSync(aria2LibsDir) && fs.statSync(aria2LibsDir).isDirectory()) {
|
||||||
|
rejectSymlinks(aria2LibsDir, 'aria2-libs/');
|
||||||
|
}
|
||||||
|
|
||||||
// ───── Check 7, 8 & 9: Engine version self-tests ─────
|
// ───── Check 7, 8 & 9: Engine version self-tests ─────
|
||||||
console.log('\n─── 7 & 8 & 9. Engine version self-tests ───');
|
console.log('\n─── 7 & 8 & 9. Engine version self-tests ───');
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ const MAX_URL_COUNT: usize = 200;
|
|||||||
const SIGNATURE_MAX_AGE_MS: u64 = 60_000;
|
const SIGNATURE_MAX_AGE_MS: u64 = 60_000;
|
||||||
const SERVER_HEADER: &str = "x-firelink-server";
|
const SERVER_HEADER: &str = "x-firelink-server";
|
||||||
const PROTOCOL_VERSION_HEADER: &str = "x-firelink-protocol-version";
|
const PROTOCOL_VERSION_HEADER: &str = "x-firelink-protocol-version";
|
||||||
const PROTOCOL_VERSION: &str = "2";
|
const CLIENT_NONCE_HEADER: &str = "x-firelink-client-nonce";
|
||||||
|
const SERVER_PROOF_HEADER: &str = "x-firelink-server-proof";
|
||||||
|
const SERVER_PORT_HEADER: &str = "x-firelink-server-port";
|
||||||
|
const SERVER_PROOF_PREFIX: &[u8] = b"firelink-server-proof\n";
|
||||||
|
const PROTOCOL_VERSION: &str = "3";
|
||||||
|
|
||||||
type HmacSha256 = Hmac<Sha256>;
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
pub type SharedExtensionToken = Arc<RwLock<String>>;
|
pub type SharedExtensionToken = Arc<RwLock<String>>;
|
||||||
@@ -41,6 +45,7 @@ pub struct ServerState {
|
|||||||
pub pairing_token: SharedExtensionToken,
|
pub pairing_token: SharedExtensionToken,
|
||||||
pub frontend_ready: SharedFrontendReady,
|
pub frontend_ready: SharedFrontendReady,
|
||||||
pub replay_cache: ReplayCache,
|
pub replay_cache: ReplayCache,
|
||||||
|
pub bound_port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -76,11 +81,13 @@ pub async fn start_server(
|
|||||||
server_port: SharedServerPort,
|
server_port: SharedServerPort,
|
||||||
mut shutdown_rx: watch::Receiver<bool>,
|
mut shutdown_rx: watch::Receiver<bool>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
let (port, listener) = bind_extension_listener().await?;
|
||||||
let state = ServerState {
|
let state = ServerState {
|
||||||
app_handle,
|
app_handle,
|
||||||
pairing_token,
|
pairing_token,
|
||||||
frontend_ready,
|
frontend_ready,
|
||||||
replay_cache: Arc::new(Mutex::new(HashMap::new())),
|
replay_cache: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
bound_port: port,
|
||||||
};
|
};
|
||||||
|
|
||||||
let cors = CorsLayer::new()
|
let cors = CorsLayer::new()
|
||||||
@@ -98,7 +105,6 @@ pub async fn start_server(
|
|||||||
.layer(middleware::from_fn(add_server_identity))
|
.layer(middleware::from_fn(add_server_identity))
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
let (port, listener) = bind_extension_listener().await?;
|
|
||||||
if let Ok(mut current_port) = server_port.write() {
|
if let Ok(mut current_port) = server_port.write() {
|
||||||
*current_port = Some(port);
|
*current_port = Some(port);
|
||||||
}
|
}
|
||||||
@@ -156,13 +162,13 @@ async fn ping_handler(
|
|||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
body: Bytes,
|
body: Bytes,
|
||||||
) -> StatusCode {
|
) -> Result<Response, StatusCode> {
|
||||||
let signature = match headers
|
let signature = match headers
|
||||||
.get("x-firelink-signature")
|
.get("x-firelink-signature")
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
{
|
{
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return StatusCode::FORBIDDEN,
|
None => return Err(StatusCode::FORBIDDEN),
|
||||||
};
|
};
|
||||||
|
|
||||||
let timestamp_str = match headers
|
let timestamp_str = match headers
|
||||||
@@ -170,14 +176,41 @@ async fn ping_handler(
|
|||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
{
|
{
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return StatusCode::FORBIDDEN,
|
None => return Err(StatusCode::FORBIDDEN),
|
||||||
|
};
|
||||||
|
|
||||||
|
let nonce = match headers
|
||||||
|
.get(CLIENT_NONCE_HEADER)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.filter(|value| is_valid_client_nonce(value))
|
||||||
|
{
|
||||||
|
Some(v) => v,
|
||||||
|
None => return Err(StatusCode::FORBIDDEN),
|
||||||
};
|
};
|
||||||
|
|
||||||
if verify_signature(signature, timestamp_str, &body, &state.pairing_token).is_err() {
|
if verify_signature(signature, timestamp_str, &body, &state.pairing_token).is_err() {
|
||||||
return StatusCode::FORBIDDEN;
|
return Err(StatusCode::FORBIDDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusCode::OK
|
let proof = sign_server_proof(
|
||||||
|
timestamp_str,
|
||||||
|
nonce,
|
||||||
|
state.bound_port,
|
||||||
|
&state.pairing_token,
|
||||||
|
)
|
||||||
|
.map_err(|_| StatusCode::FORBIDDEN)?;
|
||||||
|
|
||||||
|
let mut response = Response::new(Body::empty());
|
||||||
|
response.headers_mut().insert(
|
||||||
|
SERVER_PROOF_HEADER,
|
||||||
|
HeaderValue::from_str(&proof).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
|
||||||
|
);
|
||||||
|
response.headers_mut().insert(
|
||||||
|
SERVER_PORT_HEADER,
|
||||||
|
HeaderValue::from_str(&state.bound_port.to_string())
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?,
|
||||||
|
);
|
||||||
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download_handler(
|
async fn download_handler(
|
||||||
@@ -327,6 +360,39 @@ fn verify_signature(
|
|||||||
Ok(timestamp)
|
Ok(timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_valid_client_nonce(value: &str) -> bool {
|
||||||
|
value.len() == 32 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_server_proof(
|
||||||
|
timestamp_text: &str,
|
||||||
|
nonce: &str,
|
||||||
|
bound_port: u16,
|
||||||
|
pairing_token: &SharedExtensionToken,
|
||||||
|
) -> Result<String, ()> {
|
||||||
|
let token = pairing_token.read().unwrap_or_else(|e| e.into_inner());
|
||||||
|
if token.is_empty() {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut mac = HmacSha256::new_from_slice(token.as_bytes()).map_err(|_| ())?;
|
||||||
|
mac.update(SERVER_PROOF_PREFIX);
|
||||||
|
mac.update(timestamp_text.as_bytes());
|
||||||
|
mac.update(b"\n");
|
||||||
|
mac.update(nonce.as_bytes());
|
||||||
|
mac.update(b"\n");
|
||||||
|
mac.update(bound_port.to_string().as_bytes());
|
||||||
|
let signature = mac.finalize().into_bytes();
|
||||||
|
Ok(encode_hex(signature.as_slice()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_hex(bytes: &[u8]) -> String {
|
||||||
|
bytes
|
||||||
|
.iter()
|
||||||
|
.map(|byte| format!("{byte:02x}"))
|
||||||
|
.collect::<String>()
|
||||||
|
}
|
||||||
|
|
||||||
fn claim_request(signature: &str, timestamp: u64, replay_cache: &ReplayCache) -> bool {
|
fn claim_request(signature: &str, timestamp: u64, replay_cache: &ReplayCache) -> bool {
|
||||||
let now = match current_time_millis() {
|
let now = match current_time_millis() {
|
||||||
Some(now) => now,
|
Some(now) => now,
|
||||||
@@ -383,8 +449,14 @@ fn is_allowed_origin(origin: &str) -> bool {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{add_server_identity, PROTOCOL_VERSION_HEADER, SERVER_HEADER};
|
use super::{
|
||||||
|
add_server_identity, is_valid_client_nonce, sign_server_proof, PROTOCOL_VERSION_HEADER,
|
||||||
|
SERVER_HEADER,
|
||||||
|
};
|
||||||
use axum::{http::StatusCode, middleware, routing::get, Router};
|
use axum::{http::StatusCode, middleware, routing::get, Router};
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use sha2::Sha256;
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn identifies_every_extension_server_response() {
|
async fn identifies_every_extension_server_response() {
|
||||||
@@ -406,9 +478,48 @@ mod tests {
|
|||||||
assert_eq!(response.headers().get(SERVER_HEADER).unwrap(), "1");
|
assert_eq!(response.headers().get(SERVER_HEADER).unwrap(), "1");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
response.headers().get(PROTOCOL_VERSION_HEADER).unwrap(),
|
response.headers().get(PROTOCOL_VERSION_HEADER).unwrap(),
|
||||||
"2"
|
"3"
|
||||||
);
|
);
|
||||||
|
|
||||||
server.abort();
|
server.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validates_client_nonce_shape() {
|
||||||
|
assert!(is_valid_client_nonce("0123456789abcdef0123456789abcdef"));
|
||||||
|
assert!(is_valid_client_nonce("ABCDEF0123456789abcdef0123456789"));
|
||||||
|
assert!(!is_valid_client_nonce("0123456789abcdef0123456789abcde"));
|
||||||
|
assert!(!is_valid_client_nonce("0123456789abcdef0123456789abcdeg"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signs_server_proof_with_timestamp_nonce_and_bound_port() {
|
||||||
|
let token = Arc::new(RwLock::new("pairing-token".to_string()));
|
||||||
|
let timestamp = "1710000000000";
|
||||||
|
let nonce = "0123456789abcdef0123456789abcdef";
|
||||||
|
let port = 6414;
|
||||||
|
|
||||||
|
let mut mac = Hmac::<Sha256>::new_from_slice(b"pairing-token").unwrap();
|
||||||
|
mac.update(b"firelink-server-proof\n");
|
||||||
|
mac.update(timestamp.as_bytes());
|
||||||
|
mac.update(b"\n");
|
||||||
|
mac.update(nonce.as_bytes());
|
||||||
|
mac.update(b"\n");
|
||||||
|
mac.update(port.to_string().as_bytes());
|
||||||
|
let expected = mac
|
||||||
|
.finalize()
|
||||||
|
.into_bytes()
|
||||||
|
.iter()
|
||||||
|
.map(|byte| format!("{byte:02x}"))
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sign_server_proof(timestamp, nonce, port, &token).unwrap(),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
sign_server_proof(timestamp, nonce, port + 1, &token).unwrap(),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user