Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
secdec24 opened this issue Aug 31, 2021 · 6 comments
Open

Comments

@secdec24
Copy link
Contributor

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

@prince-chrismc
Copy link
Collaborator

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

@secdec24
Copy link
Contributor Author

secdec24 commented Sep 1, 2021

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.

@prince-chrismc
Copy link
Collaborator

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 };
      }

@secdec24
Copy link
Contributor Author

secdec24 commented Sep 5, 2021

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.

@Thalhammer
Copy link
Owner

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.

@prince-chrismc prince-chrismc added this to the 0.7.0 milestone Sep 22, 2021
@prince-chrismc prince-chrismc changed the title [Question] How were the JWKS and token generated for the jwks-verify.cpp example? How were the JWKS and token generated for the jwks-verify.cpp example? Feb 26, 2022
@prince-chrismc prince-chrismc modified the milestones: 0.7.0, 0.8.0 Dec 10, 2023
@prince-chrismc
Copy link
Collaborator

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);
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants