jwt-cpp icon indicating copy to clipboard operation
jwt-cpp copied to clipboard

How were the JWKS and token generated for the `jwks-verify.cpp` example?

Open secdec24 opened this issue 4 years ago • 6 comments

Can a JWK be generated from an RSA public key using this repo?

secdec24 avatar Aug 31 '21 07:08 secdec24

How were the JWKS and token generated for the jwks-verify.cpp example?

I am assuming the original author

  1. generate key/cert from the command line with OpenSSL
  2. base64 encoding
  3. extracted the values with openssl

Can a JWK be generated from an RSA public key using this repo?

Sadly not yet, however if you are interested in contributing... I did some work on this and I can share my notes

prince-chrismc avatar Aug 31 '21 12:08 prince-chrismc

I am assuming the original author

Makes sense. Normally I generate my keys with OpenSSH//OpenSSL in terminal and if needed encode in PEM but hadn't worked with JWKS before. Was mostly curious as to whether they had been generated in code using this repo. Thanks for the answer.

Sadly not yet, however if you are interested in contributing... I did some work on this and I can share my notes

Yeah feel free to share. Would be interesting to look at. I'd be happy to explore further.

secdec24 avatar Sep 01 '21 07:09 secdec24

I have a test "pem to pem" which is for the RSA JWK scenario making sure I can remake a public key from the modulus and exponent (since "n" and "e" are required unlike "x5c" thats the workflow I had done)

The bulk should just be parsing/formatting the structure

  • parsing
   std::unique_ptr<BIGNUM, decltype(&BN_free)> base64DecodeBigNum(const std::string& base64bignum)
   {
      const auto decode = jwt::base::decode<jwt::alphabet::base64url>(jwt::base::pad<jwt::alphabet::base64url>(base64bignum));

      return jwt::helper::raw2bn(decode);
   }

   // https://gist.github.com/polesen/2855098
   // https://stackoverflow.com/a/10903704/8480874
   std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> RsaPubKeyFromModExp(const std::string& modulus_b64, const std::string& exp_b64)
   {
      auto n = base64DecodeBigNum(modulus_b64);
      auto e = base64DecodeBigNum(exp_b64);

      if (e && n)
      {
         EVP_PKEY* pRsaKey = EVP_PKEY_new();
         RSA* rsa = RSA_new();
         RSA_set0_key(rsa, n.release(), e.release(), nullptr);
         EVP_PKEY_assign_RSA(pRsaKey, rsa);
         return std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)>(pRsaKey, EVP_PKEY_free);
      }

      return std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)>(nullptr, EVP_PKEY_free);
   }
  • generating
      std::tuple<std::string, std::string> Auth::extractModulusAndExp(const std::string& publicKey)
      {
         const auto pkey = jwt::helper::load_public_key_from_string(publicKey);

         RSA* rsa = EVP_PKEY_get1_RSA(pkey.get());
         const BIGNUM* n = RSA_get0_n(rsa);
         const BIGNUM* e = RSA_get0_e(rsa);

         const auto modulus = jwt::base::trim<jwt::alphabet::base64url>(jwt::base::encode<jwt::alphabet::base64url>(jwt::helper::bn2raw(n)));
         const auto exp = jwt::base::trim<jwt::alphabet::base64url>(jwt::base::encode<jwt::alphabet::base64url>(jwt::helper::bn2raw(e)));

         return { modulus, exp };
      }

prince-chrismc avatar Sep 01 '21 19:09 prince-chrismc

Thanks! I was able to verify the resulting token using the public key without needing to convert to a JWKS. Will definitely explore this when time permits.

secdec24 avatar Sep 05 '21 01:09 secdec24

The jwks part in jwt-cpp is very much in it's early stages, which is one of the reasons its not really announced as a feature anywhere yet. Whats there should work, but there simply isn't much yet. In particular there is no real support for writing/building jwks, the reading part does very little verification and (as you have discovered) there is no built in way to convert the jwks to something you can use for signing/verifying yet.

All of these will get implemented at some point though.

Thalhammer avatar Sep 05 '21 09:09 Thalhammer

How were the JWKS and token generated for the jwks-verify.cpp example?

Now suspect, it was just copied off the internet from an tutorial 👓


While trying to fix the example (in #307) I needed to make new values and in my research doing this purely from the CLI with openssl was very challenging because outputting the "raw binary" was not possible to then be encoded correctly so I ended up writing a bunch of code as a result

this still needs

  • better compatibility with SSL libraries
  • there is a memory leak
#include <iostream>
#include <jwt-cpp/jwt.h>

#include <openssl/rand.h>

std::string write_bio_to_string(std::unique_ptr<BIO, decltype(&BIO_free_all)>& bio_out) {
	char* ptr = nullptr;
	auto len = BIO_get_mem_data(bio_out.get(), &ptr);
	if (len <= 0 || ptr == nullptr) { throw std::exception(); }
	return {ptr, static_cast<size_t>(len)};
}

int main() {
	EVP_PKEY* pkey = NULL;

#if defined(JWT_OPENSSL_3_0)
	EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_from_name(NULL, "RSA", NULL);
	EVP_PKEY_keygen_init(pctx);
	// https://www.openssl.org/docs/man3.1/man3/EVP_PKEY_keygen_init.html
	EVP_PKEY_CTX_set_rsa_keygen_bits(pctx, 4096);
	EVP_PKEY_generate(pctx, &pkey);
#else
	pkey = EVP_PKEY_new(); // https://stackoverflow.com/questions/5313855/rsa-sign-openssl

	// https://www.dynamsoft.com/codepool/how-to-use-openssl-generate-rsa-keys-cc.html
	BIGNUM* bne = BN_new();
	BN_set_word(bne, RSA_F4);

	RSA* rsa = RSA_new();
	RSA_generate_key_ex(rsa, 4096, bne, NULL);
	EVP_PKEY_set1_RSA(pkey, rsa);
#endif

	std::string pem_public_key = [&]() {
		auto bio_out = jwt::helper::make_mem_buf_bio();
		PEM_write_bio_PUBKEY(bio_out.get(), pkey);

		const auto pub_key = write_bio_to_string(bio_out);
		std::cout << pub_key << std::endl;
		return pub_key;
	}();

	// https://stackoverflow.com/questions/69179822/jwk-key-creation-with-x5c-and-x5t-parameters
	// https://stackoverflow.com/questions/256405/programmatically-create-x509-certificate-using-openssl
	std::unique_ptr<X509, decltype(&X509_free)> cert{X509_new(), X509_free};

	ASN1_INTEGER* serial_number = X509_get_serialNumber(cert.get());
	ASN1_INTEGER_set(serial_number, 1); // serial number

#if defined(JWT_OPENSSL_1_0_0)
	auto x509_not_before = &X509_get_notBefore;
	auto x509_not_after = &X509_get_notAfter;
#else
	auto x509_not_before = &X509_getm_notBefore;
	auto x509_not_after = &X509_getm_notAfter;
#endif
	X509_gmtime_adj(x509_not_before(cert.get()), 0);				   // now
	X509_gmtime_adj(x509_not_after(cert.get()), 10 * 365 * 24 * 3600); // accepts secs

	X509_set_pubkey(cert.get(), pkey);
	X509_NAME* name = X509_get_subject_name(cert.get());

	X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, (const unsigned char*)"US", -1, -1, 0);
	X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, (const unsigned char*)"JWT-CPP", -1, -1, 0);
	X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (const unsigned char*)"localhost", -1, -1, 0);

	X509_set_issuer_name(cert.get(), name);
	X509_sign(cert.get(), pkey, EVP_sha256()); // some hash type here

	std::string base64_x5c = [&]() {
		// PEM_write_bio_X509(certFile.get(), cert.get());
		// PEM_write_bio_PrivateKey(keyFile.get(), pkey, nullptr, nullptr, 0, nullptr, nullptr);
		auto bio_out = jwt::helper::make_mem_buf_bio();
		i2d_X509_bio(bio_out.get(), cert.get());

		const auto der_cert = write_bio_to_string(bio_out);
		const auto b64_der_cert = jwt::base::encode<jwt::alphabet::base64>(der_cert);
		std::cout << b64_der_cert << std::endl;
		return b64_der_cert;
	}();

	// https://stackoverflow.com/questions/8135209/open-ssl-certificate-fingerprint-in-c

	// std::string base64_x5c = [&](){
	// 	auto bio_out = jwt::helper::make_mem_buf_bio();
	// 	i2d_PUBKEY_bio(bio_out.get(), pkey);

	// 	const auto der_pub_key = write_bio_to_string(bio_out);
	// 	const auto x5c = jwt::base::encode<jwt::alphabet::base64>(der_pub_key);
	// 	std::cout << x5c << std::endl;
	// 	return x5c;
	// }();

	std::string pem_priv_key = [&]() {
		auto bio_out = jwt::helper::make_mem_buf_bio();
		PEM_write_bio_PrivateKey(bio_out.get(), pkey, NULL, NULL, 0, 0, (void*)"");

		const auto priv_key = write_bio_to_string(bio_out);
		std::cout << priv_key << std::endl;
		return priv_key;
	}();

#if defined(JWT_OPENSSL_3_0)
	EVP_PKEY_CTX_free(pctx);
#else
	RSA_free(rsa);
	BN_free(bne);
#endif

#if defined(JWT_OPENSSL_3_0)
	BIGNUM* n = nullptr;
	EVP_PKEY_get_bn_param(pkey, "n", &n);
	BIGNUM* e = nullptr;
	EVP_PKEY_get_bn_param(pkey, "e", &e);
#elif defined(JWT_OPENSSL_1_1_1) && !defined(LIBWOLFSSL_VERSION_HEX) && !defined(LIBRESSL_VERSION_NUMBER)
	// wolfSSL is missing RSA_get0_n and needs RSA_get0_key
	RSA* r = EVP_PKEY_get1_RSA(pkey);
	const BIGNUM* n = RSA_get0_n(r);
	const BIGNUM* e = RSA_get0_e(r);
#elif defined(JWT_OPENSSL_1_1_0) || defined(LIBWOLFSSL_VERSION_HEX) || defined(LIBRESSL_VERSION_NUMBER)
	const BIGNUM* n = nullptr;
	const BIGNUM* e = nullptr;
	RSA* r = EVP_PKEY_get1_RSA(pkey);
	RSA_get0_key(r, &n, &e, nullptr);
#elif defined(JWT_OPENSSL_1_0_0)
	RSA* r = EVP_PKEY_get1_RSA(pkey);
	BIGNUM* n = r->n;
	BIGNUM* e = r->e;
#endif

	EVP_PKEY_free(pkey);

	const auto modulus =
		jwt::base::trim<jwt::alphabet::base64url>(jwt::base::encode<jwt::alphabet::base64url>(jwt::helper::bn2raw(n)));
	const auto exp =
		jwt::base::trim<jwt::alphabet::base64url>(jwt::base::encode<jwt::alphabet::base64url>(jwt::helper::bn2raw(e)));

#if defined(JWT_OPENSSL_3_0)
	BN_free(n);
	BN_free(e);
#endif

	std::cout << modulus << std::endl;
	std::cout << exp << std::endl;

	// https://stackoverflow.com/a/30138974
	unsigned char nonce[24];
	RAND_bytes(nonce, sizeof(nonce));
	std::string jti = jwt::base::encode<jwt::alphabet::base64url>(std::string{(const char*)nonce, sizeof(nonce)});

	std::string raw_jwks =
		R"({"keys": [{
		"kid":"internal-gateway-jwt.api.sc.net",
		"alg": "RS256",
    "kty": "RSA",
    "use": "sig",
    "x5c": [
      ")" +
		base64_x5c + R"("
    ],
    "n": ")" +
		modulus + R"(",
    "e": "AQAB"
	},
{
		"kid":"internal-123456",
		"use":"sig",
		"x5c":["MIIG1TCCBL2gAwIBAgIIFvMVGp6t\/cMwDQYJKoZIhvcNAQELBQAwZjELMAkGA1UEBhMCR0IxIDAeBgNVBAoMF1N0YW5kYXJkIENoYXJ0ZXJlZCBCYW5rMTUwMwYDVQQDDCxTdGFuZGFyZCBDaGFydGVyZWQgQmFuayBTaWduaW5nIENBIEcxIC0gU0hBMjAeFw0xODEwMTAxMTI2MzVaFw0yMjEwMTAxMTI2MzVaMIG9MQswCQYDVQQGEwJTRzESMBAGA1UECAwJU2luZ2Fwb3JlMRIwEAYDVQQHDAlTaW5nYXBvcmUxIDAeBgNVBAoMF1N0YW5kYXJkIENoYXJ0ZXJlZCBCYW5rMRwwGgYDVQQLDBNGb3VuZGF0aW9uIFNlcnZpY2VzMSgwJgYDVQQDDB9pbnRlcm5hbC1nYXRld2F5LWp3dC5hcGkuc2MubmV0MRwwGgYJKoZIhvcNAQkBFg1BUElQU1NAc2MuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArVWBoIi3IJ4nOWXu7\/SDxczqMou1B+c4c2FdQrOXrK31HxAaz4WEtma9BLXFdFHJ5mCCPIvdUcVxxnCynqhMOkZ\/a7acQbUD9cDzI8isMB9JL7VooDw0CctxHxffjqQQVIEhC2Q7zsM1pQayR7cl+pbBlvHIoRxq2n1B0fFvfoiosjf4kDiCpgHdM+v5Hw9aVYmUbroHxmQWqhB0iRTJQPPLZqqQVC50A1Q\/96gkwoODyotc46Uy9wYEpdGrtDG\/thWay3fmMsjpWR0U25xFIrxTrfCGBblYpD7juukWWml2E9rtE2rHgUxbymxXjEw7xrMwcGrhOGyqwoBqJy1JVwIDAQABo4ICLTCCAikwZAYIKwYBBQUHAQEEWDBWMFQGCCsGAQUFBzABhkhodHRwOi8vY29yZW9jc3AuZ2xvYmFsLnN0YW5kYXJkY2hhcnRlcmVkLmNvbS9lamJjYS9wdWJsaWN3ZWIvc3RhdHVzL29jc3AwHQYDVR0OBBYEFIinW4BNDeVEFcuLf8YjZjtySoW9MAwGA1UdEwEB\/wQCMAAwHwYDVR0jBBgwFoAUfNZMoZi33nKrcmVU3TFVQnuEi\/4wggFCBgNVHR8EggE5MIIBNTCCATGggcKggb+GgbxodHRwOi8vY29yZWNybC5nbG9iYWwuc3RhbmRhcmRjaGFydGVyZWQuY29tL2VqYmNhL3B1YmxpY3dlYi93ZWJkaXN0L2NlcnRkaXN0P2NtZD1jcmwmaXNzdWVyPUNOPVN0YW5kYXJkJTIwQ2hhcnRlcmVkJTIwQmFuayUyMFNpZ25pbmclMjBDQSUyMEcxJTIwLSUyMFNIQTIsTz1TdGFuZGFyZCUyMENoYXJ0ZXJlZCUyMEJhbmssQz1HQqJqpGgwZjE1MDMGA1UEAwwsU3RhbmRhcmQgQ2hhcnRlcmVkIEJhbmsgU2lnbmluZyBDQSBHMSAtIFNIQTIxIDAeBgNVBAoMF1N0YW5kYXJkIENoYXJ0ZXJlZCBCYW5rMQswCQYDVQQGEwJHQjAOBgNVHQ8BAf8EBAMCBsAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMA0GCSqGSIb3DQEBCwUAA4ICAQBtsoRlDHuOTDChcWdfdVUtRgP0U0ijDSeJi8vULN1rgYnqqJc4PdJno50aiu9MGlxY02O7HW7ZVD6QEG\/pqHmZ0sbWpb\/fumMgZSjP65IcGuS53zgcNtLYnyXyEv+v5T\/CK3bk4Li6tUW3ScJPUwVWwP1E0\/u6aBSb5k\/h4lTwS1o88ybS5pJOg6XutXByp991QQrrs7tp7fKNynjNZbFuG3J1e09X+zTfJOpjaDUofQTkt8IyMRI6Cs4wI1eZA+dAIL8B0n8ze1mRl1FOJqgdZrAQjoqZkCTnc0Il5VY\/dUXxGVg6D9e5pfck3FWT107K9\/5EZoxytpqYXFCjMXi5hx4YjK17OUgm82mZhvqkNdzF8Yq2vFuB3LPfyelESq99xFLykvinrVm1NtZKeDTT1Jq\/VvZt6stO\/tovq1RfJJcznpYcwOzxlnhGR6E+hxuBx7aDJzGf0JaoRxQILH1B2XV9WDI3HPYQsP7XtriX+QUJ\/aly28QkV48RmaGYCsly43YZu1MKudSsw+dhnbZzRsg\/aes3dzGW2x137bQPtux7k2LCSpsTXgedhOys28YoGlsoe8kUv0myAU4Stt+I3mrwO3BKUn+tJggvlDiiiyT1tg2HiklyU\/2FxQkZRMeB0eRrXTpg3l9x2mpF+dDFxOMKszxwD2kgoEZgo6o58A=="],
		"n":"nr9UsxnPVd21iuiGcIJ_Qli2XVlAZe5VbELA1hO2-L4k5gI4fjHZ3ysUcautLpbOYogOQgsnlpsLrCmvNDvBDVzVp2nMbpguJlt12vHSP1fRJJpipGQ8qU-VaXsC4OjOQf3H9ojAU5Vfnl5gZ7kVCd8g4M29l-IRyNpxE-Ccxc2Y7molsCHT6GHLMMBVsd11JIOXMICJf4hz2YYkQ1t7C8SaB2RFRPuGO5Mn6mfAnwdmRera4TBz6_pIPPCgCbN8KOdJItWkr9F7Tjv_0nhh-ZVlQvbQ9PXHyKTj00g3IYUlbZIWHm0Ley__fzNZk2dyAAVjNA2QSzTZJc33MQx1pQ",
		"e":"AQAB",
		"x5t":"-qC0akuyiHTV5aFsKVWM9da7lzq6DLrj09I",
		"alg":"RS256",
		"kty":"RSA"
	}
]})";

	std::string token = jwt::create()
							.set_issuer("auth0")
							.set_type("JWT")
							.set_id(jti)
							.set_key_id("internal-gateway-jwt.api.sc.net")
							.set_subject("jwt-cpp.example.localhost")
							.set_issued_at(std::chrono::system_clock::now())
							.set_expires_at(std::chrono::system_clock::now() + std::chrono::seconds{36000})
							.set_payload_claim("sample", jwt::claim(std::string{"test"}))
							.sign(jwt::algorithm::rs256("", pem_priv_key, "", ""));

	std::cout << token << std::endl;

	auto decoded_jwt = jwt::decode(token);
	auto jwks = jwt::parse_jwks(raw_jwks);
	auto jwk = jwks.get_jwk(decoded_jwt.get_key_id());

	auto issuer = decoded_jwt.get_issuer();
	auto x5c = jwk.get_x5c_key_value();

	if (!x5c.empty() && !issuer.empty()) {
		auto verifier =
			jwt::verify()
				.allow_algorithm(jwt::algorithm::rs256(jwt::helper::convert_base64_der_to_pem(x5c), "", "", ""))
				.with_issuer(issuer)
				.leeway(60UL); // value in seconds, add some to compensate timeout

		verifier.verify(decoded_jwt);
	}
	// else if the optional 'x5c' was not present
	{
		const auto modulus = jwk.get_jwk_claim("n").as_string();
		const auto exponent = jwk.get_jwk_claim("e").as_string();
		auto verifier = jwt::verify()
							.allow_algorithm(jwt::algorithm::rs256(
								jwt::helper::create_public_key_from_rsa_components(modulus, exponent)))
							.with_issuer(issuer)
							.leeway(60UL); // value in seconds, add some to compensate timeout

		verifier.verify(decoded_jwt);
	}
}

prince-chrismc avatar Dec 22 '23 13:12 prince-chrismc