hx509: Add JOSE functionality

This commit is contained in:
Nicolas Williams
2025-12-22 14:16:30 -06:00
parent aa7a1a403a
commit 220a47b000
7 changed files with 2484 additions and 3 deletions

View File

@@ -27,6 +27,7 @@ dist_libhx509_la_SOURCES = \
file.c \
hx509.h \
hx_locl.h \
jose.c \
sel.c \
sel.h \
sel-gram.y \
@@ -124,6 +125,7 @@ $(hxtool_OBJECTS): hxtool-commands.h $(nodist_include_HEADERS)
hxtool_LDADD = \
libhx509template.la \
$(top_builddir)/lib/asn1/libasn1.la \
$(top_builddir)/lib/base/libheimbase.la \
$(LIB_openssl_crypto) \
$(LIB_roken) \
$(top_builddir)/lib/sl/libsl.la

View File

@@ -63,7 +63,8 @@ libhx509_la_OBJS = \
$(OBJ)\print.obj \
$(OBJ)\softp11.obj \
$(OBJ)\req.obj \
$(OBJ)\revoke.obj
$(OBJ)\revoke.obj \
$(OBJ)\jose.obj
$(LIBHX509): $(libhx509_la_OBJS)
$(LIBCON)
@@ -100,7 +101,8 @@ dist_libhx509_la_SOURCES = \
$(SRCDIR)\softp11.c \
$(SRCDIR)\ref\pkcs11.h \
$(SRCDIR)\req.c \
$(SRCDIR)\revoke.c
$(SRCDIR)\revoke.c \
$(SRCDIR)\jose.c
{}.c{$(OBJ)}.obj::
$(C2OBJ_P) -DBUILD_HX509_LIB -DASN1_LIB
@@ -127,7 +129,7 @@ $(OBJ)\hxtool-commands.c $(OBJ)\hxtool-commands.h: hxtool-commands.in $(SLC)
cd $(SRCDIR)
$(BINDIR)\hxtool.exe: $(OBJ)\tool\hxtool.obj $(OBJ)\tool\hxtool-commands.obj $(LIBHEIMDAL) $(OBJ)\hxtool-version.res
$(EXECONLINK) $(LIBHEIMDAL) $(LIBROKEN) $(LIBSL) $(LIBVERS) $(LIBCOMERR) $(LIB_openssl_crypto)
$(EXECONLINK) $(LIBHEIMDAL) $(LIBHEIMBASE) $(LIBROKEN) $(LIBSL) $(LIBVERS) $(LIBCOMERR) $(LIB_openssl_crypto)
$(EXEPREP)
$(OBJ)\hx509-protos.h:

View File

@@ -1078,6 +1078,128 @@ command = {
argument = "certificate-store"
help = "Assert certificate content"
}
command = {
name = "jwt-sign"
option = {
long = "algorithm"
short = "a"
type = "string"
argument = "algorithm"
help = "signature algorithm (RS256, ES256, EdDSA, etc.)"
default = "RS256"
}
option = {
long = "private-key"
short = "k"
type = "string"
argument = "keystore"
help = "private key (FILE:path, PKCS12:path, PKCS11:uri, or plain path)"
}
option = {
long = "pass"
short = "P"
type = "strings"
argument = "password-spec"
help = "password for keystore (same format as other hxtool commands)"
}
option = {
long = "issuer"
short = "i"
type = "string"
argument = "issuer"
help = "issuer claim (iss)"
}
option = {
long = "subject"
short = "s"
type = "string"
argument = "subject"
help = "subject claim (sub)"
}
option = {
long = "audience"
short = "A"
type = "string"
argument = "audience"
help = "audience claim (aud)"
}
option = {
long = "lifetime"
short = "l"
type = "integer"
argument = "seconds"
help = "token lifetime in seconds"
default = "3600"
}
option = {
long = "output"
short = "o"
type = "string"
argument = "file"
help = "output file (default: stdout)"
}
min_args = "0"
max_args = "0"
help = "Create a signed JWT"
function = "jwt_sign"
}
command = {
name = "jwt-verify"
option = {
long = "public-key"
short = "k"
type = "strings"
argument = "file"
help = "public key file(s) (PEM format)"
}
option = {
long = "jwk"
short = "J"
type = "string"
argument = "jwk-or-file"
help = "JWK/JWKS JSON string or file path"
}
option = {
long = "audience"
short = "A"
type = "string"
argument = "audience"
help = "required audience"
}
option = {
long = "token"
short = "t"
type = "string"
argument = "token"
help = "JWT token (or read from stdin)"
}
min_args = "0"
max_args = "0"
help = "Verify a JWT and print claims"
function = "jwt_verify"
}
command = {
name = "pem-to-jwk"
option = {
long = "input"
short = "i"
type = "string"
argument = "file"
help = "PEM key file"
}
option = {
long = "output"
short = "o"
type = "string"
argument = "file"
help = "output file (default: stdout)"
}
min_args = "0"
max_args = "1"
argument = "[pem-file]"
help = "Convert PEM key to JWK format"
function = "pem_to_jwk"
}
command = {
name = "help"
name = "?"

View File

@@ -3267,6 +3267,251 @@ acert(struct acert_options *opt, int argc, char **argv)
return 0;
}
/*
* JWT / JWS / JWK commands
*/
static char *
read_file_to_string(const char *filename)
{
FILE *f;
long size;
char *data;
f = fopen(filename, "r");
if (f == NULL)
return NULL;
fseek(f, 0, SEEK_END);
size = ftell(f);
fseek(f, 0, SEEK_SET);
data = malloc(size + 1);
if (data == NULL) {
fclose(f);
return NULL;
}
if (fread(data, 1, size, f) != (size_t)size) {
free(data);
fclose(f);
return NULL;
}
data[size] = '\0';
fclose(f);
return data;
}
int
jwt_sign(struct jwt_sign_options *opt, int argc, char **argv)
{
hx509_lock lock = NULL;
hx509_certs certs = NULL;
hx509_private_key *keys = NULL;
hx509_private_key signer = NULL;
char *store_name = NULL;
char *token = NULL;
FILE *out = stdout;
int ret;
if (opt->private_key_string == NULL)
errx(1, "--private-key is required");
/* Set up lock for password-protected keystores */
hx509_lock_init(context, &lock);
lock_strings(lock, &opt->pass_strings);
/* Normalize store name (auto-detect FILE: for plain paths) */
store_name = fix_store_name(context, opt->private_key_string, "FILE");
/* Load keystore */
ret = hx509_certs_init(context, store_name, 0, lock, &certs);
if (ret)
hx509_err(context, 1, ret, "Failed to open keystore: %s", store_name);
/* Extract private keys */
ret = _hx509_certs_keys_get(context, certs, &keys);
if (ret)
hx509_err(context, 1, ret, "Failed to get keys from keystore: %s", store_name);
if (keys[0] == NULL)
errx(1, "No private key found in keystore: %s", store_name);
signer = _hx509_private_key_ref(keys[0]);
/* Sign JWT */
ret = hx509_jwt_sign_key(context,
opt->algorithm_string,
signer,
opt->issuer_string,
opt->subject_string,
opt->audience_string,
opt->lifetime_integer,
NULL, /* extra_claims */
&token);
_hx509_certs_keys_free(context, keys);
hx509_private_key_free(&signer);
hx509_certs_free(&certs);
hx509_lock_free(lock);
free(store_name);
if (ret)
hx509_err(context, 1, ret, "Failed to sign JWT");
if (opt->output_string) {
out = fopen(opt->output_string, "w");
if (out == NULL)
err(1, "Could not open %s for writing", opt->output_string);
}
fprintf(out, "%s\n", token);
free(token);
if (opt->output_string)
fclose(out);
return 0;
}
int
jwt_verify(struct jwt_verify_options *opt, int argc, char **argv)
{
char **pem_keys = NULL;
char *token = NULL;
char *jwk_json = NULL;
heim_dict_t claims = NULL;
heim_string_t claims_json = NULL;
size_t num_keys = 0;
int ret;
size_t i;
if (opt->public_key_strings.num_strings == 0 && opt->jwk_string == NULL)
errx(1, "--public-key or --jwk is required");
if (opt->public_key_strings.num_strings > 0 && opt->jwk_string != NULL)
errx(1, "--public-key and --jwk are mutually exclusive");
/* Get token */
if (opt->token_string) {
token = strdup(opt->token_string);
} else {
size_t sz = 0;
if (getline(&token, &sz, stdin) < 0)
errx(1, "Could not read token from stdin");
/* Remove trailing newline */
token[strcspn(token, "\r\n")] = '\0';
}
if (token == NULL)
err(1, "Out of memory");
if (opt->jwk_string) {
/* JWK/JWKS mode */
const char *jwk_arg = opt->jwk_string;
/* If it starts with { or [, it's inline JSON; otherwise read from file */
if (jwk_arg[0] == '{' || jwk_arg[0] == '[') {
jwk_json = strdup(jwk_arg);
} else {
jwk_json = read_file_to_string(jwk_arg);
if (jwk_json == NULL)
err(1, "Could not read JWK from %s", jwk_arg);
}
ret = hx509_jwt_verify_jwk(context,
token,
jwk_json,
opt->audience_string,
0, /* use current time */
&claims);
free(jwk_json);
} else {
/* PEM public key mode */
num_keys = opt->public_key_strings.num_strings;
pem_keys = calloc(num_keys, sizeof(char *));
if (pem_keys == NULL)
err(1, "Out of memory");
for (i = 0; i < num_keys; i++) {
pem_keys[i] = read_file_to_string(opt->public_key_strings.strings[i]);
if (pem_keys[i] == NULL)
err(1, "Could not read public key from %s",
opt->public_key_strings.strings[i]);
}
ret = hx509_jwt_verify(context,
token,
(const char **)pem_keys,
num_keys,
opt->audience_string,
0, /* use current time */
&claims);
for (i = 0; i < num_keys; i++)
free(pem_keys[i]);
free(pem_keys);
}
free(token);
if (ret)
hx509_err(context, 1, ret, "JWT verification failed");
/* Print claims */
claims_json = heim_json_copy_serialize(claims, HEIM_JSON_F_INDENT2, NULL);
if (claims_json)
printf("%s\n", heim_string_get_utf8(claims_json));
heim_release(claims_json);
heim_release(claims);
return 0;
}
int
pem_to_jwk(struct pem_to_jwk_options *opt, int argc, char **argv)
{
char *pem_key = NULL;
char *jwk_json = NULL;
const char *input_file;
FILE *out = stdout;
int ret;
/* Get input file from option or argument */
if (opt->input_string)
input_file = opt->input_string;
else if (argc > 0)
input_file = argv[0];
else
errx(1, "PEM file required (use --input or provide as argument)");
pem_key = read_file_to_string(input_file);
if (pem_key == NULL)
err(1, "Could not read PEM key from %s", input_file);
ret = hx509_pem_to_jwk_json(context, pem_key, &jwk_json);
free(pem_key);
if (ret)
hx509_err(context, 1, ret, "Failed to convert PEM to JWK");
if (opt->output_string) {
out = fopen(opt->output_string, "w");
if (out == NULL)
err(1, "Could not open %s for writing", opt->output_string);
}
fprintf(out, "%s\n", jwk_json);
free(jwk_json);
if (opt->output_string)
fclose(out);
return 0;
}
/*
*
*/

2078
lib/hx509/jose.c Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -305,5 +305,18 @@ EXPORTS
hx509_xfree
initialize_hx_error_table_r
; JWT/JWS/JWK functions
hx509_jwk_to_json
hx509_jws_sign
hx509_jws_sign_key
hx509_jws_verify
hx509_jws_verify_jwk
hx509_jwt_sign
hx509_jwt_sign_key
hx509_jwt_verify
hx509_jwt_verify_jwk
hx509_pem_to_jwk
hx509_pem_to_jwk_json
; pkcs11 symbols
C_GetFunctionList

View File

@@ -317,3 +317,22 @@ HEIMDAL_X509_1.3 {
hx509_ca_tbs_set_signature_algorithm;
};
HEIMDAL_X509_1.4 {
global:
hx509_jwk_to_json;
hx509_jws_sign;
hx509_jws_verify;
hx509_jwt_sign;
hx509_jwt_verify;
hx509_pem_to_jwk;
hx509_pem_to_jwk_json;
};
HEIMDAL_X509_1.5 {
global:
hx509_jws_sign_key;
hx509_jws_verify_jwk;
hx509_jwt_sign_key;
hx509_jwt_verify_jwk;
} HEIMDAL_X509_1.4;