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
13 changes: 9 additions & 4 deletions .github/workflows/openssl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ jobs:
# The Build and Archs workflows test against the OpenSSL that the platform
# packages provide, which is a recent release. This workflow builds the
# supported OpenSSL releases from source and tests against them, plus
# ml_dsa is set on the releases that have ML-DSA: RFC 9964 needs the
# implementation OpenSSL first shipped in 3.5, and CJOSE_ENABLE_ML_DSA
# refuses to configure below that, so the older entries leave it off.
# 3.0.7: the release RHEL 9 is based on and the last 3.0.x whose EdDSA
# signing dereferences a missing private key instead of failing (guarded in
# 3.0.8).
Expand All @@ -30,9 +33,9 @@ jobs:
- { openssl: 3.2.6, tag: openssl-3.2.6, sha256: 89681a9ddaa9ed7cf25ea8ef61338db805200bae47d00510490623547380c148 }
- { openssl: 3.3.7, tag: openssl-3.3.7, sha256: 4900be54e81c4dfe00bb1a10dad33fd8414833573c40d0e9e3274d4ed32e53a2 }
- { openssl: 3.4.7, tag: openssl-3.4.7, sha256: 7261c348e6e59bc7b635a30f98072e8817a1c60a7033bb570737394a3a061842 }
- { openssl: 3.5.8, tag: openssl-3.5.8, sha256: a8f84a39918ec6415ce765d9b429d313ba97b8143169c172e734b9514464f5b2 }
- { openssl: 3.6.4, tag: openssl-3.6.4, sha256: 9bffaa1ad1e07b354c21bd3324ec02fa15579f45a7d0494b3e74bc449b7333ef }
- { openssl: 4.0.2, tag: openssl-4.0.2, sha256: 736b467530f916737b7031310ccb21d8218c6229e61e8e160cd1d3458cd543a8 }
- { openssl: 3.5.8, tag: openssl-3.5.8, sha256: a8f84a39918ec6415ce765d9b429d313ba97b8143169c172e734b9514464f5b2, ml_dsa: ON }
- { openssl: 3.6.4, tag: openssl-3.6.4, sha256: 9bffaa1ad1e07b354c21bd3324ec02fa15579f45a7d0494b3e74bc449b7333ef, ml_dsa: ON }
- { openssl: 4.0.2, tag: openssl-4.0.2, sha256: 736b467530f916737b7031310ccb21d8218c6229e61e8e160cd1d3458cd543a8, ml_dsa: ON }

steps:
- uses: actions/checkout@v5
Expand All @@ -59,7 +62,9 @@ jobs:
make -j"$(nproc)"
make install_sw
- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DOPENSSL_ROOT_DIR="$OPENSSL_PREFIX"
run: |
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DOPENSSL_ROOT_DIR="$OPENSSL_PREFIX" \
-DCJOSE_ENABLE_ML_DSA=${{ matrix.ml_dsa || 'OFF' }}
- name: Build
run: cmake --build build --parallel
- name: Check which OpenSSL the tests load
Expand Down
17 changes: 17 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ option(CJOSE_BUILD_TESTS "Build the cjose unit tests (requires Check)" ${PROJEC
# with peers that cannot use RSA-OAEP.
option(CJOSE_ENABLE_RSA1_5 "Enable the RSA1_5 (RSAES-PKCS1-v1_5) key encryption algorithm" OFF)

# ML-DSA (RFC 9964) needs the ML-DSA implementation OpenSSL first shipped in
# 3.5, well above the 3.0 this project otherwise builds on, and the algorithms
# are registered as OPTIONAL. Off by default; turning it on requires that
# OpenSSL, checked at configure time rather than left to a link error.
option(CJOSE_ENABLE_ML_DSA "Enable the ML-DSA-44/65/87 signature algorithms and the AKP key type (RFC 9964)" OFF)

# PBES2 (RFC 7518 section 4.8) policy bounds. RFC 7518 sets only a minimum salt
# input length; these are cjose's own limits on the PBKDF2 work it will do and
# produce, documented with their defaults in src/include/jwe_int.h, which also
Expand Down Expand Up @@ -82,6 +88,11 @@ endif()
# Dependencies
# ---------------------------------------------------------------------------
find_package(OpenSSL 3.0.0 REQUIRED)

if(CJOSE_ENABLE_ML_DSA AND OPENSSL_VERSION VERSION_LESS 3.5.0)
message(FATAL_ERROR
"CJOSE_ENABLE_ML_DSA requires OpenSSL >= 3.5.0 for ML-DSA, found ${OPENSSL_VERSION}")
endif()
find_package(Jansson 2.3 REQUIRED)

if(WIN32)
Expand Down Expand Up @@ -150,6 +161,9 @@ target_compile_definitions(cjose_objects PRIVATE OPENSSL_NO_DEPRECATED)
if(CJOSE_ENABLE_RSA1_5)
target_compile_definitions(cjose_objects PRIVATE HAVE_RSA_PKCS1_PADDING=1)
endif()
if(CJOSE_ENABLE_ML_DSA)
target_compile_definitions(cjose_objects PRIVATE HAVE_ML_DSA=1)
endif()
if(CJOSE_JWE_PBES2_SETTINGS)
target_compile_definitions(cjose_objects PRIVATE ${CJOSE_JWE_PBES2_SETTINGS})
endif()
Expand Down Expand Up @@ -424,6 +438,9 @@ if(CJOSE_BUILD_TESTS)
if(CJOSE_ENABLE_RSA1_5)
target_compile_definitions(check_cjose PRIVATE HAVE_RSA_PKCS1_PADDING=1)
endif()
if(CJOSE_ENABLE_ML_DSA)
target_compile_definitions(check_cjose PRIVATE HAVE_ML_DSA=1)
endif()
if(CJOSE_JWE_PBES2_SETTINGS)
target_compile_definitions(check_cjose PRIVATE ${CJOSE_JWE_PBES2_SETTINGS})
endif()
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@ JWS signing algorithms (`alg`):
| `ES256`, `ES384`, `ES512` | ECDSA with P-256, P-384 and P-521 | |
| `ES256K` | ECDSA with secp256k1 | OpenSSL built with `secp256k1` |
| `Ed25519`, `Ed448` | EdDSA (RFC 9864) | |
| `ML-DSA-44`, `ML-DSA-65`, `ML-DSA-87` | ML-DSA (RFC 9964, US NIST FIPS 204) | build option `CJOSE_ENABLE_ML_DSA`, OpenSSL >= 3.5 |

The polymorphic `EdDSA` identifier of RFC 8037, deprecated by RFC 9864, and
`none` are not accepted.

The ML-DSA algorithms sign with the `AKP` key type of RFC 9964. They are pure
ML-DSA over the JWS signing input, with the empty context string RFC 9964
requires; HashML-DSA is not part of that specification and is not offered.

JWE key management algorithms (`alg`):

| Identifier | Algorithm | Requires |
Expand Down Expand Up @@ -64,6 +69,7 @@ JWK key types (`kty`):
| `EC` | `P-256`, `P-384`, `P-521`, `secp256k1` | `secp256k1`: OpenSSL built with it |
| `oct` | symmetric | |
| `OKP` | `Ed25519`, `Ed448`, `X25519`, `X448` | |
| `AKP` | `ML-DSA-44`, `ML-DSA-65`, `ML-DSA-87` (RFC 9964) | build option `CJOSE_ENABLE_ML_DSA`, OpenSSL >= 3.5 |

JWEs can be produced and consumed in both the compact and the JSON
serialization, with one or more recipients.
Expand Down Expand Up @@ -107,6 +113,7 @@ Pass options with `-D<OPTION>=<VALUE>` at configure time:
| `CJOSE_BUILD_STATIC` | `ON` | Build the static library |
| `CJOSE_BUILD_TESTS` | `ON` when top-level | Build the unit tests (requires Check) |
| `CJOSE_ENABLE_RSA1_5` | `OFF` | Enable the RSA1_5 (RSAES-PKCS1-v1_5) key encryption algorithm |
| `CJOSE_ENABLE_ML_DSA` | `OFF` | Enable the ML-DSA signature algorithms and the `AKP` key type (RFC 9964); requires OpenSSL >= 3.5 |
| `CJOSE_MSVC_STATIC_RUNTIME` | `OFF` | (MSVC) Link against the static C runtime (`/MT`) |
| `CJOSE_MACOS_DYLIB` | `OFF` | (macOS) Build a plain `.dylib` instead of a framework |
| `CJOSE_JWE_PBES2_MIN_ITERATIONS` | `1000` | Lowest PBES2 iteration count cjose will produce |
Expand Down
3 changes: 3 additions & 0 deletions cmake/exports/cjose.def
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,11 @@ EXPORTS
cjose_jwe_import
cjose_jwe_import_json
cjose_jwe_release
cjose_jwk_AKP_get_alg
cjose_jwk_EC_get_curve
cjose_jwk_OKP_get_curve
cjose_jwk_create_AKP_random
cjose_jwk_create_AKP_spec
cjose_jwk_create_EC_random
cjose_jwk_create_EC_spec
cjose_jwk_create_OKP_random
Expand Down
3 changes: 3 additions & 0 deletions cmake/exports/libcjose.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,11 @@ _cjose_jwe_get_protected
_cjose_jwe_import
_cjose_jwe_import_json
_cjose_jwe_release
_cjose_jwk_AKP_get_alg
_cjose_jwk_EC_get_curve
_cjose_jwk_OKP_get_curve
_cjose_jwk_create_AKP_random
_cjose_jwk_create_AKP_spec
_cjose_jwk_create_EC_random
_cjose_jwk_create_EC_spec
_cjose_jwk_create_OKP_random
Expand Down
3 changes: 3 additions & 0 deletions cmake/exports/libcjose.version
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,11 @@ CJOSE_1.0 {
cjose_jwe_import;
cjose_jwe_import_json;
cjose_jwe_release;
cjose_jwk_AKP_get_alg;
cjose_jwk_EC_get_curve;
cjose_jwk_OKP_get_curve;
cjose_jwk_create_AKP_random;
cjose_jwk_create_AKP_spec;
cjose_jwk_create_EC_random;
cjose_jwk_create_EC_spec;
cjose_jwk_create_OKP_random;
Expand Down
5 changes: 5 additions & 0 deletions include/cjose/header.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ extern "C" {
#define CJOSE_HDR_ALG_ES384 "ES384"
#define CJOSE_HDR_ALG_ES512 "ES512"

/** The JWS algorithm attribute values for the ML-DSA signature algorithms of RFC 9964, US NIST FIPS 204. */
#define CJOSE_HDR_ALG_ML_DSA_44 "ML-DSA-44"
#define CJOSE_HDR_ALG_ML_DSA_65 "ML-DSA-65"
#define CJOSE_HDR_ALG_ML_DSA_87 "ML-DSA-87"

/** The JWS algorithm attribute values for the fully-specified EdDSA algorithms Ed25519 and Ed448 (RFC 9864). */
#define CJOSE_HDR_ALG_ED25519 "Ed25519"
#define CJOSE_HDR_ALG_ED448 "Ed448"
Expand Down
95 changes: 94 additions & 1 deletion include/cjose/jwk.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ typedef enum
/** Octet String (Symmetric) Key */
CJOSE_JWK_KTY_OCT,
/** Octet Key Pair (RFC 8037) Public (or Private) Key */
CJOSE_JWK_KTY_OKP
CJOSE_JWK_KTY_OKP,
/** Algorithm Key Pair (RFC 9964) Public (or Private) Key */
CJOSE_JWK_KTY_AKP
} cjose_jwk_kty_t;

/**
Expand Down Expand Up @@ -406,6 +408,97 @@ cjose_jwk_t *cjose_jwk_create_OKP_spec(const cjose_jwk_okp_keyspec *spec, cjose_
*/
cjose_jwk_okp_curve cjose_jwk_OKP_get_curve(const cjose_jwk_t *jwk, cjose_err *err);

/** Enumeration of supported algorithms for Algorithm Key Pair (AKP) JWK
* objects (RFC 9964).
*
* An AKP key names its algorithm rather than a curve: RFC 9964 section 3 makes
* the "alg" JWK parameter REQUIRED for every AKP key, because the same key
* type serves algorithms whose keys are not interchangeable.
*/
typedef enum
{
/** ML-DSA-44 (US NIST FIPS 204). */
CJOSE_JWK_AKP_ML_DSA_44 = 1,
/** ML-DSA-65 (US NIST FIPS 204). */
CJOSE_JWK_AKP_ML_DSA_65,
/** ML-DSA-87 (US NIST FIPS 204). */
CJOSE_JWK_AKP_ML_DSA_87,
/** Invalid Algorithm */
CJOSE_JWK_AKP_INVALID = -1
} cjose_jwk_akp_alg;

/** Key specification for Algorithm Key Pair (AKP) JWK objects (RFC 9964).
*
* Note that cjose_jwk_get_keysize() reports the size of the public key in bits
* for an AKP key, as it does for OKP and RSA, which for the ML-DSA algorithms
* is 10496, 15616 or 20736. That is the size of the key material, not a
* measure of the strength of the algorithm.
*/
typedef struct
{
/** The algorithm the key belongs to */
cjose_jwk_akp_alg alg;
/** The private key (RFC 9964 "priv"), or NULL for a public key. For the
ML-DSA algorithms this is the 32 octet seed, never the expanded
private key (RFC 9964 section 4) */
uint8_t *priv;
/** Length of <tt>priv</tt>: 32 for the ML-DSA algorithms, or 0 */
size_t privlen;
/** The public key (RFC 9964 "pub") */
uint8_t *pub;
/** Length of <tt>pub</tt>: the fixed size of the algorithm, or 0 */
size_t publen;
} cjose_jwk_akp_keyspec;

/**
* Creates a new Algorithm Key Pair (AKP) JWK, using a secure random number
* generator.
*
* \b NOTE: The caller MUST call cjose_jwk_release() to release the JWK's
* resources.
*
* \b NOTE: Fails with CJOSE_ERR_INVALID_ARG unless the library was built with
* the CJOSE_ENABLE_ML_DSA option, which needs OpenSSL 3.5 or newer.
*
* \param alg The algorithm to generate the key pair for
* \param err [out] An optional error object which can be used to get additional
* information in the event of an error.
* \returns The generated Algorithm Key Pair JWK object
*/
cjose_jwk_t *cjose_jwk_create_AKP_random(cjose_jwk_akp_alg alg, cjose_err *err);

/**
* Creates a new Algorithm Key Pair (AKP) JWK, using the given raw values for
* the private and/or public key. When only the private key is given the public
* key is derived from it; when both are given they must belong together.
*
* \b NOTE: The caller MUST call cjose_jwk_release() to release the JWK's
* resources.
*
* \b NOTE: This function makes a copy of all provided data; the caller
* MUST free the memory for <tt>spec</tt> after calling this function.
*
* \b NOTE: Fails with CJOSE_ERR_INVALID_ARG unless the library was built with
* the CJOSE_ENABLE_ML_DSA option, which needs OpenSSL 3.5 or newer.
*
* \param spec The specified Algorithm Key Pair properties
* \param err [out] An optional error object which can be used to get additional
* information in the event of an error.
* \returns The generated Algorithm Key Pair JWK object
*/
cjose_jwk_t *cjose_jwk_create_AKP_spec(const cjose_jwk_akp_keyspec *spec, cjose_err *err);

/**
* Obtains the algorithm for the given (AKP) JWK.
*
* \param jwk [in] The AKP JWK to inspect
* \param err [out] An optional error object which can be used to get additional
* information in the event of an error.
* \returns The algorithm, or CJOSE_JWK_AKP_INVALID if <tt>jwk</tt> is not an
* AKP key
*/
cjose_jwk_akp_alg cjose_jwk_AKP_get_alg(const cjose_jwk_t *jwk, cjose_err *err);

/**
* Instantiates a new JWK given a JSON document representation conforming
* to JSON Web Key (JWK) IETF ID draft-ietf-jose-json-web-key.
Expand Down
25 changes: 25 additions & 0 deletions src/include/jwk_int.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,24 @@ typedef struct _okp_keydata_int
bool has_private;
} okp_keydata;

// RFC 9964 section 7.3: the seed is 256 bits for every ML-DSA algorithm, and
// the length check is a MUST
#define CJOSE_JWK_AKP_SEED_LEN 32

// AKP-specific keydata (RFC 9964): the EVP_PKEY holds the ML-DSA key, built
// from the 32 octet seed for a private key and from the public key otherwise.
// The seed is kept beside it because RFC 9964 section 4 puts it in "priv" and
// an OpenSSL configured with ml-dsa.retain_seed=no will not give it back, so a
// key would otherwise be unable to survive its own export; this is why
// rsa_keydata keeps its BIGNUMs too. It is wiped with the key.
typedef struct _akp_keydata_int
{
cjose_jwk_akp_alg alg;
EVP_PKEY *key;
bool has_private;
uint8_t seed[CJOSE_JWK_AKP_SEED_LEN];
} akp_keydata;

typedef struct _rsa_keydata_int
{
EVP_PKEY *key;
Expand All @@ -67,6 +85,13 @@ static inline EVP_PKEY *_cjose_jwk_rsa_key(const cjose_jwk_t *jwk) { return ((rs

bool _cjose_jwk_rsa_has_private(const cjose_jwk_t *jwk);

#ifdef HAVE_ML_DSA
// the RFC 9964 name of an AKP algorithm, which is both the JWK "alg" value and
// the name OpenSSL fetches the implementation by, and the reverse mapping
const char *_cjose_jwk_akp_name_for_alg(cjose_jwk_akp_alg alg);
bool _cjose_jwk_akp_alg_from_name(const char *name, cjose_jwk_akp_alg *alg);
#endif // HAVE_ML_DSA

// ECDH-ES runs on EC keys and on OKP X25519 and X448 keys (RFC 8037 section 3.2)
bool _cjose_jwk_is_ecdh_key(const cjose_jwk_t *jwk);
bool _cjose_jwk_ecdh_curve_match(const cjose_jwk_t *a, const cjose_jwk_t *b);
Expand Down
Loading
Loading