diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..375f331 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "(gdb) Launch-simple", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/simple", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 9e06476..fee43e5 100644 --- a/README.md +++ b/README.md @@ -161,3 +161,4 @@ this library uses code and ideas from [jwtd][1] and [jwt-go][2] [2]: https://github.com/dgrijalva/jwt-go [3]: http://semver.org [4]: https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/ +[5]: https://github.com/benmcollins/libjwt diff --git a/dub.json b/dub.json index ca67ed5..1f6d0d5 100644 --- a/dub.json +++ b/dub.json @@ -9,9 +9,9 @@ "HuntLabs" ], "description": "A D implementation of JSON Web Tokens", - "copyright": "Copyright © 2020, HuntLabs", + "copyright": "Copyright © 2021, HuntLabs", "license": "MIT", "dependencies": { - "hunt-openssl": "~>1.0.1" + "hunt-openssl": "~>1.0.4" } } \ No newline at end of file diff --git a/examples/simple/.vscode/launch.json b/examples/simple/.vscode/launch.json new file mode 100644 index 0000000..f0c3d8c --- /dev/null +++ b/examples/simple/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "(gdb) Launch", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/simple", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/simple/dub.json b/examples/simple/dub.json index 54849c2..85e5cd1 100644 --- a/examples/simple/dub.json +++ b/examples/simple/dub.json @@ -1,11 +1,15 @@ { "authors": [ - "Zhang" + "HuntLabs" ], "description": "A minimal D application.", "license": "proprietary", "name": "simple", + "versions" : [ + "HUNT_JWT_DEBUG" + ], "dependencies": { - "hunt-jwt" : {"path": "../../"} + "hunt-jwt" : {"path": "../../"}, + "hunt" : "~>1.6.6" } } \ No newline at end of file diff --git a/source/hunt/jwt/Base64Codec.d b/source/hunt/jwt/Base64Codec.d index 769e02f..037dffa 100644 --- a/source/hunt/jwt/Base64Codec.d +++ b/source/hunt/jwt/Base64Codec.d @@ -15,6 +15,6 @@ string urlsafeB64Encode(string inp) pure nothrow { /** * Decode a string with URL-safe Base64. */ -string urlsafeB64Decode(string inp) pure { - return cast(string)Base64URLNoPadding.decode(inp); +ubyte[] urlsafeB64Decode(string inp) pure { + return Base64URLNoPadding.decode(inp); } diff --git a/source/hunt/jwt/Jwt.d b/source/hunt/jwt/Jwt.d index 483fe36..9079b0a 100644 --- a/source/hunt/jwt/Jwt.d +++ b/source/hunt/jwt/Jwt.d @@ -11,6 +11,9 @@ import std.algorithm; import std.array : split; +import hunt.logging; + + /** simple version that accepts only strings as values for payload and header fields */ @@ -49,6 +52,27 @@ string encode(in ubyte[] payload, string key, JwtAlgorithm algo = JwtAlgorithm.H string encodedHeader = memoize!(getEncodedHeader, 64)(algo, header_fields); string encodedPayload = Base64URLNoPadding.encode(payload); + string signingInput = encodedHeader ~ "." ~ encodedPayload; + string signValue = sign(signingInput, key, algo); + + version(HUNT_JWT_DEBUG) { + tracef("sign: %(%02X %)", cast(ubyte[])signValue); + } + + string signature = Base64URLNoPadding.encode(cast(ubyte[])sign(signingInput, key, algo)); + return signingInput ~ "." ~ signature; +} + +// string fromDER(string data) { + +// } + +string encode(string payload, string key, JwtAlgorithm algo, + string header_fields) { + + string encodedHeader = Base64URLNoPadding.encode(cast(ubyte[])header_fields); // memoize!(getEncodedHeader, 64)(algo, header_fields); + string encodedPayload = Base64URLNoPadding.encode(cast(ubyte[])payload); + string signingInput = encodedHeader ~ "." ~ encodedPayload; string signature = Base64URLNoPadding.encode(cast(ubyte[])sign(signingInput, key, algo)); @@ -78,7 +102,7 @@ JSONValue decode(string token, string delegate(ref JSONValue jose) lazyKey) { JSONValue header; try { - header = parseJSON(urlsafeB64Decode(tokenParts[0])); + header = parseJSON(cast(string)urlsafeB64Decode(tokenParts[0])); } catch(Exception e) { throw new VerifyException("Header is incorrect."); } @@ -98,13 +122,13 @@ JSONValue decode(string token, string delegate(ref JSONValue jose) lazyKey) { } const key = lazyKey(header); - if(!verifySignature(urlsafeB64Decode(tokenParts[2]), tokenParts[0]~"."~tokenParts[1], key, alg)) + if(!verifySignature(tokenParts[0]~"."~tokenParts[1], tokenParts[2], key, alg)) throw new VerifyException("Signature is incorrect."); JSONValue payload; try { - payload = parseJSON(urlsafeB64Decode(tokenParts[1])); + payload = parseJSON(cast(string)urlsafeB64Decode(tokenParts[1])); } catch(JSONException e) { // Code coverage has to miss this line because the signature test above throws before this does throw new VerifyException("Payload JSON is incorrect."); diff --git a/source/hunt/jwt/JwtOpenSSL.d b/source/hunt/jwt/JwtOpenSSL.d index 9ff4e89..f26c6d3 100644 --- a/source/hunt/jwt/JwtOpenSSL.d +++ b/source/hunt/jwt/JwtOpenSSL.d @@ -9,126 +9,12 @@ import deimos.openssl.err; import hunt.jwt.Exceptions; import hunt.jwt.JwtAlgorithm; +import hunt.logging; -EC_KEY* getESKeypair(uint curve_type, string key) { - EC_GROUP* curve; - EVP_PKEY* pktmp; - BIO* bpo; - EC_POINT* pub; +import std.conv; +import std.range; - if(null == (curve = EC_GROUP_new_by_curve_name(curve_type))) - throw new Exception("Unsupported curve."); - scope(exit) EC_GROUP_free(curve); - - bpo = BIO_new_mem_buf(cast(char*)key.ptr, -1); - if(bpo is null) { - throw new Exception("Can't load the key."); - } - scope(exit) BIO_free(bpo); - - pktmp = PEM_read_bio_PrivateKey(bpo, null, null, null); - if(pktmp is null) { - throw new Exception("Can't load the evp_pkey."); - } - scope(exit) EVP_PKEY_free(pktmp); - - EC_KEY* eckey; - eckey = EVP_PKEY_get1_EC_KEY(pktmp); - if(eckey is null) { - throw new Exception("Can't convert evp_pkey to EC_KEY."); - } - scope(failure) EC_KEY_free(eckey); - - if(1 != EC_KEY_set_group(eckey, curve)) { - throw new Exception("Can't associate group with the key."); - } - - const BIGNUM *prv = EC_KEY_get0_private_key(eckey); - if(null == prv) { - throw new Exception("Can't get private key."); - } - - pub = EC_POINT_new(curve); - if(null == pub) { - throw new Exception("Can't allocate EC point."); - } - scope(exit) EC_POINT_free(pub); - - if (1 != EC_POINT_mul(curve, pub, prv, null, null, null)) { - throw new Exception("Can't calculate public key."); - } - - if(1 != EC_KEY_set_public_key(eckey, pub)) { - throw new Exception("Can't set public key."); - } - - return eckey; -} - - -EC_KEY* getESPrivateKey(uint curve_type, string key) { - EC_GROUP* curve; - EVP_PKEY* pktmp; - BIO* bpo; - - if(null == (curve = EC_GROUP_new_by_curve_name(curve_type))) - throw new Exception("Unsupported curve."); - scope(exit) EC_GROUP_free(curve); - - bpo = BIO_new_mem_buf(cast(char*)key.ptr, -1); - if(bpo is null) { - throw new Exception("Can't load the key."); - } - scope(exit) BIO_free(bpo); - - pktmp = PEM_read_bio_PrivateKey(bpo, null, null, null); - if(pktmp is null) { - throw new Exception("Can't load the evp_pkey."); - } - scope(exit) EVP_PKEY_free(pktmp); - - EC_KEY * eckey; - eckey = EVP_PKEY_get1_EC_KEY(pktmp); - if(eckey is null) { - throw new Exception("Can't convert evp_pkey to EC_KEY."); - } - - scope(failure) EC_KEY_free(eckey); - if(1 != EC_KEY_set_group(eckey, curve)) { - throw new Exception("Can't associate group with the key."); - } - - return eckey; -} - - -EC_KEY* getESPublicKey(uint curve_type, string key) { - EC_GROUP* curve; - - if(null == (curve = EC_GROUP_new_by_curve_name(curve_type))) - throw new Exception("Unsupported curve."); - scope(exit) EC_GROUP_free(curve); - - EC_KEY* eckey; - - BIO* bpo = BIO_new_mem_buf(cast(char*)key.ptr, -1); - if(bpo is null) { - throw new Exception("Can't load the key."); - } - scope(exit) BIO_free(bpo); - - eckey = PEM_read_bio_EC_PUBKEY(bpo, null, null, null); - scope(failure) EC_KEY_free(eckey); - - if(1 != EC_KEY_set_group(eckey, curve)) { - throw new Exception("Can't associate group with the key."); - } - - if(0 == EC_KEY_check_key(eckey)) - throw new Exception("Public key is not valid."); - - return eckey; -} +import core.stdc.stdlib : alloca; string sign(string msg, string key, JwtAlgorithm algo = JwtAlgorithm.HS256) { ubyte[] sign; @@ -151,42 +37,12 @@ string sign(string msg, string key, JwtAlgorithm algo = JwtAlgorithm.HS256) { } } - void sign_rs(ubyte* hash, int type, uint len, uint signLen) { - sign = new ubyte[len]; - RSA* rsa_private = RSA_new(); - scope(exit) RSA_free(rsa_private); - - BIO* bpo = BIO_new_mem_buf(cast(char*)key.ptr, -1); - if(bpo is null) - throw new Exception("Can't load the key."); - scope(exit) BIO_free(bpo); - - RSA* rsa = PEM_read_bio_RSAPrivateKey(bpo, &rsa_private, null, null); - if(rsa is null) { - throw new Exception("Can't create RSA key."); - } - if(0 == RSA_sign(type, hash, signLen, sign.ptr, &signLen, rsa_private)) { - throw new Exception("Can't sign RSA message digest."); - } - } - - void sign_es(uint curve_type, ubyte* hash, int hashLen) { - EC_KEY* eckey = getESPrivateKey(curve_type, key); - scope(exit) EC_KEY_free(eckey); - - ECDSA_SIG* sig = ECDSA_do_sign(hash, hashLen, eckey); - if(sig is null) { - throw new Exception("Digest sign failed."); - } - scope(exit) ECDSA_SIG_free(sig); - - sign = new ubyte[ECDSA_size(eckey)]; - ubyte* c = sign.ptr; - if(!i2d_ECDSA_SIG(sig, &c)) { - throw new Exception("Convert sign to DER format failed."); - } - } +version(HUNT_JWT_DEBUG) { + trace("msg: ", msg); + trace("key: ", key); + trace("algo: ", algo); +} switch(algo) { case JwtAlgorithm.NONE: { @@ -204,52 +60,168 @@ string sign(string msg, string key, JwtAlgorithm algo = JwtAlgorithm.HS256) { sign_hs(EVP_sha512(), SHA512_DIGEST_LENGTH); break; } + + /* RSA */ case JwtAlgorithm.RS256: { - ubyte[] hash = new ubyte[SHA256_DIGEST_LENGTH]; - SHA256(cast(const(ubyte)*)msg.ptr, msg.length, hash.ptr); - sign_rs(hash.ptr, NID_sha256, 256, SHA256_DIGEST_LENGTH); + const(EVP_MD) *alg = EVP_sha256(); + sign = signShaPem(alg, EVP_PKEY_RSA, key, msg); break; } case JwtAlgorithm.RS384: { - ubyte[] hash = new ubyte[SHA384_DIGEST_LENGTH]; - SHA384(cast(const(ubyte)*)msg.ptr, msg.length, hash.ptr); - sign_rs(hash.ptr, NID_sha384, 384, SHA384_DIGEST_LENGTH); + const(EVP_MD) *alg = EVP_sha384(); + sign = signShaPem(alg, EVP_PKEY_RSA, key, msg); break; } case JwtAlgorithm.RS512: { - ubyte[] hash = new ubyte[SHA512_DIGEST_LENGTH]; - SHA512(cast(const(ubyte)*)msg.ptr, msg.length, hash.ptr); - sign_rs(hash.ptr, NID_sha512, 512, SHA512_DIGEST_LENGTH); + const(EVP_MD) *alg = EVP_sha512(); + sign = signShaPem(alg, EVP_PKEY_RSA, key, msg); break; } + + /* ECC */ case JwtAlgorithm.ES256: { - ubyte[] hash = new ubyte[SHA256_DIGEST_LENGTH]; - SHA256(cast(const(ubyte)*)msg.ptr, msg.length, hash.ptr); - sign_es(NID_secp256k1, hash.ptr, SHA256_DIGEST_LENGTH); + const(EVP_MD) *alg = EVP_sha256(); + sign = signShaPem(alg, EVP_PKEY_EC, key, msg); break; } case JwtAlgorithm.ES384: { - ubyte[] hash = new ubyte[SHA384_DIGEST_LENGTH]; - SHA384(cast(const(ubyte)*)msg.ptr, msg.length, hash.ptr); - sign_es(NID_secp384r1, hash.ptr, SHA384_DIGEST_LENGTH); + const(EVP_MD) *alg = EVP_sha384(); + sign = signShaPem(alg, EVP_PKEY_EC, key, msg); break; } case JwtAlgorithm.ES512: { - ubyte[] hash = new ubyte[SHA512_DIGEST_LENGTH]; - SHA512(cast(const(ubyte)*)msg.ptr, msg.length, hash.ptr); - sign_es(NID_secp521r1, hash.ptr, SHA512_DIGEST_LENGTH); + const(EVP_MD) *alg = EVP_sha512(); + sign = signShaPem(alg, EVP_PKEY_EC, key, msg); break; } default: - throw new SignException("Wrong algorithm."); + throw new SignException("Wrong algorithm: " ~ to!string(algo)); } return cast(string)sign; } +// Ported from https://github.com/benmcollins/libjwt/blob/master/libjwt/jwt-openssl.c +private static ubyte[] signShaPem(const(EVP_MD) *alg, int type, string key, string msg) { + BIO * bufkey = BIO_new_mem_buf(cast(void*)key.ptr, cast(int)key.length); + if(bufkey is null) { + throw new Exception("Can't load the private key."); + } + scope(exit) BIO_free(bufkey); -bool verifySignature(string signature, string signing_input, string key, JwtAlgorithm algo = JwtAlgorithm.HS256) { + /* This uses OpenSSL's default passphrase callback if needed. The + * library caller can override this in many ways, all of which are + * outside of the scope of LibJWT and this is documented in jwt.h. */ + EVP_PKEY *pkey = PEM_read_bio_PrivateKey(bufkey, null, null, null); + if (pkey is null) + throw new Exception("Invalid argument"); + scope(exit) EVP_PKEY_free(pkey); + + int pkey_type = EVP_PKEY_id(pkey); + if (pkey_type != type) + throw new Exception("Invalid argument"); + + EVP_MD_CTX *mdctx = EVP_MD_CTX_create(); + if (mdctx is null) + throw new Exception("Out of memory"); + scope(exit) EVP_MD_CTX_destroy(mdctx); + + /* Initialize the DigestSign operation using alg */ + if (EVP_DigestSignInit(mdctx, null, alg, null, pkey) != 1) + throw new Exception("Invalid argument"); + + /* Call update with the message */ + if (EVP_DigestSignUpdate(mdctx, cast(void*)msg.ptr, msg.length) != 1) + throw new Exception("Invalid argument"); + + /* First, call EVP_DigestSignFinal with a null sig parameter to get length + * of sig. Length is returned in slen */ + size_t slen; + if (EVP_DigestSignFinal(mdctx, null, &slen) != 1) + throw new Exception("Invalid argument"); + + /* Allocate memory for signature based on returned size */ + // FIXME: Needing refactor or cleanup -@zhangxueping at 2021-03-03T19:38:11+08:00 + // Crashed + // ubyte[] sig = new ubyte[slen]; + ubyte* sig = cast(ubyte*)alloca(slen); + + /* Get the signature */ + if (EVP_DigestSignFinal(mdctx, sig, &slen) != 1) + throw new Exception("Invalid argument"); + + ubyte[] resultSig; + + if (pkey_type != EVP_PKEY_EC) { + resultSig = sig[0..slen].dup; + } else { + uint degree, bn_len, r_len, s_len, buf_len; + + /* For EC we need to convert to a raw format of R/S. */ + + /* Get the actual ec_key */ + EC_KEY *ec_key = EVP_PKEY_get1_EC_KEY(pkey); + if (ec_key is null) + throw new Exception("Out of memory"); + + degree = EC_GROUP_get_degree(EC_KEY_get0_group(ec_key)); + + EC_KEY_free(ec_key); + + /* Get the sig from the DER encoded version. */ + version(HUNT_JWT_DEBUG) { + infof("slen: %d, sig: %(%02X %)", slen, sig[0..slen]); + } + + // FIXME: Needing refactor or cleanup -@zhangxueping at 2021-03-03T19:39:16+08:00 + // Crashed here + // ECDSA_SIG *ec_sig = d2i_ECDSA_SIG(null, cast(const(ubyte) **)sig.ptr, cast(long)slen); + ECDSA_SIG *ec_sig = d2i_ECDSA_SIG(null, cast(const(ubyte) **)&sig, cast(long)slen); + if (ec_sig is null) + throw new Exception("Can't decode ECDSA signature."); + scope(exit) ECDSA_SIG_free(ec_sig); + + // version(HUNT_JWT_DEBUG) { + // tracef("slen: %d, sig: %(%02X %)", slen, sig[0..slen]); + // } + + BIGNUM *ec_sig_r; + BIGNUM *ec_sig_s; + ECDSA_SIG_get0(ec_sig, &ec_sig_r, &ec_sig_s); + r_len = BN_num_bytes(ec_sig_r); + s_len = BN_num_bytes(ec_sig_s); + bn_len = (degree + 7) / 8; + if ((r_len > bn_len) || (s_len > bn_len)) + throw new Exception("Invalid argument"); + + buf_len = 2 * bn_len; + ubyte[] raw_buf = new ubyte[buf_len]; + + /* Pad the bignums with leading zeroes. */ + // memset(raw_buf, 0, buf_len); + BN_bn2bin(ec_sig_r, raw_buf.ptr + bn_len - r_len); + BN_bn2bin(ec_sig_s, raw_buf.ptr + buf_len - s_len); + + resultSig = raw_buf; + } + + version(HUNT_JWT_DEBUG) { + tracef("%d, buffer: %(%02X %)", resultSig.length, resultSig); + } + + return resultSig; +} + + +bool verifySignature(string head, string signature, string key, JwtAlgorithm algo = JwtAlgorithm.HS256) { + import hunt.jwt.Base64Codec; + + version(HUNT_JWT_DEBUG) { + infof("signature: %s", signature); + } + + ubyte[] decodedSign = cast(ubyte[])urlsafeB64Decode(signature); bool verify_rs(ubyte* hash, int type, uint len, uint signLen) { RSA* rsa_public = RSA_new(); @@ -265,26 +237,11 @@ bool verifySignature(string signature, string signing_input, string key, JwtAlgo throw new Exception("Can't create RSA key."); } - ubyte[] sign = cast(ubyte[])signature; - int ret = RSA_verify(type, hash, signLen, sign.ptr, len, rsa_public); + // ubyte[] sign = cast(ubyte[])signature; + int ret = RSA_verify(type, hash, signLen, decodedSign.ptr, len, rsa_public); return ret == 1; } - bool verify_es(uint curve_type, ubyte* hash, int hashLen ) { - EC_KEY* eckey = getESPublicKey(curve_type, key); - scope(exit) EC_KEY_free(eckey); - - ubyte* c = cast(ubyte*)signature.ptr; - ECDSA_SIG* sig = null; - sig = d2i_ECDSA_SIG(&sig, cast(const (ubyte)**)&c, cast(int) key.length); - if (sig is null) { - throw new Exception("Can't decode ECDSA signature."); - } - scope(exit) ECDSA_SIG_free(sig); - - int ret = ECDSA_do_verify(hash, hashLen, sig, eckey); - return ret == 1; - } switch(algo) { case JwtAlgorithm.NONE: { @@ -293,38 +250,39 @@ bool verifySignature(string signature, string signing_input, string key, JwtAlgo case JwtAlgorithm.HS256: case JwtAlgorithm.HS384: case JwtAlgorithm.HS512: { - return signature == sign(signing_input, key, algo); - } - case JwtAlgorithm.RS256: { - ubyte[] hash = new ubyte[SHA256_DIGEST_LENGTH]; - SHA256(cast(const(ubyte)*)signing_input.ptr, signing_input.length, hash.ptr); - return verify_rs(hash.ptr, NID_sha256, 256, SHA256_DIGEST_LENGTH); - } - case JwtAlgorithm.RS384: { - ubyte[] hash = new ubyte[SHA384_DIGEST_LENGTH]; - SHA384(cast(const(ubyte)*)signing_input.ptr, signing_input.length, hash.ptr); - return verify_rs(hash.ptr, NID_sha384, 384, SHA384_DIGEST_LENGTH); - } - case JwtAlgorithm.RS512: { - ubyte[] hash = new ubyte[SHA512_DIGEST_LENGTH]; - SHA512(cast(const(ubyte)*)signing_input.ptr, signing_input.length, hash.ptr); - return verify_rs(hash.ptr, NID_sha512, 512, SHA512_DIGEST_LENGTH); + return decodedSign == cast(ubyte[])sign(head, key, algo); } - case JwtAlgorithm.ES256:{ - ubyte[] hash = new ubyte[SHA256_DIGEST_LENGTH]; - SHA256(cast(const(ubyte)*)signing_input.ptr, signing_input.length, hash.ptr); - return verify_es(NID_secp256k1, hash.ptr, SHA256_DIGEST_LENGTH ); + /* RSA */ + case JwtAlgorithm.RS256: { + const(EVP_MD) *alg = EVP_sha256(); + return verifyShaPem(alg, EVP_PKEY_RSA, head, decodedSign, key); } - case JwtAlgorithm.ES384:{ - ubyte[] hash = new ubyte[SHA384_DIGEST_LENGTH]; - SHA384(cast(const(ubyte)*)signing_input.ptr, signing_input.length, hash.ptr); - return verify_es(NID_secp384r1, hash.ptr, SHA384_DIGEST_LENGTH ); + case JwtAlgorithm.RS384: { + const(EVP_MD) *alg = EVP_sha384(); + return verifyShaPem(alg, EVP_PKEY_RSA, head, decodedSign, key); + } + case JwtAlgorithm.RS512: { + const(EVP_MD) *alg = EVP_sha512(); + return verifyShaPem(alg, EVP_PKEY_RSA, head, decodedSign, key); + } + + /* ECC */ + case JwtAlgorithm.ES256: { + const(EVP_MD) *alg = EVP_sha256(); + return verifyShaPem(alg, EVP_PKEY_EC, head, decodedSign, key); + + // ubyte[] hash = new ubyte[SHA256_DIGEST_LENGTH]; + // SHA256(cast(const(ubyte)*)head.ptr, head.length, hash.ptr); + // return verify_es(NID_secp256k1, hash.ptr, SHA256_DIGEST_LENGTH ); + } + case JwtAlgorithm.ES384: { + const(EVP_MD) *alg = EVP_sha384(); + return verifyShaPem(alg, EVP_PKEY_EC, head, decodedSign, key); } case JwtAlgorithm.ES512: { - ubyte[] hash = new ubyte[SHA512_DIGEST_LENGTH]; - SHA512(cast(const(ubyte)*)signing_input.ptr, signing_input.length, hash.ptr); - return verify_es(NID_secp521r1, hash.ptr, SHA512_DIGEST_LENGTH ); + const(EVP_MD) *alg = EVP_sha512(); + return verifyShaPem(alg, EVP_PKEY_EC, head, decodedSign, key); } default: @@ -332,3 +290,125 @@ bool verifySignature(string signature, string signing_input, string key, JwtAlgo } } +private bool verifyShaPem(const(EVP_MD) *alg, int type, string head, const(ubyte)[] sig, string key) { + version(HUNT_JWT_DEBUG) { + tracef("head: %s", head); + tracef("sig: %(%02X %)", sig); + } + + int slen = cast(int)sig.length; + + // sig = jwt_b64_decode(sig_b64, &slen); + if (sig.empty()) { + version(HUNT_JWT_DEBUG) warning("Invalid argument"); + return false; + } + + BIO *bufkey = BIO_new_mem_buf(cast(void*)key.ptr, cast(int)key.length); + if (bufkey is null) { + version(HUNT_JWT_DEBUG) warning("Out of memory"); + return false; + } + + scope(exit) BIO_free(bufkey); + + /* This uses OpenSSL's default passphrase callback if needed. The + * library caller can override this in many ways, all of which are + * outside of the scope of LibJWT and this is documented in jwt.h. */ + EVP_PKEY *pkey = PEM_read_bio_PUBKEY(bufkey, null, null, null); + if (pkey is null) { + version(HUNT_JWT_DEBUG) warning("Invalid argument"); + return false; + } + scope(exit) EVP_PKEY_free(pkey); + + int pkey_type = EVP_PKEY_id(pkey); + if (pkey_type != type) { + version(HUNT_JWT_DEBUG) warning("Invalid argument"); + return false; + } + + /* Convert EC sigs back to ASN1. */ + if (pkey_type == EVP_PKEY_EC) { + uint degree, bn_len; + EC_KEY *ec_key; + + ECDSA_SIG *ec_sig = ECDSA_SIG_new(); + if (ec_sig is null) { + version(HUNT_JWT_DEBUG) warning("Out of memory"); + return false; + } + scope(exit) ECDSA_SIG_free(ec_sig); + + /* Get the actual ec_key */ + ec_key = EVP_PKEY_get1_EC_KEY(pkey); + if (ec_key is null) { + version(HUNT_JWT_DEBUG) warning("Out of memory"); + return false; + } + + degree = EC_GROUP_get_degree(EC_KEY_get0_group(ec_key)); + + EC_KEY_free(ec_key); + + bn_len = (degree + 7) / 8; + if ((bn_len * 2) != slen) { + version(HUNT_JWT_DEBUG) warning("Invalid argument"); + return false; + } + + BIGNUM *ec_sig_r = BN_bin2bn(cast(const(ubyte)*)sig.ptr, bn_len, null); + BIGNUM *ec_sig_s = BN_bin2bn(cast(const(ubyte)*)sig.ptr + bn_len, bn_len, null); + if (ec_sig_r is null || ec_sig_s is null) { + version(HUNT_JWT_DEBUG) warning("Invalid argument"); + return false; + } + + ECDSA_SIG_set0(ec_sig, ec_sig_r, ec_sig_s); + + slen = i2d_ECDSA_SIG(ec_sig, null); + // sig = jwt_malloc(slen); + ubyte[] tempBuffer = new ubyte[slen]; + ubyte*p = tempBuffer.ptr; + // ubyte* tempBuffer = cast(ubyte*)alloca(slen); + // ubyte*p = tempBuffer; + slen = i2d_ECDSA_SIG(ec_sig, &p); + if (slen == 0) { + version(HUNT_JWT_DEBUG) warning("Invalid argument"); + return false; + } + sig = tempBuffer; + } + + EVP_MD_CTX *mdctx = EVP_MD_CTX_create(); + if (mdctx is null) { + version(HUNT_JWT_DEBUG) warning("Out of memory"); + return false; + } + scope(exit) EVP_MD_CTX_destroy(mdctx); + + /* Initialize the DigestVerify operation using alg */ + if (EVP_DigestVerifyInit(mdctx, null, alg, null, pkey) != 1){ + version(HUNT_JWT_DEBUG) warning("Invalid argument"); + return false; + } + + /* Call update with the message */ + if (EVP_DigestVerifyUpdate(mdctx, head.ptr, cast(int)head.length) != 1){ + version(HUNT_JWT_DEBUG) warning("Invalid argument"); + return false; + } + + version(HUNT_JWT_DEBUG) { + tracef("slen: %d, sig: %(%02X %)", slen, sig); + } + + /* Now check the sig for validity. */ + if (EVP_DigestVerifyFinal(mdctx, cast(ubyte*)sig.ptr, slen) != 1) { + version(HUNT_JWT_DEBUG) warning("Invalid argument"); + return false; + } + + return true; + +} \ No newline at end of file diff --git a/source/hunt/jwt/JwtToken.d b/source/hunt/jwt/JwtToken.d index 25247aa..4eca09a 100644 --- a/source/hunt/jwt/JwtToken.d +++ b/source/hunt/jwt/JwtToken.d @@ -14,6 +14,9 @@ import std.datetime; import std.json; import std.string; +import hunt.logging; + + /** * represents a token */ @@ -75,7 +78,8 @@ private { string token = this.data ~ "." ~ this.signature(secret); version(HUNT_AUTH_DEBUG) { - tracef("secret: %s, token: %s", secret, token); + import std.stdio; + writeln("secret: %s, token: %s", secret, token); } return token; @@ -113,7 +117,7 @@ private { import std.conv : to; import std.uni : toUpper; - version(HUNT_AUTH_DEBUG) { + version(HUNT_JWT_DEBUG) { tracef("token: %s", token); } @@ -124,7 +128,7 @@ private { JSONValue header; try { - header = parseJSON(urlsafeB64Decode(tokenParts[0])); + header = parseJSON(cast(string)urlsafeB64Decode(tokenParts[0])); } catch(Exception e) { throw new VerifyException("Header is incorrect."); } @@ -144,13 +148,13 @@ private { } const key = lazyKey(header); - if(!key.empty() && !verifySignature(urlsafeB64Decode(tokenParts[2]), tokenParts[0]~"."~tokenParts[1], key, alg)) + if(!key.empty() && !verifySignature(tokenParts[0]~"."~tokenParts[1], tokenParts[2], key, alg)) throw new VerifyException("Signature is incorrect."); JSONValue payload; try { - payload = parseJSON(urlsafeB64Decode(tokenParts[1])); + payload = parseJSON(cast(string)urlsafeB64Decode(tokenParts[1])); } catch(JSONException e) { // Code coverage has to miss this line because the signature test above throws before this does throw new VerifyException("Payload JSON is incorrect."); @@ -177,7 +181,7 @@ private { string[] tokenParts = split(token, "."); - string decHeader = urlsafeB64Decode(tokenParts[0]); + string decHeader = cast(string)urlsafeB64Decode(tokenParts[0]); JSONValue header = parseJSON(decHeader); JwtAlgorithm alg; @@ -194,7 +198,7 @@ private { throw new VerifyException("Type is incorrect."); } - return verifySignature(urlsafeB64Decode(tokenParts[2]), tokenParts[0]~"."~tokenParts[1], key, alg); + return verifySignature(tokenParts[0]~"."~tokenParts[1], tokenParts[2], key, alg); } } diff --git a/source/hunt/logging.d b/source/hunt/logging.d new file mode 100644 index 0000000..188c352 --- /dev/null +++ b/source/hunt/logging.d @@ -0,0 +1,382 @@ +module hunt.logging; + +version(HUNT_JWT_DEBUG) : + +import core.stdc.stdlib; +import core.runtime; +import core.thread; + +import std.conv; +import std.datetime; +import std.exception; +import std.format; +import std.range; +import std.regex; +import std.stdio; +import std.string; +import std.typecons; +import std.traits; + + +version (Posix) { + extern (C) nothrow @nogc size_t syscall(size_t ident, ...); + ThreadID getTid() { + version(FreeBSD) { + long tid; + enum SYS_thr_self = 432; + syscall(SYS_thr_self, &tid); + return cast(ThreadID)tid; + } else version(OSX) { + enum SYS_thread_selfid = 372; + return cast(ThreadID)syscall(SYS_thread_selfid); + } else version(linux) { + enum __NR_gettid = 186; + return cast(ThreadID)syscall(__NR_gettid); + } else { + return 0; + } + } +} else { + import core.sys.windows.winbase: GetCurrentThreadId; + ThreadID getTid() { + return GetCurrentThreadId(); + } +} + +version (Windows) { + import core.sys.windows.wincon; + import core.sys.windows.winbase; + import core.sys.windows.windef; + import toolkit.os.windows.console; + +} + +version (Posix) { + enum PRINT_COLOR_NONE = "\033[m"; + enum PRINT_COLOR_RED = "\033[0;32;31m"; + enum PRINT_COLOR_GREEN = "\033[0;32;32m"; + enum PRINT_COLOR_YELLOW = "\033[1;33m"; +} + +version (Android) { + import core.stdc.stdarg : va_end, va_list, va_start; + import core.sys.posix.sys.types; + + enum { + AASSET_MODE_UNKNOWN, + AASSET_MODE_RANDOM, + AASSET_MODE_STREAMING, + AASSET_MODE_BUFFER + } + + enum android_LogPriority { + ANDROID_LOG_UNKNOWN, + ANDROID_LOG_DEFAULT, + ANDROID_LOG_VERBOSE, + ANDROID_LOG_DEBUG, + ANDROID_LOG_INFO, + ANDROID_LOG_WARN, + ANDROID_LOG_ERROR, + ANDROID_LOG_FATAL, + ANDROID_LOG_SILENT + } + + enum LOG_TAG = "HUNT"; + + // dfmt off + extern (C): + @system: + nothrow: + @nogc: + // dfmt on + + struct AAssetManager; + struct AAssetDir; + struct AAsset; + + AAssetDir* AAssetManager_openDir(AAssetManager* mgr, const(char)* dirName); + AAsset* AAssetManager_open(AAssetManager* mgr, const(char)* filename, int mode); + const(char)* AAssetDir_getNextFileName(AAssetDir* assetDir); + void AAssetDir_rewind(AAssetDir* assetDir); + void AAssetDir_close(AAssetDir* assetDir); + int AAsset_read(AAsset* asset, void* buf, size_t count); + off_t AAsset_seek(AAsset* asset, off_t offset, int whence); + void AAsset_close(AAsset* asset); + const(void)* AAsset_getBuffer(AAsset* asset); + off_t AAsset_getLength(AAsset* asset); + off_t AAsset_getRemainingLength(AAsset* asset); + int AAsset_openFileDescriptor(AAsset* asset, off_t* outStart, off_t* outLength); + int AAsset_isAllocated(AAsset* asset); + + int __android_log_write(int prio, const(char)* tag, const(char)* text); + int __android_log_print(int prio, const(char)* tag, const(char)* fmt, ...); + int __android_log_vprint(int prio, const(char)* tag, const(char)* fmt, va_list ap); + void __android_log_assert(const(char)* cond, const(char)* tag, const(char)* fmt, ...); + +} + +enum LogLevel { + Trace = 0, + Info = 1, + Warning = 2, + Error = 3, + Fatal = 4, + Off = 5 +} + +/** +*/ +class ConsoleLogger { + private __gshared LogLevel g_logLevel = LogLevel.Trace; + private enum traceLevel = toString(LogLevel.Trace); + private enum infoLevel = toString(LogLevel.Info); + private enum warningLevel = toString(LogLevel.Warning); + private enum errorLevel = toString(LogLevel.Error); + private enum fatalLevel = toString(LogLevel.Fatal); + private enum offlLevel = toString(LogLevel.Off); + + static void setLogLevel(LogLevel level) { + g_logLevel = level; + } + + static void trace(string file = __FILE__, size_t line = __LINE__, + string func = __FUNCTION__, A...)(lazy A args) nothrow { + writeFormatColor(LogLevel.Trace, layout!(file, line, func)(logFormat(args), traceLevel)); + } + + static void tracef(string file = __FILE__, size_t line = __LINE__, + string func = __FUNCTION__, A...)(lazy A args) nothrow { + writeFormatColor(LogLevel.Trace, layout!(file, line, func)(logFormatf(args), traceLevel)); + } + + static void info(string file = __FILE__, size_t line = __LINE__, + string func = __FUNCTION__, A...)(lazy A args) nothrow { + writeFormatColor(LogLevel.Info, layout!(file, line, func)(logFormat(args), infoLevel)); + } + + static void infof(string file = __FILE__, size_t line = __LINE__, + string func = __FUNCTION__, A...)(lazy A args) nothrow { + writeFormatColor(LogLevel.Info, layout!(file, line, func)(logFormatf(args), infoLevel)); + } + + static void warning(string file = __FILE__, size_t line = __LINE__, + string func = __FUNCTION__, A...)(lazy A args) nothrow { + writeFormatColor(LogLevel.Warning, layout!(file, line, + func)(logFormat(args), warningLevel)); + } + + static void warningf(string file = __FILE__, size_t line = __LINE__, + string func = __FUNCTION__, A...)(lazy A args) nothrow { + writeFormatColor(LogLevel.Warning, layout!(file, line, + func)(logFormatf(args), warningLevel)); + } + + static void error(string file = __FILE__, size_t line = __LINE__, + string func = __FUNCTION__, A...)(lazy A args) nothrow { + writeFormatColor(LogLevel.Error, layout!(file, line, func)(logFormat(args), errorLevel)); + } + + static void errorf(string file = __FILE__, size_t line = __LINE__, + string func = __FUNCTION__, A...)(lazy A args) nothrow { + writeFormatColor(LogLevel.Error, layout!(file, line, func)(logFormatf(args), errorLevel)); + } + + static void fatal(string file = __FILE__, size_t line = __LINE__, + string func = __FUNCTION__, A...)(lazy A args) nothrow { + writeFormatColor(LogLevel.Fatal, layout!(file, line, func)(logFormat(args), fatalLevel)); + } + + static void fatalf(string file = __FILE__, size_t line = __LINE__, + string func = __FUNCTION__, A...)(lazy A args) nothrow { + writeFormatColor(LogLevel.Fatal, layout!(file, line, func)(logFormatf(args), fatalLevel)); + } + + private static string logFormatf(A...)(A args) { + Appender!string buffer; + formattedWrite(buffer, args); + return buffer.data; + } + + private static string logFormat(A...)(A args) { + auto w = appender!string(); + foreach (arg; args) { + alias A = typeof(arg); + static if (isAggregateType!A || is(A == enum)) { + import std.format : formattedWrite; + + formattedWrite(w, "%s", arg); + } else static if (isSomeString!A) { + put(w, arg); + } else static if (isIntegral!A) { + import std.conv : toTextRange; + + toTextRange(arg, w); + } else static if (isBoolean!A) { + put(w, arg ? "true" : "false"); + } else static if (isSomeChar!A) { + put(w, arg); + } else { + import std.format : formattedWrite; + + // Most general case + formattedWrite(w, "%s", arg); + } + } + return w.data; + } + + private static string layout(string file = __FILE__, size_t line = __LINE__, + string func = __FUNCTION__)(string msg, string level) { + enum lineNum = std.conv.to!string(line); + string time_prior = Clock.currTime.toString(); + string tid = std.conv.to!string(cast(int)getTid()); + + // writeln("fullname: ",func); + string fun = func; + ptrdiff_t index = lastIndexOf(func, '.'); + if (index != -1) { + if (func[index - 1] != ')') { + ptrdiff_t idx = lastIndexOf(func, '.', index); + if (idx != -1) + index = idx; + } + fun = func[index + 1 .. $]; + } + + return time_prior ~ " | " ~ tid ~ " | " ~ level ~ " | " ~ fun ~ " | " ~ msg + ~ " | " ~ file ~ ":" ~ lineNum; + } + + // private static string defaultLayout(string context, string msg, string level) + // { + // string time_prior = Clock.currTime.toString(); + // string tid = std.conv.to!string(getTid()); + + // return time_prior ~ " | " ~ tid ~ " | " ~ level ~ context ~ msg; + // } + + static string toString(LogLevel level) nothrow { + string r; + final switch (level) with (LogLevel) { + case Trace: + r = "trace"; + break; + case Info: + r = "info"; + break; + case Warning: + r = "warning"; + break; + case Error: + r = "error"; + break; + case Fatal: + r = "fatal"; + break; + case Off: + r = "off"; + break; + } + return r; + } + + private static void writeFormatColor(LogLevel level, lazy string msg) nothrow { + if (level < g_logLevel) + return; + + version (Posix) { + version (Android) { + string prior_color; + android_LogPriority logPrioity = android_LogPriority.ANDROID_LOG_INFO; + switch (level) with (LogLevel) { + case Error: + case Fatal: + prior_color = PRINT_COLOR_RED; + logPrioity = android_LogPriority.ANDROID_LOG_ERROR; + break; + case Warning: + prior_color = PRINT_COLOR_YELLOW; + logPrioity = android_LogPriority.ANDROID_LOG_WARN; + break; + case Info: + prior_color = PRINT_COLOR_GREEN; + break; + default: + prior_color = string.init; + } + + try { + __android_log_write(logPrioity, + LOG_TAG, toStringz(prior_color ~ msg ~ PRINT_COLOR_NONE)); + } catch(Exception ex) { + collectException( { + write(PRINT_COLOR_RED); + write(ex); + writeln(PRINT_COLOR_NONE); + }()); + } + + } else { + string prior_color; + switch (level) with (LogLevel) { + case Error: + case Fatal: + prior_color = PRINT_COLOR_RED; + break; + case Warning: + prior_color = PRINT_COLOR_YELLOW; + break; + case Info: + prior_color = PRINT_COLOR_GREEN; + break; + default: + prior_color = string.init; + } + try { + writeln(prior_color ~ msg ~ PRINT_COLOR_NONE); + } catch(Exception ex) { + collectException( { + write(PRINT_COLOR_RED); + write(ex); + writeln(PRINT_COLOR_NONE); + }()); + } + } + + } else version (Windows) { + enum defaultColor = FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE; + + ushort color; + switch (level) with (LogLevel) { + case Error: + case Fatal: + color = FOREGROUND_RED; + break; + case Warning: + color = FOREGROUND_GREEN | FOREGROUND_RED; + break; + case Info: + color = FOREGROUND_GREEN; + break; + default: + color = defaultColor; + } + + ConsoleHelper.writeWithAttribute(msg, color); + } else { + assert(false, "Unsupported OS."); + } + } +} + +alias trace = ConsoleLogger.trace; +alias tracef = ConsoleLogger.tracef; +alias info = ConsoleLogger.info; +alias infof = ConsoleLogger.infof; +alias warning = ConsoleLogger.warning; +alias warningf = ConsoleLogger.warningf; +alias error = ConsoleLogger.error; +alias errorf = ConsoleLogger.errorf; +// alias critical = ConsoleLogger.critical; +// alias criticalf = ConsoleLogger.criticalf; +