diff --git a/mongoose.c b/mongoose.c index 043e6382..d8d49389 100644 --- a/mongoose.c +++ b/mongoose.c @@ -36,16 +36,16 @@ static int mg_base64_encode_single(int c) { } } -static int mg_base64_decode_single(int c) { +static int base64_decode_single(int c, char plus, char slash) { if (c >= 'A' && c <= 'Z') { return c - 'A'; } else if (c >= 'a' && c <= 'z') { return c + 26 - 'a'; } else if (c >= '0' && c <= '9') { return c + 52 - '0'; - } else if (c == '+') { + } else if (c == plus) { return 62; - } else if (c == '/') { + } else if (c == slash) { return 63; } else if (c == '=') { return 64; @@ -54,6 +54,14 @@ static int mg_base64_decode_single(int c) { } } +static int mg_base64_decode_single(int c) { + return base64_decode_single(c, '+', '/'); +} + +static int mg_base64url_decode_single(int c) { + return base64_decode_single(c, '-', '_'); +} + size_t mg_base64_update(unsigned char ch, char *to, size_t n) { unsigned long rem = (n & 3) % 3; if (rem == 0) { @@ -116,6 +124,44 @@ fail: return 0; } +size_t mg_base64url_encode(const unsigned char *p, size_t n, char *to, + size_t dl) { + size_t i, len = mg_base64_encode(p, n, to, dl); + if (len == 0) return 0; + for (i = 0; i < len; i++) { + if (to[i] == '+') { + to[i] = '-'; + } else if (to[i] == '/') { + to[i] = '_'; + } + } + while (len > 0 && to[len - 1] == '=') to[--len] = '\0'; + return len; +} + +size_t mg_base64url_decode(const char *src, size_t n, char *dst, size_t dl) { + size_t i, len = 0; + unsigned int bits = 0, v = 0; + if (dl == 0 || (n & 3) == 1) goto fail; + for (i = 0; src != NULL && i < n; i++) { + int c = mg_base64url_decode_single(src[i]); + if (c == 64) break; + if (c < 0) goto fail; + v = (v << 6) | (unsigned int) c; + bits += 6; + if (bits >= 8) { + bits -= 8; + if (len + 1 >= dl) goto fail; + dst[len++] = (char) ((v >> bits) & 255U); + } + } + dst[len] = '\0'; + return len; +fail: + if (dl > 0) dst[0] = '\0'; + return 0; +} + #ifdef MG_ENABLE_LINES #line 1 "src/bsd.c" #endif @@ -5076,6 +5122,174 @@ long mg_json_get_long(struct mg_str json, const char *path, long dflt) { return result; } +#ifdef MG_ENABLE_LINES +#line 1 "src/jwt.c" +#endif + + + + + + + + +#if MG_TLS == MG_TLS_BUILTIN + +#endif + +static size_t jwt_split(struct mg_str jwt, struct mg_str *h, struct mg_str *p, + struct mg_str *s) { + size_t i, a = jwt.len, b = jwt.len; + for (i = 0; i < jwt.len; i++) { + if (jwt.buf[i] == '.') { + if (a == jwt.len) a = i; + if (a != i) b = i; + if (b != jwt.len) break; + } + } + if (a == jwt.len || b == jwt.len || b + 1 >= jwt.len) return 0; + *h = mg_str_n(jwt.buf, a); + *p = mg_str_n(jwt.buf + a + 1, b - a - 1); + *s = mg_str_n(jwt.buf + b + 1, jwt.len - b - 1); + return b; +} + +static bool jwt_alg(struct mg_str h, const char *alg) { + char got[16]; + size_t len = h.len * 3 / 4 + 2; + char *buf = (char *) mg_calloc(1, len); + bool ok; + if (buf == NULL) return false; + ok = mg_base64url_decode(h.buf, h.len, buf, len) > 0 && + mg_json_unescape(mg_str(buf), "$.alg", got, sizeof(got)) > 0 && + strcmp(got, alg) == 0; + mg_free(buf); + return ok; +} + +static size_t jwt_header_payload(const char *alg, const struct mg_jwt_opts *opts, + char *buf, size_t len) { + char *header, *kid = NULL; + size_t n, m; + if (opts->kid.len > 0) { + kid = mg_mprintf(",%m:%m", MG_ESC("kid"), mg_print_esc, + (int) opts->kid.len, opts->kid.buf); + if (kid == NULL) return 0; + } + header = mg_mprintf("{%m:%m,%m:%m%.*s%.*s%.*s}", MG_ESC("alg"), + MG_ESC(alg), MG_ESC("typ"), MG_ESC("JWT"), + kid == NULL ? 0 : (int) strlen(kid), + kid == NULL ? "" : kid, + opts->header.len > 0 ? 1 : 0, ",", + (int) opts->header.len, opts->header.buf); + mg_free(kid); + if (header == NULL) return 0; + n = mg_base64url_encode((uint8_t *) header, strlen(header), buf, len); + mg_free(header); + if (n == 0 || n + 1 >= len) return 0; + buf[n++] = '.'; + m = mg_base64url_encode((uint8_t *) opts->claims.buf, opts->claims.len, + buf + n, len - n); + return m == 0 ? 0 : n + m; +} + +size_t mg_jwt_sign_hs256(const struct mg_jwt_opts *opts, char *buf, + size_t len) { + uint8_t sig[32]; + size_t n = jwt_header_payload("HS256", opts, buf, len); + size_t m; + if (n == 0 || n + 1 >= len) return 0; + mg_hmac_sha256(sig, (uint8_t *) opts->secret.buf, opts->secret.len, + (uint8_t *) buf, n); + buf[n++] = '.'; + m = mg_base64url_encode(sig, sizeof(sig), buf + n, len - n); + return m == 0 ? 0 : n + m; +} + +size_t mg_jwt_verify_hs256(struct mg_str jwt, const struct mg_jwt_opts *opts, + char *buf, size_t len) { + struct mg_str h, p, s; + uint8_t sig[33], digest[32]; + size_t n = jwt_split(jwt, &h, &p, &s); + if (n == 0 || !jwt_alg(h, "HS256")) return 0; + if (mg_base64url_decode(s.buf, s.len, (char *) sig, sizeof(sig)) != + sizeof(digest)) + return 0; + mg_hmac_sha256(digest, (uint8_t *) opts->secret.buf, opts->secret.len, + (uint8_t *) jwt.buf, n); + if (!mg_memeq(sig, digest, sizeof(digest))) return 0; + return mg_base64url_decode(p.buf, p.len, buf, len); +} + +#if MG_TLS == MG_TLS_BUILTIN +typedef struct { + MG_UECC_HashContext uECC; + mg_sha256_ctx sha256; +} jwt_hash_ctx; + +static void jwt_hash_init(const MG_UECC_HashContext *ctx) { + jwt_hash_ctx *c = (jwt_hash_ctx *) ctx; + mg_sha256_init(&c->sha256); +} + +static void jwt_hash_update(const MG_UECC_HashContext *ctx, const uint8_t *msg, + unsigned len) { + jwt_hash_ctx *c = (jwt_hash_ctx *) ctx; + mg_sha256_update(&c->sha256, msg, len); +} + +static void jwt_hash_finish(const MG_UECC_HashContext *ctx, uint8_t *hash) { + jwt_hash_ctx *c = (jwt_hash_ctx *) ctx; + mg_sha256_final(hash, &c->sha256); +} + +size_t mg_jwt_sign_es256(const struct mg_jwt_opts *opts, char *buf, + size_t len) { + uint8_t hash[32], sig[64], tmp[2 * 32 + 64]; + jwt_hash_ctx ctx = { + {jwt_hash_init, jwt_hash_update, jwt_hash_finish, 64, 32, tmp}, + {{0}, 0, 0, {0}}}; + size_t n = jwt_header_payload("ES256", opts, buf, len); + size_t m; + if (n == 0 || n + 1 >= len) return 0; + mg_sha256(hash, (uint8_t *) buf, n); + if (mg_uecc_sign_deterministic(opts->private_key, hash, sizeof(hash), + &ctx.uECC, sig, mg_uecc_secp256r1()) != 1) + return 0; + buf[n++] = '.'; + m = mg_base64url_encode(sig, sizeof(sig), buf + n, len - n); + return m == 0 ? 0 : n + m; +} + +size_t mg_jwt_verify_es256(struct mg_str jwt, const struct mg_jwt_opts *opts, + char *buf, size_t len) { + struct mg_str h, p, s; + uint8_t sig[65], hash[32]; + size_t n = jwt_split(jwt, &h, &p, &s); + if (n == 0 || !jwt_alg(h, "ES256")) return 0; + if (mg_base64url_decode(s.buf, s.len, (char *) sig, sizeof(sig)) != 64) + return 0; + mg_sha256(hash, (uint8_t *) jwt.buf, n); + if (mg_uecc_verify(opts->public_key, hash, sizeof(hash), sig, + mg_uecc_secp256r1()) != 1) + return 0; + return mg_base64url_decode(p.buf, p.len, buf, len); +} +#else +size_t mg_jwt_sign_es256(const struct mg_jwt_opts *opts, char *buf, + size_t len) { + (void) opts, (void) buf, (void) len; + MG_ERROR(("JWT ES256 requires built-in TLS")); + return 0; +} + +size_t mg_jwt_verify_es256(struct mg_str jwt, const struct mg_jwt_opts *opts, + char *buf, size_t len) { + (void) jwt, (void) opts, (void) buf, (void) len; + return mg_jwt_sign_es256(NULL, NULL, 0); +} +#endif + #ifdef MG_ENABLE_LINES #line 1 "src/l2.c" #endif @@ -14901,6 +15115,7 @@ void mg_timer_poll(struct mg_timer **head, uint64_t now_ms) { + #if MG_TLS == MG_TLS_BUILTIN /******************************************************************************/ #define AES_DECRYPTION 1 // whether AES decryption is supported @@ -15994,8 +16209,6 @@ int mg_aes_gcm_decrypt(unsigned char *output, const unsigned char *input, int ret = 0; // our return value gcm_context ctx; // includes the AES context structure unsigned char computed_tag[16]; - size_t i; - int diff = 0; if (tag_len > sizeof(computed_tag)) return -1; @@ -16006,9 +16219,7 @@ int mg_aes_gcm_decrypt(unsigned char *output, const unsigned char *input, gcm_zero_ctx(&ctx); - // compare tags - for (i = 0; i < tag_len; i++) diff |= computed_tag[i] ^ tag[i]; - if (diff != 0) ret = -1; + if (!mg_memeq(computed_tag, tag, tag_len)) ret = -1; return (ret); } @@ -18429,7 +18640,7 @@ static int mg_rsa_parse_key(const uint8_t *der, size_t dersz, // parameters ANY OPTIONAL // } static int mg_parse_pkcs8_key(const uint8_t *der, size_t dersz, - struct tls_data *tls) { + struct mg_rsa_key *rsa, uint8_t *ec_key) { struct mg_der_tlv root, version, alg_id, private_key_octets; struct mg_der_tlv alg_oid, alg_params; @@ -18462,17 +18673,19 @@ static int mg_parse_pkcs8_key(const uint8_t *der, size_t dersz, if (alg_oid.len == sizeof(mg_rsa_oid) && memcmp(alg_oid.value, mg_rsa_oid, sizeof(mg_rsa_oid)) == 0) { struct mg_rsa_key rsa_key; + if (rsa == NULL) return -1; if (mg_rsa_parse_key(private_key_octets.value, private_key_octets.len, &rsa_key) < 0) { MG_ERROR(("PKCS#8: failed to parse inner RSA key")); return -1; } - tls->rsa = rsa_key; + *rsa = rsa_key; return 0; } else if (alg_oid.len == sizeof(mg_ec_public_key_oid) && memcmp(alg_oid.value, mg_ec_public_key_oid, sizeof(mg_ec_public_key_oid)) == 0) { + if (ec_key == NULL) return -1; if (mg_der_next(&alg_id, &alg_params) < 0 || alg_params.type != 0x06) { MG_ERROR(("PKCS#8: invalid EC parameters OID")); return -1; @@ -18486,7 +18699,7 @@ static int mg_parse_pkcs8_key(const uint8_t *der, size_t dersz, } return mg_parse_ec_private_key(private_key_octets.value, - private_key_octets.len, tls->ec_key); + private_key_octets.len, ec_key); } else { MG_ERROR(("PKCS#8: unsupported algorithm")); @@ -18591,6 +18804,20 @@ static int mg_parse_pem_certs(const struct mg_str pem, struct mg_str **ders) { return count; } +size_t mg_uecc_parse_private_key(struct mg_str key, uint8_t *buf, size_t len) { + struct mg_str der = mg_str_n(NULL, 0); + size_t n = 0; + if (buf == NULL || len < 32) return 0; + // current mg_parse_ec_private_key() only handles 32-byte keys + if (mg_parse_pem(key, mg_str_s("EC PRIVATE KEY"), &der) == 0) { + if (mg_parse_ec_private_key((uint8_t *) der.buf, der.len, buf) == 0) n = 32; + } else if (mg_parse_pem(key, mg_str_s("PRIVATE KEY"), &der) == 0) { + if (mg_parse_pkcs8_key((uint8_t *) der.buf, der.len, NULL, buf) == 0) n = 32; + } + mg_free((void *) der.buf); + return n; +} + static void sync_time_cancel(struct mg_connection *c) { c->fn = NULL; c->is_closing = 1; @@ -18733,7 +18960,8 @@ void mg_tls_init(struct mg_connection *c, const struct mg_tls_opts *opts) { // Copy parsed RSA key components to tls->rsa for signing operations tls->rsa = rsa_key; } else if (mg_parse_pem(opts->key, mg_str_s("PRIVATE KEY"), &key) == 0) { - if (mg_parse_pkcs8_key((const uint8_t *) key.buf, key.len, tls) == 0) { + if (mg_parse_pkcs8_key((const uint8_t *) key.buf, key.len, &tls->rsa, + tls->ec_key) == 0) { if (tls->rsa.n.len > 0) { tls->rsa_key_der = key; MG_INFO(("Parsed PKCS#8 RSA private key: %d bytes", (int) key.len)); @@ -18877,6 +19105,7 @@ void mg_tls_ctx_free(struct mg_mgr *mgr) { + #if MG_TLS == MG_TLS_BUILTIN && MG_ENABLE_CHACHA20 // ******* BEGIN: chacha-portable/chacha-portable.h ******** @@ -20206,8 +20435,6 @@ PORTABLE_8439_DECL size_t mg_chacha20_poly1305_decrypt( // first we calculate the mac and see if it lines up, only then do we decrypt size_t actual_size = cipher_text_size - RFC_8439_TAG_SIZE; uint8_t computed_mac[RFC_8439_TAG_SIZE]; - int diff = 0; - size_t i; if (MG_OVERLAPPING(plain_text, actual_size, cipher_text, cipher_text_size)) { return (size_t) -1; } @@ -20215,10 +20442,8 @@ PORTABLE_8439_DECL size_t mg_chacha20_poly1305_decrypt( poly1305_calculate_mac(computed_mac, cipher_text, actual_size, key, nonce, ad, ad_size); - // compare tags - for (i = 0; i < RFC_8439_TAG_SIZE; i++) - diff |= computed_mac[i] ^ cipher_text[actual_size + i]; - if (diff != 0) return (size_t) -1; + if (!mg_memeq(computed_mac, cipher_text + actual_size, RFC_8439_TAG_SIZE)) + return (size_t) -1; chacha20_xor_stream(plain_text, cipher_text, actual_size, key, nonce, 1); return actual_size; @@ -22145,11 +22370,7 @@ static cmpresult_t mg_uecc_vli_cmp_unsafe(const mg_uecc_word_t *left, #define asm_mmod_fast_secp256r1 0 #endif -#if defined(default_RNG_defined) && default_RNG_defined -static MG_UECC_RNG_Function g_rng_function = &default_RNG; -#else -static MG_UECC_RNG_Function g_rng_function = 0; -#endif +static MG_UECC_RNG_Function g_rng_function = NULL; void mg_uecc_set_rng(MG_UECC_RNG_Function rng_function) { g_rng_function = rng_function; @@ -24483,15 +24704,11 @@ MG_UECC_VLI_API int mg_uecc_generate_random_int(mg_uecc_word_t *random, mg_uecc_word_t tries; bitcount_t num_bits = mg_uecc_vli_numBits(top, num_words); - if (!g_rng_function) { - return 0; - } - for (tries = 0; tries < MG_UECC_RNG_MAX_TRIES; ++tries) { - if (!g_rng_function((uint8_t *) random, - (unsigned int) (num_words * MG_UECC_WORD_SIZE))) { + unsigned int len = (unsigned int) (num_words * MG_UECC_WORD_SIZE); + if (!(g_rng_function == NULL ? mg_random((uint8_t *) random, len) + : g_rng_function((uint8_t *) random, len))) return 0; - } random[num_words - 1] &= mask >> ((bitcount_t) (num_words * MG_UECC_WORD_SIZE * 8 - num_bits)); if (!mg_uecc_vli_isZero(random, num_words) && @@ -25619,6 +25836,14 @@ uint64_t mg_timegm(unsigned int year, unsigned int month, unsigned int day, day_secs * ((ly + 299) / 400); } +bool mg_memeq(const void *a, const void *b, size_t n) { + const uint8_t *p = (const uint8_t *) a, *q = (const uint8_t *) b; + uint8_t r = 0; + size_t i; + for (i = 0; i < n; i++) r = (uint8_t) (r | (uint8_t) (p[i] ^ q[i])); + return r == 0; +} + #if MG_ENABLE_CUSTOM_RANDOM #else bool mg_random(void *buf, size_t len) { diff --git a/mongoose.h b/mongoose.h index 20bb649d..bcb674bc 100644 --- a/mongoose.h +++ b/mongoose.h @@ -1788,6 +1788,9 @@ void mg_free(void *ptr); // sensitive data (keys, passwords). void mg_bzero(volatile unsigned char *buf, size_t len); +// Fixed-length constant-time byte equality. Use for MACs, tags, signatures. +bool mg_memeq(const void *a, const void *b, size_t n); + // Fills buf with len cryptographically random bytes. Uses the best available // hardware or OS source (hardware RNG, /dev/urandom, CryptGenRandom, etc.). // Falls back to rand() with an error log if no strong source is available. @@ -2035,6 +2038,9 @@ size_t mg_base64_update(unsigned char input_byte, char *buf, size_t len); size_t mg_base64_final(char *buf, size_t len); size_t mg_base64_encode(const unsigned char *p, size_t n, char *buf, size_t); size_t mg_base64_decode(const char *src, size_t n, char *dst, size_t); +size_t mg_base64url_encode(const unsigned char *p, size_t n, char *buf, + size_t); +size_t mg_base64url_decode(const char *src, size_t n, char *dst, size_t); @@ -2668,6 +2674,12 @@ void mg_tls_ctx_init(struct mg_mgr *); void mg_tls_ctx_free(struct mg_mgr *); #define MG_IS_DER(buf) (((uint8_t *) (buf))[0] == 0x30) // DER begins with 0x30 +#if MG_TLS == MG_TLS_BUILTIN +// Extracts a 32-byte P-256 private scalar from EC PRIVATE KEY or PKCS#8 PRIVATE +// KEY input. Accepts PEM or DER. Returns key size, or 0 on error. +size_t mg_uecc_parse_private_key(struct mg_str key, uint8_t *buf, size_t len); +#endif + // Low-level IO primitives used by TLS layer enum { MG_IO_ERR = -1, MG_IO_WAIT = -2 }; long mg_io_send(struct mg_connection *c, const void *buf, size_t len); @@ -2868,28 +2880,28 @@ if 'dest' was filled with random data, or 0 if the random data could not be generated. The filled-in values should be either truly random, or from a cryptographically-secure PRNG. -A correctly functioning RNG function must be set (using mg_uecc_set_rng()) -before calling mg_uecc_make_key() or mg_uecc_sign(). +A correctly functioning RNG is required before calling mg_uecc_make_key() or +mg_uecc_sign(). Setting a correctly functioning RNG function improves the resistance to side-channel attacks for mg_uecc_shared_secret() and mg_uecc_sign_deterministic(). - -A correct RNG function is set by default when building for Windows, Linux, or OS -X. If you are building on another POSIX-compliant system that supports -/dev/random or /dev/urandom, you can define MG_UECC_POSIX to use the predefined -RNG. For embedded platforms there is no predefined RNG function; you must -provide your own. */ typedef int (*MG_UECC_RNG_Function)(uint8_t *dest, unsigned size); +// Mongoose: mg_uecc_make_key() and mg_uecc_sign() use mg_random() when no +// explicit uECC RNG is installed. If mg_random() cannot provide a strong source, +// those operations fail. Deterministic signing works without RNG, but only an +// explicit uECC RNG enables its optional side-channel blinding. + /* mg_uecc_set_rng() function. Set the function that will be used to generate random bytes. The RNG function should return 1 if the random data was generated, or 0 if the random data could not be generated. -On platforms where there is no predefined RNG function (eg embedded platforms), -this must be called before mg_uecc_make_key() or mg_uecc_sign() are used. +Mongoose: this overrides the default mg_random() fallback used by +mg_uecc_make_key() and mg_uecc_sign(). Do not call mg_uecc_set_rng() unless the +application must provide its own cryptographic RNG. Inputs: rng_function - The function that will be used to generate random bytes. @@ -4199,6 +4211,33 @@ size_t mg_json_next(struct mg_str obj, size_t ofs, struct mg_str *key, + + +struct mg_jwt_opts { + struct mg_str claims; // JSON payload + struct mg_str header; // Extra header members, no braces; alg/typ are set + struct mg_str kid; // Key ID protected header + struct mg_str secret; // HS256 secret + const uint8_t *private_key; // ES256 private key (MG_TLS_BUILTIN only) + const uint8_t *public_key; // ES256 public key (MG_TLS_BUILTIN only) +}; + +// Signs JSON claims as a compact JWT. Returns JWT length, or 0 on error. +size_t mg_jwt_sign_hs256(const struct mg_jwt_opts *opts, char *buf, + size_t len); + +// Verifies JWT and decodes JSON claims into buf. Returns claims length, or 0. +size_t mg_jwt_verify_hs256(struct mg_str jwt, const struct mg_jwt_opts *opts, + char *buf, size_t len); + +size_t mg_jwt_sign_es256(const struct mg_jwt_opts *opts, char *buf, + size_t len); +size_t mg_jwt_verify_es256(struct mg_str jwt, const struct mg_jwt_opts *opts, + char *buf, size_t len); + + + + // In-flight JSON-RPC request. Populate and pass to mg_rpc_process() to // dispatch, or receive one pre-filled in a handler registered with mg_rpc_add(). struct mg_rpc_req { diff --git a/src/base64.c b/src/base64.c index e3f78fa3..51401563 100644 --- a/src/base64.c +++ b/src/base64.c @@ -12,16 +12,16 @@ static int mg_base64_encode_single(int c) { } } -static int mg_base64_decode_single(int c) { +static int base64_decode_single(int c, char plus, char slash) { if (c >= 'A' && c <= 'Z') { return c - 'A'; } else if (c >= 'a' && c <= 'z') { return c + 26 - 'a'; } else if (c >= '0' && c <= '9') { return c + 52 - '0'; - } else if (c == '+') { + } else if (c == plus) { return 62; - } else if (c == '/') { + } else if (c == slash) { return 63; } else if (c == '=') { return 64; @@ -30,6 +30,14 @@ static int mg_base64_decode_single(int c) { } } +static int mg_base64_decode_single(int c) { + return base64_decode_single(c, '+', '/'); +} + +static int mg_base64url_decode_single(int c) { + return base64_decode_single(c, '-', '_'); +} + size_t mg_base64_update(unsigned char ch, char *to, size_t n) { unsigned long rem = (n & 3) % 3; if (rem == 0) { @@ -91,3 +99,41 @@ fail: if (dl > 0) dst[0] = '\0'; return 0; } + +size_t mg_base64url_encode(const unsigned char *p, size_t n, char *to, + size_t dl) { + size_t i, len = mg_base64_encode(p, n, to, dl); + if (len == 0) return 0; + for (i = 0; i < len; i++) { + if (to[i] == '+') { + to[i] = '-'; + } else if (to[i] == '/') { + to[i] = '_'; + } + } + while (len > 0 && to[len - 1] == '=') to[--len] = '\0'; + return len; +} + +size_t mg_base64url_decode(const char *src, size_t n, char *dst, size_t dl) { + size_t i, len = 0; + unsigned int bits = 0, v = 0; + if (dl == 0 || (n & 3) == 1) goto fail; + for (i = 0; src != NULL && i < n; i++) { + int c = mg_base64url_decode_single(src[i]); + if (c == 64) break; + if (c < 0) goto fail; + v = (v << 6) | (unsigned int) c; + bits += 6; + if (bits >= 8) { + bits -= 8; + if (len + 1 >= dl) goto fail; + dst[len++] = (char) ((v >> bits) & 255U); + } + } + dst[len] = '\0'; + return len; +fail: + if (dl > 0) dst[0] = '\0'; + return 0; +} diff --git a/src/base64.h b/src/base64.h index b2103e24..cc27c0c4 100644 --- a/src/base64.h +++ b/src/base64.h @@ -4,3 +4,6 @@ size_t mg_base64_update(unsigned char input_byte, char *buf, size_t len); size_t mg_base64_final(char *buf, size_t len); size_t mg_base64_encode(const unsigned char *p, size_t n, char *buf, size_t); size_t mg_base64_decode(const char *src, size_t n, char *dst, size_t); +size_t mg_base64url_encode(const unsigned char *p, size_t n, char *buf, + size_t); +size_t mg_base64url_decode(const char *src, size_t n, char *dst, size_t); diff --git a/src/jwt.c b/src/jwt.c new file mode 100644 index 00000000..f08a9718 --- /dev/null +++ b/src/jwt.c @@ -0,0 +1,164 @@ +#include "jwt.h" +#include "base64.h" +#include "json.h" +#include "log.h" +#include "printf.h" +#include "sha256.h" +#include "util.h" + +#if MG_TLS == MG_TLS_BUILTIN +#include "tls_uecc.h" +#endif + +static size_t jwt_split(struct mg_str jwt, struct mg_str *h, struct mg_str *p, + struct mg_str *s) { + size_t i, a = jwt.len, b = jwt.len; + for (i = 0; i < jwt.len; i++) { + if (jwt.buf[i] == '.') { + if (a == jwt.len) a = i; + if (a != i) b = i; + if (b != jwt.len) break; + } + } + if (a == jwt.len || b == jwt.len || b + 1 >= jwt.len) return 0; + *h = mg_str_n(jwt.buf, a); + *p = mg_str_n(jwt.buf + a + 1, b - a - 1); + *s = mg_str_n(jwt.buf + b + 1, jwt.len - b - 1); + return b; +} + +static bool jwt_alg(struct mg_str h, const char *alg) { + char got[16]; + size_t len = h.len * 3 / 4 + 2; + char *buf = (char *) mg_calloc(1, len); + bool ok; + if (buf == NULL) return false; + ok = mg_base64url_decode(h.buf, h.len, buf, len) > 0 && + mg_json_unescape(mg_str(buf), "$.alg", got, sizeof(got)) > 0 && + strcmp(got, alg) == 0; + mg_free(buf); + return ok; +} + +static size_t jwt_header_payload(const char *alg, const struct mg_jwt_opts *opts, + char *buf, size_t len) { + char *header, *kid = NULL; + size_t n, m; + if (opts->kid.len > 0) { + kid = mg_mprintf(",%m:%m", MG_ESC("kid"), mg_print_esc, + (int) opts->kid.len, opts->kid.buf); + if (kid == NULL) return 0; + } + header = mg_mprintf("{%m:%m,%m:%m%.*s%.*s%.*s}", MG_ESC("alg"), + MG_ESC(alg), MG_ESC("typ"), MG_ESC("JWT"), + kid == NULL ? 0 : (int) strlen(kid), + kid == NULL ? "" : kid, + opts->header.len > 0 ? 1 : 0, ",", + (int) opts->header.len, opts->header.buf); + mg_free(kid); + if (header == NULL) return 0; + n = mg_base64url_encode((uint8_t *) header, strlen(header), buf, len); + mg_free(header); + if (n == 0 || n + 1 >= len) return 0; + buf[n++] = '.'; + m = mg_base64url_encode((uint8_t *) opts->claims.buf, opts->claims.len, + buf + n, len - n); + return m == 0 ? 0 : n + m; +} + +size_t mg_jwt_sign_hs256(const struct mg_jwt_opts *opts, char *buf, + size_t len) { + uint8_t sig[32]; + size_t n = jwt_header_payload("HS256", opts, buf, len); + size_t m; + if (n == 0 || n + 1 >= len) return 0; + mg_hmac_sha256(sig, (uint8_t *) opts->secret.buf, opts->secret.len, + (uint8_t *) buf, n); + buf[n++] = '.'; + m = mg_base64url_encode(sig, sizeof(sig), buf + n, len - n); + return m == 0 ? 0 : n + m; +} + +size_t mg_jwt_verify_hs256(struct mg_str jwt, const struct mg_jwt_opts *opts, + char *buf, size_t len) { + struct mg_str h, p, s; + uint8_t sig[33], digest[32]; + size_t n = jwt_split(jwt, &h, &p, &s); + if (n == 0 || !jwt_alg(h, "HS256")) return 0; + if (mg_base64url_decode(s.buf, s.len, (char *) sig, sizeof(sig)) != + sizeof(digest)) + return 0; + mg_hmac_sha256(digest, (uint8_t *) opts->secret.buf, opts->secret.len, + (uint8_t *) jwt.buf, n); + if (!mg_memeq(sig, digest, sizeof(digest))) return 0; + return mg_base64url_decode(p.buf, p.len, buf, len); +} + +#if MG_TLS == MG_TLS_BUILTIN +typedef struct { + MG_UECC_HashContext uECC; + mg_sha256_ctx sha256; +} jwt_hash_ctx; + +static void jwt_hash_init(const MG_UECC_HashContext *ctx) { + jwt_hash_ctx *c = (jwt_hash_ctx *) ctx; + mg_sha256_init(&c->sha256); +} + +static void jwt_hash_update(const MG_UECC_HashContext *ctx, const uint8_t *msg, + unsigned len) { + jwt_hash_ctx *c = (jwt_hash_ctx *) ctx; + mg_sha256_update(&c->sha256, msg, len); +} + +static void jwt_hash_finish(const MG_UECC_HashContext *ctx, uint8_t *hash) { + jwt_hash_ctx *c = (jwt_hash_ctx *) ctx; + mg_sha256_final(hash, &c->sha256); +} + +size_t mg_jwt_sign_es256(const struct mg_jwt_opts *opts, char *buf, + size_t len) { + uint8_t hash[32], sig[64], tmp[2 * 32 + 64]; + jwt_hash_ctx ctx = { + {jwt_hash_init, jwt_hash_update, jwt_hash_finish, 64, 32, tmp}, + {{0}, 0, 0, {0}}}; + size_t n = jwt_header_payload("ES256", opts, buf, len); + size_t m; + if (n == 0 || n + 1 >= len) return 0; + mg_sha256(hash, (uint8_t *) buf, n); + if (mg_uecc_sign_deterministic(opts->private_key, hash, sizeof(hash), + &ctx.uECC, sig, mg_uecc_secp256r1()) != 1) + return 0; + buf[n++] = '.'; + m = mg_base64url_encode(sig, sizeof(sig), buf + n, len - n); + return m == 0 ? 0 : n + m; +} + +size_t mg_jwt_verify_es256(struct mg_str jwt, const struct mg_jwt_opts *opts, + char *buf, size_t len) { + struct mg_str h, p, s; + uint8_t sig[65], hash[32]; + size_t n = jwt_split(jwt, &h, &p, &s); + if (n == 0 || !jwt_alg(h, "ES256")) return 0; + if (mg_base64url_decode(s.buf, s.len, (char *) sig, sizeof(sig)) != 64) + return 0; + mg_sha256(hash, (uint8_t *) jwt.buf, n); + if (mg_uecc_verify(opts->public_key, hash, sizeof(hash), sig, + mg_uecc_secp256r1()) != 1) + return 0; + return mg_base64url_decode(p.buf, p.len, buf, len); +} +#else +size_t mg_jwt_sign_es256(const struct mg_jwt_opts *opts, char *buf, + size_t len) { + (void) opts, (void) buf, (void) len; + MG_ERROR(("JWT ES256 requires built-in TLS")); + return 0; +} + +size_t mg_jwt_verify_es256(struct mg_str jwt, const struct mg_jwt_opts *opts, + char *buf, size_t len) { + (void) jwt, (void) opts, (void) buf, (void) len; + return mg_jwt_sign_es256(NULL, NULL, 0); +} +#endif diff --git a/src/jwt.h b/src/jwt.h new file mode 100644 index 00000000..7d8ec549 --- /dev/null +++ b/src/jwt.h @@ -0,0 +1,27 @@ +#pragma once + +#include "arch.h" +#include "str.h" +#include "tls.h" + +struct mg_jwt_opts { + struct mg_str claims; // JSON payload + struct mg_str header; // Extra header members, no braces; alg/typ are set + struct mg_str kid; // Key ID protected header + struct mg_str secret; // HS256 secret + const uint8_t *private_key; // ES256 private key (MG_TLS_BUILTIN only) + const uint8_t *public_key; // ES256 public key (MG_TLS_BUILTIN only) +}; + +// Signs JSON claims as a compact JWT. Returns JWT length, or 0 on error. +size_t mg_jwt_sign_hs256(const struct mg_jwt_opts *opts, char *buf, + size_t len); + +// Verifies JWT and decodes JSON claims into buf. Returns claims length, or 0. +size_t mg_jwt_verify_hs256(struct mg_str jwt, const struct mg_jwt_opts *opts, + char *buf, size_t len); + +size_t mg_jwt_sign_es256(const struct mg_jwt_opts *opts, char *buf, + size_t len); +size_t mg_jwt_verify_es256(struct mg_str jwt, const struct mg_jwt_opts *opts, + char *buf, size_t len); diff --git a/src/tls.h b/src/tls.h index 07a6688f..9f452e84 100644 --- a/src/tls.h +++ b/src/tls.h @@ -87,6 +87,12 @@ void mg_tls_ctx_init(struct mg_mgr *); void mg_tls_ctx_free(struct mg_mgr *); #define MG_IS_DER(buf) (((uint8_t *) (buf))[0] == 0x30) // DER begins with 0x30 +#if MG_TLS == MG_TLS_BUILTIN +// Extracts a 32-byte P-256 private scalar from EC PRIVATE KEY or PKCS#8 PRIVATE +// KEY input. Accepts PEM or DER. Returns key size, or 0 on error. +size_t mg_uecc_parse_private_key(struct mg_str key, uint8_t *buf, size_t len); +#endif + // Low-level IO primitives used by TLS layer enum { MG_IO_ERR = -1, MG_IO_WAIT = -2 }; long mg_io_send(struct mg_connection *c, const void *buf, size_t len); diff --git a/src/tls_aes128.c b/src/tls_aes128.c index 811d4eb3..61c203ca 100644 --- a/src/tls_aes128.c +++ b/src/tls_aes128.c @@ -22,6 +22,7 @@ #include "arch.h" #include "tls.h" +#include "util.h" #if MG_TLS == MG_TLS_BUILTIN /******************************************************************************/ #define AES_DECRYPTION 1 // whether AES decryption is supported @@ -1115,8 +1116,6 @@ int mg_aes_gcm_decrypt(unsigned char *output, const unsigned char *input, int ret = 0; // our return value gcm_context ctx; // includes the AES context structure unsigned char computed_tag[16]; - size_t i; - int diff = 0; if (tag_len > sizeof(computed_tag)) return -1; @@ -1127,9 +1126,7 @@ int mg_aes_gcm_decrypt(unsigned char *output, const unsigned char *input, gcm_zero_ctx(&ctx); - // compare tags - for (i = 0; i < tag_len; i++) diff |= computed_tag[i] ^ tag[i]; - if (diff != 0) ret = -1; + if (!mg_memeq(computed_tag, tag, tag_len)) ret = -1; return (ret); } diff --git a/src/tls_builtin.c b/src/tls_builtin.c index e976bfed..b6329a26 100644 --- a/src/tls_builtin.c +++ b/src/tls_builtin.c @@ -2409,7 +2409,7 @@ static int mg_rsa_parse_key(const uint8_t *der, size_t dersz, // parameters ANY OPTIONAL // } static int mg_parse_pkcs8_key(const uint8_t *der, size_t dersz, - struct tls_data *tls) { + struct mg_rsa_key *rsa, uint8_t *ec_key) { struct mg_der_tlv root, version, alg_id, private_key_octets; struct mg_der_tlv alg_oid, alg_params; @@ -2442,17 +2442,19 @@ static int mg_parse_pkcs8_key(const uint8_t *der, size_t dersz, if (alg_oid.len == sizeof(mg_rsa_oid) && memcmp(alg_oid.value, mg_rsa_oid, sizeof(mg_rsa_oid)) == 0) { struct mg_rsa_key rsa_key; + if (rsa == NULL) return -1; if (mg_rsa_parse_key(private_key_octets.value, private_key_octets.len, &rsa_key) < 0) { MG_ERROR(("PKCS#8: failed to parse inner RSA key")); return -1; } - tls->rsa = rsa_key; + *rsa = rsa_key; return 0; } else if (alg_oid.len == sizeof(mg_ec_public_key_oid) && memcmp(alg_oid.value, mg_ec_public_key_oid, sizeof(mg_ec_public_key_oid)) == 0) { + if (ec_key == NULL) return -1; if (mg_der_next(&alg_id, &alg_params) < 0 || alg_params.type != 0x06) { MG_ERROR(("PKCS#8: invalid EC parameters OID")); return -1; @@ -2466,7 +2468,7 @@ static int mg_parse_pkcs8_key(const uint8_t *der, size_t dersz, } return mg_parse_ec_private_key(private_key_octets.value, - private_key_octets.len, tls->ec_key); + private_key_octets.len, ec_key); } else { MG_ERROR(("PKCS#8: unsupported algorithm")); @@ -2571,6 +2573,20 @@ static int mg_parse_pem_certs(const struct mg_str pem, struct mg_str **ders) { return count; } +size_t mg_uecc_parse_private_key(struct mg_str key, uint8_t *buf, size_t len) { + struct mg_str der = mg_str_n(NULL, 0); + size_t n = 0; + if (buf == NULL || len < 32) return 0; + // current mg_parse_ec_private_key() only handles 32-byte keys + if (mg_parse_pem(key, mg_str_s("EC PRIVATE KEY"), &der) == 0) { + if (mg_parse_ec_private_key((uint8_t *) der.buf, der.len, buf) == 0) n = 32; + } else if (mg_parse_pem(key, mg_str_s("PRIVATE KEY"), &der) == 0) { + if (mg_parse_pkcs8_key((uint8_t *) der.buf, der.len, NULL, buf) == 0) n = 32; + } + mg_free((void *) der.buf); + return n; +} + static void sync_time_cancel(struct mg_connection *c) { c->fn = NULL; c->is_closing = 1; @@ -2713,7 +2729,8 @@ void mg_tls_init(struct mg_connection *c, const struct mg_tls_opts *opts) { // Copy parsed RSA key components to tls->rsa for signing operations tls->rsa = rsa_key; } else if (mg_parse_pem(opts->key, mg_str_s("PRIVATE KEY"), &key) == 0) { - if (mg_parse_pkcs8_key((const uint8_t *) key.buf, key.len, tls) == 0) { + if (mg_parse_pkcs8_key((const uint8_t *) key.buf, key.len, &tls->rsa, + tls->ec_key) == 0) { if (tls->rsa.n.len > 0) { tls->rsa_key_der = key; MG_INFO(("Parsed PKCS#8 RSA private key: %d bytes", (int) key.len)); diff --git a/src/tls_chacha20.c b/src/tls_chacha20.c index c69516fe..08206872 100644 --- a/src/tls_chacha20.c +++ b/src/tls_chacha20.c @@ -6,6 +6,7 @@ #include "tls_chacha20.h" #include "tls.h" +#include "util.h" #if MG_TLS == MG_TLS_BUILTIN && MG_ENABLE_CHACHA20 // ******* BEGIN: chacha-portable/chacha-portable.h ******** @@ -1336,8 +1337,6 @@ PORTABLE_8439_DECL size_t mg_chacha20_poly1305_decrypt( // first we calculate the mac and see if it lines up, only then do we decrypt size_t actual_size = cipher_text_size - RFC_8439_TAG_SIZE; uint8_t computed_mac[RFC_8439_TAG_SIZE]; - int diff = 0; - size_t i; if (MG_OVERLAPPING(plain_text, actual_size, cipher_text, cipher_text_size)) { return (size_t) -1; } @@ -1345,10 +1344,8 @@ PORTABLE_8439_DECL size_t mg_chacha20_poly1305_decrypt( poly1305_calculate_mac(computed_mac, cipher_text, actual_size, key, nonce, ad, ad_size); - // compare tags - for (i = 0; i < RFC_8439_TAG_SIZE; i++) - diff |= computed_mac[i] ^ cipher_text[actual_size + i]; - if (diff != 0) return (size_t) -1; + if (!mg_memeq(computed_mac, cipher_text + actual_size, RFC_8439_TAG_SIZE)) + return (size_t) -1; chacha20_xor_stream(plain_text, cipher_text, actual_size, key, nonce, 1); return actual_size; diff --git a/src/tls_uecc.c b/src/tls_uecc.c index 575d47de..cc50d18f 100644 --- a/src/tls_uecc.c +++ b/src/tls_uecc.c @@ -222,11 +222,7 @@ static cmpresult_t mg_uecc_vli_cmp_unsafe(const mg_uecc_word_t *left, #define asm_mmod_fast_secp256r1 0 #endif -#if defined(default_RNG_defined) && default_RNG_defined -static MG_UECC_RNG_Function g_rng_function = &default_RNG; -#else -static MG_UECC_RNG_Function g_rng_function = 0; -#endif +static MG_UECC_RNG_Function g_rng_function = NULL; void mg_uecc_set_rng(MG_UECC_RNG_Function rng_function) { g_rng_function = rng_function; @@ -2560,15 +2556,11 @@ MG_UECC_VLI_API int mg_uecc_generate_random_int(mg_uecc_word_t *random, mg_uecc_word_t tries; bitcount_t num_bits = mg_uecc_vli_numBits(top, num_words); - if (!g_rng_function) { - return 0; - } - for (tries = 0; tries < MG_UECC_RNG_MAX_TRIES; ++tries) { - if (!g_rng_function((uint8_t *) random, - (unsigned int) (num_words * MG_UECC_WORD_SIZE))) { + unsigned int len = (unsigned int) (num_words * MG_UECC_WORD_SIZE); + if (!(g_rng_function == NULL ? mg_random((uint8_t *) random, len) + : g_rng_function((uint8_t *) random, len))) return 0; - } random[num_words - 1] &= mask >> ((bitcount_t) (num_words * MG_UECC_WORD_SIZE * 8 - num_bits)); if (!mg_uecc_vli_isZero(random, num_words) && diff --git a/src/tls_uecc.h b/src/tls_uecc.h index 46cb9e46..9e751e8d 100644 --- a/src/tls_uecc.h +++ b/src/tls_uecc.h @@ -113,28 +113,28 @@ if 'dest' was filled with random data, or 0 if the random data could not be generated. The filled-in values should be either truly random, or from a cryptographically-secure PRNG. -A correctly functioning RNG function must be set (using mg_uecc_set_rng()) -before calling mg_uecc_make_key() or mg_uecc_sign(). +A correctly functioning RNG is required before calling mg_uecc_make_key() or +mg_uecc_sign(). Setting a correctly functioning RNG function improves the resistance to side-channel attacks for mg_uecc_shared_secret() and mg_uecc_sign_deterministic(). - -A correct RNG function is set by default when building for Windows, Linux, or OS -X. If you are building on another POSIX-compliant system that supports -/dev/random or /dev/urandom, you can define MG_UECC_POSIX to use the predefined -RNG. For embedded platforms there is no predefined RNG function; you must -provide your own. */ typedef int (*MG_UECC_RNG_Function)(uint8_t *dest, unsigned size); +// Mongoose: mg_uecc_make_key() and mg_uecc_sign() use mg_random() when no +// explicit uECC RNG is installed. If mg_random() cannot provide a strong source, +// those operations fail. Deterministic signing works without RNG, but only an +// explicit uECC RNG enables its optional side-channel blinding. + /* mg_uecc_set_rng() function. Set the function that will be used to generate random bytes. The RNG function should return 1 if the random data was generated, or 0 if the random data could not be generated. -On platforms where there is no predefined RNG function (eg embedded platforms), -this must be called before mg_uecc_make_key() or mg_uecc_sign() are used. +Mongoose: this overrides the default mg_random() fallback used by +mg_uecc_make_key() and mg_uecc_sign(). Do not call mg_uecc_set_rng() unless the +application must provide its own cryptographic RNG. Inputs: rng_function - The function that will be used to generate random bytes. diff --git a/src/util.c b/src/util.c index 82a4ab8e..9b55acb9 100644 --- a/src/util.c +++ b/src/util.c @@ -25,6 +25,14 @@ uint64_t mg_timegm(unsigned int year, unsigned int month, unsigned int day, day_secs * ((ly + 299) / 400); } +bool mg_memeq(const void *a, const void *b, size_t n) { + const uint8_t *p = (const uint8_t *) a, *q = (const uint8_t *) b; + uint8_t r = 0; + size_t i; + for (i = 0; i < n; i++) r = (uint8_t) (r | (uint8_t) (p[i] ^ q[i])); + return r == 0; +} + #if MG_ENABLE_CUSTOM_RANDOM #else bool mg_random(void *buf, size_t len) { diff --git a/src/util.h b/src/util.h index 3edf4d8a..8d4795c3 100644 --- a/src/util.h +++ b/src/util.h @@ -22,6 +22,9 @@ void mg_free(void *ptr); // sensitive data (keys, passwords). void mg_bzero(volatile unsigned char *buf, size_t len); +// Fixed-length constant-time byte equality. Use for MACs, tags, signatures. +bool mg_memeq(const void *a, const void *b, size_t n); + // Fills buf with len cryptographically random bytes. Uses the best available // hardware or OS source (hardware RNG, /dev/urandom, CryptGenRandom, etc.). // Falls back to rand() with an error log if no strong source is available. diff --git a/test/Makefile b/test/Makefile index 89b818c5..1d4a307b 100644 --- a/test/Makefile +++ b/test/Makefile @@ -106,7 +106,7 @@ mongoose.c: Makefile $(wildcard ../src/*.c) $(wildcard ../src/drivers/*.c) cd .. && (export LC_ALL=C ; cat src/license.h; echo; echo '#include "mongoose.h"' ; (for F in src/*.c src/drivers/*.c ; do echo; echo '#ifdef MG_ENABLE_LINES'; echo "#line 1 \"$$F\""; echo '#endif'; cat $$F | sed -e 's,#include ".*,,'; done))> $@ mongoose.h: $(HDRS) Makefile - cd .. && (export LC_ALL=C ; cat src/license.h; echo; echo '#ifndef MONGOOSE_H'; echo '#define MONGOOSE_H'; echo; cat src/version.h ; echo; echo '#ifdef __cplusplus'; echo 'extern "C" {'; echo '#endif'; cat src/arch.h src/arch_*.h src/os_*.h src/net_ft.h src/net_lwip.h src/net_rl.h src/config.h src/profile.h src/str.h src/queue.h src/fmt.h src/printf.h src/log.h src/timer.h src/fs.h src/util.h src/url.h src/iobuf.h src/base64.h src/md5.h src/sha1.h src/sha256.h src/event.h src/net.h src/http.h src/ssi.h src/tls.h src/tls_x25519.h src/tls_aes128.h src/tls_uecc.h src/tls_chacha20.h src/tls_rsa.h src/tls_mbed.h src/tls_openssl.h src/ws.h src/sntp.h src/mqtt.h src/dns.h src/modbus.h src/json.h src/rpc.h src/dash.h src/ota.h src/flash.h src/wifi.h src/l2.h src/net_builtin.h src/bsd.h src/drivers/*.h | sed -e '/keep/! s,#include ".*,,' -e 's,^#pragma once,,'; echo; echo '#ifdef __cplusplus'; echo '}'; echo '#endif'; echo '#endif // MONGOOSE_H')> $@ + cd .. && (export LC_ALL=C ; cat src/license.h; echo; echo '#ifndef MONGOOSE_H'; echo '#define MONGOOSE_H'; echo; cat src/version.h ; echo; echo '#ifdef __cplusplus'; echo 'extern "C" {'; echo '#endif'; cat src/arch.h src/arch_*.h src/os_*.h src/net_ft.h src/net_lwip.h src/net_rl.h src/config.h src/profile.h src/str.h src/queue.h src/fmt.h src/printf.h src/log.h src/timer.h src/fs.h src/util.h src/url.h src/iobuf.h src/base64.h src/md5.h src/sha1.h src/sha256.h src/event.h src/net.h src/http.h src/ssi.h src/tls.h src/tls_x25519.h src/tls_aes128.h src/tls_uecc.h src/tls_chacha20.h src/tls_rsa.h src/tls_mbed.h src/tls_openssl.h src/ws.h src/sntp.h src/mqtt.h src/dns.h src/modbus.h src/json.h src/jwt.h src/rpc.h src/dash.h src/ota.h src/flash.h src/wifi.h src/l2.h src/net_builtin.h src/bsd.h src/drivers/*.h | sed -e '/keep/! s,#include ".*,,' -e 's,^#pragma once,,'; echo; echo '#ifdef __cplusplus'; echo '}'; echo '#endif'; echo '#endif // MONGOOSE_H')> $@ # Check that all external (exported) symbols have "mg_" prefix mg_prefix: mongoose.c mongoose.h diff --git a/test/unit_test.c b/test/unit_test.c index a26bb0ac..e8af21f7 100644 --- a/test/unit_test.c +++ b/test/unit_test.c @@ -301,6 +301,13 @@ static void test_base64(void) { ASSERT(mg_base64_decode("AAA=", 4, buf, 2) == 0); ASSERT(mg_base64_decode("AAA=", 4, buf, 3) == 0); ASSERT(mg_base64_decode("AAA=", 4, buf, 4) == 2); + + ASSERT(mg_base64url_encode((uint8_t *) "???", 3, buf, sizeof(buf)) == 4); + ASSERT(strcmp(buf, "Pz8_") == 0); + ASSERT(mg_base64url_decode("Pz8_", 4, buf, sizeof(buf)) == 3); + ASSERT(strcmp(buf, "???") == 0); + ASSERT(mg_base64url_decode("Pz8/", 4, buf, sizeof(buf)) == 0); + ASSERT(mg_base64url_decode("A", 1, buf, sizeof(buf)) == 0); } static void test_iobuf(void) { @@ -4388,6 +4395,83 @@ static void test_rsa(void) { #endif } +static void test_jwt(void) { + char jwt[512], claims[128], got[128], hdr[128], extra[64], tampered[512]; + struct mg_jwt_opts opts, bad; + const char *dot; + char *str; + size_t n, m; + + mg_snprintf(claims, sizeof(claims), "{%m:%m,%m:%d}", MG_ESC("sub"), + MG_ESC("admin"), MG_ESC("iat"), 123); + mg_snprintf(extra, sizeof(extra), "%m:%m", MG_ESC("cty"), MG_ESC("JWT")); + memset(&opts, 0, sizeof(opts)); + opts.claims = mg_str(claims); + opts.header = mg_str(extra); + opts.kid = mg_str("hmac-key-1"); + opts.secret = mg_str("secret"); + n = mg_jwt_sign_hs256(&opts, jwt, sizeof(jwt)); + ASSERT(n > 0 && n + 1 < sizeof(jwt)); + jwt[n] = '\0'; + + dot = strchr(jwt, '.'); + ASSERT(dot != NULL); + ASSERT(mg_base64url_decode(jwt, (size_t) (dot - jwt), hdr, + sizeof(hdr)) > 0); + str = mg_json_get_str(mg_str(hdr), "$.kid"); + ASSERT(str != NULL && strcmp(str, "hmac-key-1") == 0); + mg_free(str); + str = mg_json_get_str(mg_str(hdr), "$.cty"); + ASSERT(str != NULL && strcmp(str, "JWT") == 0); + mg_free(str); + + m = mg_jwt_verify_hs256(mg_str_n(jwt, n), &opts, got, sizeof(got)); + ASSERT(m == strlen(claims)); + ASSERT(strcmp(got, claims) == 0); + bad = opts; + bad.secret = mg_str("bad-secret"); + ASSERT(mg_jwt_verify_hs256(mg_str_n(jwt, n), &bad, got, + sizeof(got)) == 0); + memcpy(tampered, jwt, n + 1); + tampered[n - 1] = tampered[n - 1] == 'A' ? 'B' : 'A'; + ASSERT(mg_jwt_verify_hs256(mg_str_n(tampered, n), &opts, got, + sizeof(got)) == 0); + ASSERT(mg_jwt_sign_hs256(&opts, got, 10) == 0); + ASSERT(mg_jwt_verify_hs256(mg_str_n(jwt, n), &opts, got, 4) == 0); + +#if MG_TLS == MG_TLS_BUILTIN + { + const char *key_pem = + "-----BEGIN EC PRIVATE KEY-----\n" + "MHcCAQEEIAVdo8UAScxG7jiuNY2UZESNX/KPH8qJ0u0gOMMsAzYWoAoGCCqGSM49\n" + "AwEHoUQDQgAEqN6BIhvgbk7ecmUcn8Da9Avkj/uDNERtqWJG9r/or26X4u9jR5Jl\n" + "4hf5Gx17YJkq5/z3k6ogPDPpoAYWIw1/sw==\n" + "-----END EC PRIVATE KEY-----\n"; + uint8_t key[32], pub[64]; + + memset(&opts, 0, sizeof(opts)); + opts.claims = mg_str(claims); + opts.kid = mg_str("ec-key-1"); + opts.private_key = key; + opts.public_key = pub; + ASSERT(mg_uecc_parse_private_key(mg_str(key_pem), key, sizeof(key)) == 32); + ASSERT(mg_uecc_compute_public_key(key, pub, mg_uecc_secp256r1()) == 1); + n = mg_jwt_sign_es256(&opts, jwt, sizeof(jwt)); + ASSERT(n > 0 && n + 1 < sizeof(jwt)); + jwt[n] = '\0'; + m = mg_jwt_verify_es256(mg_str_n(jwt, n), &opts, got, sizeof(got)); + ASSERT(m == strlen(claims)); + ASSERT(strcmp(got, claims) == 0); + ASSERT(mg_jwt_verify_hs256(mg_str_n(jwt, n), &bad, got, + sizeof(got)) == 0); + memcpy(tampered, jwt, n + 1); + tampered[n - 1] = tampered[n - 1] == 'A' ? 'B' : 'A'; + ASSERT(mg_jwt_verify_es256(mg_str_n(tampered, n), &opts, got, + sizeof(got)) == 0); + } +#endif +} + static void test_crypto(void) { test_md5(); test_sha1(); @@ -4395,6 +4479,7 @@ static void test_crypto(void) { test_sha384(); test_x25519(); test_rsa(); + test_jwt(); } static void dash_custom(struct mg_connection *c, int ev, void *ev_data) { diff --git a/tutorials/http/jwt-auth/Makefile b/tutorials/http/jwt-auth/Makefile new file mode 100644 index 00000000..91ea7254 --- /dev/null +++ b/tutorials/http/jwt-auth/Makefile @@ -0,0 +1,26 @@ +PROG ?= example # Program we are building +DELETE = rm -rf # Command to remove files +OUT ?= -o $(PROG) # Compiler argument for output file +SOURCES = main.c mongoose.c # Source code files +CFLAGS = -W -Wall -Wextra -g -I. # Build options + +# Mongoose build options. See https://mongoose.ws/documentation/#build-options +CFLAGS_MONGOOSE += -DMG_ENABLE_LINES=1 +CFLAGS_EXTRA ?= -DMG_TLS=MG_TLS_BUILTIN + +ifeq ($(OS),Windows_NT) # Windows settings. Assume MinGW compiler. To use VC: make CC=cl CFLAGS=/MD OUT=/Feprog.exe + PROG ?= example.exe # Use .exe suffix for the binary + CC = gcc # Use MinGW gcc compiler + CFLAGS += -lws2_32 -lbcrypt # Link against Winsock library + DELETE = cmd /C del /Q /F /S # Command prompt command to delete files + OUT ?= -o $(PROG) # Build output +endif + +all: $(PROG) # Default target. Build and run program + $(RUN) ./$(PROG) $(ARGS) + +$(PROG): $(SOURCES) # Build program from sources + $(CC) $(SOURCES) $(CFLAGS) $(CFLAGS_MONGOOSE) $(CFLAGS_EXTRA) $(OUT) + +clean: # Cleanup. Delete built program and all build artifacts + $(DELETE) $(PROG) *.o *.obj *.exe *.dSYM diff --git a/tutorials/http/jwt-auth/README.md b/tutorials/http/jwt-auth/README.md new file mode 100644 index 00000000..4cccd4a0 --- /dev/null +++ b/tutorials/http/jwt-auth/README.md @@ -0,0 +1,62 @@ +# JWT Bearer Authentication + +## Build and run: + +```sh +make +``` + +## HS256: + +This path signs and verifies JWTs with the shared secret from `main.c`. + +```sh +curl -i http://127.0.0.1:8000/login-hs256 +TOKEN=$(curl -s http://127.0.0.1:8000/login-hs256 | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') +curl -i -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8000/ +``` + +```txt +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 47 + +{"sub":"admin","scope":"dash:read dash:write"} +``` + +`scope` here is a JSON string value containing a space-delimited permission list; as an example. + +## ES256 (Requires MG_TLS_BUILTIN): + +This path parses the EC private key from `main.c`, signs the JWT with it, and +derives the public key for verification to keep the example self-contained. + +```sh +curl -i http://127.0.0.1:8000/login-es256 +TOKEN=$(curl -s http://127.0.0.1:8000/login-es256 | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') +curl -i -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8000/ +``` + +```txt +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 36 + +{"sub":"admin","scope":"dash:read"} +``` + +`scope` here is a JSON string value containing a space-delimited permission list; as an example. + +## Unauthorized access attempt example + +```sh +curl -i -H "Authorization: Bearer bad.token.here" http://127.0.0.1:8000/ +``` + +```txt +HTTP/1.1 401 Unauthorized +WWW-Authenticate: Bearer +Content-Length: 13 + +Unauthorized +``` diff --git a/tutorials/http/jwt-auth/main.c b/tutorials/http/jwt-auth/main.c new file mode 100644 index 00000000..0a253f52 --- /dev/null +++ b/tutorials/http/jwt-auth/main.c @@ -0,0 +1,108 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved +// +// HTTP server example. It issues and verifies JWT Bearer tokens. + +#include "mongoose.h" + +static const char *s_listen_on = "http://0.0.0.0:8000"; +static const char *s_secret = "change-me"; +enum { TOKEN_LEN = 1024, CLAIMS_LEN = 256, JWT_LEN = 1024 }; +static const char *s_ec_private_key = + "-----BEGIN EC PRIVATE KEY-----\n" + "MHcCAQEEIAVdo8UAScxG7jiuNY2UZESNX/KPH8qJ0u0gOMMsAzYWoAoGCCqGSM49\n" + "AwEHoUQDQgAEqN6BIhvgbk7ecmUcn8Da9Avkj/uDNERtqWJG9r/or26X4u9jR5Jl\n" + "4hf5Gx17YJkq5/z3k6ogPDPpoAYWIw1/sw==\n" + "-----END EC PRIVATE KEY-----\n"; + +#if MG_TLS == MG_TLS_BUILTIN +static size_t verify_es256(struct mg_str token, char *claims, size_t len) { + uint8_t key[32], pub[64]; + struct mg_jwt_opts opts = {.public_key = pub}; + // This keeps the example self-contained. A real verifier would usually keep + // only the issuer public key, selected by kid, not the signing private key. + if (mg_uecc_parse_private_key(mg_str(s_ec_private_key), key, sizeof(key)) != + 32 || + mg_uecc_compute_public_key(key, pub, mg_uecc_secp256r1()) != 1) { + return 0; + } + return mg_jwt_verify_es256(token, &opts, claims, len); +} +#endif + +static void fn(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_HTTP_MSG) { + struct mg_http_message *hm = (struct mg_http_message *) ev_data; + char user[1], *token, *claims, *jwt; + token = (char *) mg_calloc(1, TOKEN_LEN); + claims = (char *) mg_calloc(1, CLAIMS_LEN); + jwt = (char *) mg_calloc(1, JWT_LEN); + if (token == NULL || claims == NULL || jwt == NULL) { + mg_http_reply(c, 500, "", "OOM\n"); + mg_free(token); + mg_free(claims); + mg_free(jwt); + return; + } + mg_http_creds(hm, user, sizeof(user), token, TOKEN_LEN); + + if (mg_match(hm->uri, mg_str("/login-hs256"), NULL)) { + struct mg_jwt_opts opts = {.kid = mg_str("hmac-key-1"), + .secret = mg_str(s_secret)}; + size_t n; + mg_snprintf(claims, CLAIMS_LEN, "{%m:%m,%m:%m}", MG_ESC("sub"), + MG_ESC("admin"), MG_ESC("scope"), + MG_ESC("dash:read dash:write")); + opts.claims = mg_str(claims); + n = mg_jwt_sign_hs256(&opts, jwt, JWT_LEN); + mg_http_reply(c, n == 0 ? 500 : 200, "Content-Type: application/json\r\n", + "{%m:%m}\n", MG_ESC("token"), MG_ESC(n == 0 ? "" : jwt)); + +#if MG_TLS == MG_TLS_BUILTIN + } else if (mg_match(hm->uri, mg_str("/login-es256"), NULL)) { + uint8_t key[32]; + struct mg_jwt_opts opts = {.kid = mg_str("ec-key-1"), + .private_key = key}; + size_t keysz, n; + keysz = + mg_uecc_parse_private_key(mg_str(s_ec_private_key), key, sizeof(key)); + // ES256 uses a raw 64-byte r || s signature. mg_jwt_sign_es256() signs it + // deterministically; applications can also hash the signing input and + // call mg_uecc_sign() directly if they need random-nonce ECDSA. + mg_snprintf(claims, CLAIMS_LEN, "{%m:%m,%m:%m}", MG_ESC("sub"), + MG_ESC("admin"), MG_ESC("scope"), MG_ESC("dash:read")); + opts.claims = mg_str(claims); + n = keysz == 32 ? mg_jwt_sign_es256(&opts, jwt, JWT_LEN) : 0; + mg_http_reply(c, n == 0 ? 500 : 200, "Content-Type: application/json\r\n", + "{%m:%m}\n", MG_ESC("token"), MG_ESC(n == 0 ? "" : jwt)); +#endif + + } else { + struct mg_jwt_opts opts = {.secret = mg_str(s_secret)}; + if (mg_jwt_verify_hs256(mg_str(token), &opts, claims, CLAIMS_LEN) > 0) { + mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s\n", + claims); +#if MG_TLS == MG_TLS_BUILTIN + } else if (verify_es256(mg_str(token), claims, CLAIMS_LEN) > 0) { + mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s\n", + claims); +#endif + } else { + mg_http_reply(c, 401, "WWW-Authenticate: Bearer\r\n", "Unauthorized\n"); + } + } + mg_free(token); + mg_free(claims); + mg_free(jwt); + } +} + +int main(void) { + struct mg_mgr mgr; + mg_log_set(MG_LL_DEBUG); + mg_mgr_init(&mgr); // Initialise event manager + mg_http_listen(&mgr, s_listen_on, fn, NULL); // Create HTTP listener + for (;;) mg_mgr_poll(&mgr, 1000); // Infinite event loop + mg_mgr_free(&mgr); + return 0; +} diff --git a/tutorials/http/jwt-auth/mongoose.c b/tutorials/http/jwt-auth/mongoose.c new file mode 120000 index 00000000..5e522bbc --- /dev/null +++ b/tutorials/http/jwt-auth/mongoose.c @@ -0,0 +1 @@ +../../../mongoose.c \ No newline at end of file diff --git a/tutorials/http/jwt-auth/mongoose.h b/tutorials/http/jwt-auth/mongoose.h new file mode 120000 index 00000000..ee4ac823 --- /dev/null +++ b/tutorials/http/jwt-auth/mongoose.h @@ -0,0 +1 @@ +../../../mongoose.h \ No newline at end of file