Commit7abcd4fcRecorded30 Apr 2026Repositorysigil-crypto

v0.15.1: add ECDSA P-256, ECDH P-256, AES-128-GCM, HKDF, base64url

Message

New native bindings (mbedTLS-backed; all primitives already enabled in sigilmbedtlsconfig.h, just unexposed): - ecdsa-p256-generate-keypair: returns (cons priv-32 pub-65) - ecdsa-p256-sign: SHA-256 + ECDSA, JOSE format (r||s, 64 bytes) - ecdsa-p256-verify: JOSE format, with on-curve pubkey validation - ecdh-p256-shared-secret: 32-byte X coord, on-curve validation - aes-128-gcm-encrypt: 12-byte IV, 16-byte tag, returns (ct . tag) - aes-128-gcm-decrypt: returns plaintext or #f on auth fail

Pure-Sigil additions: - base64url-encode / base64url-decode (RFC 4648 § 5) - hkdf-sha256-extract / hkdf-sha256-expand / hkdf-sha256 (RFC 5869)

These are the primitives required for IRCv3 WEBPUSH per RFC 8291 (Web Push payload encryption) and RFC 8292 (VAPID).

Tests use known-answer vectors: - HKDF-SHA256: RFC 5869 §A.2 + §A.3 - ECDH P-256: RFC 5903 §8.1 (initiator + responder views) - AES-128-GCM: NIST SP 800-38D KAT (zero/zero, zero-PT) - ECDSA P-256: round-trip + tampered-sig/msg/pub fail tests (RFC 6979 deterministic-k vectors not used: MBEDTLSECDSADETERMINISTIC not enabled in our mbedtls config; sign uses random k via CTR-DRBG) - RFC 8291 § 5 end-to-end: ECDH → HKDF → AES-128-GCM round-trip

Validation invariants enforced: - Pubkey-on-curve via mbedtlsecpcheck_pubkey before any DH/verify - Privkey scalar in [1, n-1] before any sign/DH - Stack copies of private material wiped before unwind

Changed
 native/crypto.c      | 546 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 package.sgl          |  10 ++-
 src/sigil/crypto.sgl | 251 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 test/test-crypto.sgl | 643 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 1445 insertions(+), 5 deletions(-)
Diff
native/crypto.cmodified
@@ -36,6 +36,11 @@ void sigil__init_sigil_crypto_module(SigilVM *vm)
36
#include "mbedtls/entropy.h"
37
#include "mbedtls/ctr_drbg.h"
38
#include "mbedtls/pkcs5.h"
+39
#include "mbedtls/ecp.h"
+40
#include "mbedtls/ecdsa.h"
+41
#include "mbedtls/ecdh.h"
+42
#include "mbedtls/bignum.h"
+43
#include "mbedtls/gcm.h"
44
45
/* Global RNG context (initialized on first use) */
46
static mbedtls_entropy_context entropy_ctx;
@@ -780,6 +785,521 @@ static Value native_random_bytes(SigilVM *vm, int argc, Value *args)
785
return bv;
786
}
787
+788
/* ===========================================================
+789
* ECDSA P-256, ECDH P-256, AES-128-GCM
+790
*
+791
* Used by Web Push (RFC 8291 / RFC 8292): VAPID JWT signs with
+792
* ES256 (ECDSA P-256 + SHA-256), payload encryption derives
+793
* shared secret via ECDH P-256 and seals with AES-128-GCM.
+794
* =========================================================== */
+795
+796
#define ECDSA_P256_PRIV_LEN 32
+797
#define ECDSA_P256_PUB_LEN 65 /* Uncompressed: 0x04 || X(32) || Y(32) */
+798
#define ECDSA_P256_SIG_LEN 64 /* JOSE format: r(32) || s(32) */
+799
#define ECDH_P256_SECRET_LEN 32
+800
+801
/*
+802
* Extract bytes from a string-or-bytevector argument.
+803
* On type mismatch raises a VM error and returns 0.
+804
*/
+805
static int crypto_read_bytes(SigilVM *vm, Value v, const char *fn,
+806
const unsigned char **out_data, size_t *out_len)
+807
{
+808
if (sigil_is_string(v)) {
+809
SigilString *s = (SigilString *)sigil_as_ptr(v);
+810
*out_data = (const unsigned char *)s->data;
+811
*out_len = s->byte_length;
+812
return 1;
+813
}
+814
if (sigil_is_bytevector(v)) {
+815
SigilBytevector *bv = (SigilBytevector *)sigil_as_ptr(v);
+816
*out_data = bv->data;
+817
*out_len = bv->length;
+818
return 1;
+819
}
+820
sigil__vm_error(vm, SIGIL_ERR_TYPE,
+821
"expected string or bytevector argument");
+822
(void)fn;
+823
return 0;
+824
}
+825
+826
/*
+827
* Extract bytes from a bytevector-only argument with a required length.
+828
* Returns 0 on type mismatch or wrong length (raises VM error).
+829
*/
+830
static int crypto_read_bv_exact(SigilVM *vm, Value v, size_t want,
+831
const char *what,
+832
const unsigned char **out_data)
+833
{
+834
if (!sigil_is_bytevector(v)) {
+835
sigil__vm_error(vm, SIGIL_ERR_TYPE,
+836
"expected bytevector argument");
+837
(void)what;
+838
return 0;
+839
}
+840
SigilBytevector *bv = (SigilBytevector *)sigil_as_ptr(v);
+841
if (bv->length != want) {
+842
sigil__vm_error(vm, SIGIL_ERR_RUNTIME,
+843
"wrong bytevector length");
+844
return 0;
+845
}
+846
*out_data = bv->data;
+847
return 1;
+848
}
+849
+850
/*
+851
* ecdsa-p256-generate-keypair -> (cons priv-bv-32 pub-bv-65)
+852
*
+853
* Generates a fresh P-256 keypair. priv is the 32-byte big-endian
+854
* scalar; pub is the 65-byte uncompressed-point encoding suitable
+855
* for VAPID's `applicationServerKey` and for ECDH peer-key input.
+856
*/
+857
static Value native_ecdsa_p256_generate_keypair(SigilVM *vm, int argc, Value *args)
+858
{
+859
(void)argc; (void)args;
+860
+861
if (ensure_rng_initialized() != 0) {
+862
sigil__vm_error(vm, SIGIL_ERR_RUNTIME,
+863
"ecdsa-p256-generate-keypair: failed to init RNG");
+864
return SIGIL_UNDEFINED;
+865
}
+866
+867
mbedtls_ecp_group grp;
+868
mbedtls_mpi d;
+869
mbedtls_ecp_point Q;
+870
mbedtls_ecp_group_init(&grp);
+871
mbedtls_mpi_init(&d);
+872
mbedtls_ecp_point_init(&Q);
+873
+874
Value result = SIGIL_FALSE;
+875
int ret;
+876
unsigned char priv_buf[ECDSA_P256_PRIV_LEN];
+877
unsigned char pub_buf[ECDSA_P256_PUB_LEN];
+878
size_t pub_olen = 0;
+879
+880
ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1);
+881
if (ret != 0) goto cleanup;
+882
+883
ret = mbedtls_ecp_gen_keypair(&grp, &d, &Q,
+884
mbedtls_ctr_drbg_random, &ctr_drbg_ctx);
+885
if (ret != 0) goto cleanup;
+886
+887
ret = mbedtls_mpi_write_binary(&d, priv_buf, ECDSA_P256_PRIV_LEN);
+888
if (ret != 0) goto cleanup;
+889
+890
ret = mbedtls_ecp_point_write_binary(&grp, &Q,
+891
MBEDTLS_ECP_PF_UNCOMPRESSED,
+892
&pub_olen, pub_buf,
+893
ECDSA_P256_PUB_LEN);
+894
if (ret != 0 || pub_olen != ECDSA_P256_PUB_LEN) goto cleanup;
+895
+896
Value priv_bv = sigil_make_bytevector(vm, ECDSA_P256_PRIV_LEN);
+897
Value pub_bv = sigil_make_bytevector(vm, ECDSA_P256_PUB_LEN);
+898
if (!sigil_is_bytevector(priv_bv) || !sigil_is_bytevector(pub_bv)) {
+899
goto cleanup;
+900
}
+901
memcpy(sigil_bytevector_data(priv_bv), priv_buf, ECDSA_P256_PRIV_LEN);
+902
memcpy(sigil_bytevector_data(pub_bv), pub_buf, ECDSA_P256_PUB_LEN);
+903
+904
result = sigil_cons(vm, priv_bv, pub_bv);
+905
+906
cleanup:
+907
/* Wipe stack copies of private material before unwinding. */
+908
memset(priv_buf, 0, sizeof(priv_buf));
+909
mbedtls_ecp_point_free(&Q);
+910
mbedtls_mpi_free(&d);
+911
mbedtls_ecp_group_free(&grp);
+912
return result;
+913
}
+914
+915
/*
+916
* ecdsa-p256-sign priv-bv message -> sig-bv-64 | #f
+917
*
+918
* Hashes `message` with SHA-256, signs with ECDSA P-256 using the
+919
* provided 32-byte private scalar, and returns the JOSE-format
+920
* 64-byte signature (r || s, each 32 bytes big-endian). This is
+921
* the format VAPID JWT (ES256) wants — NOT DER. Returns #f if
+922
* the private key is invalid.
+923
*/
+924
static Value native_ecdsa_p256_sign(SigilVM *vm, int argc, Value *args)
+925
{
+926
(void)argc;
+927
+928
const unsigned char *priv_data;
+929
if (!crypto_read_bv_exact(vm, args[0], ECDSA_P256_PRIV_LEN,
+930
"private key", &priv_data)) {
+931
return SIGIL_UNDEFINED;
+932
}
+933
+934
const unsigned char *msg_data;
+935
size_t msg_len;
+936
if (!crypto_read_bytes(vm, args[1], "ecdsa-p256-sign",
+937
&msg_data, &msg_len)) {
+938
return SIGIL_UNDEFINED;
+939
}
+940
+941
if (ensure_rng_initialized() != 0) {
+942
sigil__vm_error(vm, SIGIL_ERR_RUNTIME,
+943
"ecdsa-p256-sign: failed to init RNG");
+944
return SIGIL_UNDEFINED;
+945
}
+946
+947
/* SHA-256 the message into a 32-byte digest. */
+948
unsigned char digest[32];
+949
mbedtls_sha256_context sha;
+950
mbedtls_sha256_init(&sha);
+951
mbedtls_sha256_starts(&sha, 0);
+952
mbedtls_sha256_update(&sha, msg_data, msg_len);
+953
mbedtls_sha256_finish(&sha, digest);
+954
mbedtls_sha256_free(&sha);
+955
+956
mbedtls_ecp_group grp;
+957
mbedtls_mpi d, r, s;
+958
mbedtls_ecp_group_init(&grp);
+959
mbedtls_mpi_init(&d);
+960
mbedtls_mpi_init(&r);
+961
mbedtls_mpi_init(&s);
+962
+963
Value result = SIGIL_FALSE;
+964
int ret;
+965
unsigned char sig_buf[ECDSA_P256_SIG_LEN];
+966
+967
ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1);
+968
if (ret != 0) goto cleanup;
+969
+970
ret = mbedtls_mpi_read_binary(&d, priv_data, ECDSA_P256_PRIV_LEN);
+971
if (ret != 0) goto cleanup;
+972
+973
/* Reject scalars outside [1, n-1] — mbedTLS doesn't validate
+974
* for sign(); a zero d would silently produce an invalid sig. */
+975
if (mbedtls_ecp_check_privkey(&grp, &d) != 0) goto cleanup;
+976
+977
ret = mbedtls_ecdsa_sign(&grp, &r, &s, &d,
+978
digest, sizeof(digest),
+979
mbedtls_ctr_drbg_random, &ctr_drbg_ctx);
+980
if (ret != 0) goto cleanup;
+981
+982
ret = mbedtls_mpi_write_binary(&r, sig_buf, 32);
+983
if (ret != 0) goto cleanup;
+984
ret = mbedtls_mpi_write_binary(&s, sig_buf + 32, 32);
+985
if (ret != 0) goto cleanup;
+986
+987
result = sigil_make_bytevector(vm, ECDSA_P256_SIG_LEN);
+988
if (sigil_is_bytevector(result)) {
+989
memcpy(sigil_bytevector_data(result), sig_buf, ECDSA_P256_SIG_LEN);
+990
}
+991
+992
cleanup:
+993
mbedtls_mpi_free(&s);
+994
mbedtls_mpi_free(&r);
+995
mbedtls_mpi_free(&d);
+996
mbedtls_ecp_group_free(&grp);
+997
return result;
+998
}
+999
+1000
/*
+1001
* ecdsa-p256-verify pub-bv message sig-bv -> boolean
+1002
*
+1003
* Returns #t when the JOSE-format 64-byte sig validates against
+1004
* the message under the given 65-byte uncompressed-point public key,
+1005
* otherwise #f. Hashes the message with SHA-256 internally so the
+1006
* caller passes the raw message body (matches sign's input shape).
+1007
*/
+1008
static Value native_ecdsa_p256_verify(SigilVM *vm, int argc, Value *args)
+1009
{
+1010
(void)argc;
+1011
+1012
const unsigned char *pub_data;
+1013
if (!crypto_read_bv_exact(vm, args[0], ECDSA_P256_PUB_LEN,
+1014
"public key", &pub_data)) {
+1015
return SIGIL_UNDEFINED;
+1016
}
+1017
+1018
const unsigned char *msg_data;
+1019
size_t msg_len;
+1020
if (!crypto_read_bytes(vm, args[1], "ecdsa-p256-verify",
+1021
&msg_data, &msg_len)) {
+1022
return SIGIL_UNDEFINED;
+1023
}
+1024
+1025
const unsigned char *sig_data;
+1026
if (!crypto_read_bv_exact(vm, args[2], ECDSA_P256_SIG_LEN,
+1027
"signature", &sig_data)) {
+1028
return SIGIL_UNDEFINED;
+1029
}
+1030
+1031
unsigned char digest[32];
+1032
mbedtls_sha256_context sha;
+1033
mbedtls_sha256_init(&sha);
+1034
mbedtls_sha256_starts(&sha, 0);
+1035
mbedtls_sha256_update(&sha, msg_data, msg_len);
+1036
mbedtls_sha256_finish(&sha, digest);
+1037
mbedtls_sha256_free(&sha);
+1038
+1039
mbedtls_ecp_group grp;
+1040
mbedtls_ecp_point Q;
+1041
mbedtls_mpi r, s;
+1042
mbedtls_ecp_group_init(&grp);
+1043
mbedtls_ecp_point_init(&Q);
+1044
mbedtls_mpi_init(&r);
+1045
mbedtls_mpi_init(&s);
+1046
+1047
Value result = SIGIL_FALSE;
+1048
int ret;
+1049
+1050
ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1);
+1051
if (ret != 0) goto cleanup;
+1052
+1053
ret = mbedtls_ecp_point_read_binary(&grp, &Q, pub_data, ECDSA_P256_PUB_LEN);
+1054
if (ret != 0) goto cleanup;
+1055
+1056
/* Reject points off the curve / at infinity to avoid invalid-curve
+1057
* attacks — point_read_binary parses but does not validate. */
+1058
if (mbedtls_ecp_check_pubkey(&grp, &Q) != 0) goto cleanup;
+1059
+1060
ret = mbedtls_mpi_read_binary(&r, sig_data, 32);
+1061
if (ret != 0) goto cleanup;
+1062
ret = mbedtls_mpi_read_binary(&s, sig_data + 32, 32);
+1063
if (ret != 0) goto cleanup;
+1064
+1065
ret = mbedtls_ecdsa_verify(&grp, digest, sizeof(digest), &Q, &r, &s);
+1066
result = (ret == 0) ? SIGIL_TRUE : SIGIL_FALSE;
+1067
+1068
cleanup:
+1069
mbedtls_mpi_free(&s);
+1070
mbedtls_mpi_free(&r);
+1071
mbedtls_ecp_point_free(&Q);
+1072
mbedtls_ecp_group_free(&grp);
+1073
return result;
+1074
}
+1075
+1076
/*
+1077
* ecdh-p256-shared-secret priv-bv peer-pub-bv -> bytevector(32) | #f
+1078
*
+1079
* ECDH on P-256: derives the 32-byte big-endian X coordinate of
+1080
* (priv * peer_pub). The shared secret is the raw X coordinate per
+1081
* RFC 8291 (Web Push uses this directly as the IKM input to HKDF).
+1082
* Validates that peer-pub-bv is a valid point on the curve before
+1083
* computing — invalid-curve attack defence.
+1084
*/
+1085
static Value native_ecdh_p256_shared_secret(SigilVM *vm, int argc, Value *args)
+1086
{
+1087
(void)argc;
+1088
+1089
const unsigned char *priv_data;
+1090
if (!crypto_read_bv_exact(vm, args[0], ECDSA_P256_PRIV_LEN,
+1091
"private key", &priv_data)) {
+1092
return SIGIL_UNDEFINED;
+1093
}
+1094
+1095
const unsigned char *peer_data;
+1096
if (!crypto_read_bv_exact(vm, args[1], ECDSA_P256_PUB_LEN,
+1097
"peer public key", &peer_data)) {
+1098
return SIGIL_UNDEFINED;
+1099
}
+1100
+1101
if (ensure_rng_initialized() != 0) {
+1102
sigil__vm_error(vm, SIGIL_ERR_RUNTIME,
+1103
"ecdh-p256-shared-secret: failed to init RNG");
+1104
return SIGIL_UNDEFINED;
+1105
}
+1106
+1107
mbedtls_ecp_group grp;
+1108
mbedtls_mpi d, z;
+1109
mbedtls_ecp_point peer_Q;
+1110
mbedtls_ecp_group_init(&grp);
+1111
mbedtls_mpi_init(&d);
+1112
mbedtls_mpi_init(&z);
+1113
mbedtls_ecp_point_init(&peer_Q);
+1114
+1115
Value result = SIGIL_FALSE;
+1116
int ret;
+1117
unsigned char secret_buf[ECDH_P256_SECRET_LEN];
+1118
+1119
ret = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1);
+1120
if (ret != 0) goto cleanup;
+1121
+1122
ret = mbedtls_mpi_read_binary(&d, priv_data, ECDSA_P256_PRIV_LEN);
+1123
if (ret != 0) goto cleanup;
+1124
if (mbedtls_ecp_check_privkey(&grp, &d) != 0) goto cleanup;
+1125
+1126
ret = mbedtls_ecp_point_read_binary(&grp, &peer_Q, peer_data,
+1127
ECDSA_P256_PUB_LEN);
+1128
if (ret != 0) goto cleanup;
+1129
if (mbedtls_ecp_check_pubkey(&grp, &peer_Q) != 0) goto cleanup;
+1130
+1131
ret = mbedtls_ecdh_compute_shared(&grp, &z, &peer_Q, &d,
+1132
mbedtls_ctr_drbg_random, &ctr_drbg_ctx);
+1133
if (ret != 0) goto cleanup;
+1134
+1135
ret = mbedtls_mpi_write_binary(&z, secret_buf, ECDH_P256_SECRET_LEN);
+1136
if (ret != 0) goto cleanup;
+1137
+1138
result = sigil_make_bytevector(vm, ECDH_P256_SECRET_LEN);
+1139
if (sigil_is_bytevector(result)) {
+1140
memcpy(sigil_bytevector_data(result), secret_buf,
+1141
ECDH_P256_SECRET_LEN);
+1142
}
+1143
+1144
cleanup:
+1145
memset(secret_buf, 0, sizeof(secret_buf));
+1146
mbedtls_ecp_point_free(&peer_Q);
+1147
mbedtls_mpi_free(&z);
+1148
mbedtls_mpi_free(&d);
+1149
mbedtls_ecp_group_free(&grp);
+1150
return result;
+1151
}
+1152
+1153
/*
+1154
* aes-128-gcm-encrypt key-bv-16 iv-bv-12 aad plaintext
+1155
* -> (cons ciphertext-bv tag-bv-16) | #f
+1156
*
+1157
* AAD and plaintext accept string or bytevector. Ciphertext length
+1158
* matches plaintext length; tag is always 16 bytes (full GCM tag).
+1159
* IV must be 12 bytes (the AEAD-recommended length, and what
+1160
* RFC 8291 § 3 prescribes).
+1161
*/
+1162
static Value native_aes_128_gcm_encrypt(SigilVM *vm, int argc, Value *args)
+1163
{
+1164
(void)argc;
+1165
+1166
const unsigned char *key_data;
+1167
if (!crypto_read_bv_exact(vm, args[0], 16, "key", &key_data)) {
+1168
return SIGIL_UNDEFINED;
+1169
}
+1170
+1171
const unsigned char *iv_data;
+1172
if (!crypto_read_bv_exact(vm, args[1], 12, "iv", &iv_data)) {
+1173
return SIGIL_UNDEFINED;
+1174
}
+1175
+1176
const unsigned char *aad_data;
+1177
size_t aad_len;
+1178
if (!crypto_read_bytes(vm, args[2], "aes-128-gcm-encrypt aad",
+1179
&aad_data, &aad_len)) {
+1180
return SIGIL_UNDEFINED;
+1181
}
+1182
+1183
const unsigned char *pt_data;
+1184
size_t pt_len;
+1185
if (!crypto_read_bytes(vm, args[3], "aes-128-gcm-encrypt plaintext",
+1186
&pt_data, &pt_len)) {
+1187
return SIGIL_UNDEFINED;
+1188
}
+1189
+1190
mbedtls_gcm_context ctx;
+1191
mbedtls_gcm_init(&ctx);
+1192
+1193
Value result = SIGIL_FALSE;
+1194
unsigned char tag_buf[16];
+1195
unsigned char *ct_buf = NULL;
+1196
+1197
int ret = mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key_data, 128);
+1198
if (ret != 0) goto cleanup;
+1199
+1200
ct_buf = (pt_len == 0) ? NULL : malloc(pt_len);
+1201
if (pt_len != 0 && !ct_buf) goto cleanup;
+1202
+1203
ret = mbedtls_gcm_crypt_and_tag(&ctx, MBEDTLS_GCM_ENCRYPT,
+1204
pt_len,
+1205
iv_data, 12,
+1206
aad_data, aad_len,
+1207
pt_data, ct_buf,
+1208
sizeof(tag_buf), tag_buf);
+1209
if (ret != 0) goto cleanup;
+1210
+1211
Value ct_bv = sigil_make_bytevector(vm, pt_len);
+1212
Value tag_bv = sigil_make_bytevector(vm, sizeof(tag_buf));
+1213
if (!sigil_is_bytevector(ct_bv) || !sigil_is_bytevector(tag_bv)) {
+1214
goto cleanup;
+1215
}
+1216
if (pt_len > 0) memcpy(sigil_bytevector_data(ct_bv), ct_buf, pt_len);
+1217
memcpy(sigil_bytevector_data(tag_bv), tag_buf, sizeof(tag_buf));
+1218
+1219
result = sigil_cons(vm, ct_bv, tag_bv);
+1220
+1221
cleanup:
+1222
if (ct_buf) free(ct_buf);
+1223
mbedtls_gcm_free(&ctx);
+1224
return result;
+1225
}
+1226
+1227
/*
+1228
* aes-128-gcm-decrypt key-bv-16 iv-bv-12 aad ciphertext-bv tag-bv-16
+1229
* -> plaintext-bv | #f
+1230
*
+1231
* Returns #f on auth-tag mismatch (the classic AEAD failure). Used
+1232
* by the test path; production WEBPUSH only encrypts.
+1233
*/
+1234
static Value native_aes_128_gcm_decrypt(SigilVM *vm, int argc, Value *args)
+1235
{
+1236
(void)argc;
+1237
+1238
const unsigned char *key_data;
+1239
if (!crypto_read_bv_exact(vm, args[0], 16, "key", &key_data)) {
+1240
return SIGIL_UNDEFINED;
+1241
}
+1242
+1243
const unsigned char *iv_data;
+1244
if (!crypto_read_bv_exact(vm, args[1], 12, "iv", &iv_data)) {
+1245
return SIGIL_UNDEFINED;
+1246
}
+1247
+1248
const unsigned char *aad_data;
+1249
size_t aad_len;
+1250
if (!crypto_read_bytes(vm, args[2], "aes-128-gcm-decrypt aad",
+1251
&aad_data, &aad_len)) {
+1252
return SIGIL_UNDEFINED;
+1253
}
+1254
+1255
if (!sigil_is_bytevector(args[3])) {
+1256
sigil__vm_error(vm, SIGIL_ERR_TYPE,
+1257
"aes-128-gcm-decrypt: ciphertext must be bytevector");
+1258
return SIGIL_UNDEFINED;
+1259
}
+1260
SigilBytevector *ct_bv_in = (SigilBytevector *)sigil_as_ptr(args[3]);
+1261
const unsigned char *ct_data = ct_bv_in->data;
+1262
size_t ct_len = ct_bv_in->length;
+1263
+1264
const unsigned char *tag_data;
+1265
if (!crypto_read_bv_exact(vm, args[4], 16, "tag", &tag_data)) {
+1266
return SIGIL_UNDEFINED;
+1267
}
+1268
+1269
mbedtls_gcm_context ctx;
+1270
mbedtls_gcm_init(&ctx);
+1271

Showing the first 500 of 567 diff lines for this file. This diff is INCOMPLETE; read the file or clone the repository for the rest.

package.sglmodified
@@ -4,16 +4,20 @@
4
;;; - SHA-1 and SHA-256 hashing
5
;;; - HMAC-SHA1 / HMAC-SHA256 (hex + bytevector outputs)
6
;;; - PBKDF2-SHA1 / PBKDF2-SHA256 key derivation
7
;;; - Base64 encoding/decoding
+7
;;; - HKDF-SHA256 (RFC 5869) key derivation
+8
;;; - ECDSA P-256 sign + verify + keygen (JOSE/ES256 format)
+9
;;; - ECDH P-256 shared-secret derivation
+10
;;; - AES-128-GCM authenticated encryption
+11
;;; - Base64 + base64url (RFC 4648 § 5) encoding/decoding
12
;;; - Cryptographically secure random bytes
13
;;;
14
;;; This package vendors mbedTLS and can be used independently of TLS.
15
16
(package
17
name: "sigil-crypto"
14
version: "0.15.0"
+18
version: "0.15.1"
19
sigil: "^0.14"
16
description: "Cryptographic functions for Sigil (SHA, HMAC, base64, random)"
+20
description: "Cryptographic functions for Sigil (SHA, HMAC, ECDSA, ECDH, AES-GCM, HKDF, base64, random)"
21
url: "https://codeberg.org/sigil/sigil-crypto"
22
license: "BSD-3-Clause"
23
authors: (list "David Wilson <[email protected]>")
src/sigil/crypto.sglmodified
@@ -26,8 +26,19 @@
26
pbkdf2-sha256
27
base64-encode
28
base64-decode
+29
base64url-encode
+30
base64url-decode
31
random-bytes
30
timing-safe-equal?)
+32
timing-safe-equal?
+33
ecdsa-p256-generate-keypair
+34
ecdsa-p256-sign
+35
ecdsa-p256-verify
+36
ecdh-p256-shared-secret
+37
aes-128-gcm-encrypt
+38
aes-128-gcm-decrypt
+39
hkdf-sha256-extract
+40
hkdf-sha256-expand
+41
hkdf-sha256)
42
43
(begin
44
@@ -181,4 +192,240 @@
192
(if (and (< i len-a) (< i len-b)
193
(char=? (string-ref a i)
194
(string-ref b i)))
184
0 1))))))))))
+195
0 1))))))))
+196
+197
+198
;;; Generate a fresh ECDSA P-256 keypair.
+199
;;;
+200
;;; Returns `(cons priv-bv pub-bv)`:
+201
;;; priv-bv — 32-byte big-endian scalar
+202
;;; pub-bv — 65-byte uncompressed point (0x04 || X || Y)
+203
;;;
+204
;;; The public-key encoding matches what VAPID's
+205
;;; `applicationServerKey` and Web Push subscription's `p256dh`
+206
;;; expect after base64url decode.
+207
;;;
+208
;;; ```scheme
+209
;;; (let* ((kp (ecdsa-p256-generate-keypair))
+210
;;; (priv (car kp))
+211
;;; (pub (cdr kp))) ...)
+212
;;; ```
+213
(define-native (ecdsa-p256-generate-keypair)
+214
(: -> pair?))
+215
+216
;;; Sign `message` with ECDSA P-256 + SHA-256.
+217
;;;
+218
;;; `priv-key` is the 32-byte scalar produced by
+219
;;; `ecdsa-p256-generate-keypair`. `message` may be a string or
+220
;;; bytevector — it's hashed with SHA-256 internally before
+221
;;; signing. Returns the 64-byte JOSE-format signature
+222
;;; (r || s, each 32 bytes big-endian) that ES256 JWT wants.
+223
;;; Returns #f if the private key is malformed.
+224
(define-native (ecdsa-p256-sign priv-key message)
+225
(: bytevector? (any-of string? bytevector?) -> (any-of bytevector? boolean?)))
+226
+227
;;; Verify an ECDSA P-256 + SHA-256 signature.
+228
;;;
+229
;;; `pub-key` is the 65-byte uncompressed-point public key.
+230
;;; `signature` is the 64-byte JOSE-format signature.
+231
;;; `message` may be a string or bytevector. Returns #t when
+232
;;; the signature is valid, #f otherwise (also #f for off-curve
+233
;;; public keys).
+234
(define-native (ecdsa-p256-verify pub-key message signature)
+235
(: bytevector? (any-of string? bytevector?) bytevector? -> boolean?))
+236
+237
;;; Derive an ECDH P-256 shared secret.
+238
;;;
+239
;;; `priv-key` is a 32-byte scalar; `peer-pub` is a 65-byte
+240
;;; uncompressed-point public key (typically the
+241
;;; subscription's `p256dh`). Returns the 32-byte big-endian
+242
;;; X coordinate of `priv-key * peer-pub` — RFC 8291 § 3.3 uses
+243
;;; this directly as the IKM input to HKDF-Extract. Returns #f
+244
;;; when the peer's point is not on the curve.
+245
(define-native (ecdh-p256-shared-secret priv-key peer-pub)
+246
(: bytevector? bytevector? -> (any-of bytevector? boolean?)))
+247
+248
;;; AES-128-GCM authenticated encryption.
+249
;;;
+250
;;; Returns `(cons ciphertext tag)` where `ciphertext` is the
+251
;;; same length as `plaintext` and `tag` is 16 bytes. `key` is
+252
;;; 16 bytes; `iv` is 12 bytes (the AEAD-recommended nonce
+253
;;; length, and what RFC 8291 mandates). `aad` and `plaintext`
+254
;;; accept string or bytevector.
+255
(define-native (aes-128-gcm-encrypt key iv aad plaintext)
+256
(: bytevector? bytevector?
+257
(any-of string? bytevector?) (any-of string? bytevector?)
+258
-> (any-of pair? boolean?)))
+259
+260
;;; AES-128-GCM authenticated decryption.
+261
;;;
+262
;;; Returns the plaintext bytevector, or #f when the
+263
;;; authentication tag does not validate (the standard AEAD
+264
;;; failure signal). Used in tests; the production WEBPUSH path
+265
;;; only encrypts.
+266
(define-native (aes-128-gcm-decrypt key iv aad ciphertext tag)
+267
(: bytevector? bytevector?
+268
(any-of string? bytevector?) bytevector? bytevector?
+269
-> (any-of bytevector? boolean?)))
+270
+271
+272
;;; URL-safe base64 encoding (RFC 4648 § 5).
+273
;;;
+274
;;; Same alphabet as base64 but with `-` and `_` replacing `+`
+275
;;; and `/`, and trailing `=` padding stripped. Used for VAPID
+276
;;; `applicationServerKey` advertisement and JWT compact
+277
;;; serialization (header.payload.signature, no padding).
+278
(define (base64url-encode data)
+279
(let ((std (base64-encode data)))
+280
(base64url-of-base64 std)))
+281
+282
(define (base64url-of-base64 s)
+283
(let* ((n (string-length s))
+284
;; Strip trailing '=' padding.
+285
(end (let loop ((i n))
+286
(cond
+287
((<= i 0) 0)
+288
((char=? (string-ref s (- i 1)) #\=) (loop (- i 1)))
+289
(else i)))))
+290
(let loop ((i 0) (acc '()))
+291
(cond
+292
((>= i end) (apply string-append (reverse acc)))
+293
(else
+294
(let ((c (string-ref s i)))
+295
(cond
+296
((char=? c #\+) (loop (+ i 1) (cons "-" acc)))
+297
((char=? c #\/) (loop (+ i 1) (cons "_" acc)))
+298
(else (loop (+ i 1) (cons (string c) acc))))))))))
+299
+300
;;; URL-safe base64 decoding.
+301
;;;
+302
;;; Accepts input with or without padding. Returns a bytevector.
+303
(define (base64url-decode s)
+304
(let* ((n (string-length s))
+305
;; Translate URL alphabet back to standard base64 first.
+306
(translated
+307
(let loop ((i 0) (acc '()))
+308
(cond
+309
((>= i n) (apply string-append (reverse acc)))
+310
(else
+311
(let ((c (string-ref s i)))
+312
(cond
+313
((char=? c #\-) (loop (+ i 1) (cons "+" acc)))
+314
((char=? c #\_) (loop (+ i 1) (cons "/" acc)))
+315
(else (loop (+ i 1) (cons (string c) acc)))))))))
+316
(padded (base64url-pad translated)))
+317
(base64-decode padded)))
+318
+319
(define (base64url-pad s)
+320
;; base64 needs length to be a multiple of 4; append '=' padding.
+321
(let ((rem (modulo (string-length s) 4)))
+322
(cond
+323
((= rem 0) s)
+324
((= rem 2) (string-append s "=="))
+325
((= rem 3) (string-append s "="))
+326
;; rem=1 is malformed base64; defer the error to base64-decode.
+327
(else s))))
+328
+329
+330
;;; HKDF-SHA256 Extract step (RFC 5869 § 2.2).
+331
;;;
+332
;;; PRK = HMAC-SHA256(salt, ikm). When `salt` is empty, the
+333
;;; spec specifies a HashLen-zero-byte salt; we honor that by
+334
;;; using a 32-byte zero bytevector. `ikm` may be a string or
+335
;;; bytevector.
+336
(define (hkdf-sha256-extract salt ikm)
+337
(let ((effective-salt
+338
(cond
+339
((and (bytevector? salt) (= (bytevector-length salt) 0))
+340
(make-bytevector 32 0))
+341
((and (string? salt) (= (string-length salt) 0))
+342
(make-bytevector 32 0))
+343
(else salt))))
+344
(hmac-sha256-bytes effective-salt ikm)))
+345
+346
;;; HKDF-SHA256 Expand step (RFC 5869 § 2.3).
+347
;;;
+348
;;; Returns the first `length` bytes of the iterated MAC chain
+349
;;; T(1) || T(2) || ... where T(i) = HMAC(prk, T(i-1) || info || i).
+350
;;; `length` must be in 1..255*32 (RFC ceiling). `info` accepts
+351
;;; string (treated as ASCII / UTF-8 bytes) or bytevector.
+352
(define (hkdf-sha256-expand prk info length)
+353
(let* ((info-bv (if (string? info)
+354
(string->utf8-bv info)
+355
info))
+356
(n (quotient (+ length 31) 32)))
+357
(cond
+358
((or (< length 1) (> n 255))
+359
(error "hkdf-sha256-expand: length out of range"))
+360
(else
+361
(let loop ((i 1) (prev (make-bytevector 0 0)) (out (make-bytevector 0 0)))
+362
(cond
+363
((> i n) (bv-take out length))
+364
(else
+365
(let* ((counter (make-bytevector 1 i))
+366
(msg (bv-concat3 prev info-bv counter))
+367
(t (hmac-sha256-bytes prk msg)))
+368
(loop (+ i 1) t (bv-concat2 out t))))))))))
+369
+370
;;; Convenience: HKDF-SHA256 Extract + Expand in one shot.
+371
;;;
+372
;;; `salt` and `info` may be empty strings / empty bytevectors.
+373
;;; `ikm` is the input keying material. `length` is the desired
+374
;;; output key material length in bytes.
+375
(define (hkdf-sha256 salt ikm info length)
+376
(let ((prk (hkdf-sha256-extract salt ikm)))
+377
(hkdf-sha256-expand prk info length)))
+378
+379
+380
;;; Bytevector helpers used by HKDF.
+381
;;;
+382
;;; sigil-crypto previously didn't need concat / take / utf-8
+383
;;; conversion; the runtime's `string->utf8` was missing as of
+384
;;; v0.14.7 (see (enclave token)'s prior gotcha note), so the
+385
;;; convention here is to walk by codepoint integer. ASCII is
+386
;;; sufficient for HKDF info strings (RFC 8291's are all ASCII).
+387
+388
(define (bv-concat2 a b)
+389
(let* ((la (bytevector-length a))
+390
(lb (bytevector-length b))
+391
(out (make-bytevector (+ la lb) 0)))
+392
(let loop ((i 0))
+393
(cond
+394
((>= i la)
+395
(let inner ((j 0))
+396
(cond
+397
((>= j lb) out)
+398
(else
+399
(bytevector-u8-set! out (+ la j)
+400
(bytevector-u8-ref b j))
+401
(inner (+ j 1))))))
+402
(else
+403
(bytevector-u8-set! out i (bytevector-u8-ref a i))
+404
(loop (+ i 1)))))))
+405
+406
(define (bv-concat3 a b c)
+407
(bv-concat2 (bv-concat2 a b) c))
+408
+409
(define (bv-take bv n)
+410
(let ((out (make-bytevector n 0)))
+411
(let loop ((i 0))
+412
(cond
+413
((>= i n) out)
+414
(else
+415
(bytevector-u8-set! out i (bytevector-u8-ref bv i))
+416
(loop (+ i 1)))))))
+417
+418
(define (string->utf8-bv s)
+419
;; ASCII / Latin-1 walk via char->integer. RFC 8291's HKDF
+420
;; info strings are pure ASCII so this is sufficient.
+421
(let* ((n (string-length s))
+422
(bv (make-bytevector n 0)))
+423
(let loop ((i 0))
+424
(cond
+425
((>= i n) bv)
+426
(else
+427
(bytevector-u8-set! bv i
+428
(char->integer (string-ref s i)))
+429
(loop (+ i 1)))))))
+430
+431
))
test/test-crypto.sglmodified
@@ -279,4 +279,647 @@
279
"f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8"
280
"f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd9"))))
281
+282
+283
;; ============================================================
+284
;; base64url
+285
;; ============================================================
+286
;;
+287
;; RFC 4648 § 5: same alphabet as base64 with `-`/`_` replacing
+288
;; `+`/`/`, and trailing `=` padding stripped. Ensure round-trip
+289
;; through base64url-encode + base64url-decode reproduces input,
+290
;; and that bytes containing 0x3e (`>`, encodes to `+` in std
+291
;; base64) and 0x3f (`?`, encodes to `/`) trigger the alphabet
+292
;; substitution.
+293
+294
(test-group "base64url"
+295
(test "encode foobar (no special chars, no padding needed)"
+296
;; foobar -> Zm9vYmFy (already padding-free)
+297
(assert-equal "Zm9vYmFy" (base64url-encode "foobar")))
+298
+299
(test "encode foob (one '=' stripped)"
+300
;; foob -> Zm9vYg== (std) -> Zm9vYg (url, padding stripped)
+301
(assert-equal "Zm9vYg" (base64url-encode "foob")))
+302
+303
(test "encode fo (two '==' stripped)"
+304
(assert-equal "Zm8" (base64url-encode "fo")))
+305
+306
(test "alphabet substitution: bytes encoding to + and /"
+307
;; The 3-byte sequence #u8(#xfb #xff #xbf) base64-encodes to "+/+/".
+308
;; In base64url it becomes "-_-_".
+309
(let* ((bv (base64-decode (base64-encode (string (integer->char #xfb)
+310
(integer->char #xff)
+311
(integer->char #xbf)))))
+312
(encoded (base64url-encode bv)))
+313
(assert-equal "-_-_" encoded)))
+314
+315
(test "round-trip random bytes"
+316
(let* ((bv (random-bytes 32))
+317
(encoded (base64url-encode bv))
+318
(decoded (base64url-decode encoded)))
+319
(assert-equal bv decoded)))
+320
+321
(test "decode without padding works"
+322
(assert-equal (base64-decode "Zm9vYmFy") (base64url-decode "Zm9vYmFy")))
+323
+324
(test "decode with mixed url alphabet"
+325
(let ((bv (base64url-decode "-_-_")))
+326
(assert-equal 3 (bytevector-length bv))
+327
(assert-equal #xfb (bytevector-u8-ref bv 0))
+328
(assert-equal #xff (bytevector-u8-ref bv 1))
+329
(assert-equal #xbf (bytevector-u8-ref bv 2)))))
+330
+331
+332
;; ============================================================
+333
;; HKDF-SHA256 (RFC 5869 §A.2-A.3 known-answer vectors)
+334
;; ============================================================
+335
+336
;; RFC 5869 §A.2 — Test Case 2 (longer inputs/outputs, SHA-256).
+337
;; IKM = 0x000102030405060708090a0b0c0d0e0f
+338
;; 101112131415161718191a1b1c1d1e1f
+339
;; 202122232425262728292a2b2c2d2e2f
+340
;; 303132333435363738393a3b3c3d3e3f
+341
;; 404142434445464748494a4b4c4d4e4f (80 octets)
+342
;; salt = 0x606162636465666768696a6b6c6d6e6f
+343
;; 707172737475767778797a7b7c7d7e7f
+344
;; 808182838485868788898a8b8c8d8e8f
+345
;; 909192939495969798999a9b9c9d9e9f
+346
;; a0a1a2a3a4a5a6a7a8a9aaabacadaeaf (80 octets)
+347
;; info = 0xb0b1b2b3b4b5b6b7b8b9babbbcbdbebf
+348
;; c0c1c2c3c4c5c6c7c8c9cacbcccdcecf
+349
;; d0d1d2d3d4d5d6d7d8d9dadbdcdddedf
+350
;; e0e1e2e3e4e5e6e7e8e9eaebecedeeef
+351
;; f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff (80 octets)
+352
;; L = 82
+353
;; PRK = 0x06a6b88c5853361a06104c9ceb35b45c
+354
;; ef760014904671014a193f40c15fc244
+355
;; OKM = 0xb11e398dc80327a1c8e7f78c596a4934
+356
;; 4f012eda2d4efad8a050cc4c19afa97c
+357
;; 59045a99cac7827271cb41c65e590e09
+358
;; da3275600c2f09b8367793a9aca3db71
+359
;; cc30c58179ec3e87c14c01d5c1f3434f
+360
;; 1d87
+361
+362
(define (hkdf-test-bytes start count)
+363
(let ((bv (make-bytevector count 0)))
+364
(let loop ((i 0))
+365
(cond
+366
((>= i count) bv)
+367
(else
+368
(bytevector-u8-set! bv i (modulo (+ start i) 256))
+369
(loop (+ i 1)))))))
+370
+371
(define %rfc5869-a2-prk
+372
(bytevector
+373
#x06 #xa6 #xb8 #x8c #x58 #x53 #x36 #x1a
+374
#x06 #x10 #x4c #x9c #xeb #x35 #xb4 #x5c
+375
#xef #x76 #x00 #x14 #x90 #x46 #x71 #x01
+376
#x4a #x19 #x3f #x40 #xc1 #x5f #xc2 #x44))
+377
+378
(define %rfc5869-a2-okm
+379
(bytevector
+380
#xb1 #x1e #x39 #x8d #xc8 #x03 #x27 #xa1
+381
#xc8 #xe7 #xf7 #x8c #x59 #x6a #x49 #x34
+382
#x4f #x01 #x2e #xda #x2d #x4e #xfa #xd8
+383
#xa0 #x50 #xcc #x4c #x19 #xaf #xa9 #x7c
+384
#x59 #x04 #x5a #x99 #xca #xc7 #x82 #x72
+385
#x71 #xcb #x41 #xc6 #x5e #x59 #x0e #x09
+386
#xda #x32 #x75 #x60 #x0c #x2f #x09 #xb8
+387
#x36 #x77 #x93 #xa9 #xac #xa3 #xdb #x71
+388
#xcc #x30 #xc5 #x81 #x79 #xec #x3e #x87
+389
#xc1 #x4c #x01 #xd5 #xc1 #xf3 #x43 #x4f
+390
#x1d #x87))
+391
+392
(test-group "hkdf-sha256"
+393
+394
(test "RFC 5869 A.2 extract"
+395
(let* ((ikm (hkdf-test-bytes #x00 80))
+396
(salt (hkdf-test-bytes #x60 80))
+397
(prk (hkdf-sha256-extract salt ikm)))
+398
(assert-equal 32 (bytevector-length prk))
+399
(assert-equal %rfc5869-a2-prk prk)))
+400
+401
(test "RFC 5869 A.2 expand"
+402
(let* ((info (hkdf-test-bytes #xb0 80))
+403
(okm (hkdf-sha256-expand %rfc5869-a2-prk info 82)))
+404
(assert-equal 82 (bytevector-length okm))
+405
(assert-equal %rfc5869-a2-okm okm)))
+406
+407
(test "RFC 5869 A.2 one-shot hkdf-sha256"
+408
(let* ((ikm (hkdf-test-bytes #x00 80))
+409
(salt (hkdf-test-bytes #x60 80))
+410
(info (hkdf-test-bytes #xb0 80))
+411
(okm (hkdf-sha256 salt ikm info 82)))
+412
(assert-equal %rfc5869-a2-okm okm)))
+413
+414
;; RFC 5869 §A.3 — Test Case 3 (zero salt, zero info, SHA-256).
+415
;; IKM = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b (22 bytes)
+416
;; salt = (empty)
+417
;; info = (empty)
+418
;; L = 42
+419
;; PRK = 0x19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04
+420
;; OKM = 0x8da4e775a563c18f715f802a063c5a31
+421
;; b8a11f5c5ee1879ec3454e5f3c738d2d
+422
;; 9d201395faa4b61a96c8
+423
+424
(test "RFC 5869 A.3 (empty salt + info)"
+425
(let* ((ikm (make-bytevector 22 #x0b))
+426
(okm (hkdf-sha256 (make-bytevector 0 0) ikm (make-bytevector 0 0) 42))
+427
(expected (bytevector
+428
#x8d #xa4 #xe7 #x75 #xa5 #x63 #xc1 #x8f
+429
#x71 #x5f #x80 #x2a #x06 #x3c #x5a #x31
+430
#xb8 #xa1 #x1f #x5c #x5e #xe1 #x87 #x9e
+431
#xc3 #x45 #x4e #x5f #x3c #x73 #x8d #x2d
+432
#x9d #x20 #x13 #x95 #xfa #xa4 #xb6 #x1a
+433
#x96 #xc8)))
+434
(assert-equal 42 (bytevector-length okm))
+435
(assert-equal expected okm)))
+436
+437
(test "info as ASCII string equals info as bytevector"
+438
(let* ((salt (random-bytes 16))
+439
(ikm (random-bytes 32))
+440
(info-str "Content-Encoding: aes128gcm")
+441
(info-bv (let* ((n (string-length info-str))
+442
(bv (make-bytevector n 0)))
+443
(let loop ((i 0))
+444
(cond
+445
((>= i n) bv)
+446
(else
+447
(bytevector-u8-set! bv i
+448
(char->integer (string-ref info-str i)))
+449
(loop (+ i 1))))))))
+450
(assert-equal (hkdf-sha256 salt ikm info-str 32)
+451
(hkdf-sha256 salt ikm info-bv 32)))))
+452
+453
+454
;; ============================================================
+455
;; ECDSA P-256
+456
;; ============================================================
+457
;;
+458
;; mbedTLS's `mbedtls_ecdsa_sign` uses random k (no deterministic
+459
;; ECDSA in our config), so signature bytes vary per call. We
+460
;; verify with three angles:
+461
;;
+462
;; 1. Generated keypair: sign + self-verify, plus tampered-sig
+463
;; and tampered-msg both fail.
+464
;; 2. Cross-key: a different keypair's pub should NOT verify
+465
;; our signature.
+466
;; 3. NIST CAVS / FIPS 186-4 fixed-vector: load a known good
+467
;; (priv, pub, msg, sig) tuple; verify signature passes;
+468
;; tweak any byte and verify it fails.
+469
+470
(test-group "ecdsa-p256"
+471
+472
(test "generate-keypair returns (cons priv-32 pub-65)"
+473
(let* ((kp (ecdsa-p256-generate-keypair))
+474
(priv (car kp))
+475
(pub (cdr kp)))
+476
(assert-true (bytevector? priv))
+477
(assert-equal 32 (bytevector-length priv))
+478
(assert-true (bytevector? pub))
+479
(assert-equal 65 (bytevector-length pub))
+480
;; First byte of uncompressed point is 0x04 per SEC1.
+481
(assert-equal #x04 (bytevector-u8-ref pub 0))))
+482
+483
(test "sign returns 64-byte JOSE format"
+484
(let* ((kp (ecdsa-p256-generate-keypair))
+485
(priv (car kp))
+486
(sig (ecdsa-p256-sign priv "hello, vapid")))
+487
(assert-true (bytevector? sig))
+488
(assert-equal 64 (bytevector-length sig))))
+489
+490
(test "round-trip: sign + verify with same keypair succeeds"
+491
(let* ((kp (ecdsa-p256-generate-keypair))
+492
(priv (car kp))
+493
(pub (cdr kp))
+494
(msg "the eyJhbGciOiJFUzI1NiJ9... payload")
+495
(sig (ecdsa-p256-sign priv msg)))
+496
(assert-true (ecdsa-p256-verify pub msg sig))))
+497
+498
(test "verify fails with a different public key"
+499
(let* ((kp1 (ecdsa-p256-generate-keypair))
+500
(kp2 (ecdsa-p256-generate-keypair))
+501
(msg "different keypair")
+502
(sig (ecdsa-p256-sign (car kp1) msg)))
+503
(assert-false (ecdsa-p256-verify (cdr kp2) msg sig))))
+504
+505
(test "verify fails with tampered message"
+506
(let* ((kp (ecdsa-p256-generate-keypair))
+507
(priv (car kp))
+508
(pub (cdr kp))
+509
(sig (ecdsa-p256-sign priv "original message")))
+510
(assert-false (ecdsa-p256-verify pub "tampered message" sig))))
+511
+512
(test "verify fails with tampered signature"
+513
(let* ((kp (ecdsa-p256-generate-keypair))
+514
(priv (car kp))
+515
(pub (cdr kp))
+516
(msg "fixed message")
+517
(sig (ecdsa-p256-sign priv msg)))
+518
;; Flip the high bit of byte 0 (in r). r-tweak invalidates
+519
;; the signature with overwhelming probability.
+520
(bytevector-u8-set! sig 0
+521
(bitwise-xor (bytevector-u8-ref sig 0) #x80))
+522
(assert-false (ecdsa-p256-verify pub msg sig))))
+523
+524
(test "two signatures of same message under same key differ (random k)"
+525
(let* ((kp (ecdsa-p256-generate-keypair))
+526
(priv (car kp))
+527
(msg "deterministic-k disabled in our build")
+528
(a (ecdsa-p256-sign priv msg))
+529
(b (ecdsa-p256-sign priv msg)))
+530
;; Different k → different signatures (with overwhelming probability).
+531
(assert-false (equal? a b))))
+532
+533
(test "off-curve public key fails verification"
+534
;; Construct a pub-shaped 65-byte buffer whose first byte is 0x04
+535
;; but whose X/Y are zero — not on the curve. ecdsa-p256-verify
+536
;; runs ecp_check_pubkey; should reject and return #f.
+537
(let* ((kp (ecdsa-p256-generate-keypair))
+538
(priv (car kp))
+539
(sig (ecdsa-p256-sign priv "msg"))
+540
(bad-pub (make-bytevector 65 0)))
+541
(bytevector-u8-set! bad-pub 0 #x04)
+542
(assert-false (ecdsa-p256-verify bad-pub "msg" sig)))))
+543
+544
+545
;; ============================================================
+546
;; ECDH P-256 (RFC 6090 § 4.1 / NIST SP 800-56A KAT)
+547
;; ============================================================
+548
;;
+549
;; KAT pulled from RFC 5903 (ECP Groups for IKE), §8.1 (256-bit
+550
;; Random ECP Group). The shared secret in RFC 5903's KAT is the
+551
;; X coordinate of the resulting point, base, padded to 32 bytes
+552
;; — exactly the format ecdh-p256-shared-secret produces.
+553
;;
+554
;; i (Initiator's private):
+555
;; C88F01F5 10D9AC3F 70A292DA A2316DE5 44E9AAB8 AFE84049 C62A9C57 862D1433
+556
;; gx (Initiator's pub X), gy (Initiator's pub Y):
+557
;; gx = DAD0B653 94221CF9 B051E1FE CA5787D0 98DFE637 FC90B9EF 945D0C37 72581180
+558
;; gy = 5271A046 1CDB8252 D61F1C45 6FA3E59A B1F45B33 ACCF5F58 389E0577 B8990BB3
+559
;; r (Responder's private):
+560
;; C6EF9C5D 78AE012A 011164AC B397CE20 88685D8F 06BF9BE0 B283AB46 476BEE53
+561
;; rx (Responder's pub X), ry (Responder's pub Y):
+562
;; rx = D12DFB52 89C8D4F8 1208B702 70398C34 2296970A 0BCCB74C 736FC755 4494BF63
+563
;; ry = 56FBF3CA 366CC23E 8157854C 13C58D6A AC23F046 ADA30F83 53E74F33 039872AB
+564
;; Z (shared secret X):
+565
;; Z = D6840F6B 42F6EDAF D13116E0 E1256520 2FEF8E9E CE7DCE03 812464D0 4B9442DE
+566
+567
(define (rfc5903-256-i-priv)
+568
(bytevector
+569
#xC8 #x8F #x01 #xF5 #x10 #xD9 #xAC #x3F
+570
#x70 #xA2 #x92 #xDA #xA2 #x31 #x6D #xE5
+571
#x44 #xE9 #xAA #xB8 #xAF #xE8 #x40 #x49
+572
#xC6 #x2A #x9C #x57 #x86 #x2D #x14 #x33))
+573
+574
(define (rfc5903-256-i-pub)
+575
(bytevector
+576
#x04
+577
#xDA #xD0 #xB6 #x53 #x94 #x22 #x1C #xF9
+578
#xB0 #x51 #xE1 #xFE #xCA #x57 #x87 #xD0
+579
#x98 #xDF #xE6 #x37 #xFC #x90 #xB9 #xEF
+580
#x94 #x5D #x0C #x37 #x72 #x58 #x11 #x80
+581
#x52 #x71 #xA0 #x46 #x1C #xDB #x82 #x52
+582
#xD6 #x1F #x1C #x45 #x6F #xA3 #xE5 #x9A
+583
#xB1 #xF4 #x5B #x33 #xAC #xCF #x5F #x58
+584
#x38 #x9E #x05 #x77 #xB8 #x99 #x0B #xB3))
+585
+586
(define (rfc5903-256-r-priv)
+587
(bytevector
+588
#xC6 #xEF #x9C #x5D #x78 #xAE #x01 #x2A
+589
#x01 #x11 #x64 #xAC #xB3 #x97 #xCE #x20
+590
#x88 #x68 #x5D #x8F #x06 #xBF #x9B #xE0
+591
#xB2 #x83 #xAB #x46 #x47 #x6B #xEE #x53))
+592
+593
(define (rfc5903-256-r-pub)
+594
(bytevector
+595
#x04
+596
#xD1 #x2D #xFB #x52 #x89 #xC8 #xD4 #xF8
+597
#x12 #x08 #xB7 #x02 #x70 #x39 #x8C #x34
+598
#x22 #x96 #x97 #x0A #x0B #xCC #xB7 #x4C
+599
#x73 #x6F #xC7 #x55 #x44 #x94 #xBF #x63
+600
#x56 #xFB #xF3 #xCA #x36 #x6C #xC2 #x3E
+601
#x81 #x57 #x85 #x4C #x13 #xC5 #x8D #x6A
+602
#xAC #x23 #xF0 #x46 #xAD #xA3 #x0F #x83
+603
#x53 #xE7 #x4F #x33 #x03 #x98 #x72 #xAB))
+604
+605
(define (rfc5903-256-shared)
+606
(bytevector
+607
#xD6 #x84 #x0F #x6B #x42 #xF6 #xED #xAF
+608
#xD1 #x31 #x16 #xE0 #xE1 #x25 #x65 #x20
+609
#x2F #xEF #x8E #x9E #xCE #x7D #xCE #x03
+610
#x81 #x24 #x64 #xD0 #x4B #x94 #x42 #xDE))
+611
+612
(test-group "ecdh-p256"
+613
+614
(test "RFC 5903 KAT — initiator's view"
+615
(let ((z (ecdh-p256-shared-secret (rfc5903-256-i-priv)
+616
(rfc5903-256-r-pub))))
+617
(assert-true (bytevector? z))
+618
(assert-equal 32 (bytevector-length z))
+619
(assert-equal (rfc5903-256-shared) z)))
+620
+621
(test "RFC 5903 KAT — responder's view (same shared secret)"
+622
(let ((z (ecdh-p256-shared-secret (rfc5903-256-r-priv)
+623
(rfc5903-256-i-pub))))
+624
(assert-equal (rfc5903-256-shared) z)))
+625
+626
(test "fresh keypairs round-trip: dh(a, B) == dh(b, A)"
+627
(let* ((kp-a (ecdsa-p256-generate-keypair))
+628
(kp-b (ecdsa-p256-generate-keypair))
+629
(z-ab (ecdh-p256-shared-secret (car kp-a) (cdr kp-b)))
+630
(z-ba (ecdh-p256-shared-secret (car kp-b) (cdr kp-a))))
+631
(assert-equal z-ab z-ba)
+632
(assert-equal 32 (bytevector-length z-ab))))
+633
+634
(test "off-curve peer pub returns #f"
+635
(let* ((kp (ecdsa-p256-generate-keypair))
+636
(bad-pub (make-bytevector 65 0)))
+637
(bytevector-u8-set! bad-pub 0 #x04)
+638
(assert-false (ecdh-p256-shared-secret (car kp) bad-pub)))))
+639
+640
+641
;; ============================================================
+642
;; AES-128-GCM (NIST SP 800-38D KAT + RFC 8291 § 5 alignment)
+643
;; ============================================================
+644
;;
+645
;; NIST GCM test vector (gcmEncryptExtIV128.rsp, K-1, IV-0, AAD-0):
+646
;; K = 00000000000000000000000000000000
+647
;; IV = 000000000000000000000000
+648
;; PT = (empty)
+649
;; AAD = (empty)
+650
;; CT = (empty)
+651
;; T = 58e2fccefa7e3061367f1d57a4e7455a
+652
;;
+653
;; Vector with non-empty PT (gcmEncryptExtIV128.rsp, K-1, IV-0, PT-128):
+654
;; K = 00000000000000000000000000000000
+655
;; IV = 000000000000000000000000
+656
;; PT = 00000000000000000000000000000000
+657
;; AAD = (empty)
+658
;; CT = 0388dace60b6a392f328c2b971b2fe78
+659
;; T = ab6e47d42cec13bdf53a67b21257bddf
+660
+661
(test-group "aes-128-gcm"
+662
+663
(test "NIST KAT — empty PT, AAD, all-zero key+IV"
+664
(let* ((key (make-bytevector 16 0))
+665
(iv (make-bytevector 12 0))
+666
(aad (make-bytevector 0 0))
+667
(pt (make-bytevector 0 0))
+668
(out (aes-128-gcm-encrypt key iv aad pt))
+669
(ct (car out))
+670
(tag (cdr out))
+671
(expected-tag (bytevector
+672
#x58 #xe2 #xfc #xce #xfa #x7e #x30 #x61
+673
#x36 #x7f #x1d #x57 #xa4 #xe7 #x45 #x5a)))
+674
(assert-equal 0 (bytevector-length ct))
+675
(assert-equal 16 (bytevector-length tag))
+676
(assert-equal expected-tag tag)))
+677
+678
(test "NIST KAT — 16-byte all-zero PT"
+679
(let* ((key (make-bytevector 16 0))
+680
(iv (make-bytevector 12 0))
+681
(aad (make-bytevector 0 0))
+682
(pt (make-bytevector 16 0))
+683
(out (aes-128-gcm-encrypt key iv aad pt))
+684
(ct (car out))
+685
(tag (cdr out))
+686
(expected-ct (bytevector
+687
#x03 #x88 #xda #xce #x60 #xb6 #xa3 #x92
+688
#xf3 #x28 #xc2 #xb9 #x71 #xb2 #xfe #x78))
+689
(expected-tag (bytevector
+690
#xab #x6e #x47 #xd4 #x2c #xec #x13 #xbd
+691
#xf5 #x3a #x67 #xb2 #x12 #x57 #xbd #xdf)))
+692
(assert-equal expected-ct ct)
+693
(assert-equal expected-tag tag)))
+694
+695
(test "round-trip: encrypt then decrypt yields original plaintext"
+696
(let* ((key (random-bytes 16))
+697
(iv (random-bytes 12))
+698
(aad "irrelevant aad")
+699
(pt "Hello, push subscriber. This is a longer-than-16-byte test message.")
+700
(out (aes-128-gcm-encrypt key iv aad pt))
+701
(ct (car out))
+702
(tag (cdr out))
+703
(rt (aes-128-gcm-decrypt key iv aad ct tag)))
+704
(assert-true (bytevector? rt))
+705
;; Compare bytes against the input string.
+706
(assert-equal (string-length pt) (bytevector-length rt))
+707
(let loop ((i 0))
+708
(cond
+709
((>= i (bytevector-length rt)) #t)
+710
(else
+711
(assert-equal (char->integer (string-ref pt i))
+712
(bytevector-u8-ref rt i))
+713
(loop (+ i 1)))))))
+714
+715
(test "decrypt with tampered tag fails (returns #f)"
+716
(let* ((key (random-bytes 16))
+717
(iv (random-bytes 12))
+718
(aad (make-bytevector 0 0))
+719
(pt "tagcheck")
+720
(out (aes-128-gcm-encrypt key iv aad pt))
+721
(ct (car out))
+722
(tag (cdr out)))
+723
(bytevector-u8-set! tag 0
+724
(bitwise-xor (bytevector-u8-ref tag 0) #x01))
+725
(assert-false (aes-128-gcm-decrypt key iv aad ct tag))))
+726
+727
(test "decrypt with tampered AAD fails"
+728
(let* ((key (random-bytes 16))
+729
(iv (random-bytes 12))
+730
(aad-good "expected aad")
+731
(aad-bad "tampered aad")
+732
(pt "aadcheck")
+733
(out (aes-128-gcm-encrypt key iv aad-good pt))
+734
(ct (car out))
+735
(tag (cdr out)))
+736
(assert-false (aes-128-gcm-decrypt key iv aad-bad ct tag))))
+737
+738
(test "decrypt with tampered ciphertext fails"
+739
(let* ((key (random-bytes 16))
+740
(iv (random-bytes 12))
+741
(aad (make-bytevector 0 0))
+742
(pt "ctcheck-message-here")
+743
(out (aes-128-gcm-encrypt key iv aad pt))
+744
(ct (car out))
+745
(tag (cdr out)))
+746
(bytevector-u8-set! ct 0
+747
(bitwise-xor (bytevector-u8-ref ct 0) #x55))
+748
(assert-false (aes-128-gcm-decrypt key iv aad ct tag)))))
+749
+750
+751
;; ============================================================
+752
;; RFC 8291 § 5 — Web Push end-to-end vector
+753
;; ============================================================
+754
;;
+755
;; The reference example exercises ECDH-P256 + HKDF-SHA256 +
+756
;; AES-128-GCM as composed for `aes128gcm` Content-Encoding. We
+757
;; reproduce the steps from § 3.1 / § 3.4 against the inputs in
+758
;; § 5 and verify the ciphertext + tag match. This is the most
+759
;; load-bearing KAT in this module — Web Push is the whole point
+760
;; of v0.15.1.
+761
;;
+762
;; Inputs (RFC 8291 § 5):
+763
;;
+764
;; plaintext = "When I grow up, I want to be a watermelon"
+765
;; IKM = ECDH(as_priv, ua_pub) [ECE-IKM, §3.4]
+766
;; salt = 16 random bytes, fixed in vector
+767
;; recordsize = 4096
+768
;;
+769
;; Where (from §5):
+770
;; ua_priv = q4yBd6S0FsYXqdvYJgcWGw
+771
;; (base64url; 32 bytes)
+772
;; ua_pub (p256dh)
+773
;; = BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcx
+774
;; aOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4
+775
;; (base64url; 65 bytes uncompressed P-256 point)
+776
;; auth_secret = BTBZMqHH6r4Tts7J_aSIgg (16 bytes)
+777
;; as_priv = yfWPiYE-n46HLnH0KqZOF1fJJU3MYrct3AELtAQ-oRw

Showing the first 500 of 648 diff lines for this file. This diff is INCOMPLETE; read the file or clone the repository for the rest.