Skip to content

Commit 310925a

Browse files
committed
feat(crypto): add ns_crypto::uuid (v4/v7) with C fast‑path and docs
* Introduce C helpers in tclcrypto.c: uuid_format(), uuid_v4(), uuid_v7 (OpenSSL RAND_bytes; RFC 4122/9562). While v4 variant is a faster implementation of what we had before, the v7 variant of UUID is sortable by generation time, which can speed up DB/Web applications. * Added Tcl command "ns_crypto::uuid -version v4|v7" (defaults to v4 for backward compatibility). * Keep old "ns_uuid" command and map it to "ns_crypto::uuid -version v4" * Extended ns_crypto.man with a new section documenting ns_crypto::uuid, -version option, examples, and v4 vs v7 trade‑offs. * Provided stub NsTclCryptoUUIDCmd that reports an error when NaviServer is built without OpenSSL support. Performance (µs/op, 100k iters; lower is better; Tcllib baseline = 1.00x): * Tcllib 1.0.7: 366.1429 (1.00x) * ns Tcl based: 5.5600 (65.85x faster than baseline) * ns C based v4: 0.5554 (659.19x faster) * ns C based v7: 0.5667 (646.14x faster) Rationale: * Provide a fast, built‑in UUID generator with explicit version selection. * v4 preserves privacy (no timestamp leakage); v7 improves temporal locality and insertion performance in databases. * Aligns with RFC 9562 while maintaining compatibility with existing deployments that expect v4.
1 parent 278ede3 commit 310925a

File tree

5 files changed

+346
-28
lines changed

5 files changed

+346
-28
lines changed

doc/src/naviserver/ns_crypto.man

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,55 @@ The option [option -iterations] specifies the number of iterations
531531
[example_end]
532532

533533

534+
[call [cmd "ns_crypto::uuid"] \
535+
[opt [option "-version v4|v7"]]]
536+
537+
Returns a newly generated UUID string in canonical textual form
538+
"8-4-4-4-12" (36 lowercase hexadecimal characters plus 4
539+
hyphens). The result is always NUL-free ASCII suitable for storage in
540+
CHAR(36), VARCHAR(36), or database UUID types.
541+
542+
[para]
543+
544+
If no [option "-version"] is specified, the command returns a Version
545+
4 (UUIDv4) identifier for backward compatibility with the historical
546+
[cmd ns_uuid] command. UUIDv4 values are generated from cryptographic
547+
random bytes and include no timestamp information, providing maximum
548+
entropy and privacy without revealing creation time. (UUIDv4 is
549+
specified in [uri \
550+
https://datatracker.ietf.org/doc/html/rfc4122 "RFC 4122"])
551+
552+
[para]
553+
554+
When [option "-version v7"] is specified, the command returns a
555+
Version 7 (UUIDv7) identifier as defined in [uri \
556+
https://datatracker.ietf.org/doc/html/rfc9562 "RFC 9562"].
557+
558+
UUIDv7 values embed the current time in milliseconds in the
559+
high-order bits and random data in the remaining bits. They are
560+
globally unique and monotonically increasing with creation time,
561+
which improves index locality and insertion performance in databases
562+
and ordered data stores. However, UUIDv7 values encode their creation
563+
time, so they can reveal when an ID was generated.
564+
565+
[example_begin]
566+
% ns_crypto::uuid
567+
3191eb43-60a3-4c9b-a3dc-9b5ef667641b
568+
569+
% ns_crypto::uuid -version v4
570+
75a2d2a1-7d67-4d59-8a2f-8e4f0eba4b36
571+
572+
% ns_crypto::uuid -version v7
573+
018fa74a-9f4c-7d82-9a5f-b59e22f14d35
574+
[example_end]
575+
576+
The built-in historical command [cmd ns_uuid] is equivalent to
577+
[cmd {ns_crypto::uuid -version v4}] and is retained for backward
578+
compatibility with existing applications and persisted identifiers.
579+
580+
534581
[list_end]
582+
535583
[section OPTIONS]
536584
[list_begin options]
537585

nsd/nsd.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,7 @@ NS_EXTERN TCL_OBJCMDPROC_T
13981398
NsTclCryptoPbkdf2hmacObjCmd,
13991399
NsTclCryptoRandomBytesObjCmd,
14001400
NsTclCryptoScryptObjCmd,
1401+
NsTclCryptoUUIDObjCmd,
14011402
NsTclDeleteCookieObjCmd,
14021403
NsTclDriverObjCmd,
14031404
NsTclEncodingForCharsetObjCmd,

nsd/tclcmds.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ static const Cmd basicCmds[] = {
7272
{"ns_crypto::pbkdf2_hmac", NsTclCryptoPbkdf2hmacObjCmd},
7373
{"ns_crypto::randombytes", NsTclCryptoRandomBytesObjCmd},
7474
{"ns_crypto::scrypt", NsTclCryptoScryptObjCmd},
75+
{"ns_crypto::uuid", NsTclCryptoUUIDObjCmd},
7576
{"ns_encodingforcharset", NsTclEncodingForCharsetObjCmd},
7677
{"ns_env", NsTclEnvObjCmd},
7778
{"ns_fastpath_cache_stats", NsTclFastPathCacheStatsObjCmd},

nsd/tclcrypto.c

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ static void ListMDfunc(const EVP_MD *m, const char *from, const char *to, void *
113113
static BIO *PEMOpenReadSteam(const char *fnOrData)
114114
NS_GNUC_NONNULL(1);
115115

116+
static void uuid_format(unsigned char *b, char *dst) NS_GNUC_NONNULL(1,2) NS_GNUC_PURE;
117+
static int uuid_v4(char *dst) NS_GNUC_NONNULL(1);
118+
static int uuid_v7(char *dst) NS_GNUC_NONNULL(1);
119+
116120
static TCL_OBJCMDPROC_T CryptoHmacAddObjCmd;
117121
static TCL_OBJCMDPROC_T CryptoHmacFreeObjCmd;
118122
static TCL_OBJCMDPROC_T CryptoHmacGetObjCmd;
@@ -3420,6 +3424,250 @@ NsTclCryptoRandomBytesObjCmd(ClientData UNUSED(clientData), Tcl_Interp *interp,
34203424
return result;
34213425
}
34223426

3427+
3428+
3429+
/*
3430+
*----------------------------------------------------------------------
3431+
*
3432+
* uuid_format --
3433+
*
3434+
* Convert 16 raw bytes into the canonical RFC 4122 UUID string
3435+
* form (8-4-4-4-12 hex digits, lowercase, with hyphens). The
3436+
* caller must provide a destination buffer of at least 37 bytes
3437+
* to hold the 36-character UUID plus the trailing NUL.
3438+
*
3439+
* Results:
3440+
* None. The formatted UUID string is written into 'dst'.
3441+
*
3442+
*----------------------------------------------------------------------
3443+
*/
3444+
3445+
static inline void
3446+
uuid_format(unsigned char *b, char *dst)
3447+
{
3448+
/*
3449+
* Format into canonical string 8-4-4-4-12
3450+
*/
3451+
Ns_HexString(&b[0], &dst[0], 4, NS_FALSE); /* bytes 0..3 -> dst[0..7] */
3452+
dst[8] = '-';
3453+
3454+
Ns_HexString(&b[4], &dst[9], 2, NS_FALSE); /* bytes 4..5 -> dst[9..12] */
3455+
dst[13] = '-';
3456+
3457+
Ns_HexString(&b[6], &dst[14], 2, NS_FALSE); /* bytes 6..7 -> dst[14..17] */
3458+
dst[18] = '-';
3459+
3460+
Ns_HexString(&b[8], &dst[19], 2, NS_FALSE); /* bytes 8..9 -> dst[19..22] */
3461+
dst[23] = '-';
3462+
3463+
Ns_HexString(&b[10], &dst[24], 6, NS_FALSE); /* bytes 10..15 -> dst[24..35] */
3464+
dst[36] = '\0';
3465+
}
3466+
3467+
/*
3468+
*----------------------------------------------------------------------
3469+
*
3470+
* uuid_v4 --
3471+
*
3472+
* Generate a Version 4 (random) RFC 4122 UUID string. The
3473+
* function fills a 16-byte buffer with cryptographically strong
3474+
* random bytes via RAND_bytes() and Sets the UUID version and
3475+
* variant bits:
3476+
* * byte[6] high nibble -> 0b0100 (version 4)
3477+
* * byte[8] high bits -> 0b10 (RFC 4122 variant)
3478+
*
3479+
* Finally, it formats the result into canonical text using
3480+
* uuid_format().
3481+
*
3482+
* Results:
3483+
* Returns TCL_OK on success (UUID written into 'dst'),
3484+
* or TCL_ERROR if RAND_bytes() failed and no valid UUID was produced.
3485+
*
3486+
* Side effects:
3487+
* None.
3488+
*
3489+
*----------------------------------------------------------------------
3490+
*/
3491+
static inline int
3492+
uuid_v4(char *dst)
3493+
{
3494+
unsigned char b[16];
3495+
3496+
if (RAND_bytes(b, sizeof(b)) != 1) {
3497+
return TCL_ERROR;
3498+
}
3499+
3500+
/*
3501+
* RFC 4122 version 4:
3502+
* - Set the 4 most significant bits of byte 6 (index 6) to 0100 (0x4).
3503+
* - Set the 2 most significant bits of byte 8 (index 8) to 10.
3504+
*
3505+
* Bytes are 0..15.
3506+
*/
3507+
b[6] = (unsigned char)((b[6] & 0x0F) | 0x40); /* version 4 */
3508+
b[8] = (unsigned char)((b[8] & 0x3F) | 0x80); /* variant RFC4122 */
3509+
3510+
uuid_format(b, dst);
3511+
return TCL_OK;
3512+
}
3513+
3514+
3515+
/*
3516+
*----------------------------------------------------------------------
3517+
*
3518+
* uuid_v7 --
3519+
*
3520+
* Summary: Generate a Version 7 (time-ordered) UUID per RFC 9562 and
3521+
* write its canonical textual form ("8-4-4-4-12") into 'dst'.
3522+
*
3523+
* Parameters:
3524+
* dst - Pointer to a character buffer that will receive the textual
3525+
* UUID; must not be NULL and must hold at least 37 bytes
3526+
* (36 printable characters plus terminating NUL).
3527+
*
3528+
* Returns:
3529+
* TCL_OK on success (a valid UUIDv7 string is written to 'dst' and NUL-terminated).
3530+
* TCL_ERROR on failure when RAND_bytes() reports an error; in this case the function
3531+
* does not guarantee a valid UUID in 'dst'.
3532+
*
3533+
* Side effects:
3534+
* None. The function calls Ns_GetTime(), Ns_TimeToMilliseconds(),
3535+
* RAND_bytes(), memcpy(), and uuid_format(); writes into 'dst'.
3536+
*
3537+
*----------------------------------------------------------------------
3538+
*/
3539+
static inline int
3540+
uuid_v7(char *dst)
3541+
{
3542+
unsigned char out[16];
3543+
unsigned char rnd[10];
3544+
time_t ms;
3545+
unsigned long b0, b1, b2;
3546+
Ns_Time now;
3547+
3548+
/*
3549+
* We need 10 random bytes:
3550+
* - parts of them go into the rand_a / rand_b fields.
3551+
*/
3552+
if (RAND_bytes(rnd, sizeof(rnd)) != 1) {
3553+
return TCL_ERROR;
3554+
}
3555+
3556+
/*
3557+
* We'll compose the 16 output bytes:
3558+
*
3559+
* Bytes 0..5 : timestamp ms (big-endian 48-bit)
3560+
* Byte 6 : high nibble = version(0x7), low nibble = top 4 bits of random
3561+
* Byte 7 : next 8 random bits
3562+
* Byte 8 : variant '10' in the top bits, then next 6 random bits
3563+
* Byte 9..15 : remaining random bytes
3564+
*/
3565+
3566+
Ns_GetTime(&now);
3567+
ms = Ns_TimeToMilliseconds(&now);
3568+
3569+
/* timestamp big-endian into out[0..5] */
3570+
out[0] = (unsigned char)((ms >> 40) & 0xFF);
3571+
out[1] = (unsigned char)((ms >> 32) & 0xFF);
3572+
out[2] = (unsigned char)((ms >> 24) & 0xFF);
3573+
out[3] = (unsigned char)((ms >> 16) & 0xFF);
3574+
out[4] = (unsigned char)((ms >> 8) & 0xFF);
3575+
out[5] = (unsigned char)( ms & 0xFF);
3576+
3577+
/*
3578+
* Pull first 3 random bytes for the structured fields.
3579+
*/
3580+
b0 = rnd[0];
3581+
b1 = rnd[1];
3582+
b2 = rnd[2];
3583+
3584+
/*
3585+
* Byte 6:
3586+
* high nibble = version (0111 -> 0x7)
3587+
* low nibble = low 4 bits of b0
3588+
*/
3589+
out[6] = (unsigned char)(0x70 | (b0 & 0x0F));
3590+
3591+
/*
3592+
* Byte 7:
3593+
* full 8 bits from b1
3594+
*/
3595+
out[7] = (unsigned char)b1;
3596+
3597+
/*
3598+
* Byte 8:
3599+
* variant '10' in the two MSBs -> 0b10xxxxxx
3600+
* keep lower 6 bits from b2
3601+
*
3602+
* mask lower 6 bits: b2 & 0x3F
3603+
* set top bits to 10: | 0x80
3604+
*/
3605+
out[8] = (unsigned char)((b2 & 0x3F) | 0x80);
3606+
3607+
/*
3608+
* The rest (bytes 9..15) are just rnd[3]..rnd[9]
3609+
*/
3610+
memcpy(&out[9], &rnd[3], 7);
3611+
uuid_format(out, dst);
3612+
3613+
return TCL_OK;
3614+
}
3615+
3616+
/*
3617+
*----------------------------------------------------------------------
3618+
*
3619+
* NsTclCryptoUUIDCmd --
3620+
*
3621+
* Implements "ns_crypto::uuid". Returns UUID in version v4
3622+
*
3623+
* Example: ns_crypto::randombytes 20
3624+
*
3625+
* Results:
3626+
* Tcl Result Code.
3627+
*
3628+
* Side effects:
3629+
* None
3630+
*
3631+
*----------------------------------------------------------------------
3632+
*/
3633+
int
3634+
NsTclCryptoUUIDObjCmd(ClientData UNUSED(clientData), Tcl_Interp *interp, TCL_SIZE_T objc, Tcl_Obj *const* objv)
3635+
{
3636+
int result, versionInt = 4;
3637+
static Ns_ObjvTable uuidVersions[] = {
3638+
{"v4", 4u},
3639+
{"v7", 7u},
3640+
{NULL, 0u}
3641+
};
3642+
Ns_ObjvSpec lopts[] = {
3643+
{"-version", Ns_ObjvIndex, &versionInt, uuidVersions},
3644+
{NULL, NULL, NULL, NULL}
3645+
};
3646+
3647+
if (Ns_ParseObjv(lopts, NULL, interp, 1, objc, objv) != NS_OK) {
3648+
result = TCL_ERROR;
3649+
3650+
} else {
3651+
Tcl_DString ds;
3652+
3653+
Tcl_DStringInit(&ds);
3654+
Tcl_DStringSetLength(&ds, (TCL_SIZE_T)36);
3655+
3656+
if (versionInt == 4) {
3657+
result = uuid_v4(ds.string);
3658+
} else {
3659+
result = uuid_v7(ds.string);
3660+
}
3661+
if (result == TCL_OK) {
3662+
Tcl_DStringResult(interp, &ds);
3663+
} else {
3664+
Ns_TclPrintfResult(interp, "UUID conversion failed");
3665+
}
3666+
}
3667+
3668+
return result;
3669+
}
3670+
34233671
# ifdef OPENSSL_NO_EC
34243672
int
34253673
NsTclCryptoEckeyObjCmd(ClientData UNUSED(clientData), Tcl_Interp *interp, TCL_SIZE_T UNUSED(objc), Tcl_Obj *const* UNUSED(objv))
@@ -3468,6 +3716,13 @@ NsTclCryptoRandomBytesObjCmd(ClientData UNUSED(clientData), Tcl_Interp *interp,
34683716
return TCL_ERROR;
34693717
}
34703718

3719+
int
3720+
NsTclCryptoUUIDCmd(ClientData UNUSED(clientData), Tcl_Interp *interp, TCL_SIZE_T UNUSED(objc), Tcl_Obj *const* UNUSED(objv))
3721+
{
3722+
Ns_TclPrintfResult(interp, "Command requires support for OpenSSL built into NaviServer");
3723+
return TCL_ERROR;
3724+
}
3725+
34713726
int
34723727
NsTclCryptoEckeyObjCmd(ClientData UNUSED(clientData), Tcl_Interp *interp, TCL_SIZE_T UNUSED(objc), Tcl_Obj *const* UNUSED(objv))
34733728
{

0 commit comments

Comments
 (0)