Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/core/jose/include/sourcemeta/core/jose_algorithm.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
namespace sourcemeta::core {

/// @ingroup jose
/// The asymmetric JSON Web Signature algorithms from RFC 7518 Section 3.1. The
/// symmetric HMAC family and the null algorithm are intentionally absent, which
/// makes algorithm confusion attacks unrepresentable in the type system.
/// The asymmetric JSON Web Signature algorithms from RFC 7518 Section 3.1 and
/// the Edwards-curve algorithm from RFC 8037 Section 3.1. The symmetric HMAC
/// family and the null algorithm are intentionally absent, which makes
/// algorithm confusion attacks unrepresentable in the type system.
enum class JWSAlgorithm : std::uint8_t {
RS256,
RS384,
Expand All @@ -24,7 +25,8 @@ enum class JWSAlgorithm : std::uint8_t {
PS512,
ES256,
ES384,
ES512
ES512,
EdDSA
};

/// @ingroup jose
Expand Down
12 changes: 7 additions & 5 deletions src/core/jose/include/sourcemeta/core/jose_jwk.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
namespace sourcemeta::core {

/// @ingroup jose
/// A parsed public JSON Web Key (RFC 7517), restricted to RSA and elliptic
/// curve keys. The key owns its decoded material, so the source JSON document
/// does not need to outlive it. For example:
/// A parsed public JSON Web Key (RFC 7517), restricted to RSA, elliptic curve,
/// and octet key pair (RFC 8037) keys. The key owns its decoded material, so
/// the source JSON document does not need to outlive it. For example:
///
/// ```cpp
/// #include <sourcemeta/core/jose.h>
Expand All @@ -37,7 +37,7 @@ namespace sourcemeta::core {
/// ```
class SOURCEMETA_CORE_JOSE_EXPORT JWK {
public:
enum class Type : std::uint8_t { RSA, EllipticCurve };
enum class Type : std::uint8_t { RSA, EllipticCurve, OctetKeyPair };
Comment thread
jviotti marked this conversation as resolved.
Comment thread
jviotti marked this conversation as resolved.

/// Parse a JSON Web Key from a JSON value, throwing on invalid input.
explicit JWK(const JSON &value);
Expand Down Expand Up @@ -77,7 +77,9 @@ class SOURCEMETA_CORE_JOSE_EXPORT JWK {
return this->exponent_;
}

// Elliptic curve keys (RFC 7518 Section 6.2): curve name and coordinates
// Elliptic curve keys (RFC 7518 Section 6.2) and octet key pairs (RFC 8037
// Section 2) share the curve name. An octet key pair has no second
// coordinate, so its single encoded public key is exposed through the first
[[nodiscard]] auto curve() const noexcept -> std::string_view {
return this->curve_;
}
Expand Down
2 changes: 2 additions & 0 deletions src/core/jose/jose_algorithm.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ auto to_jws_algorithm(const std::string_view value) noexcept
return JWSAlgorithm::ES384;
} else if (value == "ES512") {
return JWSAlgorithm::ES512;
} else if (value == "EdDSA") {
return JWSAlgorithm::EdDSA;
} else {
return std::nullopt;
}
Expand Down
46 changes: 44 additions & 2 deletions src/core/jose/jose_jwk.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ const auto HASH_DQ{sourcemeta::core::JSON::Object::hash("dq")};
const auto HASH_QI{sourcemeta::core::JSON::Object::hash("qi")};
const auto HASH_OTH{sourcemeta::core::JSON::Object::hash("oth")};

// The RSA algorithms only require an RSA key, while each ECDSA algorithm is
// tied to a specific curve (RFC 7518 Section 3.1)
// The RSA algorithms only require an RSA key, each ECDSA algorithm is tied to a
// specific curve (RFC 7518 Section 3.1), and the Edwards-curve algorithm
// requires an octet key pair of either curve (RFC 8037 Section 3.1)
auto algorithm_matches_key(const sourcemeta::core::JWSAlgorithm algorithm,
const sourcemeta::core::JWK::Type type,
const std::string_view curve) -> bool {
Expand All @@ -47,6 +48,8 @@ auto algorithm_matches_key(const sourcemeta::core::JWSAlgorithm algorithm,
case sourcemeta::core::JWSAlgorithm::ES512:
return type == sourcemeta::core::JWK::Type::EllipticCurve &&
curve == "P-521";
case sourcemeta::core::JWSAlgorithm::EdDSA:
return type == sourcemeta::core::JWK::Type::OctetKeyPair;
}

std::unreachable();
Expand All @@ -66,6 +69,18 @@ auto ec_coordinate_bytes(const std::string_view curve)
}
}

// The public key octet length is fixed per Edwards curve (RFC 8032 Sections
// 5.1.5 and 5.2.5)
auto okp_key_bytes(const std::string_view curve) -> std::optional<std::size_t> {
if (curve == "Ed25519") {
return 32;
} else if (curve == "Ed448") {
return 57;
} else {
return std::nullopt;
}
}

} // namespace

namespace sourcemeta::core {
Expand Down Expand Up @@ -145,6 +160,33 @@ auto JWK::parse(const JSON &value, JWK &result) -> bool {
result.curve_ = curve->to_string();
result.coordinate_x_ = std::move(decoded_x).value();
result.coordinate_y_ = std::move(decoded_y).value();
} else if (key_type_value == "OKP") {
// A public key must not carry the private parameter (RFC 8037 Section 2)
if (value.try_at("d", HASH_D) != nullptr) {
return false;
}

const auto *curve{value.try_at("crv", HASH_CRV)};
const auto *public_key{value.try_at("x", HASH_X)};
if (curve == nullptr || !curve->is_string() || public_key == nullptr ||
!public_key->is_string()) {
return false;
}

const auto key_bytes{okp_key_bytes(curve->to_string())};
if (!key_bytes.has_value()) {
return false;
}

auto decoded_public_key{base64url_decode(public_key->to_string())};
if (!decoded_public_key.has_value() ||
decoded_public_key.value().size() != key_bytes.value()) {
return false;
}

result.type_ = Type::OctetKeyPair;
result.curve_ = curve->to_string();
result.coordinate_x_ = std::move(decoded_public_key).value();
} else {
return false;
}
Expand Down
22 changes: 22 additions & 0 deletions src/core/jose/jose_jws_verify_signature.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ auto hash_for(const sourcemeta::core::JWSAlgorithm algorithm)
case JWSAlgorithm::PS512:
case JWSAlgorithm::ES512:
return SignatureHashFunction::SHA512;
// The Edwards-curve algorithm fixes its own hash, so it never reaches here
case JWSAlgorithm::EdDSA:
Comment thread
jviotti marked this conversation as resolved.
break;
}

std::unreachable();
Expand Down Expand Up @@ -80,6 +83,25 @@ auto jws_verify_signature(const std::optional<JWSAlgorithm> algorithm,
ecdsa_verify(EllipticCurve::P521, SignatureHashFunction::SHA512,
key.coordinate_x(), key.coordinate_y(), signing_input,
signature);
// The Edwards-curve algorithm names one of two curves through the key
// rather than the algorithm (RFC 8037 Section 3.1), and the public key is a
// single encoded point exposed as the first coordinate
case JWSAlgorithm::EdDSA:
if (key.type() != JWK::Type::OctetKeyPair) {
return false;
}

if (key.curve() == "Ed25519") {
return eddsa_verify(EdwardsCurve::Ed25519, key.coordinate_x(),
signing_input, signature);
}

if (key.curve() == "Ed448") {
return eddsa_verify(EdwardsCurve::Ed448, key.coordinate_x(),
signing_input, signature);
}

return false;
}

std::unreachable();
Expand Down
6 changes: 6 additions & 0 deletions test/jose/jose_algorithm_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ TEST(JOSE_algorithm, es512) {
EXPECT_EQ(result.value(), sourcemeta::core::JWSAlgorithm::ES512);
}

TEST(JOSE_algorithm, eddsa) {
const auto result{sourcemeta::core::to_jws_algorithm("EdDSA")};
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), sourcemeta::core::JWSAlgorithm::EdDSA);
}

TEST(JOSE_algorithm, rejects_none) {
EXPECT_FALSE(sourcemeta::core::to_jws_algorithm("none").has_value());
}
Expand Down
56 changes: 56 additions & 0 deletions test/jose/jose_jwk_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,62 @@ TEST(JOSE_JWK, rejects_elliptic_curve_private_key) {
EXPECT_FALSE(sourcemeta::core::JWK::from(document).has_value());
}

// The Ed25519 key is the public key from RFC 8037 Appendix A.2
TEST(JOSE_JWK, octet_key_pair_ed25519_public_key) {
const auto document{sourcemeta::core::parse_json(
R"({ "kty": "OKP", "crv": "Ed25519",
"x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" })")};
const auto key{sourcemeta::core::JWK::from(document)};
ASSERT_TRUE(key.has_value());
EXPECT_EQ(key.value().type(), sourcemeta::core::JWK::Type::OctetKeyPair);
EXPECT_EQ(key.value().curve(), "Ed25519");
EXPECT_EQ(key.value().coordinate_x().size(), 32);
}

TEST(JOSE_JWK, octet_key_pair_ed448_public_key) {
const auto document{sourcemeta::core::parse_json(
R"({ "kty": "OKP", "crv": "Ed448",
"x": "E35kUtHOEUbcTPAayux0atDpqzE8jD1lGIdbrhR5I79Gm1bDz6JMUvrGk7zVusKM8FEDCWzJMjcA" })")};
const auto key{sourcemeta::core::JWK::from(document)};
ASSERT_TRUE(key.has_value());
EXPECT_EQ(key.value().type(), sourcemeta::core::JWK::Type::OctetKeyPair);
EXPECT_EQ(key.value().curve(), "Ed448");
EXPECT_EQ(key.value().coordinate_x().size(), 57);
}

TEST(JOSE_JWK, octet_key_pair_algorithm_eddsa) {
const auto document{sourcemeta::core::parse_json(
R"({ "kty": "OKP", "crv": "Ed25519",
"x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
"alg": "EdDSA" })")};
const auto key{sourcemeta::core::JWK::from(document)};
ASSERT_TRUE(key.has_value());
ASSERT_TRUE(key.value().algorithm().has_value());
EXPECT_EQ(key.value().algorithm().value(),
sourcemeta::core::JWSAlgorithm::EdDSA);
}

TEST(JOSE_JWK, rejects_octet_key_pair_private_key) {
const auto document{sourcemeta::core::parse_json(
R"({ "kty": "OKP", "crv": "Ed25519",
"x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
"d": "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A" })")};
EXPECT_FALSE(sourcemeta::core::JWK::from(document).has_value());
}

TEST(JOSE_JWK, rejects_octet_key_pair_unsupported_curve) {
const auto document{sourcemeta::core::parse_json(
R"({ "kty": "OKP", "crv": "X25519",
"x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" })")};
EXPECT_FALSE(sourcemeta::core::JWK::from(document).has_value());
}

TEST(JOSE_JWK, rejects_octet_key_pair_wrong_public_key_length) {
const auto document{sourcemeta::core::parse_json(
R"({ "kty": "OKP", "crv": "Ed25519", "x": "dGVzdA" })")};
EXPECT_FALSE(sourcemeta::core::JWK::from(document).has_value());
}

TEST(JOSE_JWK, rejects_missing_rsa_modulus) {
const auto document{
sourcemeta::core::parse_json(R"({ "kty": "RSA", "e": "AQAB" })")};
Expand Down
44 changes: 44 additions & 0 deletions test/jose/jose_jws_verify_signature_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ constexpr std::string_view RSA_JWK_OTHER_ALGORITHM{
R"JSON({ "kty": "RSA", "n": "g6AMCEh4IMEnWr_9s8s-uUPXOWm1Zt2h4nV2ZCWsZRHnQg-SzmkNDw3SqUF9nLbjCz_HlElABwe9XZ8gfwVGKr3TNHcaTS_QQNGzX6WndznyQKvoEL3BkvMAk-p-CzUpW4XzAl7iwdpOjxh8iFAR-pOcdvCzEcwEVkwlcVL1IDXN_oFxfpldOA94Ljcp4fA0FmsTo74x93el3hzfgHYSt1UeHQkrjQwmfecbjVHpDHmpqcaAmgWpKHYnWa0WZJ5t-cm17UIydct-lEUKne_bqoUHuyqakJG6fLHbunxc0CRxqcV5r_i64D0vMDsdu3I1YehoOj9CDvzE8rKGeSA8Mw", "e": "AQAB", "alg": "RS384" })JSON"};
constexpr std::string_view EC_JWK{
R"JSON({ "kty": "EC", "crv": "P-256", "x": "uEMPr85yIqQEqUAOF7f-jpo0LA9tnUXj1q6HzanBnJs", "y": "JHRc8vYEaVwcjH20LqwKfehDU2JGg43Sx56GcEgfbXY" })JSON"};
constexpr std::string_view EDDSA_SIGNING_INPUT{
"eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc"};
constexpr std::string_view EDDSA_SIGNATURE{
"hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sV"
"vpAr_MuM0KAg"};
constexpr std::string_view OKP_JWK{
R"JSON({ "kty": "OKP", "crv": "Ed25519", "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" })JSON"};
} // namespace

TEST(JOSE_jws_verify_signature, rs256_valid) {
Expand Down Expand Up @@ -134,3 +141,40 @@ TEST(JOSE_jws_verify_signature, tampered_signature) {
sourcemeta::core::JWSAlgorithm::RS256, RS256_SIGNING_INPUT, tampered,
key.value()));
}

// The Ed25519 signature is the worked example from RFC 8037 Appendix A.4
TEST(JOSE_jws_verify_signature, eddsa_ed25519_valid) {
const auto signature{sourcemeta::core::base64url_decode(EDDSA_SIGNATURE)};
ASSERT_TRUE(signature.has_value());
const auto key{
sourcemeta::core::JWK::from(sourcemeta::core::parse_json(OKP_JWK))};
ASSERT_TRUE(key.has_value());
EXPECT_TRUE(sourcemeta::core::jws_verify_signature(
sourcemeta::core::JWSAlgorithm::EdDSA, EDDSA_SIGNING_INPUT,
signature.value(), key.value()));
}

TEST(JOSE_jws_verify_signature, eddsa_tampered_signature) {
const auto signature{sourcemeta::core::base64url_decode(EDDSA_SIGNATURE)};
ASSERT_TRUE(signature.has_value());
std::string tampered{signature.value()};
tampered.front() =
static_cast<char>(static_cast<unsigned char>(tampered.front()) ^ 0x80U);
const auto key{
sourcemeta::core::JWK::from(sourcemeta::core::parse_json(OKP_JWK))};
ASSERT_TRUE(key.has_value());
EXPECT_FALSE(sourcemeta::core::jws_verify_signature(
sourcemeta::core::JWSAlgorithm::EdDSA, EDDSA_SIGNING_INPUT, tampered,
key.value()));
}

TEST(JOSE_jws_verify_signature, eddsa_key_type_mismatch) {
const auto signature{sourcemeta::core::base64url_decode(EDDSA_SIGNATURE)};
ASSERT_TRUE(signature.has_value());
const auto key{
sourcemeta::core::JWK::from(sourcemeta::core::parse_json(EC_JWK))};
ASSERT_TRUE(key.has_value());
EXPECT_FALSE(sourcemeta::core::jws_verify_signature(
sourcemeta::core::JWSAlgorithm::EdDSA, EDDSA_SIGNING_INPUT,
signature.value(), key.value()));
}
36 changes: 36 additions & 0 deletions test/jose/jose_jwt_verify_signature_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,20 @@ constexpr std::string_view ES512_TOKEN{
"cCUUxtYG8qH09toBV2xQiMKPRLYp4O2RvpXZKYItp0dE-zaduwY6o_Z9wNoNJ"};
constexpr std::string_view ES512_JWK{
R"JSON({ "kty": "EC", "crv": "P-521", "x": "ASTBdxhoeBK4-OqrpidcXbsz_Gsd-TRgqkZGD9ROv7r_kBHW1_is2HDctAOVBWC3ywSUS5MZJIl17xJuYCyZA-Rg", "y": "AOQCwwmrhHkVTm9BgloMWDc7T3hw5yQM5Z-YhVumpCYyFIGuyrziD5iqsH8uQvsNXGqnMKhOJ26X8ADhvP20RIAW" })JSON"};
constexpr std::string_view EDDSA_ED25519_TOKEN{
"eyJhbGciOiJFZERTQSJ9."
"eyJpc3MiOiJhY21lIiwiYXVkIjoiY2xpZW50IiwiZXhwIjoyMDAwMDAwMDAwfQ."
"NXpOKGRYG6ftFqlmEnDhPkbuX4Q7WXTjAjF4_"
"Ltuzgak4sT3ytrTY2SywMAkffchdDNKOlhKvWSkO-KvFwL2DQ"};
constexpr std::string_view EDDSA_ED25519_JWK{
R"JSON({ "kty": "OKP", "crv": "Ed25519", "x": "6L5CYp-b45wSnsCo-PdL4lZ1sXl7C0DG0iF2g_UXsh8" })JSON"};
constexpr std::string_view EDDSA_ED448_TOKEN{
"eyJhbGciOiJFZERTQSJ9."
"eyJpc3MiOiJhY21lIiwiYXVkIjoiY2xpZW50IiwiZXhwIjoyMDAwMDAwMDAwfQ.6cGhapIP_"
"Yppjzb66QUyfKuTtFbVv3eQ2RQnHpOf5NqwlAXvSqMxIoExnPqjW6BrMOLQSJ-Wbz-Aqa2_"
"cK08tlqWEaWx_T5YduhDo3oqdLmKmrupiW_h2XM1ZLVbrZCKgp74CxYvOETEDq5qoaJfvQ4A"};
constexpr std::string_view EDDSA_ED448_JWK{
R"JSON({ "kty": "OKP", "crv": "Ed448", "x": "E35kUtHOEUbcTPAayux0atDpqzE8jD1lGIdbrhR5I79Gm1bDz6JMUvrGk7zVusKM8FEDCWzJMjcA" })JSON"};
} // namespace

TEST(JOSE_jwt_verify_signature, rs256_valid) {
Expand Down Expand Up @@ -200,3 +214,25 @@ TEST(JOSE_jwt_verify_signature, es512_valid) {
EXPECT_TRUE(
sourcemeta::core::jwt_verify_signature(token.value(), key.value()));
}

// Self-signed with OpenSSL, since no RFC worked example provides an EdDSA token
// with a JSON object payload (RFC 8037 signs plain text)
TEST(JOSE_jwt_verify_signature, eddsa_ed25519) {
const auto token{sourcemeta::core::JWT::from(EDDSA_ED25519_TOKEN)};
ASSERT_TRUE(token.has_value());
const auto key{sourcemeta::core::JWK::from(
sourcemeta::core::parse_json(EDDSA_ED25519_JWK))};
ASSERT_TRUE(key.has_value());
EXPECT_TRUE(
sourcemeta::core::jwt_verify_signature(token.value(), key.value()));
}

TEST(JOSE_jwt_verify_signature, eddsa_ed448) {
const auto token{sourcemeta::core::JWT::from(EDDSA_ED448_TOKEN)};
ASSERT_TRUE(token.has_value());
const auto key{sourcemeta::core::JWK::from(
sourcemeta::core::parse_json(EDDSA_ED448_JWK))};
ASSERT_TRUE(key.has_value());
EXPECT_TRUE(
sourcemeta::core::jwt_verify_signature(token.value(), key.value()));
}
Loading