diff --git a/.editorconfig b/.editorconfig index 740716dee..24f0d493c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,8 +1,11 @@ -# Copyright 2017-2023 Dyne.org foundation -# SPDX-FileCopyrightText: 2017-2021 Dyne.org foundation +# Copyright 2023 Dyne.org foundation +# SPDX-FileCopyrightText: 2023 Dyne.org foundation # # SPDX-License-Identifier: AGPL-3.0-or-later +# stop search for .editorconfig in parent directories +root = true + [*] end_of_line = lf insert_final_newline = true diff --git a/bindings/README.md b/bindings/README.md index da01865a3..a2faea44d 100644 --- a/bindings/README.md +++ b/bindings/README.md @@ -40,8 +40,12 @@ The `zencode-data-keys-conf` is a file or stream with a newline separated list o 2. zencode script (string -> base64) *newline* 3. keys (json -> base64) *newline* 4. data (json -> base64) *newline* +5. extra (json -> base64) *newline* +6. context (json -> base64) *newline* -Each line should start directly with the base64 string without any prefix and should end with a newline. Anything else will likely be rejected. +Each line should start directly with the base64 string without any prefix and should end with a newline. + +Empty lines can be a newline (or CRLF) and will be skipped, but should never be omitted: the zencode-exec expects 6 input lines in total, not less, including empty ones, all newline terminated. This executes and returns two streams: 1. `stdout` with the `json` formatted results of the execution diff --git a/src/lua/statemachine.lua b/src/lua/statemachine.lua index 5950fb0b4..20b22f727 100644 --- a/src/lua/statemachine.lua +++ b/src/lua/statemachine.lua @@ -118,25 +118,6 @@ function machine:cannot(e) return not self:can(e) end -function machine:todot(filename) - local dotfile = io.open(filename,'w') - dotfile:write('digraph {\n') - local transition = function(event,from,to) - dotfile:write(string.format('%s -> %s [label=%s];\n',from,to,event)) - end - for _, event in pairs(self.options.events) do - if type(event.from) == 'table' then - for _, from in ipairs(event.from) do - transition(event.name,from,event.to) - end - else - transition(event.name,event.from,event.to) - end - end - dotfile:write('}\n') - dotfile:close() -end - function machine:transition(event) if self.currentTransitioningEvent == event then return self[self.currentTransitioningEvent](self) diff --git a/src/lua/zencode.lua b/src/lua/zencode.lua index 4007a0b67..2751eb59d 100644 --- a/src/lua/zencode.lua +++ b/src/lua/zencode.lua @@ -364,7 +364,6 @@ end -- Zencode HEAP globals IN = {} -- Given processing, import global DATA from json -KIN = {} -- Given processing, import global KEYS from json TMP = TMP or {} -- Given processing, temp buffer for ack*->validate->push* ACK = ACK or {} -- When processing, destination for push* OUT = OUT or {} -- print out @@ -538,7 +537,6 @@ function zencode:begin() -- Reset HEAP self.machine = {} IN = {} -- Given processing, import global DATA from json - KIN = {} -- Given processing, import global KEYS from json TMP = {} -- Given processing, temp buffer for ack*->validate->push* ACK = {} -- When processing, destination for push* OUT = {} -- print out @@ -736,35 +734,45 @@ function zencode:run() end -- HEAP setup IN = {} -- import global DATA from json + local tmp + if EXTRA then + tmp = CONF.input.format.fun(EXTRA) or {} + for k, v in pairs(tmp) do + IN[k] = v + end + EXTRA = nil + end if DATA then - -- if plain array conjoin into associative - IN = CONF.input.format.fun(DATA) or {} + tmp = CONF.input.format.fun(DATA) or {} + for k, v in pairs(tmp) do + if IN[k] then + error("Object name collision in input: "..k) + end + IN[k] = v + end DATA = nil end - KIN = {} -- import global KEYS from json if KEYS then - KIN = CONF.input.format.fun(KEYS) or {} + tmp = CONF.input.format.fun(KEYS) or {} + for k, v in pairs(tmp) do + if IN[k] then + error("Object name collision in input: "..k) + end + IN[k] = v + end KEYS = nil end + tmp = nil collectgarbage 'collect' -- convert all spaces in keys to underscore IN = IN_uscore(IN) - KIN = IN_uscore(KIN) - - -- check name collisions between DATA and KEYS - if CONF.heap.check_collision then - for k in pairs(IN) do - if KIN[k] then - error("Object name collision in input: "..k) - end - end - end -- EXEC zencode -- TODO: for optimization, to develop a lua iterator, which would save lookup time -- https://www.lua.org/pil/7.1.html - while ZEN.next_instruction <= #self.AST do + local AST_size = #self.AST + while ZEN.next_instruction <= AST_size do ZEN.current_instruction = ZEN.next_instruction local x = self.AST[ZEN.current_instruction] ZEN.next_instruction = ZEN.next_instruction + 1 @@ -778,7 +786,6 @@ function zencode:run() -- trigger upon switch to when or then section if x.from == 'given' and x.to ~= 'given' then -- delete IN memory - KIN = {} IN = {} collectgarbage 'collect' end @@ -843,7 +850,6 @@ end function zencode.heap() return ({ IN = IN, - KIN = KIN, TMP = TMP, ACK = ACK, OUT = OUT diff --git a/src/lua/zencode_debug.lua b/src/lua/zencode_debug.lua index 2160b71ab..99aaf2207 100644 --- a/src/lua/zencode_debug.lua +++ b/src/lua/zencode_debug.lua @@ -61,7 +61,6 @@ local function debug_heap_dump() OCTET.from_string( JSON.encode( {GIVEN_data = HEAP.IN, - GIVEN_keys = HEAP.KIN, CODEC = ZEN.CODEC, WHEN = ack, THEN = HEAP.OUT})))) @@ -72,7 +71,6 @@ local function debug_heap_dump() ack.keyring = '(hidden)' end I.warn({a_GIVEN_in = HEAP.IN, - b_GIVEN_in = HEAP.KIN, c_WHEN_ack = ack, c_CODEC_ack = ZEN.CODEC, d_THEN_out = HEAP.OUT}) diff --git a/src/lua/zencode_dictionary.lua b/src/lua/zencode_dictionary.lua index 64ac4c263..8830e0827 100644 --- a/src/lua/zencode_dictionary.lua +++ b/src/lua/zencode_dictionary.lua @@ -231,7 +231,7 @@ When("create the copy of object named by '' from dictionary ''", function(name, end) local function take_out_f(path, dest, format) - local parr = strtok(uscore(path), '([^.]+)') + local parr = strtok(uscore(path), '.') local root = parr[1] -- first table.remove(parr, 1) if not dest then diff --git a/src/lua/zencode_given.lua b/src/lua/zencode_given.lua index e009cc4b3..768a162e2 100644 --- a/src/lua/zencode_given.lua +++ b/src/lua/zencode_given.lua @@ -60,7 +60,7 @@ local function pick(what, conv) local data local raw local name = _index_to_string(what) - raw = KIN[name] or IN[name] + raw = IN[name] if not raw then error("Cannot find '" .. name .. "' anywhere (null value?)", 2) end if raw == '' then error("Found empty string in '" .. name) end -- if not conv and ZEN.schemas[what] then conv = what end @@ -94,13 +94,7 @@ local function pickin(section, what, conv, fail) local bail -- fail local name = _index_to_string(what) - if KIN[section] then - root = KIN[section] - elseif IN[section] then - root = IN[section] - else - root = nil - end + root = IN[section] if not root then error("Cannot find '"..section.."'", 2) end @@ -253,7 +247,7 @@ Given( 'nothing', function() ZEN.assert( - (next(IN) == nil) and (next(KIN) == nil), + (next(IN) == nil), 'Undesired data passed as input' ) end @@ -354,7 +348,7 @@ Given( "a '' named by ''", function(s, n) -- local name = have(n) - local name = _index_to_string(KIN[n] or IN[n]) + local name = _index_to_string(IN[n]) -- ZEN.assert(encoder, "Invalid input encoding for '"..n.."': "..s) pick(name, s) ack(name) @@ -374,7 +368,7 @@ Given( Given( "a '' named by '' in ''", function(s, n, t) - local name = _index_to_string(KIN[n] or IN[n]) + local name = _index_to_string(IN[n]) pickin(t, name, s) ack(name) -- save it in ACK.name gc() @@ -404,7 +398,7 @@ Given( "my '' named by ''", function(s, n) -- ZEN.assert(encoder, "Invalid input encoding for '"..n.."': "..s) - local name = _index_to_string(KIN[n] or IN[n]) + local name = _index_to_string(IN[n]) pickin(WHO, name, s) ack(name) gc() @@ -441,7 +435,7 @@ Given( ) Given("a '' part of '' after string prefix ''", function(enc, src, pfx) - local whole = KIN[src] or IN[src] + local whole = IN[src] ZEN.assert(whole, "Cannot find '" .. src .. "' anywhere (null value?)") local plen = #pfx local wlen = #whole @@ -457,7 +451,7 @@ Given("a '' part of '' after string prefix ''", function(enc, src, pfx) end) Given("a '' part of '' before string suffix ''", function(enc, src, sfx) - local whole = KIN[src] or IN[src] + local whole = IN[src] ZEN.assert(whole, "Cannot find '" .. src .. "' anywhere (null value?)") local slen = #sfx local wlen = #whole @@ -473,11 +467,11 @@ Given("a '' part of '' before string suffix ''", function(enc, src, sfx) end) Given("a '' in path ''", function(enc, path) - local path_array = strtok(uscore(path), '([^.]+)') + local path_array = strtok(uscore(path), '.') local root = path_array[1] table.remove(path_array, 1) local dest = path_array[#path_array] - local res = KIN[root] or IN[root] + local res = IN[root] for k,v in pairs(path_array) do ZEN.assert(luatype(res) == 'table', "Object is not a table: "..root) ZEN.assert(res[v] ~= nil, "Key "..v.." not found in "..root) diff --git a/src/lua/zenroom_common.lua b/src/lua/zenroom_common.lua index 6c91eb1ef..4814dce9c 100644 --- a/src/lua/zenroom_common.lua +++ b/src/lua/zenroom_common.lua @@ -72,6 +72,27 @@ function xxx(s, n) end end +function strtok(str, delimiter) + delimiter = delimiter or ' ' + local result = {} + local start = 1 + local delimiterPos = string.find(str, delimiter, start, true) + + while delimiterPos do + local token = string.sub(str, start, delimiterPos - 1) + table.insert(result, token) + start = delimiterPos + 1 + delimiterPos = string.find(str, delimiter, start, true) + end + + local lastToken = string.sub(str, start) + if lastToken ~= "" then + table.insert(result, lastToken) + end + + return result +end + -- sorted iterator for deterministic ordering of tables -- from: https://www.lua.org/pil/19.3.html _G["lua_pairs"] = _G["pairs"] diff --git a/src/zen_parse.c b/src/zen_parse.c index 4af2d5e0a..281bdbafe 100644 --- a/src/zen_parse.c +++ b/src/zen_parse.c @@ -191,6 +191,11 @@ static int lua_unserialize_json(lua_State* L) { return 0; } +// removed because of unexplained segfault when used inside pcall to +// parse zencode: set_rule and set_scenario will explode, also seems +// to perform worse than pure Lua (see PR #709) + +#if 0 char* strtok_single(char* str, char const* delims) { static char* src = NULL; @@ -218,7 +223,7 @@ static int lua_strtok(lua_State* L) { const char DEFAULT_SEP[] = " "; char copy[MAX_FILE]; - char *sep = DEFAULT_SEP; + const char *sep = DEFAULT_SEP; const char *in; size_t size; @@ -226,7 +231,10 @@ static int lua_strtok(lua_State* L) { register char *token; in = luaL_checklstring(L, 1, &size); - + if(!in) { + lua_pushnil(L); + return 1; + } if (lua_gettop(L) > 1) { sep = luaL_checklstring(L, 2, NULL); } @@ -246,6 +254,7 @@ static int lua_strtok(lua_State* L) { } return 1; } +#endif void zen_add_parse(lua_State *L) { // override print() and io.write() @@ -255,7 +264,6 @@ void zen_add_parse(lua_State *L) { {"trim", lua_trim_spaces}, {"trimq", lua_trim_quotes}, {"jsontok", lua_unserialize_json}, - {"strtok", lua_strtok}, {NULL, NULL} }; lua_getglobal(L, "_G"); luaL_setfuncs(L, custom_parser, 0); // for Lua versions 5.2 or greater diff --git a/src/zencode-exec.c b/src/zencode-exec.c index cdeae8815..782576f4c 100644 --- a/src/zencode-exec.c +++ b/src/zencode-exec.c @@ -17,6 +17,8 @@ * If not, see http://www.gnu.org/licenses/agpl.txt */ +// for usage information see: bindings/README.md + #include #include #include @@ -28,6 +30,23 @@ #include #endif +static void _getline(char *in) { + register int ret; + if( ! fgets(in, MAX_FILE, stdin) ) { in[0]=0x0; return; } + ret = strlen(in); + if(in[0]=='\n') { in[0]=0x0; return; } // remove newline on empty line + if(in[0]=='\r') { in[0]=0x0; return; } // remove carriage return on empty line + ret = strlen(in); + if(ret<4) {// min base64 is 4 chars + fprintf(stderr,"zencode-exec error: input line too short.\n"); + exit(EXIT_FAILURE); + } + if(in[ret-2]=='\r') { in[ret-2]=0x0; return; } // remove ending CRLF + if(in[ret-1]=='\n') { in[ret-1]=0x0; return; } // remove ending LF + fprintf(stderr, "zencode-exec invalid input\n"); + exit(EXIT_FAILURE); +} + int main(int argc, char **argv) { (void)argc; (void)argv; @@ -42,9 +61,13 @@ int main(int argc, char **argv) { char keys_b64[MAX_FILE]; char data_b64[MAX_FILE]; char conf[MAX_CONFIG]; + char extra_b64[MAX_FILE]; + char context_b64[MAX_FILE]; script_b64[0] = 0x0; keys_b64[0] = 0x0; data_b64[0] = 0x0; + extra_b64[0] = 0x0; + context_b64[0] = 0x0; conf[0] = 0x0; // TODO(jaromil): find a way to check stdin on windows @@ -67,29 +90,46 @@ int main(int argc, char **argv) { return EXIT_FAILURE; } if(conf[0] != '\n') { + conf[strlen(conf)-1] = 0x0; // remove ending LF strcat(conf,",logfmt=json"); } else { snprintf(conf,MAX_CONFIG,"logfmt=json"); } } else { - snprintf(conf,MAX_CONFIG,"logfmt=json"); + fprintf(stderr, "zencode-exec missing conf at line 1: %s\n",strerror(errno)); + return EXIT_FAILURE; } if( ! fgets(script_b64, MAX_ZENCODE, stdin) ) { fprintf(stderr, "zencode-exec missing script at line 2: %s\n",strerror(errno)); return EXIT_FAILURE; } - - if( fgets(keys_b64, MAX_FILE, stdin) ) { - ret = strlen(keys_b64); keys_b64[ret-1] = 0x0; // remove newline - } - if( fgets(data_b64, MAX_FILE, stdin) ) { - ret = strlen(data_b64); data_b64[ret-1] = 0x0; // remove newline + ret = strlen(script_b64); + if( ret < 16) { + fprintf(stderr, "zencode-exec error: script too short.\n"); + return EXIT_FAILURE; } + if( script_b64[ret-2]=='\r' ) script_b64[ret-2] = 0x0; // remove ending CRLF + if( script_b64[ret-1]=='\n' ) script_b64[ret-1] = 0x0; // remove ending LF - Z = zen_init(conf, - keys_b64[0]?keys_b64:NULL, - data_b64[0]?data_b64:NULL); + _getline(keys_b64); + _getline(data_b64); + _getline(extra_b64); + _getline(context_b64); + + { + fprintf(stderr,"%s\n",conf); + fprintf(stderr,"%s\n",script_b64); + fprintf(stderr,"%s\n",keys_b64); + fprintf(stderr,"%s\n",data_b64); + fprintf(stderr,"%s\n",extra_b64); + fprintf(stderr,"%s\n",context_b64); + } + Z = zen_init_extra(conf, + keys_b64[0]?keys_b64:NULL, + data_b64[0]?data_b64:NULL, + extra_b64[0]?extra_b64:NULL, + context_b64[0]?context_b64:NULL); if(!Z) { fprintf(stderr, "\"[!] Initialisation failed\",\n"); fprintf(stderr,"\"ZENROOM JSON LOG END\" ]\n"); @@ -103,8 +143,6 @@ int main(int argc, char **argv) { zen_exec_script(Z, "CONF.input.format.fun = function(obj) return JSON.decode(OCTET.from_base64(obj):str()) end"); zen_exec_script(Z, "CONF.code.encoding.fun = function(obj) return OCTET.from_base64(obj):str() end"); - - ret = strlen(script_b64); script_b64[ret-1] = 0x0; // remove newline zen_exec_zencode(Z, script_b64); register int exitcode = Z->exitcode; diff --git a/src/zenroom.c b/src/zenroom.c index 29f3b5b5c..e2cc634a4 100644 --- a/src/zenroom.c +++ b/src/zenroom.c @@ -273,6 +273,21 @@ zenroom_t *zen_init(const char *conf, const char *keys, const char *data) { return(ZZ); } +zenroom_t *zen_init_extra(const char *conf, const char *keys, const char *data, + const char *extra, const char *context) { + zenroom_t *ZZ = zen_init(conf, keys, data); + if(!ZZ) return NULL; + if(extra) { + func(ZZ->lua, "declaring global: EXTRA"); + zen_setenv(ZZ->lua,"EXTRA",extra); + } + if(context) { + func(ZZ->lua, "declaring global: CONTEXT"); + zen_setenv(ZZ->lua,"CONTEXT",context); + } + return(ZZ); +} + void zen_teardown(zenroom_t *ZZ) { notice(ZZ->lua,"Zenroom teardown."); act(ZZ->lua,"Memory used: %u KB", @@ -347,7 +362,7 @@ int zen_exec_zencode(zenroom_t *ZZ, const char *script) { "if not _res then exitcode(2) ZEN.OK = false error('EXEC: '.._err,2) end\n" ); if(ret == SUCCESS) { - func(L, "Script successfully executed"); + func(L, "Zencode script successfully executed"); } else { zerror(L, "ERROR:"); zerror(L, "%s", lua_tostring(L, -1)); @@ -365,7 +380,7 @@ int zen_exec_script(zenroom_t *ZZ, const char *script) { zen_setenv(L,"CODE",(char*)script); ret = luaL_dostring(L, script); if(ret == SUCCESS) { - notice(L, "Script successfully executed"); + func(L, "Lua script successfully executed"); ZZ->exitcode = SUCCESS; } else { zerror(L, "ERROR:"); diff --git a/src/zenroom.h b/src/zenroom.h index 40aacecc3..89a882bb3 100644 --- a/src/zenroom.h +++ b/src/zenroom.h @@ -113,6 +113,8 @@ typedef struct { #define SUCCESS 0 // EXIT_SUCCESS zenroom_t *zen_init(const char *conf, const char *keys, const char *data); +zenroom_t *zen_init_extra(const char *conf, const char *keys, const char *data, + const char *extra, const char *context); int zen_exec_script(zenroom_t *Z, const char *script); int zen_exec_zencode(zenroom_t *Z, const char *script); void zen_teardown(zenroom_t *zenroom); diff --git a/test/zencode/rules.bats b/test/zencode/rules.bats index 46e5f9939..e0bc8d797 100644 --- a/test/zencode/rules.bats +++ b/test/zencode/rules.bats @@ -170,31 +170,12 @@ EOF assert_output '{"result":"test_passed"}' } -# --- collision ignore --- # -@test "collision ignore" { - cat < zencode_exec_stdin + # zencode + cat <> zencode_exec_stdin +Given I have nothing +Then print all data + +EOF + echo >> zencode_exec_stdin # keys + echo >> zencode_exec_stdin # data + echo >> zencode_exec_stdin # extra + echo >> zencode_exec_stdin # context + + cat zencode_exec_stdin | ${TR}/zencode-exec > $TMP/out + save_output empty.json +} +@test "Execute zencode-exec with keys and data stdin inputs" { # empty conf echo > zencode_exec_stdin + # zencode cat <> zencode_exec_stdin rule check version 3.0.0 Scenario 'ecdh': Bob verifies the signature from Alice @@ -30,6 +48,8 @@ Then print the 'myMessage' EOF echo >> zencode_exec_stdin + + # keys cat <> zencode_exec_stdin { "Alice": { @@ -39,6 +59,7 @@ EOF EOF echo >> zencode_exec_stdin + # data cat <> zencode_exec_stdin { "myMessage": "Dear Bob, your name is too short, goodbye - Alice.", @@ -57,7 +78,12 @@ EOF } } EOF - echo >> zencode_exec_stdin + + # empty extra + echo >> zencode_exec_stdin + + # empty context + echo >> zencode_exec_stdin cat zencode_exec_stdin | ${TR}/zencode-exec > $TMP/out save_output verified.json @@ -79,15 +105,93 @@ EOF echo >> zencode_exec_stdin # keys echo >> zencode_exec_stdin # data + echo >> zencode_exec_stdin # extra + echo >> zencode_exec_stdin # context echo > $TMP/out cat zencode_exec_stdin | ${TR}/zencode-exec 2>>full.json 1>>full.json awk '/HEAP:/ {print $4}' full.json | sed 's/",//' | base64 -d > $TMP/out save_output heap.json - assert_output '{"CODEC":{"random_object":{"encoding":"def","name":"random_object","zentype":"e"}},"GIVEN_data":[],"GIVEN_keys":[],"THEN":[],"WHEN":{"random_object":"XdjAYj+RY95+uyYMI8fR3+fmP5LyQaN54vyTTVKxZyA="}}' + assert_output '{"CODEC":{"random_object":{"encoding":"def","name":"random_object","zentype":"e"}},"GIVEN_data":[],"THEN":[],"WHEN":{"random_object":"XdjAYj+RY95+uyYMI8fR3+fmP5LyQaN54vyTTVKxZyA="}}' >&3 echo awk '/TRACE:/ {print $4}' full.json | sed 's/",//' | base64 -d > $TMP/out save_output trace.json assert_output '["Given nothing","When I create the random object of '"'"'256'"'"' bits","and debug"]' } + +@test "Execute zencode-exec with all stdin inputs including extra" { + + # empty conf + echo > zencode_exec_stdin + + # zencode + cat <> zencode_exec_stdin +rule check version 3.0.0 +Scenario 'ecdh': Bob verifies the signature from Alice +# Here we load the pubkey we'll verify the signature against +Given I have a 'public key' from 'Alice' +# Here we load the objects to be verified +Given I have a 'string' named 'myMessage' +Given I have a 'string array' named 'myStringArray' + +# Here we load the objects's signatures +Given I have a 'signature' named 'myStringArray.signature' +Given I have a 'signature' named 'myMessage.signature' + +# Here we perform the verifications +When I verify the 'myMessage' has a ecdh signature in 'myMessage.signature' by 'Alice' +When I verify the 'myStringArray' has a ecdh signature in 'myStringArray.signature' by 'Alice' + +# Here we print out the result: if the verifications succeeded, a string will be printed out +# if the verifications failed, Zenroom will throw an error +Then print the string 'Zenroom certifies that signatures are all correct!' +Then print the 'myMessage' + +EOF + echo >> zencode_exec_stdin + + # keys + cat <> zencode_exec_stdin +{ + "Alice": { + "public_key": "BBCQg21VcjsmfTmNsg+I+8m1Cm0neaYONTqRnXUjsJLPa8075IYH+a9w2wRO7rFM1cKmv19Igd7ntDZcUvLq3xI=" + } +} +EOF + echo >> zencode_exec_stdin + + # data + cat <> zencode_exec_stdin +{ + "myMessage": "Dear Bob, your name is too short, goodbye - Alice.", + "myMessage.signature": { + "r": "vWerszPubruWexUib69c7IU8Dxy1iisUmMGC7h7arDw=", + "s": "nSjxT+JAP56HMRJjrLwwB6kP+mluYySeZcG8JPBGcpY=" + } +} +EOF + + # extra +cat <> zencode_exec_stdin +{ + "myStringArray": [ + "Hello World! This is my string array, element [0]", + "Hello World! This is my string array, element [1]", + "Hello World! This is my string array, element [2]" + ], + "myStringArray.signature": { + "r": "B8qrQqYSWaTf5Q16mBCjY1tfsD4Cf6ZSMJTHCCV8Chg=", + "s": "S1/Syca6+XozVr5P9fQ6/AkQ+fJTMfwc063sbKmZ5B4=" + } +} +EOF + echo >> zencode_exec_stdin + + # empty context + echo >> zencode_exec_stdin + + cat zencode_exec_stdin | ${TR}/zencode-exec > $TMP/out + save_output verified.json + assert_output '{"myMessage":"Dear Bob, your name is too short, goodbye - Alice.","output":["Zenroom_certifies_that_signatures_are_all_correct!"]}' +}