mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 19:18:25 +00:00
Create empty peer ACL files in the Windows installer and stop on legacy ones
The service still reads peers.allow and peers.deny from \etc\fips on the system drive when they are missing from C:\ProgramData\fips, and any local user can create files there, so a planted list was enforced. install-service.ps1 now creates both files in C:\ProgramData\fips, empty, which allows every peer, so the service no longer falls back to the old location. It stops when either file exists under \etc\fips and is missing from C:\ProgramData\fips, which is exactly when the service would enforce the old file, so an upgrader's list is neither enforced nor dropped without an administrator reviewing it. The check runs after the directory is restricted and before the binaries are copied or the service is registered, and every refusal is decided before any file is created; an existing file is never truncated. The zip README says the installer creates the files, that a list is cleared by emptying its file rather than deleting it, and what to do when the installer stops on an old file. A structural test pins the conditions and their order in the script.
This commit is contained in:
@@ -105,6 +105,17 @@ Configuration:
|
|||||||
peers.deny and, with node.identity.persistent: true, fips.key
|
peers.deny and, with node.identity.persistent: true, fips.key
|
||||||
beside it. Edit fips.yaml there before starting the service.
|
beside it. Edit fips.yaml there before starting the service.
|
||||||
|
|
||||||
|
install-service.ps1 creates empty peers.allow and peers.deny
|
||||||
|
there, which allow every peer until you add entries. To clear
|
||||||
|
a list, empty the file; do not delete it. While either file
|
||||||
|
is missing from C:\ProgramData\fips, the service still reads
|
||||||
|
that file from \etc\fips on the system drive, where earlier
|
||||||
|
releases kept it and any local user can create it. The
|
||||||
|
installer stops if it finds a file there with none in
|
||||||
|
C:\ProgramData\fips: review that file, move it into
|
||||||
|
C:\ProgramData\fips or delete it, and run install-service.ps1
|
||||||
|
again.
|
||||||
|
|
||||||
install-service.ps1 restricts C:\ProgramData\fips to SYSTEM
|
install-service.ps1 restricts C:\ProgramData\fips to SYSTEM
|
||||||
and Administrators before writing into it. Reading or editing
|
and Administrators before writing into it. Reading or editing
|
||||||
files there, fipsctl keygen, fipsctl address with no argument,
|
files there, fipsctl keygen, fipsctl address with no argument,
|
||||||
|
|||||||
@@ -130,6 +130,31 @@ foreach ($item in @(Get-ChildItem -LiteralPath $ConfigDir -Force)) {
|
|||||||
|
|
||||||
Write-Host " Restricted $ConfigDir to SYSTEM and Administrators"
|
Write-Host " Restricted $ConfigDir to SYSTEM and Administrators"
|
||||||
|
|
||||||
|
# Releases before this one read peers.allow and peers.deny from \etc\fips on
|
||||||
|
# the system drive, where any local user can create files, and the service
|
||||||
|
# still reads a file there when it is missing from the config directory.
|
||||||
|
# Stop rather than enforce, or silently drop, a list nobody has reviewed.
|
||||||
|
$legacyAclDir = "$env:SystemDrive\etc\fips"
|
||||||
|
foreach ($name in @("peers.allow", "peers.deny")) {
|
||||||
|
$legacy = Join-Path $legacyAclDir $name
|
||||||
|
$current = "$ConfigDir\$name"
|
||||||
|
if ((Test-Path -LiteralPath $legacy) -and -not (Test-Path -LiteralPath $current)) {
|
||||||
|
Write-Error "$legacy exists and $current does not, so the service would enforce the old file. Earlier releases read it, and any local user can write there. Review it, then move it to $current or delete it, and run install-service.ps1 again."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Empty peer ACL files allow every peer. Having them here means the service
|
||||||
|
# never falls back to the \etc\fips copies. Empty them to clear a list; do not
|
||||||
|
# delete them.
|
||||||
|
foreach ($name in @("peers.allow", "peers.deny")) {
|
||||||
|
$aclFile = "$ConfigDir\$name"
|
||||||
|
if (-not (Test-Path -LiteralPath $aclFile)) {
|
||||||
|
New-Item -ItemType File -Path $aclFile | Out-Null
|
||||||
|
Write-Host " Created empty $aclFile (allows every peer until you add entries)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Copy binaries
|
# Copy binaries
|
||||||
$Binaries = @("fips.exe", "fipsctl.exe", "fipstop.exe")
|
$Binaries = @("fips.exe", "fipsctl.exe", "fipstop.exe")
|
||||||
foreach ($bin in $Binaries) {
|
foreach ($bin in $Binaries) {
|
||||||
|
|||||||
+208
-16
@@ -475,6 +475,54 @@ fn ps_lines(ps1: &str) -> Vec<String> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Asserts that line `i` of install-service.ps1's code lines is an `if` whose
|
||||||
|
/// body is `Write-Error` then `exit 1`, so the condition it tests stops the
|
||||||
|
/// install rather than only reporting it.
|
||||||
|
fn refuses_at(lines: &[String], i: usize, what: &str) {
|
||||||
|
let cond = &lines[i];
|
||||||
|
assert!(
|
||||||
|
cond.starts_with("if (") && cond.ends_with('{'),
|
||||||
|
"install-service.ps1: the {what} is not an if statement: {cond}"
|
||||||
|
);
|
||||||
|
let body = lines.get(i + 1..i + 3).unwrap_or_default();
|
||||||
|
assert!(
|
||||||
|
body.len() == 2 && body[0].starts_with("Write-Error ") && body[1] == "exit 1",
|
||||||
|
"install-service.ps1: the {what} is not followed by Write-Error then exit 1: \
|
||||||
|
{cond}\n then: {body:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the index of the line that closes the block opened on line
|
||||||
|
/// `start`, found by brace depth. Braces inside single- or double-quoted
|
||||||
|
/// strings are not counted.
|
||||||
|
fn block_end(lines: &[String], start: usize) -> usize {
|
||||||
|
let mut depth = 0i64;
|
||||||
|
for (i, line) in lines.iter().enumerate().skip(start) {
|
||||||
|
let mut quote = None;
|
||||||
|
for c in line.chars() {
|
||||||
|
match (quote, c) {
|
||||||
|
(None, '"' | '\'') => quote = Some(c),
|
||||||
|
(Some(q), _) if c == q => quote = None,
|
||||||
|
(None, '{') => depth += 1,
|
||||||
|
(None, '}') => depth -= 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if depth <= 0 {
|
||||||
|
assert!(
|
||||||
|
i > start,
|
||||||
|
"install-service.ps1: no block opens at code line {start}: {}",
|
||||||
|
lines[start]
|
||||||
|
);
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic!(
|
||||||
|
"install-service.ps1: the block at code line {start} never closes: {}",
|
||||||
|
lines[start]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Guards the order in which install-service.ps1 secures `C:\ProgramData\fips`.
|
/// Guards the order in which install-service.ps1 secures `C:\ProgramData\fips`.
|
||||||
///
|
///
|
||||||
/// The directory inherits `C:\ProgramData`'s access, under which any local
|
/// The directory inherits `C:\ProgramData`'s access, under which any local
|
||||||
@@ -664,19 +712,6 @@ fn windows_installer_restricts_config_dir_before_any_path_inside_it() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn windows_installer_refusal_conditions_stop_the_install() {
|
fn windows_installer_refusal_conditions_stop_the_install() {
|
||||||
let lines = ps_lines(&repo_file("packaging/windows/install-service.ps1"));
|
let lines = ps_lines(&repo_file("packaging/windows/install-service.ps1"));
|
||||||
let refuses_at = |i: usize, what: &str| {
|
|
||||||
let cond = &lines[i];
|
|
||||||
assert!(
|
|
||||||
cond.starts_with("if (") && cond.ends_with('{'),
|
|
||||||
"install-service.ps1: the {what} is not an if statement: {cond}"
|
|
||||||
);
|
|
||||||
let body = lines.get(i + 1..i + 3).unwrap_or_default();
|
|
||||||
assert!(
|
|
||||||
body.len() == 2 && body[0].starts_with("Write-Error ") && body[1] == "exit 1",
|
|
||||||
"install-service.ps1: the {what} is not followed by Write-Error then exit 1: \
|
|
||||||
{cond}\n then: {body:?}"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
let find = |what: &str, pred: &dyn Fn(&str) -> bool| -> Vec<usize> {
|
let find = |what: &str, pred: &dyn Fn(&str) -> bool| -> Vec<usize> {
|
||||||
let found: Vec<usize> = lines
|
let found: Vec<usize> = lines
|
||||||
.iter()
|
.iter()
|
||||||
@@ -708,7 +743,7 @@ fn windows_installer_refusal_conditions_stop_the_install() {
|
|||||||
for i in find("owner refusal", &|l| {
|
for i in find("owner refusal", &|l| {
|
||||||
l == "if ($trustedOwners -notcontains $ownerSid) {"
|
l == "if ($trustedOwners -notcontains $ownerSid) {"
|
||||||
}) {
|
}) {
|
||||||
refuses_at(i, "owner refusal");
|
refuses_at(&lines, i, "owner refusal");
|
||||||
}
|
}
|
||||||
|
|
||||||
let dir_links = find("refusal of $ConfigDir as a link", &|l| {
|
let dir_links = find("refusal of $ConfigDir as a link", &|l| {
|
||||||
@@ -722,7 +757,7 @@ fn windows_installer_refusal_conditions_stop_the_install() {
|
|||||||
dir_links.len()
|
dir_links.len()
|
||||||
);
|
);
|
||||||
for i in dir_links {
|
for i in dir_links {
|
||||||
refuses_at(i, "refusal of $ConfigDir as a link");
|
refuses_at(&lines, i, "refusal of $ConfigDir as a link");
|
||||||
}
|
}
|
||||||
|
|
||||||
for i in find("refusal of a link or folder entry", &|l| {
|
for i in find("refusal of a link or folder entry", &|l| {
|
||||||
@@ -737,7 +772,7 @@ fn windows_installer_refusal_conditions_stop_the_install() {
|
|||||||
"install-service.ps1: an entry must be refused if it is a link or a folder, \
|
"install-service.ps1: an entry must be refused if it is a link or a folder, \
|
||||||
either one: {cond}"
|
either one: {cond}"
|
||||||
);
|
);
|
||||||
refuses_at(i, "refusal of a link or folder entry");
|
refuses_at(&lines, i, "refusal of a link or folder entry");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,6 +841,163 @@ fn windows_installer_icacls_calls_act_on_links_and_check_exit_codes() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Guards the peer ACL files install-service.ps1 creates, and its refusal of
|
||||||
|
/// legacy ones.
|
||||||
|
///
|
||||||
|
/// Earlier releases read `peers.allow` and `peers.deny` from `\etc\fips` on
|
||||||
|
/// the system drive, where any local user can create files, and the service
|
||||||
|
/// still reads a file there when it is missing from `C:\ProgramData\fips`. So
|
||||||
|
/// for each of the two files the installer must stop when the legacy file
|
||||||
|
/// exists and the current one does not, which is exactly when the service
|
||||||
|
/// would enforce the legacy file, and otherwise create the current file empty
|
||||||
|
/// if it is missing, without truncating one that exists. Every refusal is
|
||||||
|
/// decided before any file is created, and all of it happens after the config
|
||||||
|
/// directory is secured and before the binaries are copied or the service is
|
||||||
|
/// registered, so a refusal leaves an existing install's binaries, config and
|
||||||
|
/// service as they were. Each check is bounded by its loop's closing brace,
|
||||||
|
/// since a statement moved out of its loop runs for the last file only.
|
||||||
|
#[test]
|
||||||
|
fn windows_installer_creates_empty_peer_acl_files_and_refuses_legacy_ones() {
|
||||||
|
let lines = ps_lines(&repo_file("packaging/windows/install-service.ps1"));
|
||||||
|
let all = |pred: &dyn Fn(&str) -> bool| -> Vec<usize> {
|
||||||
|
lines
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, l)| pred(l))
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
let first = |what: &str, pred: &dyn Fn(&str) -> bool| -> usize {
|
||||||
|
all(pred)
|
||||||
|
.first()
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_else(|| panic!("install-service.ps1: no line {what}"))
|
||||||
|
};
|
||||||
|
|
||||||
|
let legacy_dir = first("assigning $legacyAclDir", &|l| {
|
||||||
|
l.starts_with("$legacyAclDir = ")
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
lines[legacy_dir], r#"$legacyAclDir = "$env:SystemDrive\etc\fips""#,
|
||||||
|
"install-service.ps1: the legacy peer ACL directory is not \\etc\\fips on the \
|
||||||
|
system drive"
|
||||||
|
);
|
||||||
|
|
||||||
|
let loop_line = r#"foreach ($name in @("peers.allow", "peers.deny")) {"#;
|
||||||
|
let loops = all(&|l| {
|
||||||
|
l.starts_with("foreach") && (l.contains("peers.allow") || l.contains("peers.deny"))
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
loops.len(),
|
||||||
|
2,
|
||||||
|
"install-service.ps1: expected two loops over the peer ACL files, the refusal \
|
||||||
|
and the creation, found {}",
|
||||||
|
loops.len()
|
||||||
|
);
|
||||||
|
for &i in &loops {
|
||||||
|
assert_eq!(
|
||||||
|
lines[i], loop_line,
|
||||||
|
"install-service.ps1: a peer ACL loop does not cover both files"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let (refusal, creation) = (loops[0], loops[1]);
|
||||||
|
let (refusal_end, creation_end) = (block_end(&lines, refusal), block_end(&lines, creation));
|
||||||
|
let refusal_body = refusal + 1..refusal_end;
|
||||||
|
let creation_body = creation + 1..creation_end;
|
||||||
|
|
||||||
|
for text in [
|
||||||
|
"$legacy = Join-Path $legacyAclDir $name",
|
||||||
|
r#"$current = "$ConfigDir\$name""#,
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
lines[refusal_body.clone()].iter().any(|l| l == text),
|
||||||
|
"install-service.ps1: the refusal loop has no line {text}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let check = refusal_body
|
||||||
|
.clone()
|
||||||
|
.find(|&i| lines[i].starts_with("if (") && lines[i].contains("$legacy"))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
panic!("install-service.ps1: the refusal loop does not test the legacy file")
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
lines[check],
|
||||||
|
"if ((Test-Path -LiteralPath $legacy) -and -not (Test-Path -LiteralPath $current)) {",
|
||||||
|
"install-service.ps1: the refusal must hold only when the legacy file exists and \
|
||||||
|
the current one does not"
|
||||||
|
);
|
||||||
|
refuses_at(&lines, check, "refusal of a legacy peer ACL file");
|
||||||
|
assert!(
|
||||||
|
block_end(&lines, check) < refusal_end,
|
||||||
|
"install-service.ps1: the refusal of a legacy peer ACL file does not close inside \
|
||||||
|
the refusal loop"
|
||||||
|
);
|
||||||
|
|
||||||
|
let aclfile_line = r#"$aclFile = "$ConfigDir\$name""#;
|
||||||
|
let guard_line = "if (-not (Test-Path -LiteralPath $aclFile)) {";
|
||||||
|
let create = creation_body
|
||||||
|
.clone()
|
||||||
|
.find(|&i| lines[i].contains("New-Item") && lines[i].contains("-ItemType File"))
|
||||||
|
.unwrap_or_else(|| panic!("install-service.ps1: the creation loop does not create a file"));
|
||||||
|
let new_item = &lines[create];
|
||||||
|
assert!(
|
||||||
|
new_item.contains("$aclFile") && !new_item.to_ascii_lowercase().contains("-force"),
|
||||||
|
"install-service.ps1: the peer ACL file must be created at $aclFile without -Force, \
|
||||||
|
which would truncate an existing list: {new_item}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
lines[creation + 1..create]
|
||||||
|
.iter()
|
||||||
|
.any(|l| l == aclfile_line),
|
||||||
|
"install-service.ps1: the creation loop does not set $aclFile to the file in $ConfigDir"
|
||||||
|
);
|
||||||
|
let guard = create - 1;
|
||||||
|
assert!(
|
||||||
|
lines[guard] == guard_line && block_end(&lines, guard) < creation_end,
|
||||||
|
"install-service.ps1: the peer ACL file is not created only when it is missing"
|
||||||
|
);
|
||||||
|
for l in &lines[creation_body] {
|
||||||
|
assert!(
|
||||||
|
[aclfile_line, guard_line, new_item.as_str(), "}"].contains(&l.as_str())
|
||||||
|
|| l.starts_with("Write-Host "),
|
||||||
|
"install-service.ps1: the creation loop does more than create a missing file: {l}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_check = |l: &str| l == "& $refuseEntries";
|
||||||
|
let order = [
|
||||||
|
("check after the reset", all(&is_check).get(2).copied()),
|
||||||
|
("$legacyAclDir", Some(legacy_dir)),
|
||||||
|
("refusal loop", Some(refusal)),
|
||||||
|
("refusal of a legacy file", Some(check)),
|
||||||
|
("end of the refusal loop", Some(refusal_end)),
|
||||||
|
("creation loop", Some(creation)),
|
||||||
|
("creation of a peer ACL file", Some(create)),
|
||||||
|
("end of the creation loop", Some(creation_end)),
|
||||||
|
(
|
||||||
|
"binary copy",
|
||||||
|
all(&|l| l.contains("$Binaries")).first().copied(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"service registration",
|
||||||
|
all(&|l| l.contains("--install-service")).first().copied(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for pair in order.windows(2) {
|
||||||
|
let [(a, ia), (b, ib)] = pair else {
|
||||||
|
unreachable!("windows(2) yields pairs")
|
||||||
|
};
|
||||||
|
let (ia, ib) = (
|
||||||
|
ia.unwrap_or_else(|| panic!("install-service.ps1: no {a}")),
|
||||||
|
ib.unwrap_or_else(|| panic!("install-service.ps1: no {b}")),
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
ia < ib,
|
||||||
|
"install-service.ps1: {a} (code line {ia}) must come before {b} (code line {ib})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const COMMON_CONFIG: &str = "packaging/common/fips.yaml";
|
const COMMON_CONFIG: &str = "packaging/common/fips.yaml";
|
||||||
const OPENWRT_CONFIG: &str = "packaging/openwrt-ipk/files/etc/fips/fips.yaml";
|
const OPENWRT_CONFIG: &str = "packaging/openwrt-ipk/files/etc/fips/fips.yaml";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user