Skip to content

csrf, encoding: make luaossl optional at runtime - #820

Open
a-schaefers wants to merge 1 commit into
leafo:masterfrom
a-schaefers:feat/optional-luaossl-with-openresty-fallbacks
Open

a-schaefers wants to merge 1 commit into
leafo:masterfrom
a-schaefers:feat/optional-luaossl-with-openresty-fallbacks

Conversation

@a-schaefers

Copy link
Copy Markdown

Related to #802

lapis.util.encoding and lapis.csrf required luaossl as soon as they were loaded. Every app loads lapis.util.encoding (lapis.application -> lapis.request -> lapis.session), so an OpenResty app could not start without luaossl, even though OpenResty signs sessions with ngx.hmac_sha1 and ships a CSPRNG in resty.random. Every request failed with:

./lapis/util/encoding.lua:3: module 'openssl.hmac' not found

This is the runtime half of leafo/lapis issue 802, where luaossl fails to compile with GCC 15+ (C23). Each module now picks a backend when it loads, the way pgmoon does in pgmoon/crypto.moon. luaossl is first in every chain, so nothing changes for installs that have it.

lapis.util.encoding

  • HMACs are created with luaossl's openssl.hmac, falling back to lua-resty-openssl's resty.openssl.hmac. Both expose new(key, digest) and final(str).
  • Inside OpenResty hmac_sha1 is still ngx.hmac_sha1, so the default hmac_digest ("sha1") needs neither library there. hmac_digest = "sha256" needs luaossl or lua-resty-openssl.

lapis.csrf

  • The 32 random bytes behind the token cookie come from luaossl's openssl.rand, falling back to resty.random.bytes(n, true), which calls RAND_bytes in the OpenSSL that nginx links against.
  • resty.random is only used inside a real nginx process, detected by ngx.config. In plain LuaJIT it loads fine but crashes when called ("undefined symbol: RAND_bytes"), and the fake ngx of lapis.spec.request has no ngx.config. This checks ngx.config rather than lapis.nginx.is_simulate because specs also install fake ngx tables that lack the simulate marker (stub_queries uses { null: nil }).

Errors

  • Without a backend both modules still load. The function that needs the backend raises an error that includes why luaossl failed to load, so a broken luaossl install isn't hidden:

    lapis.util.encoding: hmac_sha256 requires luaossl or
    lua-resty-openssl, but luaossl failed to load: module
    'openssl.hmac' not found: ...

  • luaossl is probed once per module. On Lua 5.1 and LuaJIT, when a module's loader raises an error, require leaves a sentinel in package.loaded, and a second require only reports "loop or previous error loading module" instead of the real reason.

Fixed from the prototype patch in the issue 802 plan: random_bytes returned assert's message as a second value. ngx.encode_base64 rejects it ("bad no_padding: boolean expected, got string", so CSRF tokens failed on OpenResty without luaossl) and LuaSocket's mime.b64 appends it to the key. random_bytes now returns only the bytes, and the specs check the exact key.

Not changed

  • luaossl stays a dependency in the rockspec. Outside OpenResty, lapis migrate (pgmoon's password auth), sessions and CSRF still need it.
  • The simulated ngx.md5 in lapis.spec.request still loads luaossl lazily when it's called.

Specs

  • spec/helpers.moon: require_with_stubs loads a fresh copy of a module with chosen modules replaced by tables, or made to fail as if not installed, then restores package.loaded and package.preload.
  • encoding: RFC 2202 and RFC 4231 HMAC test vectors, backend order, the error without a backend, and nginx with and without lua-resty-openssl, including hmac_digest = "sha256".
  • csrf: backend order, strong bytes requested from resty.random, a failing resty.random, no resty.random outside nginx or in a simulated request, and validating tokens without a backend.

Docs: utilities.md now says which library backs the HMAC functions and the CSRF random bytes.

Tested

  • busted spec: 1026 successes, 5 pending on Lua 5.1; 1027 successes, 4 pending on LuaJIT 2.1 (OpenResty's branch) and Lua 5.4. Before this change: 1013 successes, 5 pending on Lua 5.1.
  • busted spec_cqueues: 13/13 on Lua 5.1 with lua-http 0.4.
  • busted spec_openresty/server_spec.moon against Ubuntu 24.04's nginx 1.24 with ngx_lua 0.10.26, lua-resty-core and lua-resty-string 0.16: 10/10 with luaossl, and 10/10 with luaossl hidden from nginx. Without this change the second run is 0/10.
  • A throwaway app on that nginx with luaossl hidden: the app loads, sha1 sessions round trip, CSRF tokens come from resty.random (32 bytes, padded base64) and are accepted, and hmac_sha256 raises the error above. With lua-resty-openssl 1.9.0 added, hmac_digest = "sha256" sessions round trip and hmac_sha256 matches RFC 4231. With luaossl visible, luaossl is used and no resty module is loaded.

lapis.util.encoding and lapis.csrf required luaossl as soon as they were
loaded. Every app loads lapis.util.encoding (lapis.application ->
lapis.request -> lapis.session), so an OpenResty app could not start
without luaossl, even though OpenResty signs sessions with ngx.hmac_sha1
and ships a CSPRNG in resty.random. Every request failed with:

    ./lapis/util/encoding.lua:3: module 'openssl.hmac' not found

This is the runtime half of leafo/lapis issue 802, where luaossl fails
to compile with GCC 15+ (C23). Each module now picks a backend when it
loads, the way pgmoon does in pgmoon/crypto.moon. luaossl is first in
every chain, so nothing changes for installs that have it.

lapis.util.encoding
- HMACs are created with luaossl's openssl.hmac, falling back to
  lua-resty-openssl's resty.openssl.hmac. Both expose new(key, digest)
  and final(str).
- Inside OpenResty hmac_sha1 is still ngx.hmac_sha1, so the default
  hmac_digest ("sha1") needs neither library there. hmac_digest =
  "sha256" needs luaossl or lua-resty-openssl.

lapis.csrf
- The 32 random bytes behind the token cookie come from luaossl's
  openssl.rand, falling back to resty.random.bytes(n, true), which calls
  RAND_bytes in the OpenSSL that nginx links against.
- resty.random is only used inside a real nginx process, detected by
  ngx.config. In plain LuaJIT it loads fine but crashes when called
  ("undefined symbol: RAND_bytes"), and the fake ngx of
  lapis.spec.request has no ngx.config. This checks ngx.config rather
  than lapis.nginx.is_simulate because specs also install fake ngx
  tables that lack the simulate marker (stub_queries uses
  { null: nil }).

Errors
- Without a backend both modules still load. The function that needs
  the backend raises an error that includes why luaossl failed to load,
  so a broken luaossl install isn't hidden:

    lapis.util.encoding: hmac_sha256 requires luaossl or
    lua-resty-openssl, but luaossl failed to load: module
    'openssl.hmac' not found: ...

- luaossl is probed once per module. On Lua 5.1 and LuaJIT, when a
  module's loader raises an error, require leaves a sentinel in
  package.loaded, and a second require only reports "loop or previous
  error loading module" instead of the real reason.

Fixed from the prototype patch in the issue 802 plan: random_bytes
returned assert's message as a second value. ngx.encode_base64 rejects
it ("bad no_padding: boolean expected, got string", so CSRF tokens
failed on OpenResty without luaossl) and LuaSocket's mime.b64 appends
it to the key. random_bytes now returns only the bytes, and the specs
check the exact key.

Not changed
- luaossl stays a dependency in the rockspec. Outside OpenResty,
  `lapis migrate` (pgmoon's password auth), sessions and CSRF still
  need it.
- The simulated ngx.md5 in lapis.spec.request still loads luaossl
  lazily when it's called.

Specs
- spec/helpers.moon: require_with_stubs loads a fresh copy of a module
  with chosen modules replaced by tables, or made to fail as if not
  installed, then restores package.loaded and package.preload.
- encoding: RFC 2202 and RFC 4231 HMAC test vectors, backend order, the
  error without a backend, and nginx with and without lua-resty-openssl,
  including hmac_digest = "sha256".
- csrf: backend order, strong bytes requested from resty.random, a
  failing resty.random, no resty.random outside nginx or in a simulated
  request, and validating tokens without a backend.

Docs: utilities.md now says which library backs the HMAC functions and
the CSRF random bytes.

Tested
- busted spec: 1026 successes, 5 pending on Lua 5.1; 1027 successes,
  4 pending on LuaJIT 2.1 (OpenResty's branch) and Lua 5.4. Before this
  change: 1013 successes, 5 pending on Lua 5.1.
- busted spec_cqueues: 13/13 on Lua 5.1 with lua-http 0.4.
- busted spec_openresty/server_spec.moon against Ubuntu 24.04's nginx
  1.24 with ngx_lua 0.10.26, lua-resty-core and lua-resty-string 0.16:
  10/10 with luaossl, and 10/10 with luaossl hidden from nginx. Without
  this change the second run is 0/10.
- A throwaway app on that nginx with luaossl hidden: the app loads, sha1
  sessions round trip, CSRF tokens come from resty.random (32 bytes,
  padded base64) and are accepted, and hmac_sha256 raises the error
  above. With lua-resty-openssl 1.9.0 added, hmac_digest = "sha256"
  sessions round trip and hmac_sha256 matches RFC 4231. With luaossl
  visible, luaossl is used and no resty module is loaded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014tEB9iu1sFMjM17UjVE6Tv
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants