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

Add alg RS256 support for JWT generator and validator. #281

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 34 additions & 2 deletions 3rdparty/libprocess/include/process/jwt.hpp
Expand Up @@ -16,6 +16,8 @@
#include <ostream>
#include <string>

#include <process/ssl/utilities.hpp>

#include <stout/json.hpp>
#include <stout/option.hpp>
#include <stout/try.hpp>
Expand Down Expand Up @@ -58,7 +60,8 @@ class JWT
enum class Alg
{
None,
HS256
HS256,
RS256
};

struct Header
Expand Down Expand Up @@ -89,6 +92,19 @@ class JWT
const std::string& token,
const std::string& secret);

/**
* Parse a JWT and validate its RS256 signature.
*
* @param token The JWT to parse.
* @param publicKey The public key to validate the signature with.
*
* @return The validated JWT representation if successful otherwise an
* Error.
*/
static Try<JWT, JWTError> parse(
const std::string& token,
network::openssl::RSA_shared_ptr publicKey);

/**
* Create an unsecured JWT.
*
Expand All @@ -105,7 +121,7 @@ class JWT
* When creating a payload keep in mind that of the standard claims
* currently only 'exp' is validated during parsing.
*
* @param payload The payload of the JWT
* @param payload The payload of the JWT.
* @param secret The secret to sign the JWT with.
*
* @return The signed JWT representation if successful otherwise an
Expand All @@ -115,6 +131,22 @@ class JWT
const JSON::Object& payload,
const std::string& secret);

/**
* Create a JWT with a RS256 signature.
*
* When creating a payload keep in mind that of the standard claims
* currently only 'exp' is validated during parsing.
*
* @param payload The payload of the JWT.
* @param privateKey The private key to sign the JWT with.
*
* @return The signed JWT representation if successful otherwise an
* Error.
*/
static Try<JWT, JWTError> create(
const JSON::Object& payload,
network::openssl::RSA_shared_ptr privateKey);

const Header header;
const JSON::Object payload;
const Option<std::string> signature;
Expand Down
64 changes: 64 additions & 0 deletions 3rdparty/libprocess/include/process/ssl/utilities.hpp
Expand Up @@ -130,6 +130,70 @@ Try<std::string> generate_hmac_sha256(
const std::string& message,
const std::string& key);


/**
* Helper type wrapping a dynamically allocated openssl RSA struct.
*
* One must provide a custom deleter for the pointer to be automatically
* released.
*/
typedef std::shared_ptr<RSA> RSA_shared_ptr;


/**
* Helper function converting a PEM representation of a private key
* into a RSA private key usable by openssl.
*
* @param pem The PEM representation of the private key.
*
* @return A shared pointer to an openssl-compatible private key if
* successful otherwise an Error.
*/
Try<RSA_shared_ptr> pemToRSAPrivateKey(const std::string& pem);


/**
* Helper function converting a PEM representation of a public key
* into a RSA private key usable by openssl.
*
* @param pem The PEM representation of the public key.
*
* @return A shared pointer to an openssl-compatible private key if
* successful otherwise an Error.
*/
Try<RSA_shared_ptr> pemToRSAPublicKey(const std::string& pem);


/**
* Create a signature of a message with a private key and the RSA SHA256
* algorithm.
*
* @param message The message to sign.
* @param privateKey The private key used to sign the message.
*
* @return The signature of message if successful or an Error.
*/
Try<std::string> sign_rsa_sha256(
const std::string& message,
RSA_shared_ptr privateKey);


/**
* Verify the signature of a message with a public key and the RSA SHA256
* algorithm.
*
* @param message The message to verify signature of.
* @param signature The signature to verify.
* @param publicKey The public key used to verify the signature.
*
* @return A boolean set to true if the signature is valid or false if the
* signature is invalid of if the computation failed.
*/
bool verify_rsa_sha256(
const std::string& message,
const std::string& signature,
RSA_shared_ptr publicKey);

} // namespace openssl {
} // namespace network {
} // namespace process {
Expand Down
99 changes: 95 additions & 4 deletions 3rdparty/libprocess/src/jwt.cpp
Expand Up @@ -16,8 +16,6 @@

#include <process/clock.hpp>

#include <process/ssl/utilities.hpp>

#include <stout/base64.hpp>
#include <stout/strings.hpp>

Expand All @@ -28,12 +26,14 @@ namespace authentication {
using process::Clock;

using process::network::openssl::generate_hmac_sha256;
using process::network::openssl::RSA_shared_ptr;
using process::network::openssl::verify_rsa_sha256;
using process::network::openssl::sign_rsa_sha256;

using std::ostream;
using std::string;
using std::vector;


namespace {

Try<JSON::Object> decode(const string& component)
Expand Down Expand Up @@ -100,6 +100,8 @@ Try<JWT::Header> parse_header(const string& component)
alg = JWT::Alg::None;
} else if (alg_value == "HS256") {
alg = JWT::Alg::HS256;
} else if (alg_value == "RS256") {
alg = JWT::Alg::RS256;
} else {
return Error("Unsupported token algorithm: " + alg_value);
}
Expand Down Expand Up @@ -257,6 +259,65 @@ Try<JWT, JWTError> JWT::parse(const string& token, const string& secret)
}


Try<JWT, JWTError> JWT::parse(const string& token,
RSA_shared_ptr publicKey)
{
if (!publicKey) {
return JWTError(
"Invalid public key required to sign the JWT token",
JWTError::Type::UNKNOWN);
}

const vector<string> components = strings::split(token, ".");

if (components.size() != 3) {
return JWTError(
"Expected 3 components in token, got " + stringify(components.size()),
JWTError::Type::INVALID_TOKEN);
}

Try<JWT::Header> header = parse_header(components[0]);

if (header.isError()) {
return JWTError(header.error(), JWTError::Type::INVALID_TOKEN);
}

if (header->alg != JWT::Alg::RS256) {
return JWTError(
"Token 'alg' value \"" + stringify(header->alg) +
"\" does not match, expected \"RS256\"",
JWTError::Type::INVALID_TOKEN);
}

Try<JSON::Object> payload = parse_payload(components[1]);

if (payload.isError()) {
return JWTError(payload.error(), JWTError::Type::INVALID_TOKEN);
}

const Try<string> signature = base64::decode_url_safe(components[2]);

if (signature.isError()) {
return JWTError(
"Failed to base64url-decode token signature: " + signature.error(),
JWTError::Type::INVALID_TOKEN);
}

// Validate RSA SHA-256 signature

bool valid = verify_rsa_sha256(
components[0] + "." + components[1], signature.get(), publicKey);

if (!valid) {
return JWTError(
"Failed to verify token",
JWTError::Type::INVALID_TOKEN);
}

return JWT(header.get(), payload.get(), signature.get());
}


Try<JWT, JWTError> JWT::create(const JSON::Object& payload)
{
const Header header{Alg::None, "JWT"};
Expand Down Expand Up @@ -286,6 +347,34 @@ Try<JWT, JWTError> JWT::create(
}


Try<JWT, JWTError> JWT::create(
const JSON::Object& payload,
RSA_shared_ptr privateKey)
{
if (!privateKey) {
return JWTError(
"Invalid private key required to sign the JWT token",
JWTError::Type::UNKNOWN);
}

const Header header{Alg::RS256, "JWT"};

const Try<string> signature = sign_rsa_sha256(
base64::encode_url_safe(stringify(header), false) + "." +
base64::encode_url_safe(stringify(payload), false),
privateKey);

if (signature.isError()) {
return JWTError(
"Failed to generate RSA signature: " + signature.error(),
JWTError::Type::UNKNOWN);
}

return JWT(header, payload, base64::encode_url_safe(signature.get(),
false));
}


JWT::JWT(
const Header& _header,
const JSON::Object& _payload,
Expand All @@ -302,8 +391,10 @@ ostream& operator<<(ostream& stream, const JWT::Alg& alg)
case JWT::Alg::HS256:
stream << "HS256";
break;
case JWT::Alg::RS256:
stream << "RS256";
break;
}

return stream;
}

Expand Down
71 changes: 71 additions & 0 deletions 3rdparty/libprocess/src/ssl/utilities.cpp
Expand Up @@ -369,6 +369,77 @@ Try<std::string> generate_hmac_sha256(
return std::string(reinterpret_cast<char*>(rc), md_len);
}

template<typename Reader>
Try<RSA_shared_ptr> pemToRSA(const std::string& pem, Reader reader)
{
BIO *bio = BIO_new_mem_buf(pem.c_str(), -1);
if (!bio) {
return Error("Failed to create RSA key bio");
}
RSA *rsa = reader(bio, nullptr, nullptr, nullptr);
BIO_free(bio);
if(!rsa) {
return Error("Failed to create RSA from key bio");
}
return RSA_shared_ptr(rsa, RSA_free);
}

Try<RSA_shared_ptr> pemToRSAPrivateKey(const std::string& pem) {
return pemToRSA(pem, PEM_read_bio_RSAPrivateKey);
}

Try<RSA_shared_ptr> pemToRSAPublicKey(const std::string& pem) {
return pemToRSA(pem, PEM_read_bio_RSA_PUBKEY);
}

Try<std::string> sign_rsa_sha256(
const std::string& message,
RSA_shared_ptr privateKey) {
std::unique_ptr<unsigned char, std::function<void(void*)>> signatureData(
(unsigned char*) malloc(RSA_size(privateKey.get())), free);
unsigned int signatureLength;
unsigned char hash[SHA256_DIGEST_LENGTH];

SHA256(
reinterpret_cast<const unsigned char*>(message.c_str()),
message.size(),
hash);

int r = RSA_sign(
NID_sha256,
hash,
SHA256_DIGEST_LENGTH,
signatureData.get(),
&signatureLength,
privateKey.get());
if(r == 0) {
return Error("Failed to sign the message");
}
return std::string(reinterpret_cast<char*>(signatureData.get()),
signatureLength);
}

bool verify_rsa_sha256(
const std::string& message,
const std::string& signature,
RSA_shared_ptr publicKey) {
unsigned char hash[SHA256_DIGEST_LENGTH];

SHA256(
reinterpret_cast<const unsigned char*>(message.c_str()),
message.size(),
hash);

return RSA_verify(
NID_sha256,
hash,
SHA256_DIGEST_LENGTH,
reinterpret_cast<const unsigned char*>(signature.data()),
signature.size(),
publicKey.get()) == 1;
}


} // namespace openssl {
} // namespace network {
} // namespace process {