commit da49a77185dee9ee8f61562ddc2245b37cc05c5d
parent b4dbe0642760ac930706bbf00a0c410f862ddcea
Author: triesap <tyson@radroots.org>
Date: Fri, 21 Aug 2026 20:56:19 +0000
provider: resolve canonical wrapping credentials
Diffstat:
8 files changed, 716 insertions(+), 14 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -49,6 +49,14 @@
caller-supplied entropy, create-new owner-only persistence, expected-public-
key verification, and state-backup exclusion. Credential artifact resolution
remains a separate boundary and may not introduce a sibling-key fallback.
+- Wrapping-credential resolution is frozen by
+ `contracts/services_hardening/wrapping_credential_resolution.v1.json`. It
+ derives one validated shared artifact name beneath the canonical instance
+ secrets root, reads only an existing exact owner-only artifact, and exposes
+ neither a caller path nor caller bytes. Production injection/mounting and
+ repo-local provisioning are external/offline; ordinary run, TOML,
+ environment, arguments, envelope siblings, and state backup never create or
+ carry the credential.
- Treat checked-in source, tests, and prototype behavior as implementation
evidence, not permission to preserve behavior that the active requirement
removes.
diff --git a/README b/README
@@ -98,6 +98,16 @@ wrapping credentials, and plaintext identity material are excluded. Canonical
credential artifact resolution remains Step 133 and ordinary run never
provisions an identity.
+Wrapping credentials now resolve only from the shared validated
+`ServiceCredentialArtifactName` beneath the sealed runtime context's canonical
+instance secrets root. The resolver accepts no path or credential bytes and
+reads only an existing exact 32-byte, euid-owned, single-link artifact through
+descriptor-relative no-follow admission. Service-host deployments inject or
+mount that fixed file; repo-local developers provision it offline. Interactive
+resolution is unsupported, and TOML, environment, process arguments, adjacent
+envelope files, ordinary run, and state backup neither carry nor create the
+credential.
+
Signer-request admission validates bounded client, request, event, method,
canonical request, injected operation entropy, and injected time evidence
before storage. Stable domain-separated operation and correlation identities
diff --git a/contracts/services_hardening/wrapping_credential_resolution.v1.json b/contracts/services_hardening/wrapping_credential_resolution.v1.json
@@ -0,0 +1,41 @@
+{
+ "schema": "radroots.myc.wrapping-credential-resolution",
+ "schema_version": 1,
+ "contract_version": 1,
+ "artifact_name": {
+ "shared_type": "ServiceCredentialArtifactName",
+ "maximum_utf8_bytes": 128
+ },
+ "artifact_path": "<canonical_instance_secrets>/<validated_credential_name>",
+ "artifact": {
+ "wire": "raw_32_bytes",
+ "exact_bytes": 32,
+ "symlink_follow": false,
+ "regular_file": true,
+ "single_link": true,
+ "owner_uid": "effective_uid",
+ "read_modes_octal": ["0400", "0600"],
+ "secrets_root_modes_octal": ["0500", "0700"]
+ },
+ "profiles": {
+ "service_host": "existing_injected_or_mounted",
+ "repo_local": "existing_offline_provisioned",
+ "interactive": "unsupported"
+ },
+ "resolution": {
+ "read_existing_only": true,
+ "creates_credential": false,
+ "creates_parent": false,
+ "caller_supplies_path": false,
+ "caller_supplies_bytes": false,
+ "ordinary_run_generates": false
+ },
+ "forbidden_sources": [
+ "toml_secret",
+ "environment_secret",
+ "process_argument_secret",
+ "adjacent_envelope_sibling",
+ "implicit_fallback"
+ ],
+ "backup_included": false
+}
diff --git a/src/lib.rs b/src/lib.rs
@@ -3,6 +3,7 @@
mod cli_v1;
mod config_v1;
mod provider_contract;
+mod provider_credential;
mod provider_envelope;
mod runtime_context;
mod state_catalog;
@@ -36,9 +37,14 @@ pub use provider_contract::{
MycProviderOperationInput, MycProviderPublicIdentity, MycProviderRole,
MycUntrustedProviderOutput,
};
+pub use provider_credential::{
+ MYC_WRAPPING_CREDENTIAL_ARTIFACT_BYTES, MYC_WRAPPING_CREDENTIAL_CONTRACT_VERSION,
+ MycCredentialResolutionError, MycCredentialResolutionErrorKind,
+ resolve_myc_wrapping_credential,
+};
pub use provider_envelope::{
MYC_ENCRYPTED_IDENTITY_BACKUP_INCLUDED, MYC_ENCRYPTED_IDENTITY_ENVELOPE_CONTRACT_VERSION,
- MYC_ENCRYPTED_IDENTITY_ENVELOPE_MAX_BYTES, MycCredentialResolutionProof, MycDecryptedIdentity,
+ MYC_ENCRYPTED_IDENTITY_ENVELOPE_MAX_BYTES, MycDecryptedIdentity,
MycEncryptedIdentityEnvelopeError, MycEncryptedIdentityEnvelopeErrorKind,
MycEncryptedIdentityProvisioningMaterial, MycWrappingCredential, open_myc_encrypted_identity,
provision_myc_encrypted_identity,
diff --git a/src/provider_credential.rs b/src/provider_credential.rs
@@ -0,0 +1,432 @@
+//! Canonical wrapping-credential artifact resolution.
+
+use core::fmt;
+use std::error::Error;
+
+use radroots_runtime_paths::{ServiceCredentialArtifactName, service_credential_artifact_path};
+
+use crate::{
+ MycBootstrapProfileV1, MycEncryptedIdentityEnvelopeErrorKind, MycProviderBinding,
+ MycProviderKind, MycRuntimeContext, MycWrappingCredential,
+ provider_envelope::load_resolved_wrapping_credential,
+};
+
+/// Exact fixed wrapping-credential artifact length.
+pub const MYC_WRAPPING_CREDENTIAL_ARTIFACT_BYTES: usize = 32;
+/// Exact Myc wrapping-credential resolution contract version.
+pub const MYC_WRAPPING_CREDENTIAL_CONTRACT_VERSION: u32 = 1;
+
+/// Stable source-free credential-resolution failure classification.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum MycCredentialResolutionErrorKind {
+ InvalidBinding,
+ UnsupportedProfile,
+ InvalidReference,
+ MissingCredential,
+ InsecureSecretsRoot,
+ InsecureCredential,
+ InvalidCredential,
+ Io,
+ UnsupportedPlatform,
+}
+
+impl MycCredentialResolutionErrorKind {
+ /// Returns the stable machine-facing safe code.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ match self {
+ Self::InvalidBinding => "provider_credential_binding_invalid",
+ Self::UnsupportedProfile => "provider_credential_profile_unsupported",
+ Self::InvalidReference => "provider_credential_reference_invalid",
+ Self::MissingCredential => "provider_credential_missing",
+ Self::InsecureSecretsRoot => "provider_credential_root_insecure",
+ Self::InsecureCredential => "provider_credential_artifact_insecure",
+ Self::InvalidCredential => "provider_credential_material_invalid",
+ Self::Io => "provider_credential_io_failed",
+ Self::UnsupportedPlatform => "provider_credential_platform_unsupported",
+ }
+ }
+
+ const fn message(self) -> &'static str {
+ match self {
+ Self::InvalidBinding => "provider credential binding is invalid",
+ Self::UnsupportedProfile => "provider credential profile is unsupported",
+ Self::InvalidReference => "provider credential reference is invalid",
+ Self::MissingCredential => "provider credential is missing",
+ Self::InsecureSecretsRoot => "provider credential root is insecure",
+ Self::InsecureCredential => "provider credential artifact is insecure",
+ Self::InvalidCredential => "provider credential material is invalid",
+ Self::Io => "provider credential storage failed",
+ Self::UnsupportedPlatform => "provider credential storage is unsupported",
+ }
+ }
+}
+
+/// One source-free wrapping-credential resolution failure.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct MycCredentialResolutionError {
+ kind: MycCredentialResolutionErrorKind,
+}
+
+impl MycCredentialResolutionError {
+ /// Returns the stable failure kind.
+ #[must_use]
+ pub const fn kind(self) -> MycCredentialResolutionErrorKind {
+ self.kind
+ }
+
+ /// Returns the stable machine-facing safe code.
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ self.kind.code()
+ }
+}
+
+impl fmt::Debug for MycCredentialResolutionError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("MycCredentialResolutionError")
+ .field("kind", &self.kind)
+ .finish()
+ }
+}
+
+impl fmt::Display for MycCredentialResolutionError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(self.kind.message())
+ }
+}
+
+impl Error for MycCredentialResolutionError {}
+
+const fn resolution_error(kind: MycCredentialResolutionErrorKind) -> MycCredentialResolutionError {
+ MycCredentialResolutionError { kind }
+}
+
+/// Resolves one existing wrapping credential from the canonical instance secrets root.
+///
+/// The caller supplies no path or credential bytes. Production deployment and
+/// repo-local offline tooling provision the fixed artifact externally; this
+/// operation is read-only and never creates a credential or parent directory.
+pub fn resolve_myc_wrapping_credential(
+ runtime: &MycRuntimeContext,
+ binding: &MycProviderBinding,
+) -> Result<MycWrappingCredential, MycCredentialResolutionError> {
+ if !matches!(
+ runtime.profile(),
+ MycBootstrapProfileV1::ServiceHost | MycBootstrapProfileV1::RepoLocal
+ ) {
+ return Err(resolution_error(
+ MycCredentialResolutionErrorKind::UnsupportedProfile,
+ ));
+ }
+ if binding.kind() != MycProviderKind::EncryptedFile {
+ return Err(resolution_error(
+ MycCredentialResolutionErrorKind::InvalidBinding,
+ ));
+ }
+ let reference = binding
+ .credential_reference()
+ .ok_or_else(|| resolution_error(MycCredentialResolutionErrorKind::InvalidBinding))?;
+ let name = ServiceCredentialArtifactName::new(reference.as_str())
+ .map_err(|_| resolution_error(MycCredentialResolutionErrorKind::InvalidReference))?;
+ let path = service_credential_artifact_path(runtime.context().paths(), &name);
+ if binding.encrypted_envelope_path() == Some(path.as_path()) {
+ return Err(resolution_error(
+ MycCredentialResolutionErrorKind::InvalidBinding,
+ ));
+ }
+ load_resolved_wrapping_credential(&path).map_err(|error| {
+ let kind = match error.kind() {
+ MycEncryptedIdentityEnvelopeErrorKind::InvalidPath
+ | MycEncryptedIdentityEnvelopeErrorKind::InvalidBinding => {
+ MycCredentialResolutionErrorKind::InvalidBinding
+ }
+ MycEncryptedIdentityEnvelopeErrorKind::MissingEnvelope => {
+ MycCredentialResolutionErrorKind::MissingCredential
+ }
+ MycEncryptedIdentityEnvelopeErrorKind::InsecureParent => {
+ MycCredentialResolutionErrorKind::InsecureSecretsRoot
+ }
+ MycEncryptedIdentityEnvelopeErrorKind::InsecureArtifact => {
+ MycCredentialResolutionErrorKind::InsecureCredential
+ }
+ MycEncryptedIdentityEnvelopeErrorKind::InvalidCredential => {
+ MycCredentialResolutionErrorKind::InvalidCredential
+ }
+ MycEncryptedIdentityEnvelopeErrorKind::UnsupportedPlatform => {
+ MycCredentialResolutionErrorKind::UnsupportedPlatform
+ }
+ MycEncryptedIdentityEnvelopeErrorKind::InvalidProvisioningMaterial
+ | MycEncryptedIdentityEnvelopeErrorKind::AlreadyExists
+ | MycEncryptedIdentityEnvelopeErrorKind::UnsupportedEnvelopeVersion
+ | MycEncryptedIdentityEnvelopeErrorKind::MalformedEnvelope
+ | MycEncryptedIdentityEnvelopeErrorKind::WrongCredential
+ | MycEncryptedIdentityEnvelopeErrorKind::IdentityMismatch
+ | MycEncryptedIdentityEnvelopeErrorKind::Io => MycCredentialResolutionErrorKind::Io,
+ };
+ resolution_error(kind)
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ use std::fs;
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ use std::os::unix::fs::{PermissionsExt, symlink};
+ use std::path::{Path, PathBuf};
+
+ use nostr::{Keys, SecretKey};
+ use sha2::{Digest, Sha256};
+
+ use crate::{
+ MycConfigProfile, MycProviderRole, RadrootsHostEnvironment, RadrootsPathResolver,
+ RadrootsPlatform, parse_myc_cli_v1_from, parse_myc_config_v1, resolve_myc_runtime_context,
+ };
+
+ use super::*;
+
+ const CONFIG: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
+
+ fn bytes(label: &str) -> [u8; 32] {
+ Sha256::digest(label.as_bytes()).into()
+ }
+
+ fn runtime(root: &Path, profile: &str) -> MycRuntimeContext {
+ let root = root.to_str().expect("UTF-8 test root");
+ let arguments = if profile == "repo-local" {
+ vec![
+ "myc",
+ "--profile",
+ profile,
+ "--instance",
+ "primary",
+ "--repo-local-root",
+ root,
+ "run",
+ ]
+ } else {
+ vec!["myc", "--profile", profile, "--instance", "primary", "run"]
+ };
+ let invocation = parse_myc_cli_v1_from(arguments).expect("test invocation");
+ let environment = if profile == "interactive" {
+ RadrootsHostEnvironment {
+ home_dir: Some(PathBuf::from(root)),
+ xdg_config_home: Some(PathBuf::from(root).join("config")),
+ xdg_data_home: Some(PathBuf::from(root).join("data")),
+ xdg_state_home: Some(PathBuf::from(root).join("state")),
+ xdg_cache_home: Some(PathBuf::from(root).join("cache")),
+ xdg_runtime_dir: Some(PathBuf::from(root).join("run")),
+ ..RadrootsHostEnvironment::default()
+ }
+ } else {
+ RadrootsHostEnvironment::default()
+ };
+ resolve_myc_runtime_context(
+ &RadrootsPathResolver::new(RadrootsPlatform::Linux, environment),
+ &invocation,
+ )
+ .expect("runtime context")
+ }
+
+ fn binding(envelope_path: &Path) -> MycProviderBinding {
+ let mut identity = bytes("radroots.myc.credential-test.identity.v1");
+ while SecretKey::from_slice(&identity).is_err() {
+ identity = Sha256::digest(identity).into();
+ }
+ let public_key = Keys::new(SecretKey::from_slice(&identity).expect("identity"))
+ .public_key()
+ .to_hex();
+ let source = CONFIG
+ .replace(
+ "/var/lib/radroots/services/myc/primary/secrets/transport.identity.ncrypt",
+ envelope_path.to_str().expect("UTF-8 envelope path"),
+ )
+ .replace(
+ "4444444444444444444444444444444444444444444444444444444444444444",
+ &public_key,
+ );
+ parse_myc_config_v1(source.as_bytes(), MycConfigProfile::RepoLocal)
+ .expect("configuration")
+ .provider_contract()
+ .binding(MycProviderRole::Transport)
+ .expect("transport binding")
+ .clone()
+ }
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ fn prepare_credential(runtime: &MycRuntimeContext, name: &str, contents: &[u8]) -> PathBuf {
+ let root = runtime.context().paths().secrets();
+ fs::create_dir_all(root).expect("secrets root");
+ fs::set_permissions(root, fs::Permissions::from_mode(0o700)).expect("secrets mode");
+ let path = root.join(name);
+ fs::write(&path, contents).expect("credential artifact");
+ fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).expect("credential mode");
+ path
+ }
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ #[test]
+ fn canonical_existing_artifact_resolves_without_path_or_value_exposure() {
+ let directory = tempfile::tempdir().expect("test root");
+ let runtime = runtime(directory.path(), "repo-local");
+ let envelope_parent = directory.path().join("envelopes");
+ fs::create_dir(&envelope_parent).expect("envelope parent");
+ fs::set_permissions(&envelope_parent, fs::Permissions::from_mode(0o700))
+ .expect("envelope parent mode");
+ let binding = binding(&envelope_parent.join("transport.identity.ncrypt"));
+ let credential_bytes = bytes("radroots.myc.credential-test.wrapping.v1");
+ let path = prepare_credential(
+ &runtime,
+ binding
+ .credential_reference()
+ .expect("credential reference")
+ .as_str(),
+ &credential_bytes,
+ );
+
+ let credential =
+ resolve_myc_wrapping_credential(&runtime, &binding).expect("credential resolution");
+ assert_eq!(
+ format!("{credential:?}"),
+ "MycWrappingCredential([redacted])"
+ );
+ assert_eq!(
+ path,
+ runtime
+ .context()
+ .paths()
+ .secrets()
+ .join("transport_wrapping_key")
+ );
+ assert_eq!(
+ fs::read(&path).expect("credential unchanged"),
+ credential_bytes
+ );
+ assert_eq!(
+ fs::metadata(&path)
+ .expect("credential metadata")
+ .permissions()
+ .mode()
+ & 0o777,
+ 0o600
+ );
+ fs::set_permissions(&path, fs::Permissions::from_mode(0o400))
+ .expect("read-only credential mode");
+ fs::set_permissions(
+ runtime.context().paths().secrets(),
+ fs::Permissions::from_mode(0o500),
+ )
+ .expect("read-only secrets root mode");
+ resolve_myc_wrapping_credential(&runtime, &binding)
+ .expect("owner-read-only artifact and secrets root");
+ }
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ #[test]
+ fn missing_adjacent_short_long_zero_and_insecure_artifacts_fail_closed() {
+ let directory = tempfile::tempdir().expect("test root");
+ let runtime = runtime(directory.path(), "repo-local");
+ let envelope_parent = directory.path().join("envelopes");
+ fs::create_dir(&envelope_parent).expect("envelope parent");
+ fs::set_permissions(&envelope_parent, fs::Permissions::from_mode(0o700))
+ .expect("envelope parent mode");
+ let envelope_path = envelope_parent.join("transport.identity.ncrypt");
+ let binding = binding(&envelope_path);
+ let adjacent = envelope_parent.join("transport.identity.key");
+ fs::write(&adjacent, bytes("adjacent key")).expect("adjacent file");
+ fs::create_dir_all(runtime.context().paths().secrets()).expect("secrets root");
+ fs::set_permissions(
+ runtime.context().paths().secrets(),
+ fs::Permissions::from_mode(0o700),
+ )
+ .expect("secrets mode");
+ assert_eq!(
+ resolve_myc_wrapping_credential(&runtime, &binding)
+ .expect_err("canonical credential is missing")
+ .kind(),
+ MycCredentialResolutionErrorKind::MissingCredential
+ );
+
+ let reference = binding.credential_reference().expect("reference").as_str();
+ let path = prepare_credential(&runtime, reference, &[1; 31]);
+ assert_eq!(
+ resolve_myc_wrapping_credential(&runtime, &binding)
+ .expect_err("short")
+ .kind(),
+ MycCredentialResolutionErrorKind::InsecureCredential
+ );
+ fs::write(&path, [1; 33]).expect("long");
+ assert_eq!(
+ resolve_myc_wrapping_credential(&runtime, &binding)
+ .expect_err("long")
+ .kind(),
+ MycCredentialResolutionErrorKind::InsecureCredential
+ );
+ fs::write(&path, [0; 32]).expect("zero");
+ assert_eq!(
+ resolve_myc_wrapping_credential(&runtime, &binding)
+ .expect_err("zero")
+ .kind(),
+ MycCredentialResolutionErrorKind::InvalidCredential
+ );
+ fs::write(&path, [1; 32]).expect("valid length");
+ fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("insecure mode");
+ assert_eq!(
+ resolve_myc_wrapping_credential(&runtime, &binding)
+ .expect_err("mode")
+ .kind(),
+ MycCredentialResolutionErrorKind::InsecureCredential
+ );
+ fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).expect("restore mode");
+ let second_link = runtime.context().paths().secrets().join("second-link");
+ fs::hard_link(&path, &second_link).expect("hard link");
+ assert_eq!(
+ resolve_myc_wrapping_credential(&runtime, &binding)
+ .expect_err("hard link")
+ .kind(),
+ MycCredentialResolutionErrorKind::InsecureCredential
+ );
+ fs::remove_file(&second_link).expect("remove hard link");
+ fs::remove_file(&path).expect("remove credential");
+ symlink(&adjacent, &path).expect("credential symlink");
+ assert_eq!(
+ resolve_myc_wrapping_credential(&runtime, &binding)
+ .expect_err("symlink")
+ .kind(),
+ MycCredentialResolutionErrorKind::InsecureCredential
+ );
+ }
+
+ #[test]
+ fn unsupported_interactive_profile_and_errors_are_source_free() {
+ let directory = tempfile::tempdir().expect("test root");
+ let runtime = runtime(directory.path(), "interactive");
+ let binding = binding(&directory.path().join("transport.identity.ncrypt"));
+ let error =
+ resolve_myc_wrapping_credential(&runtime, &binding).expect_err("interactive profile");
+ assert_eq!(
+ error.kind(),
+ MycCredentialResolutionErrorKind::UnsupportedProfile
+ );
+ assert!(Error::source(&error).is_none());
+ let rendered = format!("{error} {error:?}");
+ assert!(!rendered.contains(directory.path().to_string_lossy().as_ref()));
+ for kind in [
+ MycCredentialResolutionErrorKind::InvalidBinding,
+ MycCredentialResolutionErrorKind::UnsupportedProfile,
+ MycCredentialResolutionErrorKind::InvalidReference,
+ MycCredentialResolutionErrorKind::MissingCredential,
+ MycCredentialResolutionErrorKind::InsecureSecretsRoot,
+ MycCredentialResolutionErrorKind::InsecureCredential,
+ MycCredentialResolutionErrorKind::InvalidCredential,
+ MycCredentialResolutionErrorKind::Io,
+ MycCredentialResolutionErrorKind::UnsupportedPlatform,
+ ] {
+ let error = resolution_error(kind);
+ assert!(!error.code().is_empty());
+ assert!(Error::source(&error).is_none());
+ }
+ }
+}
diff --git a/src/provider_envelope.rs b/src/provider_envelope.rs
@@ -157,7 +157,7 @@ const fn envelope_error(
pub struct MycWrappingCredential(Zeroizing<[u8; WRAPPING_CREDENTIAL_BYTES]>);
/// Non-forgeable proof that owns credential bytes admitted by the governed resolver.
-pub struct MycCredentialResolutionProof {
+pub(crate) struct MycCredentialResolutionProof {
credential: Zeroizing<[u8; WRAPPING_CREDENTIAL_BYTES]>,
}
@@ -168,8 +168,7 @@ impl fmt::Debug for MycCredentialResolutionProof {
}
impl MycWrappingCredential {
- /// Consumes exact credential bytes sealed inside governed resolution proof.
- pub fn from_resolution(
+ pub(crate) fn from_resolution(
proof: MycCredentialResolutionProof,
) -> Result<Self, MycEncryptedIdentityEnvelopeError> {
if proof.credential.iter().all(|byte| *byte == 0) {
@@ -334,6 +333,17 @@ pub fn open_myc_encrypted_identity(
))
}
+pub(crate) fn load_resolved_wrapping_credential(
+ path: &Path,
+) -> Result<MycWrappingCredential, MycEncryptedIdentityEnvelopeError> {
+ ensure_supported_platform()?;
+ validate_requested_path(path)?;
+ let encoded = Zeroizing::new(read_existing_exact(path, WRAPPING_CREDENTIAL_BYTES)?);
+ let mut credential = Zeroizing::new([0_u8; WRAPPING_CREDENTIAL_BYTES]);
+ credential.copy_from_slice(&encoded);
+ MycWrappingCredential::from_resolution(MycCredentialResolutionProof { credential })
+}
+
fn require_wire_version(encoded: &[u8]) -> Result<(), MycEncryptedIdentityEnvelopeError> {
if encoded.len() < 6 || &encoded[..4] != b"RRS1" {
return Err(envelope_error(
@@ -729,7 +739,11 @@ mod native {
file.write_all(encoded)
.and_then(|()| file.sync_all())
.map_err(|_| envelope_error(MycEncryptedIdentityEnvelopeErrorKind::Io))?;
- file_identity(&file, Some(encoded.len()))?;
+ file_identity(
+ &file,
+ Some(encoded.len()),
+ MYC_ENCRYPTED_IDENTITY_ENVELOPE_MAX_BYTES,
+ )?;
validate_current_binding(
&path,
&parent,
@@ -737,6 +751,7 @@ mod native {
&file,
identity,
encoded.len(),
+ MYC_ENCRYPTED_IDENTITY_ENVELOPE_MAX_BYTES,
)?;
parent
.sync_all()
@@ -748,6 +763,7 @@ mod native {
&file,
identity,
encoded.len(),
+ MYC_ENCRYPTED_IDENTITY_ENVELOPE_MAX_BYTES,
)
})();
if result.is_err() {
@@ -757,6 +773,21 @@ mod native {
}
pub(super) fn read_existing(path: &Path) -> Result<Vec<u8>, MycEncryptedIdentityEnvelopeError> {
+ read_existing_bounded(path, MYC_ENCRYPTED_IDENTITY_ENVELOPE_MAX_BYTES, None)
+ }
+
+ pub(super) fn read_existing_exact(
+ path: &Path,
+ expected_length: usize,
+ ) -> Result<Vec<u8>, MycEncryptedIdentityEnvelopeError> {
+ read_existing_bounded(path, expected_length, Some(expected_length))
+ }
+
+ fn read_existing_bounded(
+ path: &Path,
+ maximum_length: usize,
+ expected_length: Option<usize>,
+ ) -> Result<Vec<u8>, MycEncryptedIdentityEnvelopeError> {
let path = ArtifactPath::parse(path)?;
let parent = open_parent(&path.parent_path, false)?;
let parent_identity = directory_identity(&parent, false)?;
@@ -778,7 +809,7 @@ mod native {
let mut file = File::from(descriptor);
let status = fstat(&file)
.map_err(|_| envelope_error(MycEncryptedIdentityEnvelopeErrorKind::InsecureArtifact))?;
- let length = validate_file_status(&status, None)?;
+ let length = validate_file_status(&status, expected_length, maximum_length)?;
let identity = status_identity(
&status,
MycEncryptedIdentityEnvelopeErrorKind::InsecureArtifact,
@@ -795,7 +826,15 @@ mod native {
MycEncryptedIdentityEnvelopeErrorKind::InsecureArtifact,
));
}
- validate_current_binding(&path, &parent, parent_identity, &file, identity, length)?;
+ validate_current_binding(
+ &path,
+ &parent,
+ parent_identity,
+ &file,
+ identity,
+ length,
+ maximum_length,
+ )?;
Ok(encoded)
}
@@ -854,10 +893,11 @@ mod native {
fn file_identity(
file: &File,
expected_length: Option<usize>,
+ maximum_length: usize,
) -> Result<Identity, MycEncryptedIdentityEnvelopeError> {
let status = fstat(file)
.map_err(|_| envelope_error(MycEncryptedIdentityEnvelopeErrorKind::InsecureArtifact))?;
- validate_file_status(&status, expected_length)?;
+ validate_file_status(&status, expected_length, maximum_length)?;
status_identity(
&status,
MycEncryptedIdentityEnvelopeErrorKind::InsecureArtifact,
@@ -889,6 +929,7 @@ mod native {
fn validate_file_status(
status: &rustix::fs::Stat,
expected_length: Option<usize>,
+ maximum_length: usize,
) -> Result<usize, MycEncryptedIdentityEnvelopeError> {
let mode = native_mode(status.st_mode) & 0o777;
let length = usize::try_from(status.st_size)
@@ -898,7 +939,7 @@ mod native {
|| status.st_uid != geteuid().as_raw()
|| !matches!(mode, 0o400 | 0o600)
|| length == 0
- || length > MYC_ENCRYPTED_IDENTITY_ENVELOPE_MAX_BYTES
+ || length > maximum_length
|| expected_length.is_some_and(|expected| expected != length)
{
return Err(envelope_error(
@@ -915,6 +956,7 @@ mod native {
held_file: &File,
expected_file: Identity,
expected_length: usize,
+ maximum_length: usize,
) -> Result<(), MycEncryptedIdentityEnvelopeError> {
let current_parent = open_parent(&path.parent_path, false)?;
if directory_identity(held_parent, false)? != expected_parent
@@ -933,8 +975,8 @@ mod native {
)
.map_err(|_| envelope_error(MycEncryptedIdentityEnvelopeErrorKind::InsecureArtifact))?,
);
- if file_identity(held_file, Some(expected_length))? != expected_file
- || file_identity(¤t_file, Some(expected_length))? != expected_file
+ if file_identity(held_file, Some(expected_length), maximum_length)? != expected_file
+ || file_identity(¤t_file, Some(expected_length), maximum_length)? != expected_file
{
return Err(envelope_error(
MycEncryptedIdentityEnvelopeErrorKind::InsecureArtifact,
@@ -984,7 +1026,7 @@ mod native {
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
-use native::{persist_create_new, read_existing, validate_requested_path};
+use native::{persist_create_new, read_existing, read_existing_exact, validate_requested_path};
#[cfg(any(target_os = "linux", target_os = "macos"))]
const fn ensure_supported_platform() -> Result<(), MycEncryptedIdentityEnvelopeError> {
@@ -1022,6 +1064,16 @@ fn read_existing(_path: &Path) -> Result<Vec<u8>, MycEncryptedIdentityEnvelopeEr
))
}
+#[cfg(not(any(target_os = "linux", target_os = "macos")))]
+fn read_existing_exact(
+ _path: &Path,
+ _expected_length: usize,
+) -> Result<Vec<u8>, MycEncryptedIdentityEnvelopeError> {
+ Err(envelope_error(
+ MycEncryptedIdentityEnvelopeErrorKind::UnsupportedPlatform,
+ ))
+}
+
#[cfg(test)]
mod tests {
#[cfg(any(target_os = "linux", target_os = "macos"))]
diff --git a/tests/services_hardening_credential_resolution.rs b/tests/services_hardening_credential_resolution.rs
@@ -0,0 +1,153 @@
+#![forbid(unsafe_code)]
+
+use serde_json::json;
+
+const CONTRACT: &str =
+ include_str!("../contracts/services_hardening/wrapping_credential_resolution.v1.json");
+const LIB_SOURCE: &str = include_str!("../src/lib.rs");
+const CREDENTIAL_SOURCE: &str = include_str!("../src/provider_credential.rs");
+const ENVELOPE_SOURCE: &str = include_str!("../src/provider_envelope.rs");
+const CONFIG_SCHEMA: &str = include_str!("../contracts/services_hardening/config.v1.schema.json");
+const CONFIG_EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
+const HOST_SOURCE: &str = include_str!("../src/state_host.rs");
+const MAINTENANCE_SOURCE: &str = include_str!("../src/state_maintenance.rs");
+
+#[test]
+fn machine_contract_freezes_the_canonical_read_only_credential_boundary() {
+ let actual: serde_json::Value = serde_json::from_str(CONTRACT).expect("contract JSON");
+ assert_eq!(
+ actual,
+ json!({
+ "schema": "radroots.myc.wrapping-credential-resolution",
+ "schema_version": 1,
+ "contract_version": 1,
+ "artifact_name": {
+ "shared_type": "ServiceCredentialArtifactName",
+ "maximum_utf8_bytes": 128
+ },
+ "artifact_path": "<canonical_instance_secrets>/<validated_credential_name>",
+ "artifact": {
+ "wire": "raw_32_bytes",
+ "exact_bytes": 32,
+ "symlink_follow": false,
+ "regular_file": true,
+ "single_link": true,
+ "owner_uid": "effective_uid",
+ "read_modes_octal": ["0400", "0600"],
+ "secrets_root_modes_octal": ["0500", "0700"]
+ },
+ "profiles": {
+ "service_host": "existing_injected_or_mounted",
+ "repo_local": "existing_offline_provisioned",
+ "interactive": "unsupported"
+ },
+ "resolution": {
+ "read_existing_only": true,
+ "creates_credential": false,
+ "creates_parent": false,
+ "caller_supplies_path": false,
+ "caller_supplies_bytes": false,
+ "ordinary_run_generates": false
+ },
+ "forbidden_sources": [
+ "toml_secret",
+ "environment_secret",
+ "process_argument_secret",
+ "adjacent_envelope_sibling",
+ "implicit_fallback"
+ ],
+ "backup_included": false
+ })
+ );
+}
+
+#[test]
+fn implementation_derives_only_the_shared_canonical_artifact() {
+ for required in [
+ "ServiceCredentialArtifactName::new(reference.as_str())",
+ "service_credential_artifact_path(runtime.context().paths(), &name)",
+ "load_resolved_wrapping_credential(&path)",
+ "MycBootstrapProfileV1::ServiceHost | MycBootstrapProfileV1::RepoLocal",
+ "pub fn resolve_myc_wrapping_credential(",
+ ] {
+ assert!(
+ CREDENTIAL_SOURCE.contains(required),
+ "missing canonical resolution boundary {required}"
+ );
+ }
+ assert!(LIB_SOURCE.contains("mod provider_credential;"));
+ assert!(!LIB_SOURCE.contains("pub mod provider_credential"));
+ assert!(ENVELOPE_SOURCE.contains("pub(crate) struct MycCredentialResolutionProof"));
+ assert!(ENVELOPE_SOURCE.contains("pub(crate) fn from_resolution("));
+}
+
+#[test]
+fn no_configuration_process_sibling_or_creation_fallback_exists() {
+ let production = CREDENTIAL_SOURCE
+ .split("#[cfg(test)]")
+ .next()
+ .expect("production source");
+ for forbidden in [
+ "std::env::",
+ "process::Command",
+ "clap::",
+ "create_dir",
+ "create_new",
+ "OpenOptions",
+ "keyring::",
+ "with_extension(\"key\")",
+ "set_var(",
+ "var_os(",
+ ] {
+ assert!(
+ !production.contains(forbidden),
+ "forbidden credential authority {forbidden}"
+ );
+ }
+ for source in [CONFIG_SCHEMA, CONFIG_EXAMPLE] {
+ for forbidden in [
+ "wrapping_credential =",
+ "credential_bytes",
+ "credential_hex",
+ ] {
+ assert!(!source.contains(forbidden));
+ }
+ }
+}
+
+#[test]
+fn state_backup_and_runtime_sources_remain_credential_free() {
+ for source in [HOST_SOURCE, MAINTENANCE_SOURCE] {
+ for forbidden in [
+ "resolve_myc_wrapping_credential",
+ "MycWrappingCredential",
+ "provider_credential",
+ "transport_wrapping_key",
+ ] {
+ assert!(!source.contains(forbidden));
+ }
+ }
+}
+
+#[test]
+fn public_errors_are_source_path_and_dependency_free() {
+ for required in [
+ "pub enum MycCredentialResolutionErrorKind",
+ "pub struct MycCredentialResolutionError",
+ "impl Error for MycCredentialResolutionError {}",
+ ] {
+ assert!(CREDENTIAL_SOURCE.contains(required));
+ }
+ for forbidden in [
+ "pub path:",
+ "pub source:",
+ "pub credential:",
+ "pub fn credential_path",
+ "pub fn from_resolved_bytes",
+ "pub fn from_resolution",
+ "radroots_runtime_paths::ServiceCredentialArtifactNameError",
+ "rustix::",
+ ] {
+ assert!(!LIB_SOURCE.contains(forbidden));
+ }
+}
diff --git a/tests/services_hardening_encrypted_envelope.rs b/tests/services_hardening_encrypted_envelope.rs
@@ -122,12 +122,10 @@ fn public_error_and_protected_types_are_dependency_and_path_free() {
"pub enum MycEncryptedIdentityEnvelopeErrorKind",
"pub struct MycEncryptedIdentityEnvelopeError",
"pub struct MycWrappingCredential(",
- "pub struct MycCredentialResolutionProof",
"pub struct MycEncryptedIdentityProvisioningMaterial",
"pub struct MycDecryptedIdentity",
"formatter.write_str(\"MycWrappingCredential([redacted])\")",
"impl Error for MycEncryptedIdentityEnvelopeError {}",
- "pub fn from_resolution(\n proof: MycCredentialResolutionProof,",
] {
assert!(
ENVELOPE_SOURCE.contains(required),
@@ -144,6 +142,8 @@ fn public_error_and_protected_types_are_dependency_and_path_free() {
"pub wrapping_nonce:",
"pub fn expose_secret",
"pub fn encrypted_envelope_path",
+ "pub struct MycCredentialResolutionProof",
+ "pub fn from_resolution",
] {
assert!(!ENVELOPE_SOURCE.contains(forbidden));
}