diff --git a/README.md b/README.md index 7f23c47..697a502 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,30 @@ credential-like environment value, grants no authority or qualification, activat no profile, and performs no candidate, adapter, network, publish, deploy, or external-write action. +## Inactive evidence-integrity evaluator + +`control/v1/evaluate-evidence-integrity.sh` compares caller-supplied evidence +references with one exact public-core stage tuple. It binds the policy set, +policy, decision, evaluator, and selected core package before returning a +canonical `satisfied` or `violated` identity observation. Evidence and prior +references must keep their canonical order and unique logical identities; one +prior result digest cannot describe multiple result documents. + +The launcher is an explicit trusted shell boundary; it does not claim to +self-attest bytes that Bash already loaded. The decision separately binds the +exact marked evaluation payload. The launcher extracts those fragments from a +private no-follow snapshot, verifies their content identity, and gives only the +verified bytes to the worker. Scratch producers return full final descriptor +identities over an inherited, unlinked channel; consumers bind every read to +those identities. Public-core validation uses its accounted mode +with a fixed budget, an anchored receipt descriptor, and a worker-owned root. + +The evaluator never reads proof bytes. Matching references do not prove a claim +or qualify a workflow, and equal proof digests may belong to different logical +references. The package stays inactive, stores nothing, grants no authority, and +performs no candidate, credential, network, adapter, publish, deploy, or external +write. + ## The current default team You talk **only** to yshifu, in a Claude Code session. yshifu orchestrates the other roles diff --git a/RESTORE.md b/RESTORE.md index cfb1a91..df3dd4b 100644 --- a/RESTORE.md +++ b/RESTORE.md @@ -394,6 +394,22 @@ does not read credential material or credential-like environment values, qualify claim, grant authority, activate a profile, run a candidate or adapter, or perform a network or external-write action. +Restore the five paths in the manifest's inactive evidence-integrity block, then +run: + +```sh +bash scripts/test/control-evidence-integrity.test.sh +``` + +This checks exact policy-set and public-core closure, stage and qualification +identity binding, canonical evidence/prior sets, stale or aliased references, and +deterministic observation output. It also checks the explicit trusted-launcher +boundary, exact marked-payload identity, producer-final scratch identities, +accounted core receipt and cleanup, and nested Bash environment isolation. The +launcher is not self-attested. It does not read proof bytes, establish proof +truth, qualify a workflow, store evidence, grant authority, activate a profile, +run a candidate or adapter, or perform a network or external-write action. + --- ## 5. Smoke test — prove the rebuilt team is alive diff --git a/ci/required-files.txt b/ci/required-files.txt index 24cc45c..c7df484 100644 --- a/ci/required-files.txt +++ b/ci/required-files.txt @@ -206,3 +206,10 @@ control/v1/credential-policy-decision.json control/v1/credential-policy.jq control/v1/evaluate-credential-policy.sh scripts/test/control-credential-policy.test.sh + +# Inactive evidence integrity policy and evaluator +control/v1/evidence-integrity-policy.json +control/v1/evidence-integrity-decision.json +control/v1/evidence-integrity.jq +control/v1/evaluate-evidence-integrity.sh +scripts/test/control-evidence-integrity.test.sh diff --git a/control/v1/evaluate-evidence-integrity.sh b/control/v1/evaluate-evidence-integrity.sh new file mode 100755 index 0000000..de51f71 --- /dev/null +++ b/control/v1/evaluate-evidence-integrity.sh @@ -0,0 +1,1803 @@ +#!/bin/bash +# YSTACK_EVIDENCE_PAYLOAD_SHARED_BEGIN +# shellcheck disable=SC2016,SC2329 +set -uo pipefail +export LC_ALL=C +umask 077 + +emit_error() { + local token=${1:-E_RUNTIME} + case "$token" in + E_USAGE|E_RUNTIME|E_LIMIT|E_PARSE|E_CANONICAL|E_RELATION|E_POLICY_SET|E_CORE) ;; + *) token=E_RUNTIME ;; + esac + if [ -n "${scratch:-}" ] && declare -F cleanup >/dev/null 2>&1; then + if cleanup; then + trap - EXIT HUP INT TERM + else + exec >/dev/null 2>&1 + exit 125 + fi + fi + /usr/bin/printf '%s\n' "$token" >&2 + exit 1 +} + +silent_fail() { + exec >/dev/null 2>&1 + exit 125 +} + +terminal_teardown_fail() { + exec >/dev/null 2>&1 + trap - EXIT HUP INT TERM + exit 125 +} + +physical_regular() { + local candidate=$1 parent physical + case "$candidate" in /*) ;; *) return 1 ;; esac + [ -f "$candidate" ] && [ ! -L "$candidate" ] || return 1 + parent=${candidate%/*} + [ -n "$parent" ] || parent=/ + physical=$(CDPATH='' cd -P -- "$parent" 2>/dev/null && pwd -P) || return 1 + [ "$candidate" = "$physical/${candidate##*/}" ] +} + +expected_jq_digest() { + case "$(/usr/bin/uname -s):$(/usr/bin/uname -m)" in + Darwin:*) + /usr/bin/printf '%s\n' \ + 5c0a0a3ea600f302ee458b30317425dd9632d1ad8882259fcaf4e9b868b2b1ef + ;; + Linux:x86_64) + /usr/bin/printf '%s\n' \ + af986793a515d500ab2d35f8d2aecd656e764504b789b66d7e1a0b727a124c44 + ;; + *) return 1 ;; + esac +} + +private_mode_ok() { + /usr/bin/env -i LC_ALL=C PATH=/usr/bin:/bin \ + /usr/bin/perl -MFcntl=:mode -e ' + my @st=lstat($ARGV[0]); + exit 1 unless @st && S_ISREG($st[2]) && (($st[2] & 07777) == 0500); + ' "$1" +} + +directory_identity() { + /usr/bin/env -i LC_ALL=C PATH=/usr/bin:/bin \ + /usr/bin/perl -MFcntl=:mode -MCwd=abs_path -e ' + my ($path)=@ARGV; my ($parent,$name)=$path =~ m{\A(.+)/([^/]+)\z}; + exit 1 unless defined($parent) && defined($name) && + defined(abs_path($parent)) && abs_path($parent) eq $parent; + my @parent=lstat($parent); my @dir=lstat($path); my $physical=abs_path($path); + exit 1 unless @parent && @dir && S_ISDIR($parent[2]) && S_ISDIR($dir[2]) && + (($dir[2] & 07777) == 0700) && defined($physical) && $physical eq $path; + print $parent[0],":",$parent[1],":",$dir[0],":",$dir[1],"\n"; + ' "$1" +} + +directory_matches_identity() { + local actual + actual=$(directory_identity "$1") || return 1 + [ "$actual" = "$2" ] +} + +path_identity() { + /usr/bin/env -i LC_ALL=C PATH=/usr/bin:/bin \ + /usr/bin/perl -MFcntl=:DEFAULT,:mode -MDigest::SHA -MCwd=abs_path -e ' + use strict; use warnings; + my ($path,$limit)=@ARGV; + my ($parent,$name)=$path =~ m{\A(.+)/([^/]+)\z}; + exit 2 unless defined($parent) && defined($name); + my $physical=abs_path($parent); + exit 2 unless defined($physical) && $physical eq $parent; + my @parent=lstat($parent); + exit 2 unless @parent && S_ISDIR($parent[2]); + chdir($parent) or exit 2; + my @cwd=stat("."); my @leaf=lstat($name); + exit 2 unless @cwd && @leaf && S_ISREG($leaf[2]); + sysopen(my $input,$name,O_RDONLY|O_NOFOLLOW) or exit 2; + binmode($input); my @opened=stat($input); + exit 2 unless @opened && S_ISREG($opened[2]) && + $leaf[0]==$opened[0] && $leaf[1]==$opened[1] && + $leaf[7]==$opened[7] && $leaf[9]==$opened[9] && $leaf[10]==$opened[10]; + my $sha=Digest::SHA->new(256); my $total=0; + while (1) { + my $read=sysread($input,my $buffer,65536); + exit 2 unless defined $read; last if $read==0; + $total += $read; exit 3 if $total > $limit; $sha->add($buffer); + } + my @after=stat($input); my @path_after=lstat($name); + my @parent_after=lstat($parent); my $after_physical=abs_path($parent); + exit 2 unless @after && @path_after && @parent_after && + defined($after_physical) && $after_physical eq $parent && + S_ISREG($path_after[2]) && S_ISDIR($parent_after[2]) && + $opened[0]==$after[0] && $opened[1]==$after[1] && + $opened[7]==$after[7] && $opened[9]==$after[9] && + $opened[10]==$after[10] && + $after[0]==$path_after[0] && $after[1]==$path_after[1] && + $cwd[0]==$parent_after[0] && $cwd[1]==$parent_after[1]; + print $parent[0],":",$parent[1],":",$leaf[0],":",$leaf[1],":", + $leaf[7],":",$leaf[9],":",$leaf[10],":",$sha->hexdigest,"\n"; + ' "$1" "$2" +} + +path_matches_identity() { + local actual + actual=$(path_identity "$1" "$3") || return 1 + [ "$actual" = "$2" ] +} + +snapshot_nofollow() { + local source=$1 expected=$2 target=$3 limit=$4 mode=${5:-0600} + local copy_status=0 relative + case "$target" in + "$scratch"/*) relative=${target#"$scratch/"} ;; + *) return 1 ;; + esac + /usr/bin/env -i LC_ALL=C PATH=/usr/bin:/bin \ + /usr/bin/perl -MFcntl=:DEFAULT,:mode -MDigest::SHA -MCwd=abs_path -e ' + use strict; use warnings; + my ($source,$expected,$root,$root_expected,$target,$limit,$mode)=@ARGV; + my ($p_dev,$p_ino,$f_dev,$f_ino,$size,$mtime,$ctime,$digest)= + split(/:/,$expected,8); + my ($parent,$name)=$source =~ m{\A(.+)/([^/]+)\z}; + exit 2 unless defined($parent) && defined($name) && + defined(abs_path($parent)) && abs_path($parent) eq $parent; + my @parent=lstat($parent); + exit 2 unless @parent && S_ISDIR($parent[2]) && + $parent[0]==$p_dev && $parent[1]==$p_ino; + opendir(my $source_parent_dh,$parent) or exit 2; + my @source_parent_opened=stat($source_parent_dh); + exit 2 unless @source_parent_opened && S_ISDIR($source_parent_opened[2]) && + $source_parent_opened[0]==$parent[0] && + $source_parent_opened[1]==$parent[1]; + chdir($source_parent_dh) or exit 2; + my @cwd=stat($source_parent_dh); my @leaf=lstat($name); + exit 2 unless @cwd && @leaf && S_ISREG($leaf[2]) && + $leaf[0]==$f_dev && $leaf[1]==$f_ino && $leaf[7]==$size && + $leaf[9]==$mtime && $leaf[10]==$ctime; + sysopen(my $input,$name,O_RDONLY|O_NOFOLLOW) or exit 2; + binmode($input); my @opened=stat($input); + exit 2 unless @opened && S_ISREG($opened[2]) && + $opened[0]==$f_dev && $opened[1]==$f_ino && $opened[7]==$size && + $opened[9]==$mtime && $opened[10]==$ctime; + + my ($rp_dev,$rp_ino,$rd_dev,$rd_ino)=split(/:/,$root_expected,4); + my ($root_parent,$root_name)=$root =~ m{\A(.+)/([^/]+)\z}; + exit 2 unless defined($root_parent) && defined($root_name) && + defined(abs_path($root_parent)) && abs_path($root_parent) eq $root_parent && + $target =~ m{\A[^/]+(?:/[^/]+)*\z}; + opendir(my $root_parent_dh,$root_parent) or exit 2; + my @root_parent_st=stat($root_parent_dh); + exit 2 unless @root_parent_st && S_ISDIR($root_parent_st[2]) && + $root_parent_st[0]==$rp_dev && $root_parent_st[1]==$rp_ino; + chdir($root_parent_dh) or exit 2; + my @root_named=lstat($root_name); + exit 2 unless @root_named && S_ISDIR($root_named[2]) && + $root_named[0]==$rd_dev && $root_named[1]==$rd_ino; + opendir(my $root_dh,$root_name) or exit 2; + my @root_opened=stat($root_dh); + exit 2 unless @root_opened && S_ISDIR($root_opened[2]) && + $root_opened[0]==$rd_dev && $root_opened[1]==$rd_ino; + chdir($root_dh) or exit 2; + my @parts=split(m{/},$target); my $leaf=pop @parts; + for my $component (@parts) { + exit 2 if $component eq "." or $component eq ".."; + my @named=lstat($component); + exit 2 unless @named && S_ISDIR($named[2]); + opendir(my $next,$component) or exit 2; + my @next_opened=stat($next); + exit 2 unless @next_opened && S_ISDIR($next_opened[2]) && + $next_opened[0]==$named[0] && $next_opened[1]==$named[1]; + chdir($next) or exit 2; + } + exit 2 if $leaf eq "." or $leaf eq ".."; + sysopen(my $output,$leaf,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW,0600) or exit 2; + binmode($output); my $sha=Digest::SHA->new(256); my $total=0; + while (1) { + my $read=sysread($input,my $buffer,65536); + exit 2 unless defined $read; last if $read==0; + $total += $read; exit 3 if $total > $limit; $sha->add($buffer); + my $offset=0; + while ($offset < $read) { + my $written=syswrite($output,$buffer,$read-$offset,$offset); + exit 2 unless defined($written) && $written>0; $offset += $written; + } + } + chmod(oct($mode),$output) or exit 2; + close($output) or exit 2; + chdir($source_parent_dh) or exit 2; + my @after=stat($input); my @path_after=lstat($name); + my @parent_after=lstat($parent); + exit 2 unless @after && @path_after && @parent_after && + S_ISREG($path_after[2]) && S_ISDIR($parent_after[2]) && + $opened[0]==$after[0] && $opened[1]==$after[1] && + $opened[7]==$after[7] && $opened[9]==$after[9] && + $opened[10]==$after[10] && $after[0]==$path_after[0] && + $after[1]==$path_after[1] && $cwd[0]==$parent_after[0] && + $cwd[1]==$parent_after[1] && $sha->hexdigest eq $digest; + ' "$source" "$expected" "$scratch" "$SCRATCH_ID" "$relative" "$limit" \ + "$mode" || copy_status=$? + case "$copy_status" in 0) ;; 3) emit_error E_LIMIT ;; *) return 1 ;; esac +} + +scratch_mkdirs() { + [ "$#" -gt 0 ] || return 1 + /usr/bin/env -i LC_ALL=C PATH=/usr/bin:/bin \ + /usr/bin/perl -MFcntl=:mode -MCwd=abs_path -e ' + use strict; use warnings; + my ($root,$expected,@paths)=@ARGV; + my ($p_dev,$p_ino,$d_dev,$d_ino)=split(/:/,$expected,4); + my ($parent,$name)=$root =~ m{\A(.+)/([^/]+)\z}; + exit 1 unless defined($parent) && defined($name) && + defined(abs_path($parent)) && abs_path($parent) eq $parent; + opendir(my $parent_dh,$parent) or exit 1; + my @parent_st=stat($parent_dh); + exit 1 unless @parent_st && S_ISDIR($parent_st[2]) && + $parent_st[0]==$p_dev && $parent_st[1]==$p_ino; + chdir($parent_dh) or exit 1; + my @root_named=lstat($name); + exit 1 unless @root_named && S_ISDIR($root_named[2]) && + $root_named[0]==$d_dev && $root_named[1]==$d_ino; + opendir(my $root_dh,$name) or exit 1; + my @root_opened=stat($root_dh); + exit 1 unless @root_opened && S_ISDIR($root_opened[2]) && + $root_opened[0]==$d_dev && $root_opened[1]==$d_ino; + for my $path (@paths) { + exit 1 unless $path =~ m{\A[^/]+(?:/[^/]+)*\z}; + chdir($root_dh) or exit 1; + for my $component (split(m{/},$path)) { + exit 1 if $component eq "." or $component eq ".."; + my @named=lstat($component); + if (!@named) { + mkdir($component,0700) or exit 1; + @named=lstat($component); + } + exit 1 unless @named && S_ISDIR($named[2]); + opendir(my $next,$component) or exit 1; + my @opened=stat($next); + exit 1 unless @opened && S_ISDIR($opened[2]) && + $opened[0]==$named[0] && $opened[1]==$named[1]; + chdir($next) or exit 1; + } + } + ' "$scratch" "$SCRATCH_ID" "$@" +} + +scratch_relative() { + case "$1" in + "$scratch"/*) /usr/bin/printf '%s\n' "${1#"$scratch/"}" ;; + *) return 1 ;; + esac +} + +scratch_prepare_capture() { + local output=$1 error=$2 receipt=$3 record=$4 + if { [ "$output" != - ] && + { [ "$output" = "$error" ] || [ "$output" = "$receipt" ]; }; } || + { [ "$error" != - ] && [ "$error" = "$receipt" ]; }; then + return 125 + fi + [ "$record" != - ] && [ "$record" != "$output" ] && + [ "$record" != "$error" ] && [ "$record" != "$receipt" ] || return 125 + /usr/bin/env -i LC_ALL=C PATH=/usr/bin:/bin \ + /usr/bin/perl -MFcntl=:DEFAULT,:mode -MCwd=abs_path -MPOSIX=mkfifo -e ' + use strict; use warnings; + my ($root,$expected,@names)=@ARGV; + my ($p_dev,$p_ino,$d_dev,$d_ino)=split(/:/,$expected,4); + my ($parent,$name)=$root =~ m{\A(.+)/([^/]+)\z}; + exit 125 unless defined($parent) && defined($name) && + defined(abs_path($parent)) && abs_path($parent) eq $parent; + opendir(my $parent_dh,$parent) or exit 125; + my @parent_st=stat($parent_dh); + exit 125 unless @parent_st && S_ISDIR($parent_st[2]) && + $parent_st[0]==$p_dev && $parent_st[1]==$p_ino; + chdir($parent_dh) or exit 125; + my @root_named=lstat($name); + exit 125 unless @root_named && S_ISDIR($root_named[2]) && + $root_named[0]==$d_dev && $root_named[1]==$d_ino; + opendir(my $root_dh,$name) or exit 125; + my @root_opened=stat($root_dh); + exit 125 unless @root_opened && S_ISDIR($root_opened[2]) && + $root_opened[0]==$d_dev && $root_opened[1]==$d_ino; + sub create_node { + my ($root_handle,$relative,$fifo)=@_; + return "-" if $relative eq "-"; + exit 125 unless $relative =~ m{\A[^/]+(?:/[^/]+)*\z}; + chdir($root_handle) or exit 125; + my @parts=split(m{/},$relative); my $leaf=pop @parts; + for my $component (@parts) { + exit 125 if $component eq "." or $component eq ".."; + my @named=lstat($component); + exit 125 unless @named && S_ISDIR($named[2]); + opendir(my $next,$component) or exit 125; + my @opened=stat($next); + exit 125 unless @opened && S_ISDIR($opened[2]) && + $opened[0]==$named[0] && $opened[1]==$named[1]; + chdir($next) or exit 125; + } + exit 125 if $leaf eq "." or $leaf eq ".."; + if ($fifo) { + mkfifo($leaf,0600) or exit 125; + my @created=lstat($leaf); + exit 125 unless @created && S_ISFIFO($created[2]); + return $created[0].":".$created[1]; + } + sysopen(my $file,$leaf,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW,0600) or + exit 125; + my @opened=stat($file); + exit 125 unless @opened && S_ISREG($opened[2]) && $opened[7]==0; + close($file) or exit 125; + return $opened[0].":".$opened[1]; + } + my $record=pop @names; + print join(" ",(map {create_node($root_dh,$_,0)} @names), + create_node($root_dh,$record,1)),"\n"; + ' "$scratch" "$SCRATCH_ID" "$output" "$error" "$receipt" "$record" +} + +capture_channel_seal() { + local relative=$1 key=$2 + /usr/bin/env -i LC_ALL=C PATH=/usr/bin:/bin \ + /usr/bin/perl -MFcntl=:mode -MCwd=abs_path -e ' + use strict; use warnings; + my ($root,$expected,$relative,$key)=@ARGV; + my ($p_dev,$p_ino,$d_dev,$d_ino)=split(/:/,$expected,4); + my ($f_dev,$f_ino)=split(/:/,$key,2); + exit 1 unless $relative =~ m{\A[^/]+(?:/[^/]+)*\z}; + open(my $channel,"<&=6") or exit 1; + my @channel_st=stat($channel); + exit 1 unless @channel_st && S_ISFIFO($channel_st[2]) && + $channel_st[0]==$f_dev && $channel_st[1]==$f_ino; + my ($parent,$name)=$root =~ m{\A(.+)/([^/]+)\z}; + exit 1 unless defined($parent) && defined($name) && + defined(abs_path($parent)) && abs_path($parent) eq $parent; + opendir(my $parent_dh,$parent) or exit 1; + my @parent_st=stat($parent_dh); + exit 1 unless @parent_st && $parent_st[0]==$p_dev && $parent_st[1]==$p_ino; + chdir($parent_dh) or exit 1; + my @root_named=lstat($name); + exit 1 unless @root_named && S_ISDIR($root_named[2]) && + $root_named[0]==$d_dev && $root_named[1]==$d_ino; + opendir(my $root_dh,$name) or exit 1; + my @parts=split(m{/},$relative); my $leaf=pop @parts; + chdir($root_dh) or exit 1; + for my $component (@parts) { + my @named=lstat($component); + exit 1 unless @named && S_ISDIR($named[2]); + opendir(my $next,$component) or exit 1; + my @opened=stat($next); + exit 1 unless @opened && $opened[0]==$named[0] && $opened[1]==$named[1]; + chdir($next) or exit 1; + } + my @named=lstat($leaf); + exit 1 unless @named && S_ISFIFO($named[2]) && + $named[0]==$f_dev && $named[1]==$f_ino && unlink($leaf); + exit 1 if lstat($leaf); + ' "$scratch" "$SCRATCH_ID" "$relative" "$key" || return 1 + [ ! -e "$scratch/$relative" ] && [ ! -L "$scratch/$relative" ] +} + +scratch_prepare_capture_keys() { + local prepared remainder + prepared=$(scratch_prepare_capture "$1" "$2" "$3" "$4") || return 125 + case "$prepared" in *' '*' '*' '*) ;; *) return 125 ;; esac + CAPTURE_OUTPUT_KEY=${prepared%% *} + remainder=${prepared#* } + CAPTURE_ERROR_KEY=${remainder%% *} + remainder=${remainder#* } + CAPTURE_RECEIPT_KEY=${remainder%% *} + CAPTURE_RECORD_KEY=${remainder#* } + case "$CAPTURE_RECORD_KEY" in *' '*) return 125 ;; esac + [ -n "$CAPTURE_OUTPUT_KEY" ] && [ -n "$CAPTURE_ERROR_KEY" ] && + [ -n "$CAPTURE_RECEIPT_KEY" ] && [ -n "$CAPTURE_RECORD_KEY" ] || return 125 + exec 6>&- + exec 6<>"$scratch/$4" || return 125 + if ! capture_channel_seal "$4" "$CAPTURE_RECORD_KEY"; then + exec 6>&- + return 125 + fi +} + +scratch_capture_prepared() { + local output=$1 output_key=$2 error=$3 error_key=$4 + local receipt=$5 receipt_key=$6 record=$7 record_key=$8 + local input=$9 input_identity=${10} payload_sha=${11} + shift 11 + [ "$#" -gt 0 ] || return 125 + /usr/bin/env -i LC_ALL=C PATH=/usr/bin:/bin \ + /usr/bin/perl -MFcntl=:DEFAULT,:mode -MDigest::SHA -MCwd=abs_path \ + -MPOSIX=_exit,dup2 -e ' + use strict; use warnings; + my ($root,$expected,$stdout_name,$stdout_key,$stderr_name,$stderr_key, + $receipt_name,$receipt_key,$record_name,$record_key,$stdin_name, + $stdin_identity,$payload_sha, + @command)=@ARGV; + exit 125 unless @command; + my ($p_dev,$p_ino,$d_dev,$d_ino)=split(/:/,$expected,4); + my ($parent,$name)=$root =~ m{\A(.+)/([^/]+)\z}; + exit 125 unless defined($parent) && defined($name) && + defined(abs_path($parent)) && abs_path($parent) eq $parent; + opendir(my $parent_dh,$parent) or exit 125; + my @parent_st=stat($parent_dh); + exit 125 unless @parent_st && S_ISDIR($parent_st[2]) && + $parent_st[0]==$p_dev && $parent_st[1]==$p_ino; + chdir($parent_dh) or exit 125; + my @root_named=lstat($name); + exit 125 unless @root_named && S_ISDIR($root_named[2]) && + $root_named[0]==$d_dev && $root_named[1]==$d_ino; + opendir(my $root_dh,$name) or exit 125; + my @root_opened=stat($root_dh); + exit 125 unless @root_opened && S_ISDIR($root_opened[2]) && + $root_opened[0]==$d_dev && $root_opened[1]==$d_ino; + sub open_output { + my ($root_handle,$relative,$key)=@_; + if ($relative eq "-") { + exit 125 unless $key eq "-"; + sysopen(my $null,"/dev/null",O_RDWR|O_NOFOLLOW) or exit 125; + return $null; + } + exit 125 unless $relative =~ m{\A[^/]+(?:/[^/]+)*\z} && + $key =~ m{\A[0-9]+:[0-9]+\z}; + my ($dev,$ino)=split(/:/,$key,2); + chdir($root_handle) or exit 125; + my @parts=split(m{/},$relative); my $leaf=pop @parts; + for my $component (@parts) { + exit 125 if $component eq "." or $component eq ".."; + my @named=lstat($component); + exit 125 unless @named && S_ISDIR($named[2]); + opendir(my $next,$component) or exit 125; + my @opened=stat($next); + exit 125 unless @opened && S_ISDIR($opened[2]) && + $opened[0]==$named[0] && $opened[1]==$named[1]; + chdir($next) or exit 125; + } + my @named=lstat($leaf); + exit 125 unless @named && S_ISREG($named[2]) && + $named[0]==$dev && $named[1]==$ino && $named[7]==0; + sysopen(my $file,$leaf,O_RDWR|O_NOFOLLOW) or exit 125; + my @opened=stat($file); + exit 125 unless @opened && S_ISREG($opened[2]) && + $opened[0]==$dev && $opened[1]==$ino && $opened[7]==0; + return $file; + } + sub extract_payload { + my ($text)=@_; my @spec=( + ["# YSTACK_EVIDENCE_PAYLOAD_SHARED_BEGIN\n", + "# YSTACK_EVIDENCE_PAYLOAD_SHARED_END\n"], + ["# YSTACK_EVIDENCE_PAYLOAD_CLEANUP_BEGIN\n", + "# YSTACK_EVIDENCE_PAYLOAD_CLEANUP_END\n"], + ["# YSTACK_EVIDENCE_PAYLOAD_WORKER_BEGIN\n", + "# YSTACK_EVIDENCE_PAYLOAD_WORKER_END\n"]); + my @lines=split(/(?<=\n)/,$text); my $payload=""; my $cursor=0; + for my $pair (@spec) { + my ($begin,$end)=@$pair; + exit 125 unless grep({$_ eq $begin} @lines)==1 && + grep({$_ eq $end} @lines)==1; + $cursor++ while $cursor<@lines && $lines[$cursor] ne $begin; + exit 125 if $cursor>=@lines; $cursor++; + while ($cursor<@lines && $lines[$cursor] ne $end) { + $payload .= $lines[$cursor]; $cursor++; + } + exit 125 if $cursor>=@lines; $cursor++; + } + return $payload; + } + sub input_bytes { + my ($root_handle,$relative,$identity,$expected_payload_sha)=@_; + return undef if $relative eq "-" && $expected_payload_sha eq "-"; + exit 125 unless $relative =~ m{\A[^/]+(?:/[^/]+)*\z} && + $expected_payload_sha =~ m{\A[0-9a-f]{64}\z}; + my (undef,undef,$f_dev,$f_ino,$size,$mtime,$ctime,$digest)= + split(/:/,$identity,8); + chdir($root_handle) or exit 125; + my @parts=split(m{/},$relative); my $leaf=pop @parts; + for my $component (@parts) { + my @named=lstat($component); + exit 125 unless @named && S_ISDIR($named[2]); + opendir(my $next,$component) or exit 125; + my @opened=stat($next); + exit 125 unless @opened && S_ISDIR($opened[2]) && + $opened[0]==$named[0] && $opened[1]==$named[1]; + chdir($next) or exit 125; + } + my @named=lstat($leaf); + exit 125 unless @named && S_ISREG($named[2]) && + $named[0]==$f_dev && $named[1]==$f_ino && $named[7]==$size && + $named[9]==$mtime && $named[10]==$ctime; + sysopen(my $file,$leaf,O_RDONLY|O_NOFOLLOW) or exit 125; + my @opened=stat($file); + exit 125 unless @opened && S_ISREG($opened[2]) && + $opened[0]==$f_dev && $opened[1]==$f_ino && $opened[7]==$size && + $opened[9]==$mtime && $opened[10]==$ctime; + my $sha=Digest::SHA->new(256); my $total=0; my $text=""; + while (1) { + my $read=sysread($file,my $buffer,65536); + exit 125 unless defined $read; last if $read==0; + $total += $read; exit 125 if $total > 1048576; + $sha->add($buffer); $text .= $buffer; + } + my @after=stat($file); my @path_after=lstat($leaf); + exit 125 unless $total==$size && $sha->hexdigest eq $digest && + @after && @path_after && $after[0]==$f_dev && $after[1]==$f_ino && + $path_after[0]==$f_dev && $path_after[1]==$f_ino; + my $payload=extract_payload($text); + exit 125 unless Digest::SHA::sha256_hex($payload) eq $expected_payload_sha; + return $payload; + } + my $stdout=open_output($root_dh,$stdout_name,$stdout_key); + my $stderr=open_output($root_dh,$stderr_name,$stderr_key); + my $receipt=open_output($root_dh,$receipt_name,$receipt_key); + my ($record_dev,$record_ino)=split(/:/,$record_key,2); + open(my $channel,">&=6") or exit 125; + my @channel_st=stat($channel); + exit 125 unless @channel_st && S_ISFIFO($channel_st[2]) && + $channel_st[0]==$record_dev && $channel_st[1]==$record_ino; + fcntl($channel,F_SETFD,FD_CLOEXEC) or exit 125; + my $stdin_text=input_bytes($root_dh,$stdin_name,$stdin_identity,$payload_sha); + my $writer_pid=-1; + if (defined($stdin_text)) { + pipe(my $reader,my $writer) or exit 125; + $writer_pid=fork(); exit 125 unless defined($writer_pid); + if ($writer_pid==0) { + close($reader); my $offset=0; my $length=length($stdin_text); + while ($offset < $length) { + my $written=syswrite($writer,$stdin_text,$length-$offset,$offset); + _exit(125) unless defined($written) && $written>0; + $offset += $written; + } + close($writer); _exit(0); + } + close($writer); open(STDIN,"<&",$reader) or exit 125; close($reader); + } else { + sysopen(my $null,"/dev/null",O_RDONLY|O_NOFOLLOW) or exit 125; + open(STDIN,"<&",$null) or exit 125; close($null); + } + my $command_pid=fork(); exit 125 unless defined($command_pid); + if ($command_pid==0) { + open(STDOUT,">&",$stdout) or _exit(125); + open(STDERR,">&",$stderr) or _exit(125); + my $receipt_fd=fileno($receipt); + close($channel); close($root_dh); close($parent_dh); + dup2($receipt_fd,3)>=0 or _exit(125); + close($receipt) if $receipt_fd!=3; + close($stdout); close($stderr); + exec {$command[0]} @command or _exit(126); + } + close(STDIN); close($root_dh); close($parent_dh); + waitpid($command_pid,0)==$command_pid or exit 125; + my $raw_status=$?; my $status; + if (($raw_status & 127)==0) { $status=($raw_status >> 8) & 255; } + else { $status=128+($raw_status & 127); } + if ($writer_pid>0) { + waitpid($writer_pid,0)==$writer_pid or exit 125; + $status=125 unless (($? & 127)==0 && (($? >> 8) & 255)==0); + } + sub final_identity { + my ($handle,$name)=@_; return "-" if $name eq "-"; + seek($handle,0,0) or exit 125; + my @before=stat($handle); my $sha=Digest::SHA->new(256); my $total=0; + exit 125 unless @before && S_ISREG($before[2]); + while (1) { + my $read=sysread($handle,my $buffer,65536); + exit 125 unless defined $read; last if $read==0; + $total += $read; exit 125 if $total > 2097152; $sha->add($buffer); + } + my @after=stat($handle); + exit 125 unless @after && $total==$after[7] && + $before[0]==$after[0] && $before[1]==$after[1] && + $before[7]==$after[7] && $before[9]==$after[9] && + $before[10]==$after[10]; + return join(":",$after[0],$after[1],$after[7],$after[9],$after[10], + $sha->hexdigest); + } + my $stdout_final=final_identity($stdout,$stdout_name); + my $stderr_final=final_identity($stderr,$stderr_name); + my $receipt_final=final_identity($receipt,$receipt_name); + my $record_text=join(" ","status:".$status,"output:".$stdout_final, + "error:".$stderr_final,"receipt:".$receipt_final)."\n"; + my $offset=0; + while ($offset0; $offset += $written; + } + close($stdout); close($stderr); close($receipt); close($channel) or exit 125; + exit $status; + ' "$scratch" "$SCRATCH_ID" "$output" "$output_key" "$error" \ + "$error_key" "$receipt" "$receipt_key" "$record" "$record_key" \ + "$input" "$input_identity" "$payload_sha" "$@" +} + +scratch_capture() { + local output=$1 error=$2 receipt=$3 record=$4 input=$5 input_identity=$6 + local payload_sha=$7 + shift 7 + local status=0 record_text + scratch_prepare_capture_keys "$output" "$error" "$receipt" "$record" || + return 125 + scratch_capture_prepared "$output" "$CAPTURE_OUTPUT_KEY" \ + "$error" "$CAPTURE_ERROR_KEY" "$receipt" "$CAPTURE_RECEIPT_KEY" \ + "$record" "$CAPTURE_RECORD_KEY" "$input" "$input_identity" \ + "$payload_sha" "$@" || status=$? + IFS= read -r -t 5 record_text <&6 || { + exec 6>&- + return 125 + } + exec 6>&- + capture_record_parse "$record_text" || return 125 + [ "$CAPTURE_FINAL_STATUS" -eq "$status" ] || return 125 + return "$status" +} + +capture_identity_for_key() { + local path=$1 key=$2 limit=${3:-1048576} identity file_key + [ "$key" != - ] || return 1 + identity=$(path_identity "$path" "$limit") || return 1 + file_key=$(/usr/bin/printf '%s\n' "$identity" | /usr/bin/awk -F: \ + 'NF==8 {print $3":"$4}') || return 1 + [ "$file_key" = "$key" ] || return 1 + /usr/bin/printf '%s\n' "$identity" +} + +capture_path_matches() { + capture_identity_for_key "$@" >/dev/null +} + +capture_record_parse() { + local text=$1 atom + atom='(-|[0-9]+:[0-9]+:[0-9]+:[0-9]+:[0-9]+:[0-9a-f]{64})' + if [[ "$text" =~ ^status:([0-9]+)[[:space:]]output:${atom}[[:space:]]error:${atom}[[:space:]]receipt:${atom}$ ]]; then + CAPTURE_FINAL_STATUS=${BASH_REMATCH[1]} + CAPTURE_OUTPUT_FINAL=${BASH_REMATCH[2]} + CAPTURE_ERROR_FINAL=${BASH_REMATCH[3]} + CAPTURE_RECEIPT_FINAL=${BASH_REMATCH[4]} + else + return 1 + fi + [ "$CAPTURE_FINAL_STATUS" -le 255 ] +} + +capture_identity_for_final() { + local path=$1 final=$2 limit=${3:-2097152} identity observed + [ "$final" != - ] || return 1 + identity=$(path_identity "$path" "$limit") || return 1 + observed=$(/usr/bin/printf '%s\n' "$identity" | /usr/bin/awk -F: \ + 'NF==8 {print $3":"$4":"$5":"$6":"$7":"$8}') || return 1 + [ "$observed" = "$final" ] || return 1 + /usr/bin/printf '%s\n' "$identity" +} + +capture_full_identity_matches() { + capture_identity_for_final "$@" >/dev/null +} + +scratch_write_lines() { + local target=$1 + shift + /usr/bin/env -i LC_ALL=C PATH=/usr/bin:/bin \ + /usr/bin/perl -MFcntl=:DEFAULT,:mode -MCwd=abs_path -e ' + use strict; use warnings; + my ($root,$expected,$target,@lines)=@ARGV; + my ($p_dev,$p_ino,$d_dev,$d_ino)=split(/:/,$expected,4); + my ($parent,$name)=$root =~ m{\A(.+)/([^/]+)\z}; + exit 1 unless defined($parent) && defined($name) && + defined(abs_path($parent)) && abs_path($parent) eq $parent && + $target =~ m{\A[^/]+(?:/[^/]+)*\z}; + opendir(my $parent_dh,$parent) or exit 1; + my @parent_st=stat($parent_dh); + exit 1 unless @parent_st && S_ISDIR($parent_st[2]) && + $parent_st[0]==$p_dev && $parent_st[1]==$p_ino; + chdir($parent_dh) or exit 1; + my @root_named=lstat($name); + exit 1 unless @root_named && S_ISDIR($root_named[2]) && + $root_named[0]==$d_dev && $root_named[1]==$d_ino; + opendir(my $root_dh,$name) or exit 1; + my @root_opened=stat($root_dh); + exit 1 unless @root_opened && S_ISDIR($root_opened[2]) && + $root_opened[0]==$d_dev && $root_opened[1]==$d_ino; + chdir($root_dh) or exit 1; + my @parts=split(m{/},$target); my $leaf=pop @parts; + for my $component (@parts) { + exit 1 if $component eq "." or $component eq ".."; + my @named=lstat($component); + exit 1 unless @named && S_ISDIR($named[2]); + opendir(my $next,$component) or exit 1; + my @opened=stat($next); + exit 1 unless @opened && S_ISDIR($opened[2]) && + $opened[0]==$named[0] && $opened[1]==$named[1]; + chdir($next) or exit 1; + } + exit 1 if $leaf eq "." or $leaf eq ".."; + sysopen(my $file,$leaf,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW,0600) or exit 1; + for my $line (@lines) { print {$file} $line,"\n" or exit 1; } + close($file) or exit 1; + ' "$scratch" "$SCRATCH_ID" "$target" "$@" +} + +capture_identity_text() { + /usr/bin/env -i LC_ALL=C PATH=/usr/bin:/bin \ + /usr/bin/perl -MFcntl=:DEFAULT,:mode -MDigest::SHA -MCwd=abs_path -e ' + use strict; use warnings; + my ($path,$expected)=@ARGV; + my ($p_dev,$p_ino,$f_dev,$f_ino,$size,$mtime,$ctime,$digest)= + split(/:/,$expected,8); + exit 2 if $size > 1048576; + my ($parent,$name)=$path =~ m{\A(.+)/([^/]+)\z}; + exit 2 unless defined($parent) && defined($name) && + defined(abs_path($parent)) && abs_path($parent) eq $parent; + my @parent=lstat($parent); + exit 2 unless @parent && S_ISDIR($parent[2]) && + $parent[0]==$p_dev && $parent[1]==$p_ino; + chdir($parent) or exit 2; + my @cwd=stat("."); my @leaf=lstat($name); + exit 2 unless @cwd && @leaf && S_ISREG($leaf[2]) && + $leaf[0]==$f_dev && $leaf[1]==$f_ino && $leaf[7]==$size && + $leaf[9]==$mtime && $leaf[10]==$ctime; + sysopen(my $input,$name,O_RDONLY|O_NOFOLLOW) or exit 2; + binmode($input); my @opened=stat($input); + exit 2 unless @opened && S_ISREG($opened[2]) && + $opened[0]==$f_dev && $opened[1]==$f_ino && $opened[7]==$size && + $opened[9]==$mtime && $opened[10]==$ctime; + my $sha=Digest::SHA->new(256); my $text=""; my $total=0; + while (1) { + my $read=sysread($input,my $buffer,65536); + exit 2 unless defined $read; last if $read==0; + $total += $read; exit 2 if $total > 1048576; + $sha->add($buffer); $text .= $buffer; + } + my @after=stat($input); my @path_after=lstat($name); + my @parent_after=lstat($parent); + exit 2 unless $total==$size && $sha->hexdigest eq $digest && + @after && @path_after && @parent_after && S_ISREG($path_after[2]) && + S_ISDIR($parent_after[2]) && $opened[0]==$after[0] && + $opened[1]==$after[1] && $opened[7]==$after[7] && + $opened[9]==$after[9] && $opened[10]==$after[10] && + $after[0]==$path_after[0] && $after[1]==$path_after[1] && + $cwd[0]==$parent_after[0] && $cwd[1]==$parent_after[1]; + print $text; + ' "$1" "$2" +} + +PINNED_PATHS=() +PINNED_IDENTITIES=() +pin_path() { + local path=$1 limit=${2:-1048576} identity index=0 identity_status=0 + identity=$(path_identity "$path" "$limit") || identity_status=$? + case "$identity_status" in + 0) ;; + 3) emit_error E_LIMIT ;; + *) return 1 ;; + esac + while [ "$index" -lt "${#PINNED_PATHS[@]}" ]; do + if [ "${PINNED_PATHS[$index]}" = "$path" ]; then + [ "${PINNED_IDENTITIES[$index]}" = "$identity" ] + return + fi + index=$((index + 1)) + done + PINNED_PATHS[${#PINNED_PATHS[@]}]=$path + PINNED_IDENTITIES[${#PINNED_IDENTITIES[@]}]=$identity +} +pinned_identity() { + local path=$1 index=0 + while [ "$index" -lt "${#PINNED_PATHS[@]}" ]; do + if [ "${PINNED_PATHS[$index]}" = "$path" ]; then + /usr/bin/printf '%s\n' "${PINNED_IDENTITIES[$index]}"; return 0 + fi + index=$((index + 1)) + done + return 1 +} +verify_all_pins() { + local index=0 limit + while [ "$index" -lt "${#PINNED_PATHS[@]}" ]; do + limit=1048576 + [ "${PINNED_PATHS[$index]}" != "${live_jq_path:-}" ] || limit=16777216 + [ "${PINNED_PATHS[$index]}" != "${jq_bin:-}" ] || limit=16777216 + path_matches_identity "${PINNED_PATHS[$index]}" \ + "${PINNED_IDENTITIES[$index]}" "$limit" || return 1 + index=$((index + 1)) + done +} +sha256_path() { + local identity + pin_path "$1" || return 1 + identity=$(pinned_identity "$1") || return 1 + /usr/bin/printf '%s\n' "${identity##*:}" +} + +payload_sha_from_identity() { + local wrapped text sentinel=YSTACK_EVIDENCE_PAYLOAD_SENTINEL_9f41c2 + wrapped=$({ + capture_identity_text "$1" "$2" || exit 1 + /usr/bin/printf '%s' "$sentinel" + }) || return 1 + case "$wrapped" in *"$sentinel") ;; *) return 1 ;; esac + text=${wrapped%"$sentinel"} + /usr/bin/printf '%s' "$text" | /usr/bin/env -i LC_ALL=C PATH=/usr/bin:/bin \ + /usr/bin/perl -MDigest::SHA -e ' + use strict; use warnings; local $/; my $text=; + my @spec=( + ["# YSTACK_EVIDENCE_PAYLOAD_SHARED_BEGIN\n", + "# YSTACK_EVIDENCE_PAYLOAD_SHARED_END\n"], + ["# YSTACK_EVIDENCE_PAYLOAD_CLEANUP_BEGIN\n", + "# YSTACK_EVIDENCE_PAYLOAD_CLEANUP_END\n"], + ["# YSTACK_EVIDENCE_PAYLOAD_WORKER_BEGIN\n", + "# YSTACK_EVIDENCE_PAYLOAD_WORKER_END\n"]); + my @lines=split(/(?<=\n)/,$text); my $payload=""; my $cursor=0; + for my $pair (@spec) { + my ($begin,$end)=@$pair; + exit 1 unless grep({$_ eq $begin} @lines)==1 && + grep({$_ eq $end} @lines)==1; + $cursor++ while $cursor<@lines && $lines[$cursor] ne $begin; + exit 1 if $cursor>=@lines; $cursor++; + while ($cursor<@lines && $lines[$cursor] ne $end) { + $payload .= $lines[$cursor]; $cursor++; + } + exit 1 if $cursor>=@lines; $cursor++; + } + print Digest::SHA::sha256_hex($payload),"\n"; + ' +} +# YSTACK_EVIDENCE_PAYLOAD_SHARED_END + +if [ -n "${YSTACK_EVIDENCE_STAGE+x}" ]; then silent_fail; fi +scratch= +SCRATCH_ID= +SCRATCH_OWNED=0 +ACTIVE_PID= +ACTIVE_PGID= +# YSTACK_EVIDENCE_PAYLOAD_CLEANUP_BEGIN +cleanup() { + [ -z "${scratch:-}" ] && return 0 + case "$scratch" in + /*/ystack-evidence.??????|/*/ystack-evidence.??????/worker) ;; + *) return 1 ;; + esac + [ "$SCRATCH_OWNED" -eq 1 ] && [ -n "$SCRATCH_ID" ] || return 1 + /usr/bin/env -i LC_ALL=C PATH=/usr/bin:/bin \ + /usr/bin/perl -MFcntl=:mode -MCwd=abs_path -e ' + use strict; use warnings; + my ($path,$expected)=@ARGV; + my ($p_dev,$p_ino,$d_dev,$d_ino)=split(/:/,$expected,4); + my ($parent,$name)=$path =~ m{\A(.+)/([^/]+)\z}; + exit 1 unless defined($parent) && defined($name) && + defined(abs_path($parent)) && abs_path($parent) eq $parent; + opendir(my $parent_dh,$parent) or exit 1; + my @parent_st=stat($parent_dh); + exit 1 unless @parent_st && S_ISDIR($parent_st[2]) && + $parent_st[0]==$p_dev && $parent_st[1]==$p_ino; + chdir($parent_dh) or exit 1; + my @named=lstat($name); + exit 1 unless @named && S_ISDIR($named[2]) && + $named[0]==$d_dev && $named[1]==$d_ino; + opendir(my $root_dh,$name) or exit 1; + my @opened=stat($root_dh); + exit 1 unless @opened && S_ISDIR($opened[2]) && + $opened[0]==$d_dev && $opened[1]==$d_ino; + + sub empty_dir { + my ($dh)=@_; chdir($dh) or return 0; rewinddir($dh); + while (defined(my $entry=readdir($dh))) { + next if $entry eq "." or $entry eq ".."; + my @before=lstat($entry); return 0 unless @before; + if (S_ISDIR($before[2])) { + opendir(my $child,$entry) or return 0; + my @child_st=stat($child); + return 0 unless @child_st && $child_st[0]==$before[0] && + $child_st[1]==$before[1] && empty_dir($child); + chdir($dh) or return 0; + my @after=lstat($entry); + return 0 unless @after && S_ISDIR($after[2]) && + $after[0]==$before[0] && $after[1]==$before[1] && rmdir($entry); + } else { + return 0 unless unlink($entry); + } + chdir($dh) or return 0; + } + return 1; + } + + exit 1 unless empty_dir($root_dh); + chdir($parent_dh) or exit 1; + my @final=lstat($name); + exit 1 unless @final && S_ISDIR($final[2]) && + $final[0]==$d_dev && $final[1]==$d_ino && rmdir($name); + exit 1 if lstat($name); + ' "$scratch" "$SCRATCH_ID" >/dev/null 2>&1 || return 1 + [ ! -e "$scratch" ] && [ ! -L "$scratch" ] +} +# YSTACK_EVIDENCE_PAYLOAD_CLEANUP_END +group_live_count() { + [[ "${1:-}" =~ ^[1-9][0-9]*$ ]] || return 1 + /bin/ps -axo pgid=,state= 2>/dev/null | /usr/bin/awk -v group="$1" ' + $1==group && $2!~/^Z/ {count+=1} END {print count+0}' +} +leader_state() { + /bin/ps -o state= -p "$1" 2>/dev/null | /usr/bin/tr -d ' ' +} +terminate_active() { + local group=${ACTIVE_PGID:-} leader=${ACTIVE_PID:-} state count attempt=0 + [[ "$group" =~ ^[1-9][0-9]*$ ]] && [[ "$leader" =~ ^[1-9][0-9]*$ ]] || + return 1 + /bin/kill -TERM -- "-$group" 2>/dev/null || : + state=$(leader_state "$leader") || state= + while [ -n "$state" ] && [[ "$state" != Z* ]] && [ "$attempt" -lt 100 ]; do + attempt=$((attempt + 1)); /bin/sleep 0.01 + state=$(leader_state "$leader") || state= + done + count=$(group_live_count "$group") || return 1 + if { [ -n "$state" ] && [[ "$state" != Z* ]]; } || [ "$count" -gt 0 ]; then + /bin/kill -KILL -- "-$group" 2>/dev/null || : + fi + wait "$leader" 2>/dev/null || : + state=$(leader_state "$leader") || state= + [ -z "$state" ] || return 1 + attempt=0; count=$(group_live_count "$group") || return 1 + while [ "$count" -gt 0 ] && [ "$attempt" -lt 100 ]; do + attempt=$((attempt + 1)); /bin/sleep 0.01 + count=$(group_live_count "$group") || return 1 + done + [ "$count" -eq 0 ] || return 1 + ACTIVE_PID=; ACTIVE_PGID= +} +signal_exit() { + local status=${1:-1} + exec >/dev/null 2>&1 + if [ -n "${ACTIVE_PGID:-}" ] && ! terminate_active; then + terminal_teardown_fail + fi + cleanup || status=125 + trap - EXIT HUP INT TERM + exit "$status" +} +PENDING_SIGNAL=0 +arm_signal_traps() { + trap cleanup EXIT + trap 'signal_exit 129' HUP + trap 'signal_exit 130' INT + trap 'signal_exit 143' TERM +} +defer_signal_traps() { + PENDING_SIGNAL=0 + trap 'PENDING_SIGNAL=129' HUP + trap 'PENDING_SIGNAL=130' INT + trap 'PENDING_SIGNAL=143' TERM +} +kill_unregistered_child() { + local child=$1 state attempt=0 + /bin/kill -TERM "$child" 2>/dev/null || : + state=$(leader_state "$child") || state= + while [ -n "$state" ] && [[ "$state" != Z* ]] && [ "$attempt" -lt 100 ]; do + attempt=$((attempt + 1)); /bin/sleep 0.01 + state=$(leader_state "$child") || state= + done + if [ -n "$state" ] && [[ "$state" != Z* ]]; then + /bin/kill -KILL "$child" 2>/dev/null || : + fi + wait "$child" 2>/dev/null || : + state=$(leader_state "$child") || state= + [ -z "$state" ] +} +arm_signal_traps + +run_child() { + local child pgid state attempt=0 child_status=0 count self_pgid + [ -z "${ACTIVE_PID:-}" ] && [ -z "${ACTIVE_PGID:-}" ] || return 125 + defer_signal_traps + self_pgid=$(/bin/ps -o pgid= -p "$$" 2>/dev/null | /usr/bin/tr -d ' ') || { + arm_signal_traps + [ "$PENDING_SIGNAL" -eq 0 ] || signal_exit "$PENDING_SIGNAL" + return 125 + } + if ! [[ "$self_pgid" =~ ^[1-9][0-9]*$ ]]; then + arm_signal_traps + [ "$PENDING_SIGNAL" -eq 0 ] || signal_exit "$PENDING_SIGNAL" + return 125 + fi + set -m + "$@" & + child=$! + set +m + while [ "$attempt" -lt 100 ]; do + pgid=$(/bin/ps -o pgid= -p "$child" 2>/dev/null | /usr/bin/tr -d ' ') || pgid= + if [[ "$pgid" =~ ^[1-9][0-9]*$ ]] && [ "$pgid" = "$child" ] && + [ "$pgid" != "$self_pgid" ]; then break; fi + state=$(leader_state "$child") || state= + [ -n "$state" ] && [[ "$state" != Z* ]] || break + attempt=$((attempt + 1)); /bin/sleep 0.01 + done + if ! [[ "$pgid" =~ ^[1-9][0-9]*$ ]] || [ "$pgid" != "$child" ] || + [ "$pgid" = "$self_pgid" ]; then + kill_unregistered_child "$child" || terminal_teardown_fail + arm_signal_traps + [ "$PENDING_SIGNAL" -eq 0 ] || signal_exit "$PENDING_SIGNAL" + return 125 + fi + ACTIVE_PID=$child; ACTIVE_PGID=$pgid + arm_signal_traps + [ "$PENDING_SIGNAL" -eq 0 ] || signal_exit "$PENDING_SIGNAL" + attempt=0 + state=$(leader_state "$child") || state= + while [ -n "$state" ] && [[ "$state" != Z* ]] && [ "$attempt" -lt 1000 ]; do + attempt=$((attempt + 1)); /bin/sleep 0.01 + state=$(leader_state "$child") || state= + done + if [ -n "$state" ] && [[ "$state" != Z* ]]; then + terminate_active || return 125 + return 124 + fi + wait "$child" || child_status=$? + count=$(group_live_count "$pgid") || { + terminate_active || return 125 + return 125 + } + if [ "$count" -ne 0 ]; then + ACTIVE_PID=$child; ACTIVE_PGID=$pgid + terminate_active || return 125 + return 125 + fi + ACTIVE_PID=; ACTIVE_PGID= + return "$child_status" +} + +emit_supervisor_failure() { + local token=${1:-E_RUNTIME} + case "$token" in + E_USAGE|E_RUNTIME|E_LIMIT|E_PARSE|E_CANONICAL|E_RELATION|E_POLICY_SET|E_CORE) ;; + *) token=E_RUNTIME ;; + esac + if cleanup; then trap - EXIT HUP INT TERM; /usr/bin/printf '%s\n' "$token" >&2; fi + exit 1 +} + +bootstrap_prepare() { +[ "$#" -eq 6 ] && [ "$1" = evaluate ] || emit_error E_USAGE +normalized_args=(evaluate) +for input in "${@:2}"; do + case "$input" in /*) ;; *) input="$(pwd -P)/$input" ;; esac + input_parent=$(CDPATH='' cd -P -- "${input%/*}" 2>/dev/null && pwd -P) || + emit_error E_RUNTIME + input="$input_parent/${input##*/}" + physical_regular "$input" || emit_error E_RUNTIME + normalized_args+=("$input") +done +origin=${BASH_SOURCE[0]} +case "$origin" in /*) ;; *) origin="$(pwd -P)/$origin" ;; esac +origin_dir=$(CDPATH='' cd -P -- "${origin%/*}" 2>/dev/null && pwd -P) || + emit_error E_RUNTIME +origin="$origin_dir/${origin##*/}" +[ "$origin" = "$origin_dir/evaluate-evidence-integrity.sh" ] || emit_error E_RUNTIME +origin_identity=$(path_identity "$origin" 1048576) || emit_error E_RUNTIME +live_jq=$(command -v jq 2>/dev/null) || emit_error E_RUNTIME +case "$live_jq" in /*) ;; *) emit_error E_RUNTIME ;; esac +live_jq_parent=$(CDPATH='' cd -P -- "${live_jq%/*}" 2>/dev/null && pwd -P) || + emit_error E_RUNTIME +live_jq="$live_jq_parent/${live_jq##*/}" +physical_regular "$live_jq" || emit_error E_RUNTIME +live_jq_identity=$(path_identity "$live_jq" 16777216) || emit_error E_RUNTIME +expected_jq=$(expected_jq_digest) || emit_error E_RUNTIME +[ "${live_jq_identity##*:}" = "$expected_jq" ] || emit_error E_RUNTIME +scratch=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/ystack-evidence.XXXXXX" 2>/dev/null) || + emit_error E_RUNTIME +scratch=$(CDPATH='' cd -P -- "$scratch" 2>/dev/null && pwd -P) || emit_error E_RUNTIME +SCRATCH_ID=$(directory_identity "$scratch") || emit_error E_RUNTIME +SCRATCH_OWNED=1 +scratch_mkdirs bin io worker || emit_error E_RUNTIME +worker_scratch="$scratch/worker" +worker_scratch_id=$(directory_identity "$worker_scratch") || emit_error E_RUNTIME +source_path="$scratch/driver.sh" +jq_bin="$scratch/bin/jq" +snapshot_nofollow "$origin" "$origin_identity" "$source_path" 1048576 0500 || + emit_error E_RUNTIME +private_driver_identity=$(path_identity "$source_path" 1048576) || + emit_error E_RUNTIME +private_payload_sha=$(payload_sha_from_identity "$source_path" \ + "$private_driver_identity") || emit_error E_RUNTIME +snapshot_nofollow "$live_jq" "$live_jq_identity" "$jq_bin" 16777216 0500 || + emit_error E_RUNTIME +private_jq_identity=$(path_identity "$jq_bin" 16777216) || emit_error E_RUNTIME + +[ -n "$scratch" ] || emit_error E_RUNTIME +case "$scratch" in /*/ystack-evidence.??????) ;; *) emit_error E_RUNTIME ;; esac +if [ -z "$SCRATCH_ID" ] || + ! directory_matches_identity "$scratch" "$SCRATCH_ID"; then + emit_error E_RUNTIME +fi +SCRATCH_OWNED=1 +[ "$source_path" = "$scratch/driver.sh" ] && [ "$jq_bin" = "$scratch/bin/jq" ] || + emit_error E_RUNTIME +origin_id=$origin_identity +private_driver_id=$private_driver_identity +live_jq_path=$live_jq +live_jq_id=$live_jq_identity +private_jq_id=$private_jq_identity +PINNED_PATHS=("$origin" "$source_path" "$live_jq_path" "$jq_bin") +PINNED_IDENTITIES=("$origin_id" "$private_driver_id" "$live_jq_id" "$private_jq_id") +for input in "${normalized_args[@]:1}"; do + pin_path "$input" || emit_error E_RUNTIME +done +for internal_identity in "${PINNED_IDENTITIES[@]}"; do + [ -n "$internal_identity" ] || emit_error E_RUNTIME +done +expected_jq=$(expected_jq_digest) || emit_error E_RUNTIME +[ "${origin_id##*:}" = "${private_driver_id##*:}" ] || emit_error E_RUNTIME +[ "${live_jq_id##*:}" = "$expected_jq" ] && + [ "${private_jq_id##*:}" = "$expected_jq" ] || + emit_error E_RUNTIME +if ! private_mode_ok "$source_path" || ! private_mode_ok "$jq_bin"; then + emit_error E_RUNTIME +fi +verify_all_pins || emit_error E_RELATION +} + +supervisor_main() { + worker_status=0 + scratch_prepare_capture_keys io/worker.out io/worker.err - io/worker.identity || + emit_supervisor_failure E_RUNTIME + worker_output_key=$CAPTURE_OUTPUT_KEY + worker_error_key=$CAPTURE_ERROR_KEY + worker_record_key=$CAPTURE_RECORD_KEY + run_child scratch_capture_prepared io/worker.out "$worker_output_key" \ + io/worker.err "$worker_error_key" - - io/worker.identity "$worker_record_key" \ + driver.sh "$private_driver_id" \ + "$private_payload_sha" /usr/bin/env -i LC_ALL=C PATH=/usr/bin:/bin \ + TMPDIR="$scratch" HOME=/nonexistent /bin/bash -c \ + 'source /dev/stdin || exit 125; worker_entry "$@"' ystack-evidence-worker \ + "$worker_scratch" "$worker_scratch_id" "$origin" "$origin_id" "$source_path" \ + "$private_driver_id" "$private_payload_sha" "$live_jq_path" "$live_jq_id" \ + "$jq_bin" "$private_jq_id" "$@" || + worker_status=$? + if [ -n "${ACTIVE_PGID:-}" ] || [ -n "${ACTIVE_PID:-}" ]; then + signal_exit 125 + fi + IFS= read -r -t 5 worker_record_text <&6 || { + exec 6>&- + emit_supervisor_failure E_RUNTIME + } + exec 6>&- + capture_record_parse "$worker_record_text" || emit_supervisor_failure E_RUNTIME + [ "$CAPTURE_FINAL_STATUS" -eq "$worker_status" ] || + emit_supervisor_failure E_RUNTIME + worker_output_final=$CAPTURE_OUTPUT_FINAL + worker_error_final=$CAPTURE_ERROR_FINAL + error_identity=$(capture_identity_for_final "$scratch/io/worker.err" \ + "$worker_error_final") || + emit_supervisor_failure E_RUNTIME + error_text=$(capture_identity_text "$scratch/io/worker.err" "$error_identity") || + emit_supervisor_failure E_RUNTIME + capture_full_identity_matches "$scratch/io/worker.err" "$worker_error_final" || + emit_supervisor_failure E_RUNTIME + if [ "$worker_status" -ne 0 ]; then emit_supervisor_failure "$error_text"; fi + [ -z "$error_text" ] || emit_supervisor_failure E_RUNTIME + worker_output_identity=$(capture_identity_for_final "$scratch/io/worker.out" \ + "$worker_output_final") || + emit_supervisor_failure E_RUNTIME + worker_output_text=$(capture_identity_text "$scratch/io/worker.out" \ + "$worker_output_identity") || emit_supervisor_failure E_RUNTIME + [ -n "$worker_output_text" ] || emit_supervisor_failure E_RUNTIME + capture_full_identity_matches "$scratch/io/worker.out" "$worker_output_final" || + emit_supervisor_failure E_RUNTIME + policy_set_expected=$(pinned_identity "$2") || emit_supervisor_failure E_RUNTIME + request_expected=$(pinned_identity "$3") || emit_supervisor_failure E_RUNTIME + resolved_expected=$(pinned_identity "$4") || emit_supervisor_failure E_RUNTIME + result_expected=$(pinned_identity "$5") || emit_supervisor_failure E_RUNTIME + presentation_expected=$(pinned_identity "$6") || emit_supervisor_failure E_RUNTIME + worker_canonical=$("$jq_bin" -S -c . "$scratch/io/worker.out" 2>/dev/null) || + emit_supervisor_failure E_RUNTIME + [ "$worker_canonical" = "$worker_output_text" ] || + emit_supervisor_failure E_RUNTIME + "$jq_bin" -e --arg policy_set_sha "${policy_set_expected##*:}" \ + --arg request_sha "${request_expected##*:}" \ + --arg resolved_sha "${resolved_expected##*:}" \ + --arg result_sha "${result_expected##*:}" \ + --arg presentation_sha "${presentation_expected##*:}" \ + --slurpfile policy_set "$2" --slurpfile request "$3" \ + --slurpfile result "$5" --slurpfile presentation "$6" ' + (keys|sort)==["body","id","kind","schema_version"] and + .schema_version==1 and .kind=="evidence_integrity_evaluation" and + .id==$result[0].id and + (.body|keys|sort)==["activation_state","authority_effect","core_contract", + "decision_ref","evaluation_mode","evidence_refs","policy_ref","policy_set", + "presentation_ref","prior_evidence_refs","qualification_observation", + "qualification_semantics","reason_ids","reference_semantics","stage", + "storage_effect","verdict"] and + .body.activation_state=="inactive" and .body.authority_effect=="none" and + .body.storage_effect=="none" and .body.evaluation_mode=="observation-only" and + .body.reference_semantics=="identity-only" and + .body.qualification_semantics=="identity-only-unqualified" and + .body.core_contract==$policy_set[0].body.core_contract and + .body.policy_set=={id:$policy_set[0].id,sha256:$policy_set_sha} and + .body.stage.request_ref.sha256==$request_sha and + .body.stage.resolved_profile_ref.sha256==$resolved_sha and + .body.stage.result_ref.sha256==$result_sha and + .body.presentation_ref.sha256==$presentation_sha and + .body.evidence_refs==($result[0].body.evidence| + map({evidence_id,kind,proof_ref,verdict})) and + .body.prior_evidence_refs==$request[0].body.prior_evidence_refs and + .body.qualification_observation== + (if $request[0].body|has("qualification_ref") then + {state:"present",value:$request[0].body.qualification_ref} + else {state:"absent"} end) and + (.body.verdict=="satisfied" or .body.verdict=="violated") and + (.body.reason_ids|type=="array" and length>=1 and .==(sort|unique)) and + (if .body.verdict=="satisfied" then + .body.reason_ids==["evidence.integrity-satisfied"] + else (.body.reason_ids|index("evidence.integrity-satisfied")==null) end) and + ((.body|has("grant_ref") or has("qualification_ref") or has("activation") or + has("credential") or has("network") or has("candidate_execution"))|not) + ' "$scratch/io/worker.out" >/dev/null 2>&1 || emit_supervisor_failure E_RUNTIME + capture_full_identity_matches "$scratch/io/worker.out" "$worker_output_final" || + emit_supervisor_failure E_RUNTIME + output_text=$worker_output_text + verify_all_pins || emit_supervisor_failure E_RELATION + if ! cleanup; then exit 1; fi + trap - EXIT HUP INT TERM + /usr/bin/printf '%s\n' "$output_text" || exit 1 + exit 0 +} + +# YSTACK_EVIDENCE_PAYLOAD_WORKER_BEGIN +worker_entry() { + [ "$#" -eq 17 ] || silent_fail + trap - EXIT HUP INT TERM + scratch=$1 + SCRATCH_ID=$2 + origin=$3 + origin_id=$4 + source_path=$5 + private_driver_id=$6 + private_payload_sha=$7 + live_jq_path=$8 + live_jq_id=$9 + jq_bin=${10} + private_jq_id=${11} + shift 11 + SCRATCH_OWNED=0 + ACTIVE_PID= + ACTIVE_PGID= + [ "$#" -eq 6 ] && [ "$1" = evaluate ] || silent_fail + case "$scratch" in /*/ystack-evidence.??????/worker) ;; *) silent_fail ;; esac + directory_matches_identity "$scratch" "$SCRATCH_ID" || silent_fail + SCRATCH_OWNED=1 + outer_scratch=${scratch%/worker} + [ "$source_path" = "$outer_scratch/driver.sh" ] && + [ "$jq_bin" = "$outer_scratch/bin/jq" ] || emit_error E_RUNTIME + PINNED_PATHS=("$origin" "$source_path" "$live_jq_path" "$jq_bin") + PINNED_IDENTITIES=("$origin_id" "$private_driver_id" "$live_jq_id" "$private_jq_id") + expected_jq=$(expected_jq_digest) || emit_error E_RUNTIME + [ "${origin_id##*:}" = "${private_driver_id##*:}" ] && + [ "${live_jq_id##*:}" = "$expected_jq" ] && + [ "${private_jq_id##*:}" = "$expected_jq" ] || emit_error E_RUNTIME + if ! private_mode_ok "$source_path" || ! private_mode_ok "$jq_bin"; then + emit_error E_RUNTIME + fi + observed_payload_sha=$(payload_sha_from_identity "$source_path" \ + "$private_driver_id") || emit_error E_RUNTIME + [ "$observed_payload_sha" = "$private_payload_sha" ] || emit_error E_RUNTIME + verify_all_pins || emit_error E_RUNTIME +trap - EXIT HUP INT TERM +ulimit -f 2048 || emit_error E_RUNTIME + +shift +source_dir=$(CDPATH='' cd -P -- "${origin%/*}" 2>/dev/null && pwd -P) || + emit_error E_RUNTIME +[ "$origin" = "$source_dir/evaluate-evidence-integrity.sh" ] || emit_error E_RUNTIME +repo=$(CDPATH='' cd -P -- "$source_dir/../.." 2>/dev/null && pwd -P) || + emit_error E_RUNTIME +policy="$source_dir/evidence-integrity-policy.json" +decision="$source_dir/evidence-integrity-decision.json" +program="$source_dir/evidence-integrity.jq" +policy_validator="$source_dir/validate.sh" +validator_program="$source_dir/policy-set.jq" +core_driver="$repo/scripts/core-contract.sh" +for required in "$source_path" "$origin" "$policy" "$decision" "$program" \ + "$policy_validator" "$validator_program" "$core_driver" "$@"; do + pin_path "$required" || emit_error E_RUNTIME +done + +sha256_text() { + /usr/bin/printf '%s' "$1" | /usr/bin/shasum -a 256 | /usr/bin/awk '{print $1}' +} +selected_core_generation() { + local wrapper=$1 selected assignment_count + assignment_count=$(/usr/bin/grep -Ec \ + '^[[:space:]]*PORTABLE_CORE_GENERATION=' "$wrapper") || return 1 + [ "$assignment_count" -eq 1 ] || return 1 + selected=$(/usr/bin/sed -n \ + "s/^PORTABLE_CORE_GENERATION='\(g-[0-9a-f]\{64\}\)'$/\1/p" \ + "$wrapper") || return 1 + [[ "$selected" =~ ^g-[0-9a-f]{64}$ ]] || return 1 + /usr/bin/printf '%s\n' "$selected" +} + +snapshot_fixed() { + local source=$1 target=$2 expected + pin_path "$source" || emit_error E_RUNTIME + expected=$(pinned_identity "$source") || emit_error E_RUNTIME + snapshot_nofollow "$source" "$expected" "$target" 1048576 0600 || + emit_error E_RUNTIME + pin_path "$target" || emit_error E_RUNTIME +} +snapshot_executable() { + local source=$1 target=$2 expected + pin_path "$source" || emit_error E_RUNTIME + expected=$(pinned_identity "$source") || emit_error E_RUNTIME + snapshot_nofollow "$source" "$expected" "$target" 1048576 0500 || + emit_error E_RUNTIME + pin_path "$target" || emit_error E_RUNTIME +} +canonical_json() { + local input=$1 canonical=$2 canonical_relative bom canonical_identity input_identity + bom=$(/usr/bin/od -An -tx1 -N3 "$input" 2>/dev/null | /usr/bin/tr -d ' \n') || + emit_error E_RUNTIME + [ "$bom" != efbbbf ] || emit_error E_PARSE + "$jq_bin" -e 'true' "$input" /dev/null 2>&1 || emit_error E_PARSE + "$jq_bin" -s -e 'length==1' "$input" /dev/null 2>&1 || + emit_error E_PARSE + canonical_relative=$(scratch_relative "$canonical") || emit_error E_RUNTIME + scratch_capture "$canonical_relative" - - "$canonical_relative.identity" \ + - - - "$jq_bin" -S -c . "$input" || + emit_error E_PARSE + canonical_identity=$(capture_identity_for_final "$canonical" \ + "$CAPTURE_OUTPUT_FINAL") || emit_error E_RUNTIME + input_identity=$(path_identity "$input" 1048576) || emit_error E_RUNTIME + [ "${canonical_identity##*:}" = "${input_identity##*:}" ] || + emit_error E_CANONICAL + capture_full_identity_matches "$canonical" "$CAPTURE_OUTPUT_FINAL" || + emit_error E_RUNTIME + "$jq_bin" -e ' + def depth: + if type=="array" then if length==0 then 1 else 1+([.[]|depth]|max) end + elif type=="object" then if length==0 then 1 else 1+([.[]|depth]|max) end + else 1 end; + def members: + if type=="array" then length+([.[]|members]|add//0) + elif type=="object" then (keys_unsorted|length)+([.[]|members]|add//0) + else 0 end; + def strings_ok: + if type=="array" then all(.[];strings_ok) + elif type=="object" then + all(keys_unsorted[];utf8bytelength<=8192) and all(.[];strings_ok) + elif type=="string" then utf8bytelength<=8192 else true end; + depth<=32 and members<=4096 and strings_ok + ' "$input" >/dev/null 2>&1 || emit_error E_LIMIT +} +validator_pair_ok() { + local pair_dir=$1 driver=$2 validator_jq=$3 expected_driver=$4 expected_program=$5 + local physical_dir + [ -d "$pair_dir" ] && [ ! -L "$pair_dir" ] || return 1 + physical_dir=$(CDPATH='' cd -P -- "$pair_dir" 2>/dev/null && pwd -P) || return 1 + [ "$physical_dir" = "$pair_dir" ] && + [ "$driver" = "$pair_dir/validate.sh" ] && + [ "$validator_jq" = "$pair_dir/policy-set.jq" ] && + [ -f "$driver" ] && [ ! -L "$driver" ] && + [ -f "$validator_jq" ] && [ ! -L "$validator_jq" ] && + [ "$(sha256_path "$driver")" = "$expected_driver" ] && + [ "$(sha256_path "$validator_jq")" = "$expected_program" ] +} +build_validator_mirror() { + local mirror="$scratch/policy-validator/control/v1" source target + scratch_mkdirs policy-validator/control/v1 || return 1 + for source in "$policy_validator" "$validator_program"; do + target="$mirror/${source##*/}" + case "$source" in + "$policy_validator") snapshot_executable "$source" "$target" ;; + *) snapshot_fixed "$source" "$target" ;; + esac + done + /usr/bin/printf '%s\n' "$mirror" +} +core_closure_sha() { + local root=$1 wrapper=$2 selected=$3 tag=$4 registry generation_root canonical + local relative file digest members descriptor physical selected_sha count modules + local members_identity members_text canonical_identity registry_identity + local -a paths member_lines + registry="$root/core/v2/generation-registry.json" + generation_root="$root/core/v2/generations/$selected" + for required_dir in "$root" "$root/scripts" "$root/core" "$root/core/v2" \ + "$root/core/v2/generations" "$generation_root" "$generation_root/modules"; do + [ -d "$required_dir" ] && [ ! -L "$required_dir" ] || return 1 + physical=$(CDPATH='' cd -P -- "$required_dir" 2>/dev/null && pwd -P) || return 1 + [ "$physical" = "$required_dir" ] || return 1 + done + [ "$wrapper" = "$root/scripts/core-contract.sh" ] || return 1 + [ "$(selected_core_generation "$wrapper")" = "$selected" ] || return 1 + count=$(/usr/bin/find "$generation_root" -mindepth 1 -maxdepth 1 -print 2>/dev/null | + /usr/bin/wc -l | /usr/bin/tr -d ' ') || return 1 + modules=$(/usr/bin/find "$generation_root/modules" -mindepth 1 -maxdepth 1 \ + -print 2>/dev/null | /usr/bin/wc -l | /usr/bin/tr -d ' ') || return 1 + [ "$count" -eq 3 ] && [ "$modules" -eq 5 ] || return 1 + [ -f "$registry" ] && [ ! -L "$registry" ] || return 1 + canonical="$scratch/registry-$tag.json" + scratch_capture "registry-$tag.json" - - "registry-$tag.identity" \ + - - - "$jq_bin" -s -S -c \ + 'if length==1 then .[0] else error("root-count") end' "$registry" || return 1 + canonical_identity=$(capture_identity_for_final "$canonical" \ + "$CAPTURE_OUTPUT_FINAL") || return 1 + registry_identity=$(path_identity "$registry" 1048576) || return 1 + [ "${canonical_identity##*:}" = "${registry_identity##*:}" ] || return 1 + capture_full_identity_matches "$canonical" "$CAPTURE_OUTPUT_FINAL" || return 1 + "$jq_bin" -e --arg selected "$selected" ' + type=="array" and length>=1 and + ([.[]|select(.generation_id==$selected and .semantic_identity=="core.contracts.v2")] + |length)==1 + ' "$registry" >/dev/null 2>&1 || return 1 + paths=( + scripts/core-contract.sh + core/v2/generation-registry.json + "core/v2/generations/$selected/contracts.jq" + "core/v2/generations/$selected/core-ingress.sh" + "core/v2/generations/$selected/modules/profile_graph.jq" + "core/v2/generations/$selected/modules/result_facts.jq" + "core/v2/generations/$selected/modules/result_truth.jq" + "core/v2/generations/$selected/modules/schema.jq" + "core/v2/generations/$selected/modules/stage_request.jq" + ) + members="$scratch/core-members-$tag.tsv" + member_lines=() + for relative in "${paths[@]}"; do + file="$root/$relative" + [ -f "$file" ] && [ ! -L "$file" ] || return 1 + digest=$(sha256_path "$file") || return 1 + member_lines[${#member_lines[@]}]="$relative"$'\t'"$digest" + done + scratch_write_lines "core-members-$tag.tsv" "${member_lines[@]}" || return 1 + pin_path "$members" || return 1 + members_identity=$(pinned_identity "$members") || return 1 + members_text=$(capture_identity_text "$members" "$members_identity") || return 1 + selected_sha=$(sha256_text "$selected") || return 1 + descriptor=$("$jq_bin" -Rn -S -c --arg selected_sha "$selected_sha" \ + --arg members "$members_text" ' + ($members|split("\n")|map(select(length>0)|split("\t")| + {path:.[0],sha256:.[1]})) as $members | + {schema_version:1,kind:"core_contract_package_closure", + semantic_identity:"core.contracts.v2", + selected_generation_id_sha256:$selected_sha,members:$members} + ') || return 1 + sha256_text "$descriptor" +} +build_core_mirror() { + local selected=$1 mirror="$scratch/core-package" relative source target + local -a paths + scratch_mkdirs core-package/scripts \ + "core-package/core/v2/generations/$selected/modules" || + return 1 + paths=( + scripts/core-contract.sh + core/v2/generation-registry.json + "core/v2/generations/$selected/contracts.jq" + "core/v2/generations/$selected/core-ingress.sh" + "core/v2/generations/$selected/modules/profile_graph.jq" + "core/v2/generations/$selected/modules/result_facts.jq" + "core/v2/generations/$selected/modules/result_truth.jq" + "core/v2/generations/$selected/modules/schema.jq" + "core/v2/generations/$selected/modules/stage_request.jq" + ) + for relative in "${paths[@]}"; do + source="$repo/$relative" + target="$mirror/$relative" + case "$relative" in + scripts/core-contract.sh) snapshot_executable "$source" "$target" ;; + *) snapshot_fixed "$source" "$target" ;; + esac + done + /usr/bin/printf '%s\n' "$mirror" +} +pin_core_package() { + local root=$1 selected=$2 relative + local -a paths + paths=( + scripts/core-contract.sh + core/v2/generation-registry.json + "core/v2/generations/$selected/contracts.jq" + "core/v2/generations/$selected/core-ingress.sh" + "core/v2/generations/$selected/modules/profile_graph.jq" + "core/v2/generations/$selected/modules/result_facts.jq" + "core/v2/generations/$selected/modules/result_truth.jq" + "core/v2/generations/$selected/modules/schema.jq" + "core/v2/generations/$selected/modules/stage_request.jq" + ) + for relative in "${paths[@]}"; do pin_path "$root/$relative" || return 1; done +} +fixed_files_ok() { + [ "$(sha256_path "$source_path")" = "$launcher_sha" ] && + [ "$(payload_sha_from_identity "$source_path" "$private_driver_id")" = \ + "$payload_sha" ] && + [ "$(sha256_path "$program")" = "$program_sha" ] && + [ "$(sha256_path "$policy")" = "$policy_sha" ] && + [ "$(sha256_path "$decision")" = "$decision_sha" ] +} + +names=(policy-set request resolved result presentation) +index=0 +for input in "$@"; do + snapshot_fixed "$input" "$scratch/${names[$index]}.json" + canonical_json "$scratch/${names[$index]}.json" \ + "$scratch/${names[$index]}.canonical" || emit_error E_RELATION + index=$((index + 1)) +done +"$jq_bin" -e ' + (keys|sort)==["body","id","kind","schema_version"] and + .schema_version==1 and .kind=="evidence_integrity_presentation" and + (.id|type=="string" and test("\\A[a-z0-9][a-z0-9._:-]{0,127}\\z")) and + (.body|type=="object") +' "$scratch/presentation.json" >/dev/null 2>&1 || emit_error E_RELATION +snapshot_fixed "$policy" "$scratch/policy.json" +snapshot_fixed "$decision" "$scratch/decision.json" +snapshot_fixed "$program" "$scratch/program.jq" +canonical_json "$scratch/policy.json" "$scratch/policy.canonical" || + emit_error E_RELATION +canonical_json "$scratch/decision.json" "$scratch/decision.canonical" || + emit_error E_RELATION +for control_dir in "$repo/control" "$source_dir"; do + [ -d "$control_dir" ] && [ ! -L "$control_dir" ] || emit_error E_RELATION +done +[ "$source_dir" = "$repo/control/v1" ] || emit_error E_RELATION + +launcher_sha=$(sha256_path "$source_path") || emit_error E_RUNTIME +payload_sha=$(payload_sha_from_identity "$source_path" "$private_driver_id") || + emit_error E_RUNTIME +[ "$payload_sha" = "$private_payload_sha" ] || emit_error E_RELATION +program_sha=$(sha256_path "$scratch/program.jq") || emit_error E_RUNTIME +policy_sha=$(sha256_path "$scratch/policy.json") || emit_error E_RUNTIME +decision_sha=$(sha256_path "$scratch/decision.json") || emit_error E_RUNTIME +validator_driver_sha=$(sha256_path "$policy_validator") || emit_error E_RUNTIME +validator_program_sha=$(sha256_path "$validator_program") || emit_error E_RUNTIME +"$jq_bin" -n -e --arg policy_sha "$policy_sha" --arg launcher_sha "$launcher_sha" \ + --arg payload_sha "$payload_sha" \ + --arg program_sha "$program_sha" --arg validator_driver_sha "$validator_driver_sha" \ + --arg validator_program_sha "$validator_program_sha" \ + --slurpfile policy "$scratch/policy.json" \ + --slurpfile definition "$scratch/decision.json" ' + $definition[0] == { + schema_version:1,kind:"evidence_integrity_decision", + id:"control-decision.evidence-integrity", + body:{activation_state:"inactive",decision:"allow-observation-only-evaluation", + evaluator:{ + trusted_launcher_ref:{ + content_id:"control-evaluator-launcher.evidence-integrity.v1", + media_type:"text/x-shellscript",sha256:$launcher_sha}, + evaluation_payload_ref:{ + content_id:"control-evaluator-payload.evidence-integrity.v1", + media_type:"text/x-shellscript-fragment",sha256:$payload_sha}, + policy_set_validator:{ + driver_ref:{content_id:"control-policy-set-validator-driver.v1", + media_type:"text/x-shellscript",sha256:$validator_driver_sha}, + program_ref:{content_id:"control-policy-set-validator-program.v1", + media_type:"text/x-jq",sha256:$validator_program_sha}}, + program_ref:{content_id:"control-evaluator-program.evidence-integrity.v1", + media_type:"text/x-jq",sha256:$program_sha}}, + fail_mode:"closed", + policy_ref:{content_id:$policy[0].id, + media_type:"application/vnd.ystack.control-policy+json",sha256:$policy_sha}, + semantics:{authority_effect:"none",candidate_execution:"none", + credential_access:"none", + input_contract:"control-policy-set+public-core-stage-run+evidence-integrity-presentation.v1", + launcher_attestation:"trusted-boundary-not-self-attested", + network_access:"none",output_kind:"evidence_integrity_evaluation", + output_schema_version:1,qualification_effect:"none", + reference_semantics:"identity-only",storage_effect:"none", + verdicts:["satisfied","violated"]}} + } +' >/dev/null 2>&1 || emit_error E_RELATION + +validator_pair_ok "$source_dir" "$policy_validator" "$validator_program" \ + "$validator_driver_sha" "$validator_program_sha" || emit_error E_RELATION +mirror_validator_dir=$(build_validator_mirror) || emit_error E_RELATION +mirror_policy_validator="$mirror_validator_dir/validate.sh" +mirror_validator_program="$mirror_validator_dir/policy-set.jq" +pin_path "$mirror_policy_validator" || emit_error E_RELATION +pin_path "$mirror_validator_program" || emit_error E_RELATION +validator_pair_ok "$mirror_validator_dir" "$mirror_policy_validator" \ + "$mirror_validator_program" "$validator_driver_sha" "$validator_program_sha" || + emit_error E_RELATION +policy_status=0 +scratch_capture policy.out policy.err - policy.identity - - - \ + /usr/bin/env -i LC_ALL=C \ + PATH="${jq_bin%/*}:/usr/bin:/bin" TMPDIR="$scratch" HOME=/nonexistent \ + "$mirror_policy_validator" validate "$scratch/policy-set.json" || + policy_status=$? +capture_full_identity_matches "$scratch/policy.out" "$CAPTURE_OUTPUT_FINAL" || + emit_error E_RUNTIME +capture_full_identity_matches "$scratch/policy.err" "$CAPTURE_ERROR_FINAL" || + emit_error E_RUNTIME +if ! validator_pair_ok "$source_dir" "$policy_validator" "$validator_program" \ + "$validator_driver_sha" "$validator_program_sha" || + ! validator_pair_ok "$mirror_validator_dir" "$mirror_policy_validator" \ + "$mirror_validator_program" "$validator_driver_sha" "$validator_program_sha"; then + emit_error E_RELATION +fi +[ "$policy_status" -eq 0 ] || emit_error E_POLICY_SET + +selected=$(selected_core_generation "$core_driver") || emit_error E_RELATION +selected_sha=$(sha256_text "$selected") || emit_error E_RUNTIME +pin_core_package "$repo" "$selected" || emit_error E_RELATION +live_core_sha=$(core_closure_sha "$repo" "$core_driver" "$selected" live-pre) || + emit_error E_RELATION +mirror_root=$(build_core_mirror "$selected") || emit_error E_RELATION +mirror_core_driver="$mirror_root/scripts/core-contract.sh" +pin_core_package "$mirror_root" "$selected" || emit_error E_RELATION +mirror_core_sha=$(core_closure_sha "$mirror_root" "$mirror_core_driver" "$selected" mirror-pre) || + emit_error E_RELATION +[ "$mirror_core_sha" = "$live_core_sha" ] || emit_error E_RELATION +"$jq_bin" -e --arg policy_sha "$policy_sha" --arg decision_sha "$decision_sha" \ + --arg selected "$selected" --arg selected_sha "$selected_sha" \ + --arg core_sha "$live_core_sha" --slurpfile policy "$scratch/policy.json" ' + .body.core_contract == { + semantic_identity:$policy[0].body.core_contract.semantic_identity, + generation_id:$selected,package_ref:$policy[0].body.core_contract.package_ref} and + $selected_sha == $policy[0].body.core_contract.generation_id_sha256 and + $core_sha == $policy[0].body.core_contract.package_ref.sha256 and + ([.body.sections[]|select(.section_id=="evidence-integrity")]|length)==1 and + ([.body.sections[]|select(.section_id=="evidence-integrity")][0]) == { + section_id:"evidence-integrity", + policy_ref:{content_id:$policy[0].id, + media_type:"application/vnd.ystack.control-policy+json",sha256:$policy_sha}, + decision_ref:{content_id:"control-decision.evidence-integrity", + media_type:"application/vnd.ystack.control-decision+json",sha256:$decision_sha}} +' "$scratch/policy-set.json" >/dev/null 2>&1 || emit_error E_RELATION + +core_status=0 +scratch_mkdirs core-accounted || emit_error E_RUNTIME +core_accounted_root="$scratch/core-accounted" +core_accounted_id=$(directory_identity "$core_accounted_root") || + emit_error E_RUNTIME +core_byte_budget=16777216 +scratch_capture core.out core.err core.receipt core.identity - - - \ + /usr/bin/env -i LC_ALL=C \ + PATH="${jq_bin%/*}:/usr/bin:/bin" TMPDIR="$scratch" HOME=/nonexistent \ + "$mirror_core_driver" --accounted-validation "$core_accounted_root" \ + "$core_byte_budget" validate-stage-run \ + "$scratch/request.json" "$scratch/resolved.json" "$scratch/result.json" \ + || core_status=$? +core_output_final=$CAPTURE_OUTPUT_FINAL +core_error_final=$CAPTURE_ERROR_FINAL +core_receipt_final=$CAPTURE_RECEIPT_FINAL +capture_full_identity_matches "$scratch/core.out" "$core_output_final" || + emit_error E_RUNTIME +capture_full_identity_matches "$scratch/core.err" "$core_error_final" || + emit_error E_RUNTIME +capture_full_identity_matches "$scratch/core.receipt" "$core_receipt_final" || + emit_error E_RUNTIME +core_receipt_identity=$(capture_identity_for_final "$scratch/core.receipt" \ + "$core_receipt_final") || emit_error E_RUNTIME +core_receipt_text=$(capture_identity_text "$scratch/core.receipt" \ + "$core_receipt_identity") || emit_error E_RUNTIME +capture_full_identity_matches "$scratch/core.receipt" "$core_receipt_final" || + emit_error E_RUNTIME +case "$core_receipt_text" in written-bytes:*) ;; *) emit_error E_RUNTIME ;; esac +core_written_bytes=${core_receipt_text#written-bytes:} +[[ "$core_written_bytes" =~ ^(0|[1-9][0-9]*)$ ]] || emit_error E_RUNTIME +[ "$core_written_bytes" -gt 0 ] && + [ "$core_written_bytes" -le "$core_byte_budget" ] || emit_error E_RUNTIME +directory_matches_identity "$core_accounted_root" "$core_accounted_id" || + emit_error E_RUNTIME +[ -z "$(/usr/bin/find "$core_accounted_root" -mindepth 1 -print -quit \ + 2>/dev/null)" ] || emit_error E_RUNTIME +directory_matches_identity "$core_accounted_root" "$core_accounted_id" || + emit_error E_RUNTIME +post_live_core_sha=$(core_closure_sha "$repo" "$core_driver" "$selected" live-post) || + emit_error E_RELATION +post_mirror_core_sha=$(core_closure_sha \ + "$mirror_root" "$mirror_core_driver" "$selected" mirror-post) || emit_error E_RELATION +[ "$post_live_core_sha" = "$live_core_sha" ] && + [ "$post_mirror_core_sha" = "$mirror_core_sha" ] || emit_error E_RELATION +if ! validator_pair_ok "$source_dir" "$policy_validator" "$validator_program" \ + "$validator_driver_sha" "$validator_program_sha" || + ! validator_pair_ok "$mirror_validator_dir" "$mirror_policy_validator" \ + "$mirror_validator_program" "$validator_driver_sha" "$validator_program_sha"; then + emit_error E_RELATION +fi +[ "$core_status" -eq 0 ] || emit_error E_CORE + +policy_set_sha=$(sha256_path "$scratch/policy-set.json") || emit_error E_RUNTIME +request_sha=$(sha256_path "$scratch/request.json") || emit_error E_RUNTIME +resolved_sha=$(sha256_path "$scratch/resolved.json") || emit_error E_RUNTIME +result_sha=$(sha256_path "$scratch/result.json") || emit_error E_RUNTIME +presentation_sha=$(sha256_path "$scratch/presentation.json") || emit_error E_RUNTIME +scratch_capture evaluation.json - - evaluation.identity - - - \ + "$jq_bin" -S -c -n \ + -f "$scratch/program.jq" \ + --slurpfile policy "$scratch/policy.json" \ + --slurpfile decision "$scratch/decision.json" \ + --slurpfile policy_set "$scratch/policy-set.json" \ + --slurpfile request "$scratch/request.json" \ + --slurpfile resolved "$scratch/resolved.json" \ + --slurpfile result "$scratch/result.json" \ + --slurpfile presentation "$scratch/presentation.json" \ + --arg policy_sha "$policy_sha" --arg decision_sha "$decision_sha" \ + --arg policy_set_sha "$policy_set_sha" --arg request_sha "$request_sha" \ + --arg resolved_sha "$resolved_sha" --arg result_sha "$result_sha" \ + --arg presentation_sha "$presentation_sha" || + emit_error E_RUNTIME +evaluation_final=$CAPTURE_OUTPUT_FINAL +evaluation_identity=$(capture_identity_for_final "$scratch/evaluation.json" \ + "$evaluation_final") || emit_error E_RUNTIME +fixed_files_ok || emit_error E_RELATION +final_live_core_sha=$(core_closure_sha "$repo" "$core_driver" "$selected" live-final) || + emit_error E_RELATION +final_mirror_core_sha=$(core_closure_sha \ + "$mirror_root" "$mirror_core_driver" "$selected" mirror-final) || emit_error E_RELATION +[ "$final_live_core_sha" = "$live_core_sha" ] && + [ "$final_mirror_core_sha" = "$mirror_core_sha" ] || emit_error E_RELATION +if ! validator_pair_ok "$source_dir" "$policy_validator" "$validator_program" \ + "$validator_driver_sha" "$validator_program_sha" || + ! validator_pair_ok "$mirror_validator_dir" "$mirror_policy_validator" \ + "$mirror_validator_program" "$validator_driver_sha" "$validator_program_sha"; then + emit_error E_RELATION +fi +canonical_json "$scratch/evaluation.json" "$scratch/evaluation.canonical" || + emit_error E_RUNTIME +"$jq_bin" -e --arg policy_sha "$policy_sha" --arg decision_sha "$decision_sha" \ + --arg policy_set_sha "$policy_set_sha" --arg request_sha "$request_sha" \ + --arg resolved_sha "$resolved_sha" --arg result_sha "$result_sha" \ + --arg presentation_sha "$presentation_sha" \ + --slurpfile policy_set "$scratch/policy-set.json" \ + --slurpfile request "$scratch/request.json" --slurpfile resolved "$scratch/resolved.json" \ + --slurpfile result "$scratch/result.json" --slurpfile presentation "$scratch/presentation.json" ' + (keys|sort)==["body","id","kind","schema_version"] and .schema_version==1 and + .kind=="evidence_integrity_evaluation" and .id==$result[0].id and + (.body|keys|sort)==["activation_state","authority_effect","core_contract", + "decision_ref","evaluation_mode","evidence_refs","policy_ref","policy_set", + "presentation_ref","prior_evidence_refs","qualification_observation", + "qualification_semantics","reason_ids","reference_semantics","stage", + "storage_effect","verdict"] and + .body.activation_state=="inactive" and .body.authority_effect=="none" and + .body.evaluation_mode=="observation-only" and .body.storage_effect=="none" and + .body.reference_semantics=="identity-only" and + .body.qualification_semantics=="identity-only-unqualified" and + .body.core_contract==$policy_set[0].body.core_contract and + .body.policy_set=={id:$policy_set[0].id,sha256:$policy_set_sha} and + .body.policy_ref=={content_id:"control-policy.evidence-integrity", + media_type:"application/vnd.ystack.control-policy+json",sha256:$policy_sha} and + .body.decision_ref=={content_id:"control-decision.evidence-integrity", + media_type:"application/vnd.ystack.control-decision+json",sha256:$decision_sha} and + .body.presentation_ref=={content_id:$presentation[0].id, + media_type:"application/vnd.ystack.evidence-integrity-presentation+json", + sha256:$presentation_sha} and + .body.stage=={ + request_ref:{schema_version:$request[0].schema_version,kind:$request[0].kind, + id:$request[0].id,sha256:$request_sha}, + resolved_profile_ref:{schema_version:$resolved[0].schema_version,kind:$resolved[0].kind, + id:$resolved[0].id,sha256:$resolved_sha}, + result_ref:{schema_version:$result[0].schema_version,kind:$result[0].kind, + id:$result[0].id,sha256:$result_sha}} and + .body.evidence_refs==($result[0].body.evidence|map({evidence_id,kind,proof_ref,verdict})) and + .body.prior_evidence_refs==$request[0].body.prior_evidence_refs and + .body.qualification_observation== + (if $request[0].body|has("qualification_ref") then + {state:"present",value:$request[0].body.qualification_ref} + else {state:"absent"} end) and + (.body.verdict=="satisfied" or .body.verdict=="violated") and + (.body.reason_ids|type=="array" and length>=1 and .==(sort|unique)) and + (if .body.verdict=="satisfied" then + .body.reason_ids==["evidence.integrity-satisfied"] + else (.body.reason_ids|index("evidence.integrity-satisfied")==null) end) and + ((.body|has("grant_ref") or has("qualification_ref") or has("activation") or + has("credential") or has("network") or has("candidate_execution"))|not) +' "$scratch/evaluation.json" >/dev/null 2>&1 || emit_error E_RUNTIME + +capture_full_identity_matches "$scratch/evaluation.json" "$evaluation_final" || + emit_error E_RUNTIME +verify_all_pins || emit_error E_RELATION +output_text=$(capture_identity_text "$scratch/evaluation.json" \ + "$evaluation_identity") || + emit_error E_RUNTIME +capture_full_identity_matches "$scratch/evaluation.json" "$evaluation_final" || + emit_error E_RUNTIME +if ! cleanup; then silent_fail; fi +trap - EXIT HUP INT TERM +/usr/bin/printf '%s\n' "$output_text" || exit 1 +return 0 +} +# YSTACK_EVIDENCE_PAYLOAD_WORKER_END + +if [ "${BASH_SOURCE[0]}" = "$0" ]; then + bootstrap_prepare "$@" + supervisor_main "${normalized_args[@]}" +fi diff --git a/control/v1/evidence-integrity-decision.json b/control/v1/evidence-integrity-decision.json new file mode 100644 index 0000000..526d9bb --- /dev/null +++ b/control/v1/evidence-integrity-decision.json @@ -0,0 +1 @@ +{"body":{"activation_state":"inactive","decision":"allow-observation-only-evaluation","evaluator":{"evaluation_payload_ref":{"content_id":"control-evaluator-payload.evidence-integrity.v1","media_type":"text/x-shellscript-fragment","sha256":"79e2a3c8817e19f1e3fc9d5571b68cb1eb15d55b69eb10f5090bb943220d1afc"},"policy_set_validator":{"driver_ref":{"content_id":"control-policy-set-validator-driver.v1","media_type":"text/x-shellscript","sha256":"cf173ad0eaa08244bf636e3937845e894b21f14291fc5e66753e8673bdd2bd2a"},"program_ref":{"content_id":"control-policy-set-validator-program.v1","media_type":"text/x-jq","sha256":"2be97550574ee4522fc0bd14780c92dee3c1b455f2c04b7763b0e437665a8d58"}},"program_ref":{"content_id":"control-evaluator-program.evidence-integrity.v1","media_type":"text/x-jq","sha256":"5b61b900b71e9485072a2d65fe52a221c25d9e270c942d8d3b00ef53aedf117f"},"trusted_launcher_ref":{"content_id":"control-evaluator-launcher.evidence-integrity.v1","media_type":"text/x-shellscript","sha256":"f0ecbe036601102db082d0a94c860ea868614ff79d06f2b37ec8dcd672c367a2"}},"fail_mode":"closed","policy_ref":{"content_id":"control-policy.evidence-integrity","media_type":"application/vnd.ystack.control-policy+json","sha256":"171b89c49c7dd6a58e4c5aa6ca13e8c95d109acf7f67429ecb33fcf1dae7582a"},"semantics":{"authority_effect":"none","candidate_execution":"none","credential_access":"none","input_contract":"control-policy-set+public-core-stage-run+evidence-integrity-presentation.v1","launcher_attestation":"trusted-boundary-not-self-attested","network_access":"none","output_kind":"evidence_integrity_evaluation","output_schema_version":1,"qualification_effect":"none","reference_semantics":"identity-only","storage_effect":"none","verdicts":["satisfied","violated"]}},"id":"control-decision.evidence-integrity","kind":"evidence_integrity_decision","schema_version":1} diff --git a/control/v1/evidence-integrity-policy.json b/control/v1/evidence-integrity-policy.json new file mode 100644 index 0000000..0d448c0 --- /dev/null +++ b/control/v1/evidence-integrity-policy.json @@ -0,0 +1 @@ +{"body":{"activation_state":"inactive","core_contract":{"generation_id_sha256":"6f6acbbd0cf40ab3c913328d6c0070635424ffe920bcdb900fbd0718345d7137","package_ref":{"content_id":"core-contract-package.v2","media_type":"application/vnd.ystack.core-contract+json","sha256":"005431c5c7e3a39dc3ab75dfcafd0f09359331667fdcacb140514a4384592716"},"semantic_identity":"core.contracts.v2"},"evaluation_mode":"observation-only","evidence_contract":{"current_result_binding":"exact-document-digest","presentation_kind":"evidence_integrity_presentation","prior_evidence_binding":"exact-stage-result-digest-and-evidence-id","proof_binding":"content-ref-sha256","qualification_binding":"exact-scope-ref-or-absent"},"fail_mode":"closed","policy_version":"v1","qualification_semantics":"identity-only-unqualified","storage_effect":"none"},"id":"control-policy.evidence-integrity","kind":"evidence_integrity_policy","schema_version":1} diff --git a/control/v1/evidence-integrity.jq b/control/v1/evidence-integrity.jq new file mode 100644 index 0000000..0acbd8b --- /dev/null +++ b/control/v1/evidence-integrity.jq @@ -0,0 +1,263 @@ +def exact($fields): + type == "object" and (keys | sort) == ($fields | sort); + +def id_ok: + type == "string" and test("\\A[a-z0-9][a-z0-9._:-]{0,127}\\z"); + +def sha256_ok: + type == "string" and test("\\A[0-9a-f]{64}\\z"); + +def media_type_ok: + type == "string" and length <= 127 and + test("\\A[a-z0-9][a-z0-9!#$&^_.+-]*/[a-z0-9][a-z0-9!#$&^_.+-]*\\z"); + +def evidence_kind_ok: + type == "string" and + (. as $kind | + ["architecture","behavioral","deterministic","independent-review"] | + index($kind) != null); + +def content_ref_ok: + exact(["content_id","media_type","sha256"]) and + (.content_id | id_ok) and (.content_id | contains(":") | not) and + (.content_id | contains("/") | not) and (.media_type | media_type_ok) and + (.sha256 | sha256_ok); + +def document_ref_any_ok: + exact(["id","kind","schema_version","sha256"]) and + .schema_version == 2 and + (.kind as $kind | + ["adapter_manifest","profile","resolved_profile","stage_request","stage_result"] | + index($kind) != null) and (.id | id_ok) and + (.sha256 | sha256_ok); + +def document_ref_ok($kind): + document_ref_any_ok and .kind == $kind; + +def repo_path_ok: + type == "string" and length > 0 and + (test("[\\x{0000}-\\x{001f}\\x{007f}-\\x{009f}]") | not) and + (contains("\\") | not) and + (split("/") | all(.[];. != "" and . != "." and . != "..")); + +def git_revision_ref_ok: + exact(["repository_id","hash_algorithm","commit_id"]) and + (.repository_id | id_ok) and + (.hash_algorithm == "sha1" or .hash_algorithm == "sha256") and + (if .hash_algorithm == "sha1" then + (.commit_id | type == "string" and test("\\A[0-9a-f]{40}\\z")) + else (.commit_id | type == "string" and test("\\A[0-9a-f]{64}\\z")) end); + +def git_location_ok: + (exact(["kind"]) and .kind == "root") or + (exact(["kind","value"]) and .kind == "path" and (.value | repo_path_ok)); + +def git_object_ref_ok: + exact(["revision","location","object_type","object_id","mode"]) and + (.revision | git_revision_ref_ok) and (.location | git_location_ok) and + (.object_type == "blob" or .object_type == "tree") and + (if .revision.hash_algorithm == "sha1" then + (.object_id | type == "string" and test("\\A[0-9a-f]{40}\\z")) + else (.object_id | type == "string" and test("\\A[0-9a-f]{64}\\z")) end) and + (if .location.kind == "root" then .object_type == "tree" else true end) and + (if .object_type == "tree" then .mode == "040000" + else (.mode == "100644" or .mode == "100755") end); + +def artifact_ref_ok: + exact(["type","value"]) and + ((.type == "git-object" and (.value | git_object_ref_ok)) or + (.type == "content" and (.value | content_ref_ok))); + +def scope_subject_ok: + exact(["type","value"]) and + ((.type == "artifact" and (.value | artifact_ref_ok)) or + (.type == "document" and (.value | document_ref_any_ok))); + +def scope_ref_ok($purpose): + exact(["decision_record_ref","purpose","scope_sha256","subject_ref"]) and + .purpose == $purpose and (.decision_record_ref | content_ref_ok) and + (.scope_sha256 | sha256_ok) and (.subject_ref | scope_subject_ok); + +def presence_ok(value_ok): + type == "object" and + ((exact(["state"]) and .state == "absent") or + (exact(["state","value"]) and .state == "present" and (.value | value_ok))); + +def evidence_ok: + exact(["evidence_id","kind","proof_ref","verdict"]) and + (.evidence_id | id_ok) and (.kind | evidence_kind_ok) and + (.verdict == "passed" or .verdict == "failed" or .verdict == "inconclusive") and + (.proof_ref | content_ref_ok); + +def evidence_ref_ok: + exact(["evidence_id","stage_result_ref"]) and + (.evidence_id | id_ok) and (.stage_result_ref | document_ref_ok("stage_result")); + +def expected_core: + {semantic_identity:"core.contracts.v2", + generation_id_sha256: + "6f6acbbd0cf40ab3c913328d6c0070635424ffe920bcdb900fbd0718345d7137", + package_ref:{content_id:"core-contract-package.v2", + media_type:"application/vnd.ystack.core-contract+json", + sha256:"005431c5c7e3a39dc3ab75dfcafd0f09359331667fdcacb140514a4384592716"}}; + +def policy_ok: + exact(["body","id","kind","schema_version"]) and .schema_version == 1 and + .kind == "evidence_integrity_policy" and + .id == "control-policy.evidence-integrity" and + (.body | + exact(["activation_state","core_contract","evaluation_mode", + "evidence_contract","fail_mode","policy_version", + "qualification_semantics","storage_effect"]) and + .activation_state == "inactive" and .core_contract == expected_core and + .evaluation_mode == "observation-only" and .fail_mode == "closed" and + .policy_version == "v1" and .qualification_semantics == "identity-only-unqualified" and + .storage_effect == "none" and + .evidence_contract == { + current_result_binding:"exact-document-digest", + presentation_kind:"evidence_integrity_presentation", + prior_evidence_binding:"exact-stage-result-digest-and-evidence-id", + proof_binding:"content-ref-sha256", + qualification_binding:"exact-scope-ref-or-absent"}); + +def presentation_structure_ok: + exact(["body","id","kind","schema_version"]) and .schema_version == 1 and + .kind == "evidence_integrity_presentation" and (.id | id_ok) and + (.body | + exact(["evidence","prior_evidence_refs","qualification_ref","request_ref", + "resolved_profile_ref","result_ref"]) and + (.request_ref | document_ref_ok("stage_request")) and + (.resolved_profile_ref | document_ref_ok("resolved_profile")) and + (.result_ref | document_ref_ok("stage_result")) and + (.evidence | type == "array" and length <= 256 and all(.[];evidence_ok)) and + (.prior_evidence_refs | + type == "array" and length <= 256 and all(.[];evidence_ref_ok)) and + (.qualification_ref | presence_ok(scope_ref_ok("qualification")))); + +def canonical_presentation_sets: + (.body.evidence == (.body.evidence | sort_by(.evidence_id))) and + ((.body.evidence | map(.evidence_id) | length) == + (.body.evidence | map(.evidence_id) | unique | length)) and + ((.body.evidence | map(.kind) | length) == + (.body.evidence | map(.kind) | unique | length)) and + (.body.prior_evidence_refs == + (.body.prior_evidence_refs | sort_by([.stage_result_ref.sha256,.evidence_id]))) and + ((.body.prior_evidence_refs | + map([.stage_result_ref.sha256,.evidence_id]) | length) == + (.body.prior_evidence_refs | + map([.stage_result_ref.sha256,.evidence_id]) | unique | length)); + +def document_ref($document;$digest): + {schema_version:$document.schema_version,kind:$document.kind, + id:$document.id,sha256:$digest}; + +def content_ref($id;$media;$digest): + {content_id:$id,media_type:$media,sha256:$digest}; + +def normalized_array($value): + if ($value | type) == "array" then $value else [] end; + +def duplicate_scalar_key($entries;$field): + ($entries | normalized_array(.)) as $items | + if all($items[];evidence_ok) then + ($items | map(.[$field])) as $keys | + ($keys | length) != ($keys | unique | length) + else false end; + +def duplicate_prior_key($entries): + ($entries | normalized_array(.)) as $items | + if all($items[];evidence_ref_ok) then + ($items | map([.stage_result_ref.sha256,.evidence_id])) as $keys | + ($keys | length) != ($keys | unique | length) + else false end; + +def prior_document_alias($entries): + ($entries | normalized_array(.)) as $items | + if all($items[];evidence_ref_ok) then + any($items | group_by(.stage_result_ref.sha256)[]; + ([.[] | [.stage_result_ref.schema_version,.stage_result_ref.kind, + .stage_result_ref.id]] | unique | length) > 1) + else false end; + +($policy[0]) as $p | +($decision[0]) as $definition | +($policy_set[0]) as $set | +($request[0]) as $request_doc | +($resolved[0]) as $resolved_doc | +($result[0]) as $result_doc | +($presentation[0]) as $presented | +(if ($p | policy_ok) then true else error("invalid shipped evidence policy") end) | + +document_ref($request_doc;$request_sha) as $expected_request_ref | +document_ref($resolved_doc;$resolved_sha) as $expected_resolved_ref | +document_ref($result_doc;$result_sha) as $expected_result_ref | +(if $request_doc.body | has("qualification_ref") then + {state:"present",value:$request_doc.body.qualification_ref} + else {state:"absent"} end) as $expected_qualification | +(if ($presented.body? | type) == "object" then $presented.body else {} end) as + $presented_body | +($presented_body.evidence? | normalized_array(.)) as $presented_evidence | +($presented_body.prior_evidence_refs? | normalized_array(.)) as $presented_prior | + +content_ref($p.id;"application/vnd.ystack.control-policy+json";$policy_sha) as + $policy_ref | +content_ref($definition.id;"application/vnd.ystack.control-decision+json"; + $decision_sha) as $decision_ref | +([$set.body.sections[] | select(.section_id == "evidence-integrity")]) as + $evidence_sections | +(if ($evidence_sections | length) == 1 and + $evidence_sections[0].policy_ref == $policy_ref and + $evidence_sections[0].decision_ref == $decision_ref + then true else error("invalid policy-set evidence binding") end) | + +((if (($presented | presentation_structure_ok) and + ($presented | canonical_presentation_sets)) then [] + else ["evidence.presentation-malformed"] end) + + (if $presented_body.request_ref? == $expected_request_ref then [] + else ["evidence.request-moved"] end) + + (if $presented_body.resolved_profile_ref? == $expected_resolved_ref then [] + else ["evidence.resolved-profile-moved"] end) + + (if $presented_body.result_ref? == $expected_result_ref then [] + else ["evidence.result-moved"] end) + + (if $presented_evidence == $result_doc.body.evidence then [] + else ["evidence.current-mismatch"] end) + + (if $presented_prior == $request_doc.body.prior_evidence_refs then [] + else ["evidence.prior-stale"] end) + + (if $presented_body.qualification_ref? == $expected_qualification then [] + else ["evidence.qualification-mismatch"] end) + + (if duplicate_scalar_key($presented_evidence;"evidence_id") or + duplicate_scalar_key($presented_evidence;"kind") or + duplicate_prior_key($presented_prior) or + prior_document_alias($presented_prior) + then ["evidence.presentation-ambiguous"] else [] end) | + sort | unique) as $violations | +(if ($violations | length) == 0 then + {verdict:"satisfied",reasons:["evidence.integrity-satisfied"]} + else {verdict:"violated",reasons:$violations} end) as $evaluation | + +{ + schema_version:1, + kind:"evidence_integrity_evaluation", + id:$result_doc.id, + body:{ + activation_state:"inactive", + authority_effect:"none", + core_contract:$set.body.core_contract, + decision_ref:$decision_ref, + evaluation_mode:"observation-only", + evidence_refs:($result_doc.body.evidence | map({evidence_id,kind,proof_ref,verdict})), + policy_ref:$policy_ref, + policy_set:{id:$set.id,sha256:$policy_set_sha}, + presentation_ref:content_ref($presented.id; + "application/vnd.ystack.evidence-integrity-presentation+json";$presentation_sha), + prior_evidence_refs:$request_doc.body.prior_evidence_refs, + qualification_observation:$expected_qualification, + qualification_semantics:"identity-only-unqualified", + reason_ids:$evaluation.reasons, + reference_semantics:"identity-only", + stage:{request_ref:$expected_request_ref,resolved_profile_ref:$expected_resolved_ref, + result_ref:$expected_result_ref}, + storage_effect:"none", + verdict:$evaluation.verdict + } +} diff --git a/scripts/test/control-evidence-integrity.test.sh b/scripts/test/control-evidence-integrity.test.sh new file mode 100755 index 0000000..501b884 --- /dev/null +++ b/scripts/test/control-evidence-integrity.test.sh @@ -0,0 +1,1276 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +export LC_ALL=C +umask 077 + +if [ "${YSTACK_EVIDENCE_TEST_BOUNDED:-0}" != 1 ]; then + YSTACK_EVIDENCE_TEST_BOUNDED=1 exec /usr/bin/perl -e 'alarm 360; exec @ARGV' "$0" +fi + +root=$(CDPATH='' cd -P -- "${BASH_SOURCE[0]%/*}/../.." && pwd -P) +evaluator="$root/control/v1/evaluate-evidence-integrity.sh" +policy="$root/control/v1/evidence-integrity-policy.json" +definition="$root/control/v1/evidence-integrity-decision.json" +program="$root/control/v1/evidence-integrity.jq" +core_wrapper="$root/scripts/core-contract.sh" +tmp=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/ystack-evidence-test.XXXXXX") +cleanup() { /bin/rm -rf -- "$tmp"; } +trap cleanup EXIT HUP INT TERM +# Accounted Linux paths can exceed 7s; 4000 x 5ms leaves a 20s CI margin +# inside the suite's 360s hard cap for markers reached after that work. +late_marker_attempts=4000 +fail() { /usr/bin/printf 'FAIL: %s\n' "$1" >&2; exit 1; } +passes=0 +pass() { passes=$((passes + 1)); /usr/bin/printf 'ok %s - %s\n' "$passes" "$1"; } +sha256_path() { /usr/bin/shasum -a 256 "$1" | /usr/bin/awk '{print $1}'; } +test_path_identity() { + /usr/bin/env -i LC_ALL=C PATH=/usr/bin:/bin \ + /usr/bin/perl -MFcntl=:DEFAULT,:mode -MDigest::SHA -MCwd=abs_path -e ' + my ($path)=@ARGV; my ($parent,$name)=$path =~ m{\A(.+)/([^/]+)\z}; + exit 2 unless defined($parent) && defined($name) && + defined(abs_path($parent)) && abs_path($parent) eq $parent; + my @parent=lstat($parent); chdir($parent) or exit 2; + my @cwd=stat("."); my @leaf=lstat($name); + exit 2 unless @parent && @cwd && @leaf && S_ISDIR($parent[2]) && + S_ISREG($leaf[2]); + sysopen(my $input,$name,O_RDONLY|O_NOFOLLOW) or exit 2; + binmode($input); my @opened=stat($input); my $sha=Digest::SHA->new(256); + while (1) { my $read=sysread($input,my $buffer,65536); exit 2 unless + defined($read); last if $read==0; $sha->add($buffer); } + my @after=stat($input); my @path_after=lstat($name); + exit 2 unless @opened && @after && @path_after && + $leaf[0]==$opened[0] && $leaf[1]==$opened[1] && + $opened[0]==$after[0] && $opened[1]==$after[1] && + $after[0]==$path_after[0] && $after[1]==$path_after[1]; + print $parent[0],":",$parent[1],":",$leaf[0],":",$leaf[1],":", + $leaf[7],":",$leaf[9],":",$leaf[10],":",$sha->hexdigest,"\n"; + ' "$1" +} +test_directory_identity() { + /usr/bin/env -i LC_ALL=C PATH=/usr/bin:/bin \ + /usr/bin/perl -MFcntl=:mode -MCwd=abs_path -e ' + my ($path)=@ARGV; my ($parent,$name)=$path =~ m{\A(.+)/([^/]+)\z}; + exit 1 unless defined($parent) && defined($name) && + defined(abs_path($parent)) && abs_path($parent) eq $parent; + my @parent=lstat($parent); my @dir=lstat($path); my $physical=abs_path($path); + exit 1 unless @parent && @dir && S_ISDIR($parent[2]) && S_ISDIR($dir[2]) && + (($dir[2] & 07777) == 0700) && defined($physical) && $physical eq $path; + print $parent[0],":",$parent[1],":",$dir[0],":",$dir[1],"\n"; + ' "$1" +} +test_payload_sha() { + local identity=${2:-} + [ -n "$identity" ] || identity=$(test_path_identity "$1") + [ -n "$identity" ] || return 1 + /usr/bin/env -i LC_ALL=C PATH=/usr/bin:/bin /bin/bash -c ' + source "$1" || exit 1 + payload_sha_from_identity "$1" "$2" + ' payload-hash "$1" "$identity" +} + +platform=$(/usr/bin/uname -s):$(/usr/bin/uname -m) +case "$platform" in + Darwin:*) + jq_asset=jq-osx-amd64 + jq_sha=5c0a0a3ea600f302ee458b30317425dd9632d1ad8882259fcaf4e9b868b2b1ef + ;; + Linux:x86_64) + jq_asset=jq-linux64 + jq_sha=af986793a515d500ab2d35f8d2aecd656e764504b789b66d7e1a0b727a124c44 + ;; + *) fail "unsupported host $platform" ;; +esac +jq_cache_dir="${TMPDIR:-/tmp}/ystack-portable-core-jq16" +/bin/mkdir -p "$jq_cache_dir" +jq_cache="$jq_cache_dir/$jq_asset" +if [ ! -f "$jq_cache" ] || [ "$(sha256_path "$jq_cache")" != "$jq_sha" ]; then + download=$(/usr/bin/mktemp "$jq_cache_dir/.jq-1.6.XXXXXX") + /usr/bin/curl --proto '=https' --tlsv1.2 -fsSL \ + "https://github.com/jqlang/jq/releases/download/jq-1.6/$jq_asset" -o "$download" + [ "$(sha256_path "$download")" = "$jq_sha" ] || fail 'jq release digest' + /bin/chmod 0555 "$download" + /bin/mv "$download" "$jq_cache" +fi +bin="$tmp/bin" +/bin/mkdir -m 700 "$bin" +/bin/cp "$jq_cache" "$bin/jq" +/bin/chmod 0555 "$bin/jq" +jq_bin="$bin/jq" +[ "$($jq_bin --version)" = jq-1.6 ] || fail 'jq identity' + +generation=$(/usr/bin/sed -n \ + "s/^PORTABLE_CORE_GENERATION='\(g-[0-9a-f]\{64\}\)'$/\1/p" "$core_wrapper") || + fail 'selected generation' +[[ "$generation" =~ ^g-[0-9a-f]{64}$ ]] || fail 'selected generation shape' +policy_sha=$(sha256_path "$policy") +definition_sha=$(sha256_path "$definition") +launcher_sha=$(sha256_path "$evaluator") +launcher_identity=$(test_path_identity "$evaluator") +payload_sha=$(test_payload_sha "$evaluator" "$launcher_identity") +core_package_sha=$("$jq_bin" -er '.body.core_contract.package_ref.sha256' "$policy") +for canonical_source in "$policy" "$definition"; do + "$jq_bin" -S -c . "$canonical_source" >"$tmp/canonical" + /usr/bin/cmp -s "$canonical_source" "$tmp/canonical" || + fail "canonical ${canonical_source##*/}" +done +for source_path in control/v1/evidence-integrity-policy.json \ + control/v1/evidence-integrity-decision.json control/v1/evidence-integrity.jq \ + control/v1/evaluate-evidence-integrity.sh scripts/test/control-evidence-integrity.test.sh; do + ! /usr/bin/grep -Fq "$generation" "$root/$source_path" || + fail "raw generation $source_path" +done +pass 'canonical definitions and opaque core generation' +"$jq_bin" -e --arg launcher "$launcher_sha" --arg payload "$payload_sha" ' + .body.evaluator.trusted_launcher_ref=={ + content_id:"control-evaluator-launcher.evidence-integrity.v1", + media_type:"text/x-shellscript",sha256:$launcher} and + .body.evaluator.evaluation_payload_ref=={ + content_id:"control-evaluator-payload.evidence-integrity.v1", + media_type:"text/x-shellscript-fragment",sha256:$payload} and + .body.semantics.launcher_attestation=="trusted-boundary-not-self-attested" +' "$definition" >/dev/null || fail 'launcher and payload identity contract' +if ! /usr/bin/grep -Fq 'self-attest bytes that Bash already loaded' "$root/README.md" || + ! /usr/bin/grep -Fq 'launcher is not self-attested.' "$root/RESTORE.md"; then + fail 'launcher boundary docs' +fi +pass 'trusted launcher and exact evaluation payload are distinct identities' + +policy_set="$tmp/policy-set.json" +"$jq_bin" -S -c -n --arg policy_sha "$policy_sha" \ + --arg definition_sha "$definition_sha" --arg generation "$generation" \ + --arg core_package_sha "$core_package_sha" ' + def ref($id;$media;$sha): {content_id:$id,media_type:$media,sha256:$sha}; + def section($id;$policy_sha;$decision_sha): + {section_id:$id, + policy_ref:ref("control-policy."+$id; + "application/vnd.ystack.control-policy+json";$policy_sha), + decision_ref:ref("control-decision."+$id; + "application/vnd.ystack.control-decision+json";$decision_sha)}; + {schema_version:1,kind:"control_policy_set",id:"control-policy-set.test", + body:{activation_state:"inactive",fail_mode:"closed",policy_version:"v1", + core_contract:{semantic_identity:"core.contracts.v2",generation_id:$generation, + package_ref:ref("core-contract-package.v2"; + "application/vnd.ystack.core-contract+json";$core_package_sha)}, + sections:[section("credential-policy";("1"*64);("a"*64)), + section("duty-separation";("2"*64);("b"*64)), + section("evidence-integrity";$policy_sha;$definition_sha), + section("kill-switch";("4"*64);("d"*64)), + section("risk-gates";("5"*64);("e"*64)), + section("sandbox";("6"*64);("f"*64))]}} +' >"$policy_set" + +resolved="$tmp/resolved.json" +"$jq_bin" -L "$root/scripts/test" -S -c -n ' + import "portable-core-profile-graph-fixtures" as profile; + def v2: walk(if type == "object" and has("schema_version") + then .schema_version=2 else . end); + def forge_binding($sha): + {binding_id:"binding.forge",role:"forge", + manifest_ref:{schema_version:2,kind:"adapter_manifest",id:"manifest.forge",sha256:$sha}, + execution_kind:"deterministic",adapter_instance_id:"instance.forge", + principal_id:"principal.forge",execution_boundary_id:"boundary.forge", + authority_ref:profile::scope("authority";"authority-forge";profile::sha("5")), + package_ref:profile::blob("packages/forge.bin";"6"),skill_refs:[],requested_tools:[], + requested_capabilities:["core.forge.materialize-candidate.v2"], + requested_permissions:["core.perm.candidate-repository.write.v2", + "core.perm.evidence.write.v1","core.perm.scratch.write.v1", + "core.perm.target.read.v1"]}; + {forge:("1"*64),producer:("2"*64),publisher:("3"*64), + reviewer:("4"*64),verifier:("5"*64)} as $shas | + (profile::profile_doc($shas) | v2 | .body.profile_version="v2" | + .body.bindings += [forge_binding($shas.forge)] | + .body.bindings |= sort_by(.binding_id)) as $profile | + profile::resolved_profile_doc($profile;("0"*64);$shas) | v2 | + .body.bindings |= map(if .binding.role == "forge" then + .adapter_implementation.version="v2" | + .manifest_source=profile::source_value( + profile::blob("manifests/forge.json";"a");"canonical-json";$shas.forge) + else . end) +' >"$resolved" +resolved_sha=$(sha256_path "$resolved") + +request="$tmp/request.json" +"$jq_bin" -L "$root/scripts/test" -S -c -n --arg resolved_sha "$resolved_sha" ' + import "portable-core-profile-graph-fixtures" as profile; + import "portable-core-stage-request-fixtures" as request; + request::request_doc("producer";$resolved_sha) | + walk(if type == "object" and has("schema_version") then .schema_version=2 else . end) | + .body.qualification_ref=profile::scope("qualification";"qualification.test";("7"*64)) | + .body.prior_evidence_refs=[{stage_result_ref:{schema_version:2,kind:"stage_result", + id:"result.previous",sha256:("8"*64)},evidence_id:"evidence.previous"}] +' >"$request" +request_sha=$(sha256_path "$request") +result="$tmp/result.json" +"$jq_bin" -L "$root/scripts/test" -S -c -n \ + --slurpfile request "$request" --slurpfile resolved "$resolved" \ + --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" ' + import "portable-core-result-truth-fixtures" as result; + result::completed_result_doc($request[0];$request_sha;$resolved[0];$resolved_sha) | + walk(if type == "object" and has("schema_version") then .schema_version=2 else . end) +' >"$result" +result_sha=$(sha256_path "$result") +presentation="$tmp/presentation.json" +"$jq_bin" -S -c -n --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" \ + --arg result_sha "$result_sha" --slurpfile request "$request" \ + --slurpfile resolved "$resolved" --slurpfile result "$result" ' + def doc($value;$sha): + {schema_version:$value.schema_version,kind:$value.kind,id:$value.id,sha256:$sha}; + {schema_version:1,kind:"evidence_integrity_presentation",id:"evidence.presentation.test", + body:{evidence:$result[0].body.evidence, + prior_evidence_refs:$request[0].body.prior_evidence_refs, + qualification_ref:{state:"present",value:$request[0].body.qualification_ref}, + request_ref:doc($request[0];$request_sha), + resolved_profile_ref:doc($resolved[0];$resolved_sha), + result_ref:doc($result[0];$result_sha)}} +' >"$presentation" + +run_eval() { + local name=$1 input=${2:-$presentation} runtime=${3:-$root} run_status=0 + local out="$tmp/$name.out" err="$tmp/$name.err" + PATH="$bin:/usr/bin:/bin" "$runtime/control/v1/evaluate-evidence-integrity.sh" evaluate \ + "$policy_set" "$request" "$resolved" "$result" "$input" >"$out" 2>"$err" || + run_status=$? + if [ "$run_status" -ne 0 ]; then + /usr/bin/printf 'diagnostic %s status=%s stderr=' "$name" "$run_status" >&2 + /bin/cat "$err" >&2 + fail "$name status" + fi + [ ! -s "$err" ] || fail "$name stderr" + "$jq_bin" -S -c . "$out" >"$tmp/$name.canonical" + /usr/bin/cmp -s "$out" "$tmp/$name.canonical" || fail "$name canonical" +} +run_eval_tuple() { + local name=$1 request_input=$2 resolved_input=$3 result_input=$4 input=$5 + local runtime=${6:-$root} run_status=0 + local out="$tmp/$name.out" err="$tmp/$name.err" + PATH="$bin:/usr/bin:/bin" "$runtime/control/v1/evaluate-evidence-integrity.sh" evaluate \ + "$policy_set" "$request_input" "$resolved_input" "$result_input" "$input" \ + >"$out" 2>"$err" || run_status=$? + if [ "$run_status" -ne 0 ]; then + /usr/bin/printf 'diagnostic %s status=%s stderr=' "$name" "$run_status" >&2 + /bin/cat "$err" >&2 + fail "$name status" + fi + [ ! -s "$err" ] || fail "$name stderr" + "$jq_bin" -S -c . "$out" >"$tmp/$name.canonical" + /usr/bin/cmp -s "$out" "$tmp/$name.canonical" || fail "$name canonical" +} +expect_error() { + local name=$1 expected=$2 policy_input=${3:-$policy_set} request_input=${4:-$request} + local resolved_input=${5:-$resolved} result_input=${6:-$result} + local presentation_input=${7:-$presentation} runtime=${8:-$root} status=0 + PATH="$bin:/usr/bin:/bin" "$runtime/control/v1/evaluate-evidence-integrity.sh" evaluate \ + "$policy_input" "$request_input" "$resolved_input" "$result_input" \ + "$presentation_input" >"$tmp/$name.out" 2>"$tmp/$name.err" || status=$? + if [ "$status" -eq 0 ] || [ -s "$tmp/$name.out" ] || + [ "$(/bin/cat "$tmp/$name.err")" != "$expected" ]; then + /usr/bin/printf 'diagnostic %s status=%s stderr=' "$name" "$status" >&2 + /bin/cat "$tmp/$name.err" >&2 + fail "$name error" + fi + pass "$name" +} +pure_eval() { + local input=$1 output=$2 + "$jq_bin" -S -c -n -f "$program" --slurpfile policy "$policy" \ + --slurpfile decision "$definition" --slurpfile policy_set "$policy_set" \ + --slurpfile request "$request" --slurpfile resolved "$resolved" \ + --slurpfile result "$result" --slurpfile presentation "$input" \ + --arg policy_sha "$policy_sha" --arg decision_sha "$definition_sha" \ + --arg policy_set_sha "$(sha256_path "$policy_set")" --arg request_sha "$request_sha" \ + --arg resolved_sha "$resolved_sha" --arg result_sha "$result_sha" \ + --arg presentation_sha "$(sha256_path "$input")" >"$output" +} +expect_pure_violation() { + local name=$1 filter=$2 reason=$3 second_reason=${4:-} + local input="$tmp/$name.presentation" output="$tmp/$name.pure" + "$jq_bin" -S -c "$filter" "$presentation" >"$input" + pure_eval "$input" "$output" + "$jq_bin" -e --arg reason "$reason" --arg second "$second_reason" ' + .body.verdict=="violated" and (.body.reason_ids|index($reason)!=null) and + ($second=="" or (.body.reason_ids|index($second)!=null)) and + .body.authority_effect=="none" and .body.storage_effect=="none" and + ((.body|has("grant_ref") or has("qualification_ref") or has("activation"))|not) + ' "$output" >/dev/null || fail "$name" + pass "$name" +} +expect_full_malformed_element() { + local name=$1 filter=$2 mismatch_reason=$3 input="$tmp/$1.presentation" + "$jq_bin" -S -c "$filter" "$presentation" >"$input" + run_eval "$name" "$input" + "$jq_bin" -e --arg mismatch "$mismatch_reason" ' + .body.verdict=="violated" and + .body.reason_ids==(["evidence.presentation-malformed",$mismatch]|sort) and + (.body.reason_ids|index("evidence.presentation-ambiguous")==null) and + .body.authority_effect=="none" and .body.storage_effect=="none" + ' "$tmp/$name.out" >/dev/null || fail "$name reasons" + pass "$name" +} + +run_eval valid +"$jq_bin" -e '.body.verdict=="satisfied" and + .body.reason_ids==["evidence.integrity-satisfied"] and + .body.qualification_semantics=="identity-only-unqualified" and + (.body.evidence_refs|length)==1' "$tmp/valid.out" >/dev/null || fail 'valid output' +pass 'valid exact immutable evidence presentation' +run_eval portable-valid +/usr/bin/cmp -s "$tmp/valid.out" "$tmp/portable-valid.out" || + fail 'portable valid output' +pass "portable valid evaluation on $platform" + +bash_env_hook="$tmp/conditional-bash-env.sh" +bash_env_sentinel="$tmp/conditional-bash-env.sentinel" +/usr/bin/printf '%s\n' \ + 'case "$0" in' \ + ' */validate.sh|*/core-contract.sh)' \ + ' /usr/bin/printf "%s\\n" "${AWS_SECRET_ACCESS_KEY:-missing}" >'\ +"\"$bash_env_sentinel\"" \ + ' ;;' \ + 'esac' >"$bash_env_hook" +bash_env_status=0 +BASH_ENV="$bash_env_hook" AWS_SECRET_ACCESS_KEY=must-not-reach-nested-bash \ + PATH="$bin:/usr/bin:/bin" "$evaluator" evaluate "$policy_set" "$request" \ + "$resolved" "$result" "$presentation" >"$tmp/bash-env.out" \ + 2>"$tmp/bash-env.err" || bash_env_status=$? +if ! { [ "$bash_env_status" -eq 0 ] && [ -s "$tmp/bash-env.out" ] && + [ ! -s "$tmp/bash-env.err" ] && [ ! -e "$bash_env_sentinel" ] && + /usr/bin/cmp -s "$tmp/valid.out" "$tmp/bash-env.out"; }; then + fail 'nested bash environment isolation' +fi +pass 'nested bash ignores conditional BASH_ENV and credential-like values' + +expect_pure_violation result-moved '.body.result_ref.sha256=("0"*64)' \ + evidence.result-moved +expect_pure_violation request-moved '.body.request_ref.sha256=("0"*64)' \ + evidence.request-moved +expect_pure_violation resolved-moved '.body.resolved_profile_ref.sha256=("0"*64)' \ + evidence.resolved-profile-moved +expect_pure_violation current-mismatch '.body.evidence[0].proof_ref.sha256=("0"*64)' \ + evidence.current-mismatch +expect_pure_violation prior-stale '.body.prior_evidence_refs[0].stage_result_ref.sha256=("0"*64)' \ + evidence.prior-stale +expect_pure_violation qualification-mismatch '.body.qualification_ref={state:"absent"}' \ + evidence.qualification-mismatch +expect_pure_violation malformed-presentation '.body.evidence=1' \ + evidence.presentation-malformed +expect_pure_violation ambiguous-presentation '.body.evidence += [.body.evidence[0]]' \ + evidence.presentation-ambiguous + +expect_pure_violation duplicate-evidence-id \ + '.body.evidence += [(.body.evidence[0] | .kind="behavioral")]' \ + evidence.presentation-ambiguous evidence.presentation-malformed +expect_pure_violation duplicate-evidence-kind \ + '.body.evidence += [(.body.evidence[0] | .evidence_id="evidence.zzz")]' \ + evidence.presentation-ambiguous evidence.presentation-malformed +expect_pure_violation reversed-evidence \ + '.body.evidence += [(.body.evidence[0] | .evidence_id="evidence.zzz" | + .kind="behavioral")] | .body.evidence |= reverse' \ + evidence.presentation-malformed +expect_pure_violation duplicate-prior-key \ + '.body.prior_evidence_refs += [.body.prior_evidence_refs[0]]' \ + evidence.presentation-ambiguous evidence.presentation-malformed +expect_pure_violation reversed-prior \ + '.body.prior_evidence_refs += [(.body.prior_evidence_refs[0] | + .stage_result_ref.sha256=("9"*64) | .stage_result_ref.id="result.zzz" | + .evidence_id="evidence.zzz")] | .body.prior_evidence_refs |= reverse' \ + evidence.presentation-malformed +expect_pure_violation prior-document-alias \ + '.body.prior_evidence_refs += [(.body.prior_evidence_refs[0] | + .stage_result_ref.id="result.alias" | .evidence_id="evidence.alias")] | + .body.prior_evidence_refs |= sort_by([.stage_result_ref.sha256,.evidence_id])' \ + evidence.presentation-ambiguous + +expect_full_malformed_element current-scalar '.body.evidence=[1]' \ + evidence.current-mismatch +expect_full_malformed_element current-null '.body.evidence=[null]' \ + evidence.current-mismatch +expect_full_malformed_element current-array '.body.evidence=[[]]' \ + evidence.current-mismatch +expect_full_malformed_element current-object-missing '.body.evidence=[{},{}]' \ + evidence.current-mismatch +expect_full_malformed_element current-object-wrong-type \ + '.body.evidence=[{evidence_id:1,kind:1,proof_ref:1,verdict:1}, + {evidence_id:1,kind:1,proof_ref:1,verdict:1}]' evidence.current-mismatch +expect_full_malformed_element prior-scalar '.body.prior_evidence_refs=[1]' \ + evidence.prior-stale +expect_full_malformed_element prior-null '.body.prior_evidence_refs=[null]' \ + evidence.prior-stale +expect_full_malformed_element prior-array '.body.prior_evidence_refs=[[]]' \ + evidence.prior-stale +expect_full_malformed_element prior-object-missing \ + '.body.prior_evidence_refs=[{},{}]' evidence.prior-stale +expect_full_malformed_element prior-object-wrong-type \ + '.body.prior_evidence_refs=[{evidence_id:1,stage_result_ref:1}, + {evidence_id:1,stage_result_ref:1}]' evidence.prior-stale +expect_full_malformed_element invalid-evidence-kind \ + '.body.evidence[0].kind="runtime-alt"' evidence.current-mismatch +expect_full_malformed_element invalid-proof-content-id \ + '.body.evidence[0].proof_ref.content_id="proof:invalid"' evidence.current-mismatch +expect_full_malformed_element invalid-proof-media-type \ + '.body.evidence[0].proof_ref.media_type=""' evidence.current-mismatch +expect_full_malformed_element invalid-qualification-subject \ + '.body.qualification_ref.value.subject_ref={}' evidence.qualification-mismatch + +shared_request="$tmp/shared-proof.request" +"$jq_bin" -L "$root/scripts/test" -S -c -n --arg resolved_sha "$resolved_sha" ' + import "portable-core-profile-graph-fixtures" as profile; + import "portable-core-stage-request-fixtures" as request; + request::request_doc("verifier";$resolved_sha) | + walk(if type == "object" and has("schema_version") then .schema_version=2 else . end) | + .body.qualification_ref=profile::scope("qualification";"qualification.shared";("7"*64)) +' >"$shared_request" +shared_request_sha=$(sha256_path "$shared_request") +shared_result="$tmp/shared-proof.result" +shared_presentation="$tmp/shared-proof.presentation" +"$jq_bin" -L "$root/scripts/test" -S -c -n \ + --slurpfile request "$shared_request" --slurpfile resolved "$resolved" \ + --arg request_sha "$shared_request_sha" --arg resolved_sha "$resolved_sha" ' + import "portable-core-result-truth-fixtures" as result; + result::completed_result_doc($request[0];$request_sha;$resolved[0];$resolved_sha) | + walk(if type == "object" and has("schema_version") then .schema_version=2 else . end) +' >"$shared_result" +shared_result_sha=$(sha256_path "$shared_result") +"$jq_bin" -S -c -n --arg request_sha "$shared_request_sha" \ + --arg resolved_sha "$resolved_sha" --arg result_sha "$shared_result_sha" \ + --slurpfile request "$shared_request" --slurpfile resolved "$resolved" \ + --slurpfile result "$shared_result" ' + def doc($value;$sha): + {schema_version:$value.schema_version,kind:$value.kind,id:$value.id,sha256:$sha}; + {schema_version:1,kind:"evidence_integrity_presentation",id:"evidence.presentation.shared", + body:{evidence:$result[0].body.evidence, + prior_evidence_refs:$request[0].body.prior_evidence_refs, + qualification_ref:{state:"present",value:$request[0].body.qualification_ref}, + request_ref:doc($request[0];$request_sha), + resolved_profile_ref:doc($resolved[0];$resolved_sha), + result_ref:doc($result[0];$result_sha)}} +' >"$shared_presentation" +run_eval_tuple shared-proof "$shared_request" "$resolved" "$shared_result" \ + "$shared_presentation" +"$jq_bin" -e ' + .body.verdict=="satisfied" and + .body.reason_ids==["evidence.integrity-satisfied"] and + (.body.evidence_refs|length)==3 and + (.body.evidence_refs|map(.proof_ref.sha256)|unique|length)==1 and + (.body.evidence_refs|map(.proof_ref.content_id)|unique|length)==3 +' "$tmp/shared-proof.out" >/dev/null || { + /bin/cat "$tmp/shared-proof.out" >&2 + fail 'same proof bytes distinct logical refs' +} +pass 'same proof digest under distinct logical refs remains valid' + +shared_mismatch="$tmp/shared-proof-mismatch.presentation" +"$jq_bin" -S -c \ + '.body.evidence[0].proof_ref.content_id="proof.presentation-only"' \ + "$shared_presentation" >"$shared_mismatch" +run_eval_tuple shared-proof-mismatch "$shared_request" "$resolved" "$shared_result" \ + "$shared_mismatch" +"$jq_bin" -e ' + .body.verdict=="violated" and + (.body.reason_ids|index("evidence.current-mismatch")!=null) +' "$tmp/shared-proof-mismatch.out" >/dev/null || fail 'logical ref identity mismatch' +pass 'changed logical proof identity with retained digest fails closed' + +absent_request="$tmp/qualification-absent.request" +absent_result="$tmp/qualification-absent.result" +absent_presentation="$tmp/qualification-absent.presentation" +absent_output="$tmp/qualification-absent.out" +"$jq_bin" -S -c 'del(.body.qualification_ref)' "$request" >"$absent_request" +absent_request_sha=$(sha256_path "$absent_request") +"$jq_bin" -L "$root/scripts/test" -S -c -n \ + --slurpfile request "$absent_request" --slurpfile resolved "$resolved" \ + --arg request_sha "$absent_request_sha" --arg resolved_sha "$resolved_sha" ' + import "portable-core-result-truth-fixtures" as result; + result::failed_result_doc($request[0];$request_sha;$resolved[0];$resolved_sha) | + walk(if type == "object" and has("schema_version") then .schema_version=2 else . end) +' >"$absent_result" +absent_result_sha=$(sha256_path "$absent_result") +"$jq_bin" -S -c --slurpfile request "$absent_request" \ + --slurpfile result "$absent_result" --arg request_sha "$absent_request_sha" \ + --arg result_sha "$absent_result_sha" ' + .body.qualification_ref={state:"absent"} | + .body.request_ref={schema_version:$request[0].schema_version,kind:$request[0].kind, + id:$request[0].id,sha256:$request_sha} | + .body.result_ref={schema_version:$result[0].schema_version,kind:$result[0].kind, + id:$result[0].id,sha256:$result_sha} | + .body.evidence=$result[0].body.evidence +' "$presentation" >"$absent_presentation" +run_eval_tuple qualification-absent "$absent_request" "$resolved" "$absent_result" \ + "$absent_presentation" +"$jq_bin" -e ' + .body.verdict=="satisfied" and + .body.qualification_observation=={state:"absent"} and + .body.qualification_semantics=="identity-only-unqualified" and + .body.authority_effect=="none" and .body.storage_effect=="none" and + (.body.evidence_refs[0].verdict=="failed") +' "$absent_output" >/dev/null || fail 'absent qualification identity-only' +pass 'absent qualification and inconclusive proof remain identity-only' + +malformed="$tmp/malformed.full" +"$jq_bin" -S -c '.body.evidence=1' "$presentation" >"$malformed" +run_eval malformed-full "$malformed" +"$jq_bin" -e '.body.verdict=="violated" and + (.body.reason_ids|index("evidence.presentation-malformed")!=null)' \ + "$tmp/malformed-full.out" >/dev/null || fail 'malformed full output' +pass 'malformed presentation returns canonical fail-closed observation' + +bad_result="$tmp/result.bad" +"$jq_bin" -S -c '.body.evidence[0].proof_ref.sha256="bad"' "$result" >"$bad_result" +expect_error malformed-core E_CORE "$policy_set" "$request" "$resolved" "$bad_result" + +bad_policy_set="$tmp/policy-set.bad" +"$jq_bin" -S -c '.body.sections[] |= + if .section_id=="evidence-integrity" then .decision_ref.sha256=("9"*64) else . end' \ + "$policy_set" >"$bad_policy_set" +expect_error policy-binding E_RELATION "$bad_policy_set" + +link="$tmp/presentation-link.json" +/bin/ln -s "$presentation" "$link" +expect_error symlink-input E_RUNTIME "$policy_set" "$request" "$resolved" "$result" "$link" + +/usr/bin/printf '{' >"$tmp/invalid.json" +expect_error invalid-json E_PARSE "$policy_set" "$request" "$resolved" "$result" \ + "$tmp/invalid.json" +/usr/bin/printf '{}\n{}\n' >"$tmp/multi-root.json" +expect_error multi-root E_PARSE "$policy_set" "$request" "$resolved" "$result" \ + "$tmp/multi-root.json" +"$jq_bin" . "$presentation" >"$tmp/noncanonical.json" +expect_error noncanonical E_CANONICAL "$policy_set" "$request" "$resolved" "$result" \ + "$tmp/noncanonical.json" +/usr/bin/perl -e 'print "["x33,"0","]"x33,"\n"' >"$tmp/deep.json" +expect_error depth-limit E_LIMIT "$policy_set" "$request" "$resolved" "$result" \ + "$tmp/deep.json" +"$jq_bin" -S -c '.id=("x"*8193)' "$presentation" >"$tmp/string-limit.json" +expect_error string-limit E_LIMIT "$policy_set" "$request" "$resolved" "$result" \ + "$tmp/string-limit.json" +/usr/bin/perl -e 'print "{\"x\":\"","x"x1048576,"\"}\n"' >"$tmp/oversize.json" +expect_error byte-limit E_LIMIT "$policy_set" "$request" "$resolved" "$result" \ + "$tmp/oversize.json" + +relative_bin="$tmp/relative-bin" +/bin/mkdir "$relative_bin" +/bin/cp "$jq_bin" "$relative_bin/jq" +status=0 +(cd "$relative_bin" && PATH=".:/usr/bin:/bin" "$evaluator" evaluate "$policy_set" \ + "$request" "$resolved" "$result" "$presentation") >"$tmp/relative.out" \ + 2>"$tmp/relative.err" || status=$? +[ "$status" -ne 0 ] && [ ! -s "$tmp/relative.out" ] && + [ "$(/bin/cat "$tmp/relative.err")" = E_RUNTIME ] || fail 'relative jq rejection' +pass 'relative jq interpreter rejected' + +wrapper_bin="$tmp/wrapper-bin" +/bin/mkdir "$wrapper_bin" +/usr/bin/printf '%s\n' '#!/bin/bash' \ + 'if [ "${1:-}" = --version ]; then echo jq-1.6; exit 0; fi' \ + 'exec /usr/bin/jq "$@"' >"$wrapper_bin/jq" +/bin/chmod 0555 "$wrapper_bin/jq" +unbound_status=0 +PATH="$wrapper_bin:/usr/bin:/bin" "$evaluator" evaluate "$policy_set" \ + "$request" "$resolved" "$result" "$presentation" >"$tmp/unbound-jq.out" \ + 2>"$tmp/unbound-jq.err" || unbound_status=$? +[ "$unbound_status" -ne 0 ] && [ ! -s "$tmp/unbound-jq.out" ] && + [ "$(/bin/cat "$tmp/unbound-jq.err")" = E_RUNTIME ] || fail 'unbound jq result' +pass 'only official jq 1.6 bytes are accepted' + +inherited_scratch="$tmp/ystack-evidence.KEEPIT" +/bin/mkdir -m 0700 "$inherited_scratch" +: >"$inherited_scratch/owned-by-caller" +inherited_status=0 +YSTACK_EVIDENCE_SCRATCH="$inherited_scratch" PATH="$bin:/usr/bin:/bin" \ + "$evaluator" invalid >"$tmp/inherited.out" 2>"$tmp/inherited.err" || + inherited_status=$? +[ "$inherited_status" -ne 0 ] && [ ! -s "$tmp/inherited.out" ] && + [ "$(/bin/cat "$tmp/inherited.err")" = E_USAGE ] && + [ -f "$inherited_scratch/owned-by-caller" ] || fail 'inherited scratch ownership' +pass 'bootstrap ignores inherited scratch cleanup authority' + +/usr/bin/grep -Fq \ + '"$jq_bin" -n -e --arg policy_sha "$policy_sha" --arg launcher_sha "$launcher_sha"' \ + "$evaluator" || fail 'decision envelope null-input mode' +pass 'decision envelope explicitly uses null input' + +copy_runtime() { + local destination=$1 path + /bin/mkdir -p "$destination/control/v1" "$destination/scripts" "$destination/core" + for path in evidence-integrity-policy.json evidence-integrity-decision.json \ + evidence-integrity.jq evaluate-evidence-integrity.sh validate.sh policy-set.jq; do + /bin/cp "$root/control/v1/$path" "$destination/control/v1/$path" + done + /bin/cp "$root/scripts/core-contract.sh" "$destination/scripts/core-contract.sh" + /bin/cp -R "$root/core/v2" "$destination/core/v2" +} + +forged_stage_case() { + local stage=$1 jq_mode=${2:-fake} + local suffix runtime="$tmp/forged-$stage-$jq_mode-runtime" scratch + local sentinel="$tmp/forged-$1-$jq_mode-sentinel" physical_tmp origin live + local driver_id live_id capability + local scratch_id + local private_driver_id + local status=0 + case "$stage:$jq_mode" in + supervisor:fake) suffix=FAKESV ;; + worker:fake) suffix=FAKEWK ;; + supervisor:official) suffix=OFFISV ;; + worker:official) suffix=OFFIWK ;; + esac + scratch="$tmp/ystack-evidence.$suffix" + copy_runtime "$runtime" + runtime=$(CDPATH='' cd -P -- "$runtime" && pwd -P) + /bin/mkdir -m 0700 "$scratch" "$scratch/bin" + scratch=$(CDPATH='' cd -P -- "$scratch" && pwd -P) + origin="$runtime/control/v1/evaluate-evidence-integrity.sh" + /bin/cp "$origin" "$scratch/driver.sh" + /bin/chmod 0500 "$scratch/driver.sh" + if [ "$jq_mode" = official ]; then + /bin/cp "$jq_bin" "$scratch/bin/jq" + else + /usr/bin/printf '%s\n' '#!/bin/bash' \ + "sentinel='$sentinel'" \ + 'if [ "${1:-}" = --version ]; then echo jq-1.6; exit 0; fi' \ + ': >"$sentinel"' \ + 'exec /usr/bin/jq "$@"' >"$scratch/bin/jq" + fi + /bin/chmod 0500 "$scratch/bin/jq" + live="$scratch/bin/jq" + scratch_id=$(test_directory_identity "$scratch") + driver_id=$(test_path_identity "$origin") + private_driver_id=$(test_path_identity "$scratch/driver.sh") + live_id=$(test_path_identity "$live") + physical_tmp=$(CDPATH='' cd -P -- "$tmp" && pwd -P) + capability="$scratch/caller.cap" + /usr/bin/mkfifo -m 0600 "$capability" + if [ "$stage" = supervisor ]; then + exec 8<>"$capability" + /bin/rm -f "$capability" + /usr/bin/printf 'supervisor\n' >&8 + else + exec 9<>"$capability" + /bin/rm -f "$capability" + /usr/bin/printf 'worker\n' >&9 + fi + /usr/bin/env -i LC_ALL=C PATH="$scratch/bin:/usr/bin:/bin" \ + YSTACK_EVIDENCE_STAGE="$stage" YSTACK_EVIDENCE_SCRATCH="$scratch" \ + YSTACK_EVIDENCE_SCRATCH_ID="$scratch_id" \ + YSTACK_EVIDENCE_ORIGIN="$origin" YSTACK_EVIDENCE_ORIGIN_ID="$driver_id" \ + YSTACK_EVIDENCE_PRIVATE_DRIVER_ID="$private_driver_id" \ + YSTACK_EVIDENCE_LIVE_JQ="$live" YSTACK_EVIDENCE_LIVE_JQ_ID="$live_id" \ + YSTACK_EVIDENCE_PRIVATE_JQ="$live" YSTACK_EVIDENCE_PRIVATE_JQ_ID="$live_id" \ + /bin/bash "$scratch/driver.sh" evaluate "$physical_tmp/policy-set.json" \ + "$physical_tmp/request.json" "$physical_tmp/resolved.json" \ + "$physical_tmp/result.json" "$physical_tmp/presentation.json" \ + >"$tmp/forged-$stage-$jq_mode.out" \ + 2>"$tmp/forged-$stage-$jq_mode.err" || status=$? + if [ "$stage" = supervisor ]; then exec 8>&-; else exec 9>&-; fi + [ "$status" -ne 0 ] && [ ! -s "$tmp/forged-$stage-$jq_mode.out" ] && + [ ! -s "$tmp/forged-$stage-$jq_mode.err" ] && + [ ! -e "$sentinel" ] || fail "forged $stage stage" + pass "forged $stage stage with $jq_mode jq cannot bypass parent capability" +} + +forged_stage_case supervisor +forged_stage_case worker +forged_stage_case supervisor official +forged_stage_case worker official + +direct_runtime="$tmp/direct-worker-runtime" +direct_scratch="$tmp/ystack-evidence.DIRECT" +copy_runtime "$direct_runtime" +/bin/mkdir -m 0700 "$direct_scratch" "$direct_scratch/bin" "$direct_scratch/worker" +direct_runtime=$(CDPATH='' cd -P -- "$direct_runtime" && pwd -P) +direct_scratch=$(CDPATH='' cd -P -- "$direct_scratch" && pwd -P) +direct_worker="$direct_scratch/worker" +direct_live_parent=$(CDPATH='' cd -P -- "${jq_bin%/*}" && pwd -P) +direct_live="$direct_live_parent/${jq_bin##*/}" +direct_input_parent=$(CDPATH='' cd -P -- "$tmp" && pwd -P) +direct_origin="$direct_runtime/control/v1/evaluate-evidence-integrity.sh" +/bin/cp "$direct_origin" "$direct_scratch/driver.sh" +/bin/chmod 0500 "$direct_scratch/driver.sh" +/bin/cp "$jq_bin" "$direct_scratch/bin/jq" +/bin/chmod 0500 "$direct_scratch/bin/jq" +direct_origin_id=$(test_path_identity "$direct_origin") +direct_driver_id=$(test_path_identity "$direct_scratch/driver.sh") +direct_payload_sha=$(test_payload_sha "$direct_scratch/driver.sh" "$direct_driver_id") +direct_live_id=$(test_path_identity "$direct_live") +direct_jq_id=$(test_path_identity "$direct_scratch/bin/jq") +direct_worker_id=$(test_directory_identity "$direct_worker") +direct_status=0 +/usr/bin/env -i LC_ALL=C PATH=/usr/bin:/bin TMPDIR="$direct_scratch" \ + HOME=/nonexistent /bin/bash -c \ + 'source "$1" || exit 125; shift; declare -F worker_main >/dev/null && exit 124; + worker_entry "$@"' direct-worker \ + "$direct_origin" "$direct_worker" "$direct_worker_id" "$direct_origin" \ + "$direct_origin_id" "$direct_scratch/driver.sh" "$direct_driver_id" \ + "$direct_payload_sha" "$direct_live" "$direct_live_id" \ + "$direct_scratch/bin/jq" "$direct_jq_id" \ + evaluate "$direct_input_parent/policy-set.json" "$direct_input_parent/request.json" \ + "$direct_input_parent/resolved.json" "$direct_input_parent/result.json" \ + "$direct_input_parent/presentation.json" \ + >"$tmp/direct-worker.out" 2>"$tmp/direct-worker.err" || direct_status=$? +if ! { [ "$direct_status" -eq 0 ] && [ -s "$tmp/direct-worker.out" ] && + [ ! -s "$tmp/direct-worker.err" ] && [ ! -e "$direct_worker" ] && + [ -z "$(/usr/bin/find "$direct_scratch" -name evaluation.json -print -quit)" ] && + "$jq_bin" -e '.kind=="evidence_integrity_evaluation"' \ + "$tmp/direct-worker.out" >/dev/null; }; then + fail 'direct sourced worker cleanup' +fi +pass 'direct sourced worker has observation output but no retained worker effects' + +mutated_decision_runtime="$tmp/mutated-decision-runtime" +copy_runtime "$mutated_decision_runtime" +"$jq_bin" -S -c '.body.semantics.authority_effect="unexpected"' \ + "$mutated_decision_runtime/control/v1/evidence-integrity-decision.json" \ + >"$mutated_decision_runtime/decision.next" +/bin/mv "$mutated_decision_runtime/decision.next" \ + "$mutated_decision_runtime/control/v1/evidence-integrity-decision.json" +mutated_decision_sha=$(sha256_path \ + "$mutated_decision_runtime/control/v1/evidence-integrity-decision.json") +mutated_decision_set="$tmp/mutated-decision-policy-set.json" +"$jq_bin" -S -c --arg digest "$mutated_decision_sha" ' + .body.sections[] |= if .section_id=="evidence-integrity" + then .decision_ref.sha256=$digest else . end +' "$policy_set" >"$mutated_decision_set" +expect_error mutated-decision E_RELATION "$mutated_decision_set" "$request" \ + "$resolved" "$result" "$presentation" "$mutated_decision_runtime" + +wait_exit() { + local pid=$1 limit=${2:-500} attempt=0 + while kill -0 "$pid" 2>/dev/null && [ "$attempt" -lt "$limit" ]; do + attempt=$((attempt + 1)) + /bin/sleep 0.01 + done + ! kill -0 "$pid" 2>/dev/null +} +slice_until_path() { + local pid=$1 search_root=$2 suffix=$3 attempt=0 found + /bin/kill -STOP "$pid" 2>/dev/null || return 1 + while [ "$attempt" -lt 800 ]; do + found=$(/usr/bin/find "$search_root" -type f -path "*/$suffix" -print -quit \ + 2>/dev/null) || found= + if [ -n "$found" ]; then /usr/bin/printf '%s\n' "$found"; return 0; fi + kill -0 "$pid" 2>/dev/null || return 1 + /bin/kill -CONT "$pid" 2>/dev/null || return 1 + /bin/sleep 0.005 + /bin/kill -STOP "$pid" 2>/dev/null || return 1 + attempt=$((attempt + 1)) + done + return 1 +} + +replace_identity_case() { + local name=$1 target_kind=$2 expected=${3:-E_RELATION} + local runtime="$tmp/$1-runtime" live_bin="$tmp/$1-bin" + local scratch_root="$tmp/$1-scratch" process target observed suffix case_status=0 + copy_runtime "$runtime" + /bin/mkdir "$live_bin" "$scratch_root" + /bin/cp "$jq_bin" "$live_bin/jq" + /bin/chmod 0555 "$live_bin/jq" + TMPDIR="$scratch_root" PATH="$live_bin:/usr/bin:/bin" \ + "$runtime/control/v1/evaluate-evidence-integrity.sh" evaluate "$policy_set" \ + "$request" "$resolved" "$result" "$presentation" >"$tmp/$name.out" \ + 2>"$tmp/$name.err" & + process=$! + suffix=driver.sh + case "$target_kind" in + private-driver) suffix=bin/jq ;; + live-jq|private-jq) suffix=evaluation.json ;; + esac + observed=$(slice_until_path "$process" "$scratch_root" "$suffix") || { + /bin/kill -KILL "$process" 2>/dev/null || : + wait "$process" 2>/dev/null || : + fail "$name observed path" + } + case "$target_kind" in + origin) target="$runtime/control/v1/evaluate-evidence-integrity.sh" ;; + private-driver) target="${observed%/bin/jq}/driver.sh" ;; + live-jq) target="$live_bin/jq" ;; + private-jq) target="${observed%/worker/evaluation.json}/bin/jq" ;; + *) fail "$name target" ;; + esac + /bin/cp "$target" "$target.next" + /bin/chmod 0500 "$target.next" + /bin/mv "$target.next" "$target" + /bin/kill -CONT "$process" 2>/dev/null || : + wait "$process" || case_status=$? + if [ "$case_status" -eq 0 ] || [ -s "$tmp/$name.out" ] || + [ "$(/bin/cat "$tmp/$name.err")" != "$expected" ] || + [ -n "$(/usr/bin/find "$scratch_root" -mindepth 1 -print -quit)" ]; then + /usr/bin/printf 'diagnostic %s status=%s stderr=' "$name" "$case_status" >&2 + /bin/cat "$tmp/$name.err" >&2 + fail "$name result" + fi + pass "$name" +} + +replace_identity_case origin-driver-swap origin +replace_identity_case executing-driver-swap private-driver +replace_identity_case live-jq-swap live-jq +replace_identity_case private-jq-swap private-jq + +program_runtime="$tmp/program-race-runtime" +program_scratch="$tmp/program-race-scratch" +copy_runtime "$program_runtime" +/bin/mkdir "$program_scratch" +TMPDIR="$program_scratch" PATH="$bin:/usr/bin:/bin" \ + "$program_runtime/control/v1/evaluate-evidence-integrity.sh" evaluate "$policy_set" \ + "$request" "$resolved" "$result" "$presentation" >"$tmp/program-race.out" \ + 2>"$tmp/program-race.err" & +program_pid=$! +program_snapshot= +program_attempt=0 +while [ -z "$program_snapshot" ] && [ "$program_attempt" -lt 800 ]; do + program_snapshot=$(/usr/bin/find "$program_scratch" -type f -name program.jq \ + -print -quit 2>/dev/null) || program_snapshot= + [ -z "$program_snapshot" ] || break + kill -0 "$program_pid" 2>/dev/null || break + program_attempt=$((program_attempt + 1)) + /bin/sleep 0.005 +done +[ -n "$program_snapshot" ] || fail 'program race snapshot' +/usr/bin/printf '\n' >>"$program_runtime/control/v1/evidence-integrity.jq" +program_status=0 +wait "$program_pid" || program_status=$? +[ "$program_status" -ne 0 ] && [ ! -s "$tmp/program-race.out" ] && + [ "$(/bin/cat "$tmp/program-race.err")" = E_RELATION ] || fail 'program race' +pass 'live program mutation closes after no-follow snapshot' + +find_owned_leader() { + /bin/ps -axo pid=,ppid=,pgid= 2>/dev/null | /usr/bin/awk -v parent="$1" \ + '$2==parent && $1==$3 {print $1; exit}' +} +signal_scratch="$tmp/signal-scratch" +/bin/mkdir "$signal_scratch" +TMPDIR="$signal_scratch" PATH="$bin:/usr/bin:/bin" "$evaluator" evaluate \ + "$policy_set" "$request" "$resolved" "$result" "$presentation" \ + >"$tmp/signal.out" 2>"$tmp/signal.err" & +signal_pid=$! +signal_leader= +signal_attempt=0 +while [ -z "$signal_leader" ] && [ "$signal_attempt" -lt 800 ]; do + signal_leader=$(find_owned_leader "$signal_pid") || signal_leader= + [ -z "$signal_leader" ] || break + kill -0 "$signal_pid" 2>/dev/null || break + signal_attempt=$((signal_attempt + 1)) + /bin/sleep 0.005 +done +[[ "$signal_leader" =~ ^[1-9][0-9]*$ ]] || fail 'owned child leader' +/bin/kill -STOP -- "-$signal_leader" +/bin/kill -TERM "$signal_pid" +wait_exit "$signal_pid" 500 || fail 'signal bounded exit' +signal_status=0 +wait "$signal_pid" || signal_status=$? +signal_live=$(/bin/ps -axo pgid=,state= 2>/dev/null | /usr/bin/awk \ + -v group="$signal_leader" '$1==group && $2!~/^Z/ {count++} END {print count+0}') +[ "$signal_status" -ne 0 ] && [ "$signal_live" -eq 0 ] && + [ ! -s "$tmp/signal.out" ] && + [ -z "$(/usr/bin/find "$signal_scratch" -mindepth 1 -print -quit)" ] || + fail 'signal cleanup' +pass 'signal kills and reaps the stopped owned child group' + +bind_modified_driver() { + local runtime=$1 output_set=$2 driver_digest payload_digest decision_digest + runtime=$(CDPATH='' cd -P -- "$runtime" && pwd -P) + driver_digest=$(sha256_path "$runtime/control/v1/evaluate-evidence-integrity.sh") + payload_digest=$(test_payload_sha \ + "$runtime/control/v1/evaluate-evidence-integrity.sh") + "$jq_bin" -S -c --arg launcher "$driver_digest" --arg payload "$payload_digest" ' + .body.evaluator.trusted_launcher_ref.sha256=$launcher | + .body.evaluator.evaluation_payload_ref.sha256=$payload + ' \ + "$runtime/control/v1/evidence-integrity-decision.json" >"$runtime/decision.next" + /bin/mv "$runtime/decision.next" \ + "$runtime/control/v1/evidence-integrity-decision.json" + decision_digest=$(sha256_path "$runtime/control/v1/evidence-integrity-decision.json") + "$jq_bin" -S -c --arg digest "$decision_digest" ' + .body.sections[] |= if .section_id=="evidence-integrity" + then .decision_ref.sha256=$digest else . end + ' "$policy_set" >"$output_set" +} + +core_stall_scratch="$tmp/core-stall-scratch" +/bin/mkdir "$core_stall_scratch" +core_stall_scratch=$(CDPATH='' cd -P -- "$core_stall_scratch" && pwd -P) +TMPDIR="$core_stall_scratch" PATH="$bin:/usr/bin:/bin" \ + "$evaluator" evaluate "$policy_set" "$request" "$resolved" "$result" "$presentation" \ + >"$tmp/core-stall.out" 2>"$tmp/core-stall.err" & +core_stall_parent=$! +core_stall_attempt=0 +core_stall_pid= +while [ -z "$core_stall_pid" ] && kill -0 "$core_stall_parent" 2>/dev/null && + [ "$core_stall_attempt" -lt "$late_marker_attempts" ]; do + core_stall_temp=$(/usr/bin/find "$core_stall_scratch" -type d \ + -path '*/worker/core-accounted/portable-core-accounted-v2.*' \ + -print -quit 2>/dev/null) || core_stall_temp= + if [ -n "$core_stall_temp" ]; then + core_stall_candidate=$(find_owned_leader "$core_stall_parent") || + core_stall_candidate= + if [[ "$core_stall_candidate" =~ ^[1-9][0-9]*$ ]] && + /bin/kill -STOP -- "-$core_stall_candidate" 2>/dev/null; then + core_stall_pid=$core_stall_candidate + break + fi + fi + core_stall_attempt=$((core_stall_attempt + 1)); /bin/sleep 0.005 +done +if [ -z "$core_stall_pid" ]; then + core_stall_early_status=0 + wait "$core_stall_parent" || core_stall_early_status=$? + /usr/bin/printf 'diagnostic nested-core status=%s stderr=' \ + "$core_stall_early_status" >&2 + /bin/cat "$tmp/core-stall.err" >&2 + fail 'nested core stall marker' +fi +[[ "$core_stall_pid" =~ ^[1-9][0-9]*$ ]] || fail 'nested core stall pid' +/bin/kill -TERM "$core_stall_parent" +wait_exit "$core_stall_parent" 500 || fail 'nested core bounded exit' +core_stall_status=0 +wait "$core_stall_parent" || core_stall_status=$? +[ "$core_stall_status" -ne 0 ] && ! kill -0 "$core_stall_pid" 2>/dev/null && + [ ! -s "$tmp/core-stall.out" ] && [ ! -s "$tmp/core-stall.err" ] && + [ -z "$(/usr/bin/find "$core_stall_scratch" -mindepth 1 -print -quit)" ] || + fail 'nested core signal cleanup' +pass 'stopped nested core is killed and worker-owned scratch is removed' + +/usr/bin/grep -Fq 'ulimit -f 2048' "$evaluator" || fail 'child output cap' +/usr/bin/grep -Fq '[ "$attempt" -lt 1000 ]' "$evaluator" || fail 'child deadline' +pass 'child runtime and output are bounded' + +launch_runtime="$tmp/launch-signal-runtime" +launch_scratch="$tmp/launch-signal-scratch" +launch_ready="$tmp/launch-signal.ready" +launch_go="$tmp/launch-signal.go" +copy_runtime "$launch_runtime" +/bin/mkdir "$launch_scratch" +LAUNCH_READY="$launch_ready" LAUNCH_GO="$launch_go" \ + /usr/bin/perl -0777 -pi -e ' + my $ready=$ENV{"LAUNCH_READY"}; my $go=$ENV{"LAUNCH_GO"}; + my $replacement = qq{ child=\$!\n} . + qq{ /usr/bin/printf "%s\\n" "\$child" >"$ready" || exit 125\n} . + qq{ while [ ! -e "$go" ]; do /bin/sleep 0.01; done}; + s{ child=\$!}{$replacement} or exit 2; + ' "$launch_runtime/control/v1/evaluate-evidence-integrity.sh" +bind_modified_driver "$launch_runtime" "$tmp/launch-signal-set.json" +TMPDIR="$launch_scratch" PATH="$bin:/usr/bin:/bin" \ + "$launch_runtime/control/v1/evaluate-evidence-integrity.sh" evaluate \ + "$tmp/launch-signal-set.json" "$request" "$resolved" "$result" "$presentation" \ + >"$tmp/launch-signal.out" 2>"$tmp/launch-signal.err" & +launch_pid=$! +launch_attempt=0 +while [ ! -s "$launch_ready" ] && kill -0 "$launch_pid" 2>/dev/null && + [ "$launch_attempt" -lt 1200 ]; do + launch_attempt=$((launch_attempt + 1)); /bin/sleep 0.005 +done +[ -s "$launch_ready" ] || fail 'launch signal ready' +launch_child=$(/bin/cat "$launch_ready") +[[ "$launch_child" =~ ^[1-9][0-9]*$ ]] || fail 'launch signal child' +/bin/kill -TERM "$launch_pid" +: >"$launch_go" +wait_exit "$launch_pid" 500 || fail 'launch signal bounded exit' +launch_status=0 +wait "$launch_pid" || launch_status=$? +launch_live=$(/bin/ps -axo pgid=,state= 2>/dev/null | /usr/bin/awk \ + -v group="$launch_child" '$1==group && $2!~/^Z/ {count++} END {print count+0}') +[ "$launch_status" -ne 0 ] && ! kill -0 "$launch_child" 2>/dev/null && + [ "$launch_live" -eq 0 ] && [ ! -s "$tmp/launch-signal.out" ] && + [ ! -s "$tmp/launch-signal.err" ] && + [ -z "$(/usr/bin/find "$launch_scratch" -mindepth 1 -print -quit)" ] || + fail 'launch signal cleanup' +pass 'launch-window signals wait for owned child registration and reap' + +output_runtime="$tmp/output-swap-runtime" +output_scratch="$tmp/output-swap-scratch" +copy_runtime "$output_runtime" +/usr/bin/perl -0777 -pi -e ' + s{capture_full_identity_matches "\$scratch/evaluation\.json" "\$evaluation_final" \|\|}{ + /bin/mv "\$scratch/evaluation.json" "\$scratch/evaluation.saved" || exit 1; + /bin/ln -s "\$scratch/evaluation.saved" "\$scratch/evaluation.json" || exit 1; + capture_full_identity_matches "\$scratch/evaluation.json" "\$evaluation_final" || + } or exit 2 +' "$output_runtime/control/v1/evaluate-evidence-integrity.sh" +bind_modified_driver "$output_runtime" "$tmp/output-swap-set.json" +/bin/mkdir "$output_scratch" +output_status=0 +TMPDIR="$output_scratch" PATH="$bin:/usr/bin:/bin" \ + "$output_runtime/control/v1/evaluate-evidence-integrity.sh" evaluate \ + "$tmp/output-swap-set.json" "$request" "$resolved" "$result" "$presentation" \ + >"$tmp/output-swap.out" 2>"$tmp/output-swap.err" || output_status=$? +[ "$output_status" -ne 0 ] && [ ! -s "$tmp/output-swap.out" ] && + [ "$(/bin/cat "$tmp/output-swap.err")" = E_RUNTIME ] && + [ -z "$(/usr/bin/find "$output_scratch" -mindepth 1 -print -quit)" ] || + fail 'output symlink swap' +pass 'output path replacement is rejected before cleanup and emission' + +post_capture_runtime="$tmp/post-capture-runtime" +post_capture_scratch="$tmp/post-capture-scratch" +post_capture_ready="$tmp/post-capture.ready" +post_capture_go="$tmp/post-capture.go" +copy_runtime "$post_capture_runtime" +POST_CAPTURE_READY="$post_capture_ready" POST_CAPTURE_GO="$post_capture_go" \ + /usr/bin/perl -0777 -pi -e ' + my $ready=$ENV{"POST_CAPTURE_READY"}; my $go=$ENV{"POST_CAPTURE_GO"}; + my $replacement = qq{ worker_status=\$?\n} . + qq{ /usr/bin/printf "ready\\n" >"$ready" || exit 1\n} . + qq{ while [ ! -e "$go" ]; do /bin/sleep 0.01; done\n} . + qq{ if [ -n}; + s{ worker_status=\$\?\n if \[ -n}{$replacement} or exit 2; + ' "$post_capture_runtime/control/v1/evaluate-evidence-integrity.sh" +bind_modified_driver "$post_capture_runtime" "$tmp/post-capture-set.json" +/bin/mkdir "$post_capture_scratch" +TMPDIR="$post_capture_scratch" PATH="$bin:/usr/bin:/bin" \ + "$post_capture_runtime/control/v1/evaluate-evidence-integrity.sh" evaluate \ + "$tmp/post-capture-set.json" "$request" "$resolved" "$result" "$presentation" \ + >"$tmp/post-capture.out" 2>"$tmp/post-capture.err" & +post_capture_pid=$! +post_capture_attempt=0 +while [ ! -e "$post_capture_ready" ] && kill -0 "$post_capture_pid" 2>/dev/null && + [ "$post_capture_attempt" -lt "$late_marker_attempts" ]; do + post_capture_attempt=$((post_capture_attempt + 1)); /bin/sleep 0.005 +done +[ -e "$post_capture_ready" ] || fail 'post-capture replacement ready' +post_capture_root=$(/usr/bin/find "$post_capture_scratch" -mindepth 1 -maxdepth 1 \ + -type d -name 'ystack-evidence.??????' -print -quit) +[ -n "$post_capture_root" ] || fail 'post-capture replacement root' +[ ! -e "$post_capture_root/io/worker.identity" ] || + fail 'post-capture metadata channel remained path-backed' +/bin/mv "$post_capture_root/io/worker.out" "$post_capture_root/io/worker.saved" +/usr/bin/printf '{"forged":true}\n' >"$post_capture_root/io/worker.out" +: >"$post_capture_go" +post_capture_status=0 +wait "$post_capture_pid" || post_capture_status=$? +[ "$post_capture_status" -ne 0 ] && [ ! -s "$tmp/post-capture.out" ] && + [ "$(/bin/cat "$tmp/post-capture.err")" = E_RUNTIME ] && + [ -z "$(/usr/bin/find "$post_capture_scratch" -mindepth 1 -print -quit)" ] || + fail 'post-capture replacement result' +pass 'post-creation output replacement is rejected before consumption' + +post_inplace_runtime="$tmp/post-inplace-runtime" +post_inplace_scratch="$tmp/post-inplace-scratch" +post_inplace_ready="$tmp/post-inplace.ready" +post_inplace_go="$tmp/post-inplace.go" +copy_runtime "$post_inplace_runtime" +POST_INPLACE_READY="$post_inplace_ready" POST_INPLACE_GO="$post_inplace_go" \ + /usr/bin/perl -0777 -pi -e ' + my $ready=$ENV{"POST_INPLACE_READY"}; my $go=$ENV{"POST_INPLACE_GO"}; + my $replacement = qq{ worker_status=\$?\n} . + qq{ /usr/bin/printf "ready\\n" >"$ready" || exit 1\n} . + qq{ while [ ! -e "$go" ]; do /bin/sleep 0.01; done\n} . + qq{ if [ -n}; + s{ worker_status=\$\?\n if \[ -n}{$replacement} or exit 2; + ' "$post_inplace_runtime/control/v1/evaluate-evidence-integrity.sh" +bind_modified_driver "$post_inplace_runtime" "$tmp/post-inplace-set.json" +/bin/mkdir "$post_inplace_scratch" +TMPDIR="$post_inplace_scratch" PATH="$bin:/usr/bin:/bin" \ + "$post_inplace_runtime/control/v1/evaluate-evidence-integrity.sh" evaluate \ + "$tmp/post-inplace-set.json" "$request" "$resolved" "$result" "$presentation" \ + >"$tmp/post-inplace.out" 2>"$tmp/post-inplace.err" & +post_inplace_pid=$! +post_inplace_attempt=0 +while [ ! -e "$post_inplace_ready" ] && kill -0 "$post_inplace_pid" 2>/dev/null && + [ "$post_inplace_attempt" -lt "$late_marker_attempts" ]; do + post_inplace_attempt=$((post_inplace_attempt + 1)); /bin/sleep 0.005 +done +[ -e "$post_inplace_ready" ] || fail 'post-producer in-place ready' +post_inplace_root=$(/usr/bin/find "$post_inplace_scratch" -mindepth 1 -maxdepth 1 \ + -type d -name 'ystack-evidence.??????' -print -quit) +[ -n "$post_inplace_root" ] || fail 'post-producer in-place root' +post_inplace_root=$(CDPATH='' cd -P -- "$post_inplace_root" && pwd -P) +[ ! -e "$post_inplace_root/io/worker.identity" ] || + fail 'post-producer metadata channel remained path-backed' +post_inplace_identity_before=$(test_path_identity "$post_inplace_root/io/worker.out") +post_inplace_inode_before=$(/usr/bin/printf '%s\n' "$post_inplace_identity_before" | + /usr/bin/awk -F: '{print $3":"$4}') +/usr/bin/printf '{"forged":true}\n' >"$post_inplace_root/io/worker.out" +post_inplace_identity_after=$(test_path_identity "$post_inplace_root/io/worker.out") +post_inplace_inode_after=$(/usr/bin/printf '%s\n' "$post_inplace_identity_after" | + /usr/bin/awk -F: '{print $3":"$4}') +[ "$post_inplace_inode_before" = "$post_inplace_inode_after" ] || + fail 'post-producer mutation changed inode' +: >"$post_inplace_go" +post_inplace_status=0 +wait "$post_inplace_pid" || post_inplace_status=$? +[ "$post_inplace_status" -ne 0 ] && [ ! -s "$tmp/post-inplace.out" ] && + [ "$(/bin/cat "$tmp/post-inplace.err")" = E_RUNTIME ] && + [ -z "$(/usr/bin/find "$post_inplace_scratch" -mindepth 1 -print -quit)" ] || + fail 'post-producer in-place result' +pass 'producer-final full identity rejects same-inode output mutation' + +cleanup_runtime="$tmp/cleanup-failure-runtime" +cleanup_scratch="$tmp/cleanup-failure-scratch" +copy_runtime "$cleanup_runtime" +/usr/bin/perl -0777 -pi -e 's/cleanup\(\) \{\n/cleanup() {\n return 1;\n/' \ + "$cleanup_runtime/control/v1/evaluate-evidence-integrity.sh" +bind_modified_driver "$cleanup_runtime" "$tmp/cleanup-failure-set.json" +/bin/mkdir "$cleanup_scratch" +cleanup_status=0 +TMPDIR="$cleanup_scratch" PATH="$bin:/usr/bin:/bin" \ + "$cleanup_runtime/control/v1/evaluate-evidence-integrity.sh" evaluate \ + "$tmp/cleanup-failure-set.json" "$request" "$resolved" "$result" "$presentation" \ + >"$tmp/cleanup-failure.out" 2>"$tmp/cleanup-failure.err" || cleanup_status=$? +[ "$cleanup_status" -ne 0 ] && [ ! -s "$tmp/cleanup-failure.out" ] || + fail 'cleanup failure output' +pass 'cleanup failure remains non-success and emits no stdout' + +cleanup_error_scratch="$tmp/cleanup-error-scratch" +/bin/mkdir "$cleanup_error_scratch" +cleanup_error_status=0 +TMPDIR="$cleanup_error_scratch" PATH="$bin:/usr/bin:/bin" \ + "$cleanup_runtime/control/v1/evaluate-evidence-integrity.sh" evaluate \ + "$tmp/cleanup-failure-set.json" "$request" "$resolved" "$result" \ + "$tmp/invalid.json" >"$tmp/cleanup-error.out" 2>"$tmp/cleanup-error.err" || + cleanup_error_status=$? +[ "$cleanup_error_status" -ne 0 ] && [ ! -s "$tmp/cleanup-error.out" ] && + [ ! -s "$tmp/cleanup-error.err" ] || fail 'cleanup failure error-path output' +pass 'error paths emit nothing when scratch cleanup fails' + +capture_swap_case() { + local mode=$1 runtime="$tmp/capture-$1-runtime" + local parent="$tmp/capture-$1-parent" outside="$tmp/capture-$1-outside" + local ready="$tmp/capture-$1.ready" go="$tmp/capture-$1.go" + local process scratch_path status=0 attempt=0 + copy_runtime "$runtime" + /bin/mkdir "$parent" "$outside" + /usr/bin/printf 'outside-unchanged\n' >"$outside/sentinel" + CAPTURE_READY="$ready" CAPTURE_GO="$go" /usr/bin/perl -0777 -pi -e ' + my $ready=$ENV{"CAPTURE_READY"}; my $go=$ENV{"CAPTURE_GO"}; + my $replacement = qq{ /usr/bin/printf "ready\\n" >"$ready" || exit 1\n} . + qq{ while [ ! -e "$go" ]; do /bin/sleep 0.01; done\n} . + qq{ run_child scratch_capture_prepared}; + s{ run_child scratch_capture_prepared}{$replacement} or exit 2; + ' "$runtime/control/v1/evaluate-evidence-integrity.sh" + bind_modified_driver "$runtime" "$tmp/capture-$1-set.json" + TMPDIR="$parent" PATH="$bin:/usr/bin:/bin" \ + "$runtime/control/v1/evaluate-evidence-integrity.sh" evaluate \ + "$tmp/capture-$1-set.json" "$request" "$resolved" "$result" "$presentation" \ + >"$tmp/capture-$1.out" 2>"$tmp/capture-$1.err" & + process=$! + while [ ! -e "$ready" ] && kill -0 "$process" 2>/dev/null && + [ "$attempt" -lt 1200 ]; do + attempt=$((attempt + 1)); /bin/sleep 0.005 + done + [ -e "$ready" ] || fail "capture $mode ready" + scratch_path=$(/usr/bin/find "$parent" -mindepth 1 -maxdepth 1 -type d \ + -name 'ystack-evidence.??????' -print -quit) + [ -n "$scratch_path" ] || fail "capture $mode scratch" + case "$mode" in + ancestor) + /bin/mv "$scratch_path/io" "$scratch_path/io.saved" + /bin/ln -s "$outside" "$scratch_path/io" + ;; + leaf) + /bin/mv "$scratch_path/io/worker.out" "$scratch_path/io/worker.out.saved" + /bin/ln -s "$outside/sentinel" "$scratch_path/io/worker.out" + ;; + *) fail "capture $mode mode" ;; + esac + : >"$go" + wait "$process" || status=$? + [ "$status" -ne 0 ] && [ ! -s "$tmp/capture-$1.out" ] && + [ "$(/bin/cat "$tmp/capture-$1.err")" = E_RUNTIME ] && + [ "$(/bin/cat "$outside/sentinel")" = outside-unchanged ] && + [ ! -e "$outside/worker.out" ] && [ ! -e "$outside/worker.err" ] && + [ -z "$(/usr/bin/find "$parent" -mindepth 1 -print -quit)" ] || + fail "capture $mode result" + pass "anchored scratch capture rejects $mode replacement" +} + +capture_swap_case leaf +capture_swap_case ancestor + +bin_swap_runtime="$tmp/bin-swap-runtime" +bin_swap_parent="$tmp/bin-swap-parent" +bin_swap_outside="$tmp/bin-swap-outside" +bin_swap_ready="$tmp/bin-swap.ready" +bin_swap_go="$tmp/bin-swap.go" +copy_runtime "$bin_swap_runtime" +/bin/mkdir "$bin_swap_parent" "$bin_swap_outside" +/usr/bin/printf 'outside-unchanged\n' >"$bin_swap_outside/sentinel" +BIN_SWAP_READY="$bin_swap_ready" BIN_SWAP_GO="$bin_swap_go" \ + /usr/bin/perl -0777 -pi -e ' + my $ready=$ENV{"BIN_SWAP_READY"}; my $go=$ENV{"BIN_SWAP_GO"}; + my $replacement = qq{scratch_mkdirs bin io worker || emit_error E_RUNTIME\n} . + qq{/usr/bin/printf "ready\\n" >"$ready" || exit 1\n} . + qq{while [ ! -e "$go" ]; do /bin/sleep 0.01; done}; + s{scratch_mkdirs bin io worker \|\| emit_error E_RUNTIME}{$replacement} or exit 2; + ' "$bin_swap_runtime/control/v1/evaluate-evidence-integrity.sh" +bind_modified_driver "$bin_swap_runtime" "$tmp/bin-swap-set.json" +TMPDIR="$bin_swap_parent" PATH="$bin:/usr/bin:/bin" \ + "$bin_swap_runtime/control/v1/evaluate-evidence-integrity.sh" evaluate \ + "$tmp/bin-swap-set.json" "$request" "$resolved" "$result" "$presentation" \ + >"$tmp/bin-swap.out" 2>"$tmp/bin-swap.err" & +bin_swap_pid=$! +bin_swap_attempt=0 +while [ ! -e "$bin_swap_ready" ] && kill -0 "$bin_swap_pid" 2>/dev/null && + [ "$bin_swap_attempt" -lt 1200 ]; do + bin_swap_attempt=$((bin_swap_attempt + 1)); /bin/sleep 0.005 +done +[ -e "$bin_swap_ready" ] || fail 'bin ancestor swap ready' +bin_swap_scratch=$(/usr/bin/find "$bin_swap_parent" -mindepth 1 -maxdepth 1 \ + -type d -name 'ystack-evidence.??????' -print -quit) +[ -n "$bin_swap_scratch" ] || fail 'bin ancestor swap scratch' +/bin/mv "$bin_swap_scratch/bin" "$bin_swap_scratch/bin.saved" +/bin/ln -s "$bin_swap_outside" "$bin_swap_scratch/bin" +: >"$bin_swap_go" +bin_swap_status=0 +wait "$bin_swap_pid" || bin_swap_status=$? +[ "$bin_swap_status" -ne 0 ] && [ ! -s "$tmp/bin-swap.out" ] && + [ "$(/bin/cat "$tmp/bin-swap.err")" = E_RUNTIME ] && + [ "$(/bin/cat "$bin_swap_outside/sentinel")" = outside-unchanged ] && + [ ! -e "$bin_swap_outside/jq" ] && + [ -z "$(/usr/bin/find "$bin_swap_parent" -mindepth 1 -print -quit)" ] || + fail 'bin ancestor swap result' +pass 'scratch bin ancestor swap cannot create or chmod outside files' + +scratch_swap_runtime="$tmp/scratch-swap-runtime" +scratch_swap_parent="$tmp/scratch-swap-parent" +scratch_swap_ready="$tmp/scratch-swap.ready" +scratch_swap_go="$tmp/scratch-swap.go" +copy_runtime "$scratch_swap_runtime" +/bin/mkdir "$scratch_swap_parent" +SCRATCH_READY="$scratch_swap_ready" SCRATCH_GO="$scratch_swap_go" \ + /usr/bin/perl -0777 -pi -e ' + my $ready=$ENV{"SCRATCH_READY"}; my $go=$ENV{"SCRATCH_GO"}; + my $replacement = qq{ /usr/bin/printf "ready\\n" >"$ready" || exit 1;\n} . + qq{ while [ ! -e "$go" ]; do /bin/sleep 0.01; done\n} . + qq{ if ! cleanup; then exit 1; fi}; + s{ if ! cleanup; then exit 1; fi}{$replacement} or exit 2; + ' "$scratch_swap_runtime/control/v1/evaluate-evidence-integrity.sh" +bind_modified_driver "$scratch_swap_runtime" "$tmp/scratch-swap-set.json" +TMPDIR="$scratch_swap_parent" PATH="$bin:/usr/bin:/bin" \ + "$scratch_swap_runtime/control/v1/evaluate-evidence-integrity.sh" evaluate \ + "$tmp/scratch-swap-set.json" "$request" "$resolved" "$result" "$presentation" \ + >"$tmp/scratch-swap.out" 2>"$tmp/scratch-swap.err" & +scratch_swap_pid=$! +scratch_swap_attempt=0 +while [ ! -e "$scratch_swap_ready" ] && kill -0 "$scratch_swap_pid" 2>/dev/null && + [ "$scratch_swap_attempt" -lt "$late_marker_attempts" ]; do + scratch_swap_attempt=$((scratch_swap_attempt + 1)); /bin/sleep 0.005 +done +[ -e "$scratch_swap_ready" ] || fail 'scratch swap ready' +scratch_original=$(/usr/bin/find "$scratch_swap_parent" -mindepth 1 -maxdepth 1 \ + -type d -name 'ystack-evidence.??????' -print -quit) +[ -n "$scratch_original" ] || fail 'scratch swap original' +/bin/mv "$scratch_original" "$scratch_original.saved" +/bin/mkdir -m 0700 "$scratch_original" +: >"$scratch_original/replacement" +: >"$scratch_swap_go" +scratch_swap_status=0 +wait "$scratch_swap_pid" || scratch_swap_status=$? +[ "$scratch_swap_status" -ne 0 ] && [ ! -s "$tmp/scratch-swap.out" ] && + [ ! -s "$tmp/scratch-swap.err" ] && [ -d "$scratch_original.saved" ] && + [ -f "$scratch_original/replacement" ] || fail 'scratch swap cleanup identity' +pass 'scratch path replacement cannot authorize cleanup or output' + +for required in control/v1/evidence-integrity-policy.json \ + control/v1/evidence-integrity-decision.json control/v1/evidence-integrity.jq \ + control/v1/evaluate-evidence-integrity.sh \ + scripts/test/control-evidence-integrity.test.sh; do + [ "$(/usr/bin/grep -Fxc "$required" "$root/ci/required-files.txt")" -eq 1 ] || + fail "manifest $required" +done +/usr/bin/grep -Fq 'Inactive evidence-integrity evaluator' "$root/README.md" || + fail 'README docs' +/usr/bin/grep -Fq 'control-evidence-integrity.test.sh' "$root/RESTORE.md" || + fail 'RESTORE docs' +pass 'restore manifest and docs' + +/usr/bin/printf 'control evidence integrity: %s passed\n' "$passes"