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:
-
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.
-
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.
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.affineis three externs: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:
Sign a JWT:
private_keyis a PEM file (PKCS#1 or PKCS#8, DER inside base64), 2048-bit.POST /app/installations/{id}/access_tokenswithAuthorization: Bearer <jwt>and apermissionsbody, receiving aone-hour installation token.
Step 2 is fine —
Http.request/Http.postwith headers already work throughstd/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
openssl— there is no process-execution builtin. The seededsurface has
arg_at/env_at, file IO,getenv/setenv,exit/panic, andthe
cmd_none/cmd_performpair (which is the TEA runtime's obligation type,not a subprocess). No
exec,spawnorsystem. So this is not available.docs/CAPABILITY-MATRIX.adocdescribes typed-wasm as a "narrow contract", and aforeign crypto path is exactly where a verification-oriented project should not
want opaque failure modes.
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:
Notes:
rsa_verify_*is not decoration — it makes the signing path testable, and it iswhat webhook-signature verification needs.
String-based variant (sha256_hex(s) -> String) would cover the common casewith less ceremony.
hmac_sha256is included because webhook verification (X-Hub-Signature-256) isthe 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 howCrypto.affineis documented towork ("the Node-CJS shim wraps Node's built-in
node:crypto"). So the non-wasmtargets 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:
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, includingthe negative cases (wrong key, tampered payload).
Impact, stated plainly
applier: the write path).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.
and they are precisely the users who need hashing and signing available.
Environment
affinescriptv0.1.1, linux-x64 release binary (sha256b8f2cab7…fef5dc), Deno-ESM output executed on Node 20.20.2.stdlib/Crypto.affine,lib/resolve.ml(seed_builtins),lib/parser.mly, anddocs/CAPABILITY-MATRIX.adoc; confirmed by compiling aprobe that calls each name — every one above is currently unresolvable.