Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ Preserve:
- native context access now goes through `zen_get_context(...)` or `zen_get_global_context()`, pool-backed allocations use explicit `zmalloc(...)`/`zfree(...)`/`zrealloc(...)`, and helper names ending in `_clone_free` or `hash_release` own heap-backed temporary clones rather than borrowed userdata.
- vendored Lua modules (`json.lua`, `msgpack.lua`, `inspect.lua`, `semver.lua`) include origin/local-change headers; preserve them when upgrading.
- for array-like data in hot paths, prefer `ipairs` over deterministic sorted traversal helpers unless order affects serialized or signed output.
- TIME values are now stored and parsed as signed 64-bit (`ztime_t = int64_t`); keep parser inputs integer-only and preserve legacy native-endian 4-byte TIME octets for int32-range compatibility while accepting 8-byte octets for widened values.
- Lua-side Y2038 follow-up is mostly migrated: `zencode_math.lua`, `crypto_ulid.lua`, `crypto_dcql_query.lua`, `zencode_sd_jwt.lua`, `crypto_longfellow.lua`, and `zencode_longfellow.lua` now avoid libc calendar conversions; `crypto_fsp.lua` still carries a tracked `time-y2038` TODO because the timetable-based UTC formatter currently overflows in plain-Lua FSP calls.
- TIME now has an explicit signed 64-bit contract end-to-end. For calendar conversion in Lua, prefer `timetable.lua` and exact integer/TIME values over host `os.time` or float-backed timestamp math.


## Rule Of Thumb
Expand Down
8 changes: 4 additions & 4 deletions docs/pages/ldoc/doc/modules/TIME.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,12 @@ <h1>TIME</h1>

<p>This class allows to work with TIME objects.</p>
</p>
<p> All TIME objects are float number.
Since all TIME objects are 32 bit signed, there are two limitations for values allowed:</p>
<p>TIME values are signed 64-bit Unix seconds.
Input accepts exact integers only (userdata, integer number, decimal string, or TIME octet).</p>

<p>-The MAXIMUM TIME value allowed is the number 2147483647 (<code>t_max = TIME.new(2147483647)</code>)</p>
<p>-The MAXIMUM TIME value allowed is the number 9223372036854775807 (<code>t_max = TIME.new(9223372036854775807)</code>)</p>

<p>-The MINIMUM TIME value allowed is the number -2147483647 (<code>t_min = TIME.new(-2147483647)</code>) </p>
<p>-The MINIMUM TIME value allowed is the number -9223372036854775808 (<code>t_min = TIME.new(-9223372036854775808)</code>) </p>


<h2><a href="#Class_TIME">Class TIME </a></h2>
Expand Down
8 changes: 4 additions & 4 deletions docs/pages/ldoc/o/modules/TIME.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,12 @@ <h1>TIME</h1>

<p>This class allows to work with TIME objects.</p>
</p>
<p> All TIME objects are float number.
Since all TIME objects are 32 bit signed, there are two limitations for values allowed:</p>
<p>TIME values are signed 64-bit Unix seconds.
Input accepts exact integers only (userdata, integer number, decimal string, or TIME octet).</p>

<p>-The MAXIMUM TIME value allowed is the number 2147483647 (<code>t_max = TIME.new(2147483647)</code>)</p>
<p>-The MAXIMUM TIME value allowed is the number 9223372036854775807 (<code>t_max = TIME.new(9223372036854775807)</code>)</p>

<p>-The MINIMUM TIME value allowed is the number -2147483647 (<code>t_min = TIME.new(-2147483647)</code>) </p>
<p>-The MINIMUM TIME value allowed is the number -9223372036854775808 (<code>t_min = TIME.new(-9223372036854775808)</code>) </p>


<h2><a href="#Class_TIME">Class TIME </a></h2>
Expand Down
6 changes: 3 additions & 3 deletions docs/pages/zencode-cookbook-when.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,11 @@ can produce the following output
## Numbers statements

This section will discuss numbers, specifically integers, floats, and time. Floating-point numbers are a fundamental data type in computing used to represent real numbers (numbers with fractional parts). They are designed to handle a wide range of magnitudes, from very small to very large values, by using a scientific notation-like format in binary.
All TIME objects are float number. Since all TIME objects are 32 bit signed, there are two limitations for values allowed:
TIME values are signed 64-bit Unix seconds. Input accepts exact integers only, either as a TIME object, an exact integer number, a decimal string, or a TIME octet.

-The MAXIMUM TIME value allowed is the number 2147483647
-The MAXIMUM TIME value allowed is the number 9223372036854775807

-The MINIMUM TIME value allowed is the number -2147483647
-The MINIMUM TIME value allowed is the number -9223372036854775808

### Create a number

Expand Down
4 changes: 2 additions & 2 deletions src/lua/crypto_dcql_query.lua
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ DCQL.check_fn.ldp_vc = function(cred, string_query, out)
end
-- not expired
local cred_validUntil <const> = zulu2timestamp(cred.validUntil)
local now <const> = TIME.new(os.time())
local now <const> = time_now()
if (now > cred_validUntil) then
warn("Credential is expired")
return false
Expand Down Expand Up @@ -441,7 +441,7 @@ DCQL.check_fn['dc+sd-jwt'] = function(cred, string_query, out)
end
-- not expired
local cred_exp <const> = TIME.new(parsed_cred.payload.exp)
local now <const> = TIME.new(os.time())
local now <const> = time_now()
if (now > cred_exp) then
warn("Credential is expired")
return false
Expand Down
3 changes: 2 additions & 1 deletion src/lua/crypto_fsp.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
--]]

local T = { }

-- The length of RSK marks the maximum message length
-- to make sure that its XOR covers the whole message

Expand Down Expand Up @@ -58,6 +57,8 @@ end

-- generate a nonce with default format
function T:makenonce()
-- TODO(time-y2038): migrate this formatting path once timetable-based
-- UTC rendering is safe in plain-Lua FSP calls without stack overflows.
return OCTET.from_string(os.date("%Y%m%d%H%M%S", os.time()))
end

Expand Down
17 changes: 7 additions & 10 deletions src/lua/crypto_longfellow.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@
--]]

local c_zk = require_once'longfellow'
local TIMETABLE <const> = require_once'timetable'

local longfellow = { }

local function default_now_zulu()
return O.from_string(TIMETABLE.to_string(TIMETABLE.from_seconds(tostring(time_now()))))
end

-- values used as true and false in mdoc
longfellow['yes'] = O.from_hex'f5'

Expand Down Expand Up @@ -88,11 +93,7 @@ longfellow.mdoc_prover = function(circuit, mdoc,
error("Invalid table id in attributes",2)
end
end
-- get UTC time (ZULU) using '!*t' to avoid timezone conversion
-- then format as "YYYY-MM-DDTHH:MM:SSZ" (20 chars)
local nownow <const> = now
or O.from_string(os.date("!%Y-%m-%dT%H:%M:%SZ",
os.time(os.date("!*t"))))
local nownow <const> = now or default_now_zulu()
if not is_zulu_date(nownow) then
error("Timestamp is not in ISO 8601 format",2)
end
Expand Down Expand Up @@ -142,11 +143,7 @@ longfellow.mdoc_verifier = function(circuit, proof,
error("Invalid table id in attributes",2)
end
end
-- get UTC time (ZULU) using '!*t' to avoid timezone conversion
-- then format as "YYYY-MM-DDTHH:MM:SSZ" (20 chars)
local nownow <const> = now
or O.from_string(os.date("!%Y-%m-%dT%H:%M:%SZ",
os.time(os.date("!*t"))))
local nownow <const> = now or default_now_zulu()
if not is_zulu_date(nownow) then
error("Timestamp is not in ISO 8601 format",2)
end
Expand Down
133 changes: 127 additions & 6 deletions src/lua/crypto_sd_jwt.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,108 @@

local sd_jwt = {}

local function is_wide_decimal_string(value)
if type(value) ~= 'string' or value == '' then
return false
end
local negative = value:sub(1, 1) == '-'
local digits = negative and value:sub(2) or value
digits = digits:gsub('^0+', '')
if digits == '' then
digits = '0'
end
local limit = negative and '2147483648' or '2147483647'
if #digits ~= #limit then
return #digits > #limit
end
return digits > limit
end

local function needs_precise_number_encoding(value)
local tv <const> = type(value)
if tv == 'number' then
local integral, fractional = math.modf(value)
return fractional == 0.0 and is_wide_decimal_string(string.format('%.0f', integral))
elseif tv == 'zenroom.float' then
local numeric = tonumber(tostring(value))
if not numeric then
return false
end
local integral, fractional = math.modf(numeric)
return fractional == 0.0 and is_wide_decimal_string(string.format('%.0f', integral))
elseif tv == 'zenroom.time' then
return is_wide_decimal_string(tostring(value))
elseif tv == 'zenroom.big' then
return true
elseif tv == 'table' then
for _, item in pairs(value) do
if needs_precise_number_encoding(item) then
return true
end
end
end
return false
end

local function encode_sd_jwt_number(v)
local integral, fractional = math.modf(v)
if fractional == 0.0 then
return string.format("%.0f", integral)
end
local s = tostring(v)
local n = tonumber(s)
if not n then
error("Not a number: "..s, 2)
end
return tostring(n)
end

local function encode_sd_jwt_value(v)
local tv <const> = type(v)
if tv == 'string' then
return JSON.raw_encode(v, true)
elseif tv == 'number' then
return encode_sd_jwt_number(v)
elseif tv == 'zenroom.time' then
return tostring(v)
elseif tv == 'zenroom.big' then
return v:decimal()
elseif tv == 'zenroom.float' then
local numeric = tonumber(tostring(v))
if numeric then
local integral, fractional = math.modf(numeric)
if fractional == 0.0 then
return string.format("%.0f", integral)
end
end
return tostring(v)
elseif tv == 'zenroom.octet' then
return JSON.raw_encode(v:str(), true)
elseif tv == 'boolean' or tv == 'nil' then
return JSON.raw_encode(v, true)
elseif tv == 'table' then
local res = {}
local separator = ", "
if rawget(v, 1) ~= nil or next(v) == nil then
local _ipairs <const> = fif(CONF.output.sorting, sort_ipairs, ipairs)
for _, item in _ipairs(v) do
res[#res + 1] = encode_sd_jwt_value(item)
end
return "[" .. table.concat(res, separator) .. "]"
end
local _pairs <const> = fif(CONF.output.sorting, sort_pairs, pairs)
for key, value in _pairs(v) do
table.insert(res, JSON.raw_encode(key, true) .. ": " .. encode_sd_jwt_value(value))
end
return "{" .. table.concat(res, separator) .. "}"
end
error("Invalid value found in SD-JWT array: "..tv)
end

function sd_jwt.encode_dictionary(obj)
return encode_sd_jwt_value(obj)
end

function sd_jwt.prepare_dictionary(obj)
-- values in input may be string, number or bool
local fun = function(v)
Expand Down Expand Up @@ -56,6 +158,28 @@ function sd_jwt.prepare_dictionary(obj)
return deepmap(fun, obj)
end

local function prepare_dictionary_precise(obj)
local fun = function(v)
local tv <const> = type(v)
if tv == 'string' or tv == 'number' or tv == 'boolean'
or tv == 'zenroom.time' or tv == 'zenroom.big'
or tv == 'zenroom.float' then
return v
elseif tv == 'zenroom.octet' then
return v:str()
end
error("Invalid value found in SD-JWT array: "..tv)
end
return deepmap(fun, obj)
end

function sd_jwt.encode_prepared_dictionary(obj)
if needs_precise_number_encoding(obj) then
return sd_jwt.encode_dictionary(prepare_dictionary_precise(obj))
end
return JSON.raw_encode(sd_jwt.prepare_dictionary(obj), true)
end

-- Given as input a "disclosure array" of the form {salt, key, value}
-- Return: disclosure = the array in octet form
-- hashed = the sha256 digest of the encoded array (for _sd)
Expand All @@ -65,8 +189,7 @@ function sd_jwt.create_disclosure(dis_arr)

local encoded_dis <const> =
O.from_string(
JSON.raw_encode(
sd_jwt.prepare_dictionary(dis_arr), true)):url64()
sd_jwt.encode_prepared_dictionary(dis_arr)):url64()
local disclosure = {}
for i = 1, #dis_arr do
if type(dis_arr[i]) == 'table' then
Expand Down Expand Up @@ -148,10 +271,8 @@ function sd_jwt.create_jwt(payload, sk, algo)
alg=O.from_string(algo.IANA), -- TODO: does JWT contains .alg ?!
typ=O.from_string("dc+sd-jwt")
}
local payload_str <const> = sd_jwt.prepare_dictionary(payload)
local b64payload <const> = O.from_string(JSON.raw_encode(payload_str, true)):url64()
local header_str <const> = sd_jwt.prepare_dictionary(header)
local b64header <const> = O.from_string(JSON.raw_encode(header_str, true)):url64()
local b64payload <const> = O.from_string(sd_jwt.encode_prepared_dictionary(payload)):url64()
local b64header <const> = O.from_string(sd_jwt.encode_prepared_dictionary(header)):url64()

local signature = algo.sign(sk, O.from_string(b64header .. "." .. b64payload))
return {
Expand Down
5 changes: 3 additions & 2 deletions src/lua/crypto_ulid.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ local ENCODING = {
}

local function encode_time()
time = big.new(os.time())*big.new(1000)
local time = big.from_decimal(tostring(time_now())) * big.new(1000)
len = 10
local result = {}
for i = len, 1, -1 do
Expand All @@ -59,7 +59,8 @@ end


function uu.uuid_v1()
time = big.new(os.time())*big.from_decimal(10000000)+big.from_decimal("122192928000000000")
local time = big.from_decimal(tostring(time_now())) * big.from_decimal(10000000)
+ big.from_decimal("122192928000000000")
local time_low = time:octet():sub(5,8):hex()
local time_mid = time:octet():sub(3,4):hex()
local time_high = time:octet():sub(1,2):__shl(4)
Expand Down
38 changes: 37 additions & 1 deletion src/lua/crypto_w3c.lua
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,47 @@ function W3C.create_string_jwk(alg, sk_flag, pk)
end

function W3C.import_jwt(obj)
local function representable_integer_string(value)
if type(value) ~= 'number' then
return nil
end
if math.type(value) == 'integer' then
return tostring(value)
end
if value ~= value or value == math.huge or value == -math.huge then
return nil
end
local integer, fractional = math.modf(value)
if fractional ~= 0.0 then
return nil
end
return string.format('%.0f', integer)
end

local function decimal_lte(left, right)
if #left ~= #right then
return #left < #right
end
return left <= right
end

local function is_autodetected_time_string(value)
if not value or value:sub(1, 1) == '-' then
return false
end
return decimal_lte('1500000000', value)
and decimal_lte(value, '4102444800')
end

local function decode_jwt_parts(s)
if type(s) == 'string' then
return O.from_string(s)
elseif type(s) == 'number' then
return fif(TIME.detect_time_value(s), TIME.new, FLOAT.new)(s)
local exact = representable_integer_string(s)
if exact and is_autodetected_time_string(exact) then
return TIME.new(exact)
end
return FLOAT.new(s)
else
return s
end
Expand Down
Loading
Loading