diff --git a/packaging/windows/build-zip.ps1 b/packaging/windows/build-zip.ps1 index a098fff4..027c145f 100644 --- a/packaging/windows/build-zip.ps1 +++ b/packaging/windows/build-zip.ps1 @@ -83,6 +83,13 @@ Windows Service: # Install (requires Administrator) powershell -File install-service.ps1 + # Upgrade: stop the service first, or the installer fails + # copying fips.exe after it has already changed the config + # directory. Then rerun the installer and start the service. + sc stop fips + powershell -File install-service.ps1 + sc start fips + # Manage sc start fips sc stop fips @@ -125,10 +132,26 @@ Configuration: saying so, or may fail with an access error. A foreground run takes -c , or reads + \etc\fips\fips.yaml on the current drive, then C:\ProgramData\fips\fips.yaml and then, as per-user overrides the service does not read, %APPDATA%\fips\fips.yaml, %USERPROFILE%\.fips.yaml and .\fips.yaml. The key file sits - beside the last config loaded. + beside the last config loaded. \etc\fips\fips.yaml was the + system config of earlier releases, and any local user can + create it; the daemon warns when it loads it, and v0.6.0 stops + reading it. Move what you need from it into + C:\ProgramData\fips\fips.yaml and delete it. + + A service that earlier releases ran from \etc\fips reads + only C:\ProgramData\fips once install-service.ps1 has run, + so it loses that config and may come up with a new identity. + The installer stops if it finds \etc\fips\fips.key with no + fips.key in C:\ProgramData\fips. If the key is this node's, + move it there, carry the settings you need from + \etc\fips\fips.yaml, node.identity.persistent: true among + them, into C:\ProgramData\fips\fips.yaml, delete + \etc\fips\fips.yaml, and run the installer again. If you did + not put the key there, delete it. fipsctl keygen writes to C:\ProgramData\fips by default and needs an elevated prompt. Run install-service.ps1 before it: diff --git a/packaging/windows/install-service.ps1 b/packaging/windows/install-service.ps1 index ee293629..21a04c81 100644 --- a/packaging/windows/install-service.ps1 +++ b/packaging/windows/install-service.ps1 @@ -134,9 +134,9 @@ Write-Host " Restricted $ConfigDir to SYSTEM and Administrators" # 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" +$legacyDir = "$env:SystemDrive\etc\fips" foreach ($name in @("peers.allow", "peers.deny")) { - $legacy = Join-Path $legacyAclDir $name + $legacy = Join-Path $legacyDir $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." @@ -144,6 +144,17 @@ foreach ($name in @("peers.allow", "peers.deny")) { } } +# A service that earlier releases ran from \etc\fips reads only $ConfigDir +# once FIPS_CONFIG is set below, and would come up with a new identity. Stop +# until the key is moved into $ConfigDir or deleted. The installer does not +# move it itself: any local user can write \etc\fips, so a key there may not +# be this node's. +$legacyKey = Join-Path $legacyDir "fips.key" +if ((Test-Path -LiteralPath $legacyKey) -and -not (Test-Path -LiteralPath "$ConfigDir\fips.key")) { + Write-Error "$legacyKey exists and $ConfigDir\fips.key does not, so a service that ran from \etc\fips would come up with a new identity. If that key is this node's, move it to $ConfigDir\fips.key and carry the settings you need from \etc\fips\fips.yaml, node.identity.persistent: true among them, into $ConfigDir\fips.yaml, then delete \etc\fips\fips.yaml, which the service warns about on every start until it is gone. If you did not put the key there, delete it: any local user can write to \etc\fips. Then 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. diff --git a/src/packaging_tests.rs b/src/packaging_tests.rs index a361af42..55d9c301 100644 --- a/src/packaging_tests.rs +++ b/src/packaging_tests.rs @@ -874,11 +874,9 @@ fn windows_installer_creates_empty_peer_acl_files_and_refuses_legacy_ones() { .unwrap_or_else(|| panic!("install-service.ps1: no line {what}")) }; - let legacy_dir = first("assigning $legacyAclDir", &|l| { - l.starts_with("$legacyAclDir = ") - }); + let legacy_dir = first("assigning $legacyDir", &|l| l.starts_with("$legacyDir = ")); assert_eq!( - lines[legacy_dir], r#"$legacyAclDir = "$env:SystemDrive\etc\fips""#, + lines[legacy_dir], r#"$legacyDir = "$env:SystemDrive\etc\fips""#, "install-service.ps1: the legacy peer ACL directory is not \\etc\\fips on the \ system drive" ); @@ -906,7 +904,7 @@ fn windows_installer_creates_empty_peer_acl_files_and_refuses_legacy_ones() { let creation_body = creation + 1..creation_end; for text in [ - "$legacy = Join-Path $legacyAclDir $name", + "$legacy = Join-Path $legacyDir $name", r#"$current = "$ConfigDir\$name""#, ] { assert!( @@ -967,7 +965,7 @@ fn windows_installer_creates_empty_peer_acl_files_and_refuses_legacy_ones() { let is_check = |l: &str| l == "& $refuseEntries"; let order = [ ("check after the reset", all(&is_check).get(2).copied()), - ("$legacyAclDir", Some(legacy_dir)), + ("$legacyDir", Some(legacy_dir)), ("refusal loop", Some(refusal)), ("refusal of a legacy file", Some(check)), ("end of the refusal loop", Some(refusal_end)), @@ -998,6 +996,93 @@ fn windows_installer_creates_empty_peer_acl_files_and_refuses_legacy_ones() { } } +/// Guards install-service.ps1's refusal of an identity key left in +/// `\etc\fips`. +/// +/// A service that earlier releases ran from `\etc\fips` reads only +/// `C:\ProgramData\fips` once the installer sets `FIPS_CONFIG`, so a key left +/// in `\etc\fips` with none in the config directory means the node would +/// come up with a new identity. The installer must stop in exactly that case, +/// and must decide it before it creates any file in the config directory, +/// copies the binaries or registers the service, so a refusal leaves an +/// existing install as it was. +#[test] +fn windows_installer_refuses_a_legacy_identity_key_with_none_in_the_config_dir() { + let lines = ps_lines(&repo_file("packaging/windows/install-service.ps1")); + let first = |what: &str, pred: &dyn Fn(&str) -> bool| -> usize { + lines + .iter() + .position(|l| pred(l)) + .unwrap_or_else(|| panic!("install-service.ps1: no line {what}")) + }; + + let dir = first("assigning $legacyDir", &|l| l.starts_with("$legacyDir = ")); + let key = first("assigning $legacyKey", &|l| l.starts_with("$legacyKey = ")); + assert_eq!( + lines[key], r#"$legacyKey = Join-Path $legacyDir "fips.key""#, + "install-service.ps1: the legacy key is not fips.key in $legacyDir" + ); + let checks: Vec = lines + .iter() + .enumerate() + .filter(|(_, l)| l.starts_with("if (") && l.contains("$legacyKey")) + .map(|(i, _)| i) + .collect(); + assert_eq!( + checks.len(), + 1, + "install-service.ps1: expected one test of the legacy key, found {}", + checks.len() + ); + let check = checks[0]; + assert_eq!( + lines[check], + r#"if ((Test-Path -LiteralPath $legacyKey) -and -not (Test-Path -LiteralPath "$ConfigDir\fips.key")) {"#, + "install-service.ps1: the refusal must hold only when the legacy key exists and \ + the config directory has none" + ); + refuses_at(&lines, check, "refusal of a legacy identity key"); + + let order = [ + ("$legacyDir", Some(dir)), + ("$legacyKey", Some(key)), + ("refusal of a legacy identity key", Some(check)), + ( + "creation of a peer ACL file", + lines + .iter() + .position(|l| l.contains("New-Item") && l.contains("-ItemType File")), + ), + ( + "binary copy", + lines.iter().position(|l| l.contains("$Binaries")), + ), + ( + "default config copy", + lines + .iter() + .position(|l| l.contains("Copy-Item") && l.contains("$ConfigDir\\fips.yaml")), + ), + ( + "service registration", + lines.iter().position(|l| l.contains("--install-service")), + ), + ]; + 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 OPENWRT_CONFIG: &str = "packaging/openwrt-ipk/files/etc/fips/fips.yaml";