fed-sx-m1: Step 6d-cs — outbox:construct + sign + cid_of + 13 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 32s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 32s
This commit is contained in:
55
next/kernel/outbox.erl
Normal file
55
next/kernel/outbox.erl
Normal file
@@ -0,0 +1,55 @@
|
||||
-module(outbox).
|
||||
-export([construct/4, sign/2, cid_of/1]).
|
||||
|
||||
%% Outbox envelope construction + signing per design §3.1.
|
||||
%%
|
||||
%% construct/4 builds an unsigned activity envelope from caller-supplied
|
||||
%% (Type, ActorId, Published, Object). The envelope's `:id` field is
|
||||
%% derived from the host `cid:to_string` BIF over a skeleton tag, so
|
||||
%% recipients can address the activity by its content hash. The
|
||||
%% returned property list is the canonical key-sorted form that
|
||||
%% `envelope:canonical_bytes/1` operates on.
|
||||
%%
|
||||
%% sign/2 takes the unsigned envelope plus a KeySpec proplist that
|
||||
%% mirrors a `public_keys` entry: `[{key_id, _}, {algorithm, _},
|
||||
%% {value, KeyMaterial}]`. It computes the v1 HMAC stand-in
|
||||
%% `crypto:hash(sha256, <<KeyMaterial/binary, CanonicalBytes/binary>>)`
|
||||
%% — the same scheme `envelope:verify_signature/2` checks — and
|
||||
%% appends a `:signature` pair.
|
||||
%%
|
||||
%% Real Ed25519 / RSA signing arrives in milestone 2 once
|
||||
%% `crypto:sign_ed25519/2` BIFs land; the API shape doesn't change.
|
||||
|
||||
%% construct/4 — Type and ActorId are atoms; Published is an
|
||||
%% integer timestamp the caller supplies (no clock BIF in this
|
||||
%% port; the HTTP layer / outbox:publish caller injects it).
|
||||
%% Object can be any term, including a property list of inner
|
||||
%% fields.
|
||||
construct(Type, ActorId, Published, Object) ->
|
||||
Skeleton = [{actor, ActorId},
|
||||
{object, Object},
|
||||
{published, Published},
|
||||
{type, Type}],
|
||||
Id = cid:to_string({activity_envelope, Skeleton}),
|
||||
[{actor, ActorId},
|
||||
{id, Id},
|
||||
{object, Object},
|
||||
{published, Published},
|
||||
{type, Type}].
|
||||
|
||||
%% sign/2 — KeySpec carries key_id, algorithm, value (key material).
|
||||
sign(Envelope, KeySpec) ->
|
||||
{ok, KeyId} = envelope:get_field(key_id, KeySpec),
|
||||
{ok, Alg} = envelope:get_field(algorithm, KeySpec),
|
||||
{ok, KM} = envelope:get_field(value, KeySpec),
|
||||
CB = envelope:canonical_bytes(Envelope),
|
||||
SigValue = crypto:hash(sha256, <<KM/binary, CB/binary>>),
|
||||
Sig = [{algorithm, Alg}, {key_id, KeyId}, {value, SigValue}],
|
||||
Envelope ++ [{signature, Sig}].
|
||||
|
||||
%% cid_of/1 — extract the :id field from a constructed envelope.
|
||||
%% Convenience for callers that don't want to thread the CID
|
||||
%% separately when both the envelope and its ID matter.
|
||||
cid_of(Envelope) ->
|
||||
{ok, Id} = envelope:get_field(id, Envelope),
|
||||
Id.
|
||||
124
next/tests/outbox_construct.sh
Executable file
124
next/tests/outbox_construct.sh
Executable file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
# next/tests/outbox_construct.sh — Step 6d-cs acceptance test.
|
||||
#
|
||||
# Exercises outbox:construct/4, outbox:sign/2, outbox:cid_of/1.
|
||||
# Closes the loop by verifying that construct→sign produces an
|
||||
# envelope that envelope:verify_signature/2 accepts. 11 cases.
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||
fi
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
echo "ERROR: sx_server.exe not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERBOSE="${1:-}"
|
||||
PASS=0; FAIL=0; ERRORS=""
|
||||
TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT
|
||||
|
||||
cat > "$TMPFILE" <<'EPOCHS'
|
||||
(epoch 1)
|
||||
(load "lib/erlang/tokenizer.sx")
|
||||
(load "lib/erlang/parser.sx")
|
||||
(load "lib/erlang/parser-core.sx")
|
||||
(load "lib/erlang/parser-expr.sx")
|
||||
(load "lib/erlang/parser-module.sx")
|
||||
(load "lib/erlang/transpile.sx")
|
||||
(load "lib/erlang/runtime.sx")
|
||||
(load "lib/erlang/vm/dispatcher.sx")
|
||||
|
||||
(epoch 2)
|
||||
(eval "(get (erlang-load-module (file-read \"next/kernel/envelope.erl\")) :name)")
|
||||
(epoch 3)
|
||||
(eval "(get (erlang-load-module (file-read \"next/kernel/outbox.erl\")) :name)")
|
||||
|
||||
;; construct: required fields present
|
||||
(epoch 10)
|
||||
(eval "(get (erlang-eval-ast \"Env = outbox:construct(create, alice, 100, nil), envelope:get_field(actor, Env) =:= {ok, alice}\") :name)")
|
||||
(epoch 11)
|
||||
(eval "(get (erlang-eval-ast \"Env = outbox:construct(create, alice, 100, nil), envelope:get_field(type, Env) =:= {ok, create}\") :name)")
|
||||
(epoch 12)
|
||||
(eval "(get (erlang-eval-ast \"Env = outbox:construct(create, alice, 100, nil), envelope:get_field(published, Env) =:= {ok, 100}\") :name)")
|
||||
|
||||
;; construct: :id is a non-trivial CID
|
||||
(epoch 13)
|
||||
(eval "(get (erlang-eval-ast \"Env = outbox:construct(create, alice, 100, nil), {ok, Id} = envelope:get_field(id, Env), is_binary(Id) and (byte_size(Id) > 50)\") :name)")
|
||||
|
||||
;; construct deterministic across calls with same args
|
||||
(epoch 14)
|
||||
(eval "(get (erlang-eval-ast \"E1 = outbox:construct(create, alice, 100, nil), E2 = outbox:construct(create, alice, 100, nil), outbox:cid_of(E1) =:= outbox:cid_of(E2)\") :name)")
|
||||
|
||||
;; construct distinct CIDs for distinct types
|
||||
(epoch 15)
|
||||
(eval "(get (erlang-eval-ast \"E1 = outbox:construct(create, alice, 100, nil), E2 = outbox:construct(update, alice, 100, nil), outbox:cid_of(E1) =/= outbox:cid_of(E2)\") :name)")
|
||||
|
||||
;; construct distinct CIDs for distinct timestamps
|
||||
(epoch 16)
|
||||
(eval "(get (erlang-eval-ast \"E1 = outbox:construct(create, alice, 100, nil), E2 = outbox:construct(create, alice, 101, nil), outbox:cid_of(E1) =/= outbox:cid_of(E2)\") :name)")
|
||||
|
||||
;; sign adds a :signature field
|
||||
(epoch 17)
|
||||
(eval "(get (erlang-eval-ast \"KS = [{key_id, k1}, {algorithm, ed25519}, {value, <<1,2,3>>}], Unsigned = outbox:construct(create, alice, 100, nil), Signed = outbox:sign(Unsigned, KS), envelope:get_field(signature, Signed) =/= not_found\") :name)")
|
||||
|
||||
;; signed envelope passes envelope:verify_signature with matching key
|
||||
(epoch 18)
|
||||
(eval "(get (erlang-eval-ast \"KM = <<1,2,3,4>>, KS = [{key_id, k1}, {algorithm, ed25519}, {value, KM}], Unsigned = outbox:construct(create, alice, 100, nil), Signed = outbox:sign(Unsigned, KS), AS = [{public_keys, [[{id, k1}, {created, 50}, {value, KM}]]}], envelope:verify_signature(Signed, AS) =:= ok\") :name)")
|
||||
|
||||
;; signed envelope fails verify with a wrong key
|
||||
(epoch 19)
|
||||
(eval "(get (erlang-eval-ast \"KM = <<1,2,3,4>>, OtherKM = <<9,9,9,9>>, KS = [{key_id, k1}, {algorithm, ed25519}, {value, KM}], Unsigned = outbox:construct(create, alice, 100, nil), Signed = outbox:sign(Unsigned, KS), AS = [{public_keys, [[{id, k1}, {created, 50}, {value, OtherKM}]]}], envelope:verify_signature(Signed, AS) =:= {error, bad_signature}\") :name)")
|
||||
|
||||
;; Round-trip through the full pipeline:
|
||||
;; construct → sign → stage_envelope → stage_signature → ok
|
||||
(epoch 20)
|
||||
(eval "(get (erlang-eval-ast \"KM = <<1,2,3,4>>, KS = [{key_id, k1}, {algorithm, ed25519}, {value, KM}], Unsigned = outbox:construct(create, alice, 100, nil), Signed = outbox:sign(Unsigned, KS), AS = [{public_keys, [[{id, k1}, {created, 50}, {value, KM}]]}], envelope:validate_shape(Signed) =:= ok and envelope:verify_signature(Signed, AS) =:= ok\") :name)")
|
||||
EPOCHS
|
||||
|
||||
OUTPUT=$(timeout 180 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
|
||||
|
||||
check() {
|
||||
local epoch="$1" desc="$2" expected="$3"
|
||||
local actual
|
||||
actual=$(echo "$OUTPUT" | awk -v e="$epoch" '
|
||||
$0 ~ "^\\(ok-len " e " " { getline; print; exit }
|
||||
$0 ~ "^\\(ok " e " " { print; exit }
|
||||
$0 ~ "^\\(error " e " " { print; exit }
|
||||
')
|
||||
[ -z "$actual" ] && actual="<no output for epoch $epoch>"
|
||||
if echo "$actual" | grep -qF -- "$expected"; then
|
||||
PASS=$((PASS+1))
|
||||
[ "$VERBOSE" = "-v" ] && echo " ok $desc"
|
||||
else
|
||||
FAIL=$((FAIL+1))
|
||||
ERRORS+=" FAIL [$desc] (epoch $epoch) expected: $expected | actual: $actual
|
||||
"
|
||||
fi
|
||||
}
|
||||
|
||||
check 2 "envelope module loaded" "envelope"
|
||||
check 3 "outbox module loaded" "outbox"
|
||||
check 10 "construct sets :actor" "true"
|
||||
check 11 "construct sets :type" "true"
|
||||
check 12 "construct sets :published" "true"
|
||||
check 13 "construct :id is a CID" "true"
|
||||
check 14 "construct deterministic" "true"
|
||||
check 15 "distinct types -> distinct CIDs" "true"
|
||||
check 16 "distinct ts -> distinct CIDs" "true"
|
||||
check 17 "sign adds :signature" "true"
|
||||
check 18 "signed verifies against key" "true"
|
||||
check 19 "signed fails against wrong key" "true"
|
||||
check 20 "full pipeline round-trip" "true"
|
||||
|
||||
TOTAL=$((PASS+FAIL))
|
||||
if [ $FAIL -eq 0 ]; then
|
||||
echo "ok $PASS/$TOTAL next/tests/outbox_construct.sh passed"
|
||||
else
|
||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
||||
echo "$ERRORS"
|
||||
fi
|
||||
[ $FAIL -eq 0 ]
|
||||
Reference in New Issue
Block a user