Skip to content

Crypto: no hashing, HMAC or RSA signing - blocks GitHub App auth from AffineScript (priority) #746

Description

@hyperpolymath

Crypto: no hashing, HMAC or RSA signing — blocks GitHub App auth from AffineScript

Priority: high. This blocks a 13-module port of a working automation kit
(≈2,200 lines currently in Python) into AffineScript, at the one step that cannot
be worked around: minting a GitHub App installation token.

Summary

stdlib/Crypto.affine is three externs:

pub extern fn random_string(n: Int) -> String;
pub extern fn random_f64() -> Float;
pub extern fn time_ms() -> Int;

There is no digest, no HMAC, no asymmetric signing and no base64. Any program that
has to authenticate as a GitHub App therefore cannot be written in
AffineScript today, because that requires an RS256 JWT signed with an RSA private
key. Everything else in the port — reading the taxonomy, validating, diffing,
generating artefacts — is already done and running.

What the use case needs, concretely

GitHub App authentication is a two-step exchange:

  1. Sign a JWT:

    header  = {"alg":"RS256","typ":"JWT"}
    payload = {"iat":…, "exp":…, "iss":<app id>}
    signing_input = base64url(header) "." base64url(payload)
    jwt = signing_input "." base64url(rsassa_pkcs1_v1_5_sha256(private_key, signing_input))
    

    private_key is a PEM file (PKCS#1 or PKCS#8, DER inside base64), 2048-bit.

  2. POST /app/installations/{id}/access_tokens with
    Authorization: Bearer <jwt> and a permissions body, receiving a
    one-hour installation token.

Step 2 is fine — Http.request/Http.post with headers already work through
std/http. Step 1 is the blocker: it needs SHA-256, RSASSA-PKCS1-v1_5,
base64url, and PEM→key parsing.

Why the existing workarounds do not apply

  • Shell out to openssl — there is no process-execution builtin. The seeded
    surface has arg_at/env_at, file IO, getenv/setenv, exit/panic, and
    the cmd_none/cmd_perform pair (which is the TEA runtime's obligation type,
    not a subprocess). No exec, spawn or system. So this is not available.
  • FFI to a Zig/Rust crypto library — possible in principle, but
    docs/CAPABILITY-MATRIX.adoc describes typed-wasm as a "narrow contract", and a
    foreign crypto path is exactly where a verification-oriented project should not
    want opaque failure modes.
  • Keep that one step in another language — that is the situation this port
    exists to remove; the surrounding project's policy also excludes Python, which
    is what the current implementation uses.

Suggested minimal surface

Shapes are the maintainer's call — the capability is the ask, not the API:

pub extern fn sha256(bytes: [Int]) -> [Int];
pub extern fn hmac_sha256(key: [Int], msg: [Int]) -> [Int];
pub extern fn rsa_sign_pkcs1_sha256(key_pem: String, msg: [Int]) -> [Int];
pub extern fn rsa_verify_pkcs1_sha256(key_pem: String, msg: [Int], sig: [Int]) -> Bool;
pub extern fn base64url_encode(bytes: [Int]) -> String;
pub extern fn base64url_decode(s: String) -> [Int];

Notes:

  • rsa_verify_* is not decoration — it makes the signing path testable, and it is
    what webhook-signature verification needs.
  • A String-based variant (sha256_hex(s) -> String) would cover the common case
    with less ceremony.
  • hmac_sha256 is included because webhook verification (X-Hub-Signature-256) is
    the other half of the same integration story.

Implementation note on targets

The Deno-ESM and Node-CJS paths can satisfy all of this with the host runtime
(node:crypto / WebCrypto) — which is already how Crypto.affine is documented to
work ("the Node-CJS shim wraps Node's built-in node:crypto"). So the non-wasm
targets may be cheap
: add the externs, extend the shim, done. The typed-wasm
target is the harder one and could reasonably land later behind the same externs,
with a trap or explicit unsupported error until then.

Acceptance test I will contribute

Test vectors for each primitive, in the shape of the existing stdlib tests:

SHA-256("abc") = ba7816bf 8f01cfea 414140de 5dae2223 b00361a3 96177a9c b410ff61 f20015ad
SHA-256("")    = e3b0c442 98fc1c14 9afbf4c8 996fb924 27ae41e4 649b934c a495991b 7852b855

plus an RFC 7515 §A.2 RS256 JWT vector (a 2048-bit key, a known compact
serialisation, a known signature) and the corresponding PEM parsing case. I am
happy to open that PR against stdlib/ once there is a surface to test, including
the negative cases (wrong key, tampered payload).

Impact, stated plainly

  • Blocks phase 7 of 13 in an in-progress port (applier: the write path).
  • Blocks any third-party tool written in AffineScript that talks to an API with
    signed requests — GitHub Apps, Slack, Stripe-style webhooks, JWT-based auth
    generally. This is not a niche need; it is the standard way modern APIs
    authenticate server-to-server automation.
  • Verification-inclined projects are the natural early adopters of this language,
    and they are precisely the users who need hashing and signing available.

Environment

  • affinescript v0.1.1, linux-x64 release binary (sha256
    b8f2cab7…fef5dc), Deno-ESM output executed on Node 20.20.2.
  • Method: read stdlib/Crypto.affine, lib/resolve.ml (seed_builtins),
    lib/parser.mly, and docs/CAPABILITY-MATRIX.adoc; confirmed by compiling a
    probe that calls each name — every one above is currently unresolvable.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions