Preserve public trust-anchor parent permissions
enroll_service_identity_at and export_service_identity_trust_anchor
both routed through prepare_parent, which chmods the target directory
to 0700 on unix. That is correct for the private enrollment path but
wrong for exporting a public trust anchor: Idunn's provisioning and
control-plane code export the public anchor into a directory another
service already owns and reads from (Idunn/src/provisioning.rs,
Idunn/src/control_plane.rs), and forcibly narrowing that directory's
permissions on every export would lock out the legitimate reader
instead of protecting anything.
Split the helper: ensure_parent only creates the directory, and the
renamed prepare_private_parent (used solely by enrollment) also locks
it down. Ported from GameCult/cultnet-rs commit 69f2498 ("Preserve
public anchor parent permissions"), which fixed the same bug before
that standalone repo was superseded by this package.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
e9e9c60b0f
commit
e9b91c1f50
1 changed files with 42 additions and 3 deletions
|
|
@ -229,7 +229,7 @@ pub fn enroll_service_identity_at<P: ServiceIdentityProfile>(
|
||||||
path.display()
|
path.display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
prepare_parent(path)?;
|
prepare_private_parent(path)?;
|
||||||
let mut seed = Zeroizing::new([0u8; 32]);
|
let mut seed = Zeroizing::new([0u8; 32]);
|
||||||
rand::rng().fill_bytes(seed.as_mut());
|
rand::rng().fill_bytes(seed.as_mut());
|
||||||
let signing_key = SigningKey::from_bytes(&*seed);
|
let signing_key = SigningKey::from_bytes(&*seed);
|
||||||
|
|
@ -338,7 +338,7 @@ pub fn export_service_identity_trust_anchor<P: ServiceIdentityProfile>(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
) -> Result<ServiceIdentityTrustAnchor> {
|
) -> Result<ServiceIdentityTrustAnchor> {
|
||||||
let anchor = signer.trust_anchor()?;
|
let anchor = signer.trust_anchor()?;
|
||||||
prepare_parent(path)?;
|
ensure_parent(path)?;
|
||||||
let envelope = CultCacheEnvelope {
|
let envelope = CultCacheEnvelope {
|
||||||
key: P::TRUST_ANCHOR_KEY.into(),
|
key: P::TRUST_ANCHOR_KEY.into(),
|
||||||
r#type: P::TRUST_ANCHOR_TYPE.into(),
|
r#type: P::TRUST_ANCHOR_TYPE.into(),
|
||||||
|
|
@ -470,14 +470,27 @@ fn private_envelope<P: ServiceIdentityProfile>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_parent(path: &Path) -> Result<()> {
|
fn ensure_parent(path: &Path) -> Result<()> {
|
||||||
let parent = path
|
let parent = path
|
||||||
.parent()
|
.parent()
|
||||||
.ok_or_else(|| anyhow!("service identity path has no parent"))?;
|
.ok_or_else(|| anyhow!("service identity path has no parent"))?;
|
||||||
std::fs::create_dir_all(parent)?;
|
std::fs::create_dir_all(parent)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [`ensure_parent`], but also locks the directory down to owner-only
|
||||||
|
/// access. Reserved for paths that hold private key material: a public trust
|
||||||
|
/// anchor is routinely exported into a directory another service already
|
||||||
|
/// owns and reads from, and forcing that directory to `0700` would lock the
|
||||||
|
/// legitimate reader out rather than protect anything.
|
||||||
|
fn prepare_private_parent(path: &Path) -> Result<()> {
|
||||||
|
ensure_parent(path)?;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let parent = path
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| anyhow!("service identity path has no parent"))?;
|
||||||
std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))?;
|
std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -830,6 +843,32 @@ mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exporting_a_public_trust_anchor_does_not_narrow_its_parent_permissions() -> Result<()> {
|
||||||
|
let temp = tempfile::tempdir()?;
|
||||||
|
let signer =
|
||||||
|
enroll_service_identity_at::<IdunnServiceIdentity>(&temp.path().join("private.ccmp"))?;
|
||||||
|
let public_parent = temp.path().join("public");
|
||||||
|
std::fs::create_dir(&public_parent)?;
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
std::fs::set_permissions(&public_parent, std::fs::Permissions::from_mode(0o750))?;
|
||||||
|
}
|
||||||
|
let output = public_parent.join("public.ccmp");
|
||||||
|
export_service_identity_trust_anchor(&signer, &output)?;
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::metadata(&public_parent)?.permissions().mode() & 0o777,
|
||||||
|
0o750,
|
||||||
|
"exporting a public trust anchor must not reassign its parent directory's permissions"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn exported_anchor_is_public_only_immutable_and_rejects_identity_substitution() -> Result<()> {
|
fn exported_anchor_is_public_only_immutable_and_rejects_identity_substitution() -> Result<()> {
|
||||||
let temp = tempfile::tempdir()?;
|
let temp = tempfile::tempdir()?;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue