Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,25 @@ jobs:
- name: doc
run: cargo doc --workspace --no-deps

# Advisory (not a required context): proves the Windows build and the owner-only
# ACL on .sc/local-key (#214). Deliberately not `cargo test --workspace`; the
# wider suite still has unix-only assumptions.
windows:
name: windows (check + key ACL tests)
runs-on: windows-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@2fe4ca74464c5902a4f6e302d0a619b4ea911ccc # 1.96.1
with:
components: clippy
- uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2
- name: check
run: cargo check --workspace --all-targets --keep-going
- name: clippy
run: cargo clippy --workspace --all-targets -- -D warnings
- name: key ACL and cache tests
run: cargo test -p scl-repo --lib "cache::"

desktop:
name: desktop (typecheck + test + audit)
runs-on: ubuntu-latest
Expand Down
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,9 @@ transport-adjacent. The rest, imperatively:
(`.sc/protected-cache`, `<ws session root>/cache-<i>`) are never committed and never
transferred.** Cache saves are best-effort and ordered after ref moves; a
cache failure may cause a spurious re-seal but never incorrectness. The key
is owner-only on unix only; non-unix keeps default permissions and warns once
(#214, ACL deferred).
is owner-only on unix (`0600`) and Windows (single-ACE protected DACL, from the first byte;
`crates/repo/src/winacl.rs`); other targets, or a failed Windows ACL call, keep default
permissions and warn once (#214).
- **Revoke is standing-only.** A revoked recipient keeps decrypting ciphertext
sealed before the revoke (they hold the old wraps); cryptographic cutover is
`sc rewrap --rotate` at the tip (fresh key per protected blob; a plain
Expand Down Expand Up @@ -359,8 +360,7 @@ transport-adjacent. The rest, imperatively:

Deferred work is tracked in **`ROADMAP.md` → Deferred** (single source of
truth — do not re-list items here); entries that name an issue mirror the open
GitHub issues. Standing entries: the non-unix
`.sc/local-key` ACL (#214), and the open
GitHub issues. Standing entry: the open
user-file safety limits of the sparse guard (ADR-0034 refinement 8).

## Agent skills
Expand Down
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ integration tests — if you touch a subsystem, run the relevant `demo/run_*.sh`
CI (`ci.yml`) runs `fmt --check`, `clippy -D warnings`, `cargo test --workspace
--all-targets` (Linux and macOS) and `cargo doc` for the Rust workspace, plus a
`desktop` job (`npm ci`, `npm audit --audit-level=high`, typecheck, test).
Your PR must be green on all of them. Dependency changes also run the RustSec
An advisory `windows` job (`cargo check` and `clippy` for the workspace, plus
the `scl-repo` `cache::` tests, which verify the owner-only ACL on
`.sc/local-key`) is not a required check; keep it green when you touch
Windows-specific code. Your PR must be green on all required jobs. Dependency changes also run the RustSec
`audit` workflow, and CodeQL, Scorecard, the SBOM generation, the
`vendor-provenance` check and a nightly `wire_decode` fuzz run live in their own
workflows. In addition:
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 6 additions & 7 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,12 @@ across every phase.
- Checkout restores the executable bit on the write-out paths (#242, #243);
`sc resolve` after a deleted conflicted file restores it too.
- A warning when the protected-cache key cannot be permission-restricted on a
non-unix target (#225; the owner-only ACL is #214).
non-unix target (#225), and an owner-only Windows ACL on `.sc/local-key`,
verified by the advisory `windows` CI job (#214). Windows keys created
before that keep their inherited DACL and no longer warn; delete
`.sc/local-key` to recreate it owner-only. `sc keygen` now refuses to
overwrite an existing identity on Windows too (#281 tracks owner-only
protection for the identity file and the TLS serve key).

## Active

Expand Down Expand Up @@ -810,12 +815,6 @@ P28 review follow-ons, the port de-flake, the clippy allow-list) are recorded in
- **`sc rewrap` on partial clones — refused (P37).** `Repo::rewrap` calls
`refuse_on_partial`; a paths-only/streaming rebuild that would lift the
refusal remains a possible follow-on.
- **Non-unix local-key permissions (#214, P33 follow-on).** `.sc/local-key` is
minted 0600-from-first-byte on unix only. On a non-unix target it keeps default
permissions and `sc` warns once when a cache holds a protected entry (#225; the
exposure is bounded, see THREAT-MODEL.md). Deferred: an owner-only Windows ACL
(`windows-sys` is already in `Cargo.lock`) and a manual check on a real Windows
host.
- **fd secret injection beyond `sc run` (ADR-0052 follow-on).** `sc run
--secrets-fd N` shipped. Not built: the same mode for `sc ws run` and
`sc work --with-secrets`, and a stdin variant.
Expand Down
31 changes: 15 additions & 16 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1683,6 +1683,7 @@ fn run_keygen(out: Option<PathBuf>) -> Result<()> {
// Only tighten a directory we create ourselves: chmod-ing a pre-existing
// parent (e.g. $HOME for `--out ~/identity`, or CWD for `--out ./key`)
// would retroactively narrow the user's own dir — a footgun.
#[cfg(unix)]
let parent_preexisted = parent.as_os_str().is_empty() || parent.exists();
std::fs::create_dir_all(parent)?;
#[cfg(unix)]
Expand All @@ -1700,29 +1701,27 @@ fn run_keygen(out: Option<PathBuf>) -> Result<()> {
let sig_pk = signing.public();
// Create the key file 0600 *atomically* so the private key is never visible
// group/world-readable through the umask window between write and chmod.
// `create_new(true)` refuses to clobber an existing identity.
#[cfg(unix)]
// `create_new(true)` refuses to clobber an existing identity, on every
// platform. Windows has no owner-only protection for this file (#281).
{
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut f = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(&path)
.with_context(|| {
format!(
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create_new(true);
#[cfg(unix)]
std::os::unix::fs::OpenOptionsExt::mode(&mut opts, 0o600);
let mut f = opts.open(&path).map_err(|e| {
if e.kind() == std::io::ErrorKind::AlreadyExists {
anyhow::anyhow!(
"identity already exists at {} (refusing to overwrite)",
path.display()
)
})?;
} else {
anyhow::Error::from(e).context(format!("cannot create {}", path.display()))
}
})?;
f.write_all(seed_line.as_bytes())?;
}
#[cfg(not(unix))]
{
std::fs::write(&path, &seed_line)?;
}
println!("wrote identity: {} (0600)", path.display());
println!("wrote identity: {}", path.display());
println!("public key: {}", enc_pk.to_key_string());
println!("signing key: {}", sig_pk.to_key_string());
println!("recipient id: {}", enc_pk.recipient_id());
Expand Down
33 changes: 33 additions & 0 deletions crates/cli/tests/keygen_overwrite.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//! `sc keygen --out` must never overwrite an existing identity, on any
//! platform: the seed cannot be regenerated (#214 review).

use std::process::Command;

#[test]
fn keygen_refuses_to_overwrite_an_existing_identity() {
let dir = std::env::temp_dir().join(format!("scl-cli-keygen-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let id = dir.join("alice.id");
let keygen = || {
Command::new(env!("CARGO_BIN_EXE_sc"))
.args(["keygen", "--out", id.to_str().unwrap()])
.current_dir(&dir)
.output()
.expect("sc runs")
};

assert!(keygen().status.success());
let first = std::fs::read(&id).unwrap();

let again = keygen();
assert!(!again.status.success(), "second keygen must fail");
assert!(
String::from_utf8_lossy(&again.stderr).contains("refusing to overwrite"),
"{again:?}"
);
assert_eq!(std::fs::read(&id).unwrap(), first, "identity untouched");

std::fs::remove_dir_all(&dir).unwrap();
assert!(!dir.exists());
}
2 changes: 2 additions & 0 deletions crates/gitio/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ mod tests {
std::fs::remove_dir_all(&root).unwrap();
}

// The shim is a /bin/sh script made executable with a mode bit: unix only.
#[cfg(unix)]
#[test]
fn sc_git_override_and_missing_binary_error() {
let _g = GIT_ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
Expand Down
4 changes: 4 additions & 0 deletions crates/repo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,9 @@ scl-objio = { version = "0.1.0", path = "../objio" }
hex = "0.4.3"
blake3 = "1.8.5"

# Owner-only DACL for .sc/local-key (#214); the only unsafe is in src/winacl.rs.
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Threading"] }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add windows-sys to the dependency inventory

This makes windows-sys a direct dependency of scl-repo, but docs/security/third-party-inventory.md still lists only blake3, hex, libc, regex, serde, thiserror, and toml for that crate. The inventory's Maintenance section explicitly requires updating it in the same PR whenever a direct dependency is added, so include windows-sys and its Windows ACL role to keep the security inventory accurate.

Useful? React with 👍 / 👎.


[lints]
workspace = true
Loading
Loading