Skip to content

Commit e83fb52

Browse files
Introduced crypto.subtle.generateCertificate().
It fixes #766 on GitHub.
1 parent 4fd3ff9 commit e83fb52

File tree

7 files changed

+1517
-0
lines changed

7 files changed

+1517
-0
lines changed

external/njs_webcrypto_module.c

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ static njs_int_t njs_ext_export_key(njs_vm_t *vm, njs_value_t *args,
119119
njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
120120
static njs_int_t njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args,
121121
njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
122+
static njs_int_t njs_ext_generate_certificate(njs_vm_t *vm, njs_value_t *args,
123+
njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
122124
static njs_int_t njs_ext_import_key(njs_vm_t *vm, njs_value_t *args,
123125
njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
124126
static njs_int_t njs_ext_sign(njs_vm_t *vm, njs_value_t *args,
@@ -519,6 +521,17 @@ static njs_external_t njs_ext_subtle_webcrypto[] = {
519521
}
520522
},
521523

524+
{
525+
.flags = NJS_EXTERN_METHOD,
526+
.name.string = njs_str("generateCertificate"),
527+
.writable = 1,
528+
.configurable = 1,
529+
.enumerable = 1,
530+
.u.method = {
531+
.native = njs_ext_generate_certificate,
532+
}
533+
},
534+
522535
{
523536
.flags = NJS_EXTERN_METHOD,
524537
.name.string = njs_str("importKey"),
@@ -2891,6 +2904,331 @@ njs_ext_generate_key(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
28912904
}
28922905

28932906

2907+
static njs_int_t
2908+
njs_ext_generate_certificate(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
2909+
njs_index_t unused, njs_value_t *retval)
2910+
{
2911+
njs_int_t ret;
2912+
njs_value_t *options, *keyPair, *privKey, *pubKey, *val;
2913+
njs_opaque_value_t result, lvalue;
2914+
njs_webcrypto_key_t *privateKey, *publicKey;
2915+
njs_str_t subject, issuer, serialNumber, cert_data;
2916+
X509 *cert = NULL;
2917+
X509_NAME *name = NULL;
2918+
ASN1_INTEGER *serial_asn1 = NULL;
2919+
EVP_PKEY *pkey = NULL;
2920+
BIGNUM *serial_bn = NULL;
2921+
u_char *cert_buf = NULL;
2922+
int cert_len;
2923+
int64_t notBefore, notAfter;
2924+
2925+
static const njs_str_t string_subject = njs_str("subject");
2926+
static const njs_str_t string_issuer = njs_str("issuer");
2927+
static const njs_str_t string_serialNumber = njs_str("serialNumber");
2928+
static const njs_str_t string_notBefore = njs_str("notBefore");
2929+
static const njs_str_t string_notAfter = njs_str("notAfter");
2930+
static const njs_str_t string_privateKey = njs_str("privateKey");
2931+
static const njs_str_t string_publicKey = njs_str("publicKey");
2932+
2933+
if (nargs < 3) {
2934+
njs_vm_type_error(vm, "generateCertificate requires at least 2 arguments");
2935+
return NJS_ERROR;
2936+
}
2937+
2938+
options = njs_arg(args, nargs, 1);
2939+
keyPair = njs_arg(args, nargs, 2);
2940+
2941+
if (!njs_value_is_object(options)) {
2942+
njs_vm_type_error(vm, "generateCertificate options must be an object");
2943+
return NJS_ERROR;
2944+
}
2945+
2946+
if (!njs_value_is_object(keyPair)) {
2947+
njs_vm_type_error(vm, "generateCertificate keyPair must be an object");
2948+
return NJS_ERROR;
2949+
}
2950+
2951+
/* Get private key from keyPair */
2952+
privKey = njs_vm_object_prop(vm, keyPair, &string_privateKey, &lvalue);
2953+
if (njs_slow_path(privKey == NULL)) {
2954+
njs_vm_type_error(vm, "keyPair.privateKey is required");
2955+
return NJS_ERROR;
2956+
}
2957+
2958+
privateKey = njs_vm_external(vm, njs_webcrypto_crypto_key_proto_id, privKey);
2959+
if (njs_slow_path(privateKey == NULL)) {
2960+
njs_vm_type_error(vm, "keyPair.privateKey is not a CryptoKey object");
2961+
return NJS_ERROR;
2962+
}
2963+
2964+
/* Get public key from keyPair */
2965+
pubKey = njs_vm_object_prop(vm, keyPair, &string_publicKey, &lvalue);
2966+
if (njs_slow_path(pubKey == NULL)) {
2967+
njs_vm_type_error(vm, "keyPair.publicKey is required");
2968+
return NJS_ERROR;
2969+
}
2970+
2971+
publicKey = njs_vm_external(vm, njs_webcrypto_crypto_key_proto_id, pubKey);
2972+
if (njs_slow_path(publicKey == NULL)) {
2973+
njs_vm_type_error(vm, "keyPair.publicKey is not a CryptoKey object");
2974+
return NJS_ERROR;
2975+
}
2976+
2977+
/* Extract subject */
2978+
val = njs_vm_object_prop(vm, options, &string_subject, &lvalue);
2979+
if (njs_slow_path(val == NULL)) {
2980+
njs_vm_type_error(vm, "certificate subject is required");
2981+
return NJS_ERROR;
2982+
}
2983+
2984+
ret = njs_vm_value_to_bytes(vm, &subject, val);
2985+
if (njs_slow_path(ret != NJS_OK)) {
2986+
return NJS_ERROR;
2987+
}
2988+
2989+
/* Extract issuer (optional, defaults to subject for self-signed) */
2990+
val = njs_vm_object_prop(vm, options, &string_issuer, &lvalue);
2991+
if (val != NULL) {
2992+
ret = njs_vm_value_to_bytes(vm, &issuer, val);
2993+
if (njs_slow_path(ret != NJS_OK)) {
2994+
return NJS_ERROR;
2995+
}
2996+
} else {
2997+
issuer = subject; /* Self-signed certificate */
2998+
}
2999+
3000+
/* Extract serial number (optional, defaults to "1") */
3001+
val = njs_vm_object_prop(vm, options, &string_serialNumber, &lvalue);
3002+
if (val != NULL) {
3003+
ret = njs_vm_value_to_bytes(vm, &serialNumber, val);
3004+
if (njs_slow_path(ret != NJS_OK)) {
3005+
return NJS_ERROR;
3006+
}
3007+
} else {
3008+
serialNumber.start = NULL;
3009+
serialNumber.length = 0;
3010+
}
3011+
3012+
/* Extract validity period */
3013+
val = njs_vm_object_prop(vm, options, &string_notBefore, &lvalue);
3014+
if (val != NULL) {
3015+
ret = njs_value_to_integer(vm, val, &notBefore);
3016+
if (njs_slow_path(ret != NJS_OK)) {
3017+
return NJS_ERROR;
3018+
}
3019+
} else {
3020+
notBefore = 0; /* Now */
3021+
}
3022+
3023+
val = njs_vm_object_prop(vm, options, &string_notAfter, &lvalue);
3024+
if (val != NULL) {
3025+
ret = njs_value_to_integer(vm, val, &notAfter);
3026+
if (njs_slow_path(ret != NJS_OK)) {
3027+
return NJS_ERROR;
3028+
}
3029+
} else {
3030+
notAfter = 365LL * 24 * 60 * 60 * 1000; /* 1 year from now */
3031+
}
3032+
3033+
/* Create X509 certificate */
3034+
cert = X509_new();
3035+
if (njs_slow_path(cert == NULL)) {
3036+
njs_webcrypto_error(vm, "X509_new() failed");
3037+
goto fail;
3038+
}
3039+
3040+
/* Set version (X509v3) */
3041+
if (X509_set_version(cert, 2) != 1) {
3042+
njs_webcrypto_error(vm, "X509_set_version() failed");
3043+
goto fail;
3044+
}
3045+
3046+
/* Set serial number */
3047+
serial_asn1 = ASN1_INTEGER_new();
3048+
if (njs_slow_path(serial_asn1 == NULL)) {
3049+
njs_webcrypto_error(vm, "ASN1_INTEGER_new() failed");
3050+
goto fail;
3051+
}
3052+
3053+
if (serialNumber.start != NULL && serialNumber.length > 0) {
3054+
/* Convert serialNumber to BIGNUM */
3055+
serial_bn = BN_new();
3056+
if (njs_slow_path(serial_bn == NULL)) {
3057+
njs_webcrypto_error(vm, "BN_new() failed");
3058+
goto fail;
3059+
}
3060+
3061+
if (serialNumber.length == 1 && serialNumber.start[0] >= '0' &&
3062+
serialNumber.start[0] <= '9') {
3063+
/* Handle single digit decimal serial numbers */
3064+
char digit_str[2];
3065+
digit_str[0] = serialNumber.start[0];
3066+
digit_str[1] = '\0';
3067+
3068+
if (BN_dec2bn(&serial_bn, digit_str) == 0) {
3069+
njs_webcrypto_error(vm, "BN_dec2bn() failed");
3070+
goto fail;
3071+
}
3072+
} else {
3073+
/* Try to parse as decimal string first */
3074+
3075+
/* Create null-terminated string for parsing */
3076+
char *serial_str = njs_mp_alloc(njs_vm_memory_pool(vm),
3077+
serialNumber.length + 1);
3078+
if (njs_slow_path(serial_str == NULL)) {
3079+
njs_webcrypto_error(vm, "memory allocation failed");
3080+
goto fail;
3081+
}
3082+
memcpy(serial_str, serialNumber.start, serialNumber.length);
3083+
serial_str[serialNumber.length] = '\0';
3084+
3085+
if (BN_dec2bn(&serial_bn, serial_str) == 0) {
3086+
/* If decimal parsing failed, try hex */
3087+
BN_free(serial_bn);
3088+
serial_bn = BN_new();
3089+
if (njs_slow_path(serial_bn == NULL)) {
3090+
njs_webcrypto_error(vm, "BN_new() failed");
3091+
njs_mp_free(njs_vm_memory_pool(vm), serial_str);
3092+
goto fail;
3093+
}
3094+
3095+
if (BN_hex2bn(&serial_bn, serial_str) == 0) {
3096+
/* If both failed, fallback to treating as raw bytes */
3097+
BN_free(serial_bn);
3098+
serial_bn = BN_bin2bn(serialNumber.start, serialNumber.length, NULL);
3099+
if (njs_slow_path(serial_bn == NULL)) {
3100+
njs_mp_free(njs_vm_memory_pool(vm), serial_str);
3101+
njs_webcrypto_error(vm, "BN_bin2bn() failed");
3102+
goto fail;
3103+
}
3104+
}
3105+
}
3106+
3107+
njs_mp_free(njs_vm_memory_pool(vm), serial_str);
3108+
}
3109+
3110+
if (BN_to_ASN1_INTEGER(serial_bn, serial_asn1) == NULL) {
3111+
njs_webcrypto_error(vm, "BN_to_ASN1_INTEGER() failed");
3112+
goto fail;
3113+
}
3114+
} else {
3115+
/* Default to serial number 1 */
3116+
if (ASN1_INTEGER_set_uint64(serial_asn1, 1) != 1) {
3117+
njs_webcrypto_error(vm, "ASN1_INTEGER_set_uint64() failed");
3118+
goto fail;
3119+
}
3120+
}
3121+
3122+
if (X509_set_serialNumber(cert, serial_asn1) != 1) {
3123+
njs_webcrypto_error(vm, "X509_set_serialNumber() failed");
3124+
goto fail;
3125+
}
3126+
3127+
/* Set validity period */
3128+
if (X509_gmtime_adj(X509_getm_notBefore(cert), notBefore / 1000) == NULL) {
3129+
njs_webcrypto_error(vm, "X509_gmtime_adj(notBefore) failed");
3130+
goto fail;
3131+
}
3132+
3133+
if (X509_gmtime_adj(X509_getm_notAfter(cert), notAfter / 1000) == NULL) {
3134+
njs_webcrypto_error(vm, "X509_gmtime_adj(notAfter) failed");
3135+
goto fail;
3136+
}
3137+
3138+
/* Set subject name */
3139+
name = X509_NAME_new();
3140+
if (njs_slow_path(name == NULL)) {
3141+
njs_webcrypto_error(vm, "X509_NAME_new() failed");
3142+
goto fail;
3143+
}
3144+
3145+
if (X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
3146+
subject.start, subject.length, -1, 0) != 1) {
3147+
njs_webcrypto_error(vm, "X509_NAME_add_entry_by_txt(subject) failed");
3148+
goto fail;
3149+
}
3150+
3151+
if (X509_set_subject_name(cert, name) != 1) {
3152+
njs_webcrypto_error(vm, "X509_set_subject_name() failed");
3153+
goto fail;
3154+
}
3155+
3156+
/* Set issuer name */
3157+
if (X509_set_issuer_name(cert, name) != 1) {
3158+
njs_webcrypto_error(vm, "X509_set_issuer_name() failed");
3159+
goto fail;
3160+
}
3161+
3162+
/* Set public key */
3163+
pkey = privateKey->u.a.pkey;
3164+
if (X509_set_pubkey(cert, pkey) != 1) {
3165+
njs_webcrypto_error(vm, "X509_set_pubkey() failed");
3166+
goto fail;
3167+
}
3168+
3169+
/* Sign the certificate with the private key */
3170+
if (X509_sign(cert, pkey, EVP_sha256()) == 0) {
3171+
njs_webcrypto_error(vm, "X509_sign() failed");
3172+
goto fail;
3173+
}
3174+
3175+
/* Convert certificate to DER format */
3176+
cert_len = i2d_X509(cert, &cert_buf);
3177+
if (njs_slow_path(cert_len <= 0)) {
3178+
njs_webcrypto_error(vm, "i2d_X509() failed");
3179+
goto fail;
3180+
}
3181+
3182+
/* Create result array buffer */
3183+
cert_data.start = cert_buf;
3184+
cert_data.length = cert_len;
3185+
3186+
ret = njs_webcrypto_array_buffer(vm, njs_value_arg(&result),
3187+
cert_data.start, cert_data.length);
3188+
if (njs_slow_path(ret != NJS_OK)) {
3189+
goto fail;
3190+
}
3191+
3192+
/* Cleanup */
3193+
if (serial_bn != NULL) {
3194+
BN_free(serial_bn);
3195+
}
3196+
if (serial_asn1 != NULL) {
3197+
ASN1_INTEGER_free(serial_asn1);
3198+
}
3199+
if (name != NULL) {
3200+
X509_NAME_free(name);
3201+
}
3202+
if (cert != NULL) {
3203+
X509_free(cert);
3204+
}
3205+
if (cert_buf != NULL) {
3206+
OPENSSL_free(cert_buf);
3207+
}
3208+
3209+
return njs_webcrypto_result(vm, &result, NJS_OK, retval);
3210+
3211+
fail:
3212+
if (serial_bn != NULL) {
3213+
BN_free(serial_bn);
3214+
}
3215+
if (serial_asn1 != NULL) {
3216+
ASN1_INTEGER_free(serial_asn1);
3217+
}
3218+
if (name != NULL) {
3219+
X509_NAME_free(name);
3220+
}
3221+
if (cert != NULL) {
3222+
X509_free(cert);
3223+
}
3224+
if (cert_buf != NULL) {
3225+
OPENSSL_free(cert_buf);
3226+
}
3227+
3228+
return njs_webcrypto_result(vm, NULL, NJS_ERROR, retval);
3229+
}
3230+
3231+
28943232
static BIGNUM *
28953233
njs_import_base64url_bignum(njs_vm_t *vm, njs_opaque_value_t *value)
28963234
{

0 commit comments

Comments
 (0)