JSON WEB Signature (JWS)

HaO 2025-11-07 JSON WEB Signature came to me with MQTT in fabric automation. Description of JWS: [L1 ] A companion is "JSON WEB Encryption "JWE": [L2 ]

I have implemented two schemas, following example A.1 and A.2 of the JWS document. I am on MS-Windows and I use TWAPI.

JWS Using HMAC SHA-256

Example A.1 of [L3 ] is aimed. The same result is acheved for me, if the sample data is used.

BASE64URL

First, we need the BASE64URL function, to have the binary data encoded and decoded.

proc BASE64URL {ModeIn DataIn} {
    switch -exact -- $ModeIn {
        encode {
            set DataIn [binary encode base64 $DataIn]
            set DataIn [string trimright $DataIn =]
            set DataIn [string map {+ - / _} $DataIn]
        }
        decode {
            set DataIn [string map {- + _ / %3d = %3D =} $DataIn]
            set DataIn [binary decode base64 $DataIn]
        }
    }
    return $DataIn
}

Example data

Prepare the example data from the encoded data to be sure to not have additional cr/lf/whitespace

package require json
set headerEnc "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9"
set payloadEnc "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ"
set JWS [string cat $headerEnc "." $payloadEnc]
set header [BASE64URL decode $headerEnc]
set dHeader [::json::json2dict $header]
if { ![dict exists $dHeader alg] } {
    return -code error "Key alg missing"
} elseif { [string toupper [dict get $dHeader alg]] ne "HS256" } {
    return -code error "Wrong algorithm"
}

Key

Now check the key:

package require sha256

set key {{"kty":"oct",
      "k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75
           aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
     }}
set dKey [::json::json2dict $key]
if { ![dict exists $dKey kty] } {
    return -core error "Key type missing"
} elseif { [string toupper [dict get $dKey kty]] ne "OCT" } {
    return -core error "Wrong key type"
} elseif { ! [dict exists $dKey k] } {
    return -core error "Key value missing"
}
set keyCur [BASE64URL decode [dict get $dKey k]]

Signature

Now add the signature:

set signature [BASE64URL encode\
        [::sha2::hmac -bin $keyCur $JWS]]
append JWS "." $signature

RSASSA-PKCS1-v1_5 SHA-256

Example A.2 of [L4 ] uses RSA signing and SHA-256 hash.

This example specially features the transformation of a JSON WEB Key (JWK) [L5 ] into a MS-Win32 API Key blob to be used with TWAPI for an RSA key. See also this feature request for twapi inclusion of the key format transformation: [L6 ]

The upper example is continued, as some data fields like the payload are identical

Example data

Only the header changed, payload is the same. In addition, no whitespace, so use plain text for the header.

package require json
set header {{"alg":"RS256"}}
set headerEnc [BASE64URL encode $header]
set JWS [string cat $headerEnc "." $payloadEnc]
set dHeader [::json::json2dict $header]
if { ![dict exists $dHeader alg] } {
    return -code error "Key alg missing"
} elseif { [string toupper [dict get $dHeader alg]] ne "RS256" } {
    return -code error "Wrong algorithm"
}

Key

TWAPI is used. OpenSSL may perhaps be easier, as there are more key conversion capabilities. I use TWAPI to use the Windows internal encryption to use eventual security updates. The necessary key format transformation below is probably quite unstable to small changes.

package require twapi
set key {{"kty":"RSA",
      "n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddx
           HmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMs
           D1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSH
           SXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdV
           MTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8
           NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ",
      "e":"AQAB",
      "d":"Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97I
           jlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0
           BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn
           439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYT
           CBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLh
           BOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ",
      "p":"4BzEEOtIpmVdVEZNCqS7baC4crd0pqnRH_5IB3jw3bcxGn6QLvnEtfdUdi
           YrqBdss1l58BQ3KhooKeQTa9AB0Hw_Py5PJdTJNPY8cQn7ouZ2KKDcmnPG
           BY5t7yLc1QlQ5xHdwW1VhvKn-nXqhJTBgIPgtldC-KDV5z-y2XDwGUc",
      "q":"uQPEfgmVtjL0Uyyx88GZFF1fOunH3-7cepKmtH4pxhtCoHqpWmT8YAmZxa
           ewHgHAjLYsp1ZSe7zFYHj7C6ul7TjeLQeZD_YwD66t62wDmpe_HlB-TnBA
           -njbglfIsRLtXlnDzQkv5dTltRJ11BKBBypeeF6689rjcJIDEz9RWdc",
      "dp":"BwKfV3Akq5_MFZDFZCnW-wzl-CCo83WoZvnLQwCTeDv8uzluRSnm71I3Q
           CLdhrqE2e9YkxvuxdBfpT_PI7Yz-FOKnu1R6HsJeDCjn12Sk3vmAktV2zb
           34MCdy7cpdTh_YVr7tss2u6vneTwrA86rZtu5Mbr1C1XsmvkxHQAdYo0",
      "dq":"h_96-mK1R_7glhsum81dZxjTnYynPbZpHziZjeeHcXYsXaaMwkOlODsWa
           7I9xXDoRwbKgB719rrmI2oKr6N3Do9U0ajaHF-NKJnwgjMd2w9cjz3_-ky
           NlxAr2v4IKhGNpmM5iIgOS1VZnOZ68m6_pbLBSp3nssTdlqvd0tIiTHU",
      "qi":"IYd7DHOhrWvxkwPQsRM2tOgrjbcrfvtQJipd-DlcxyVuuM9sQLdgjVk2o
           y26F0EmpScGLq2MowX7fhd_QJQ3ydy5cY7YIBi87w93IKLEdfnbJtoOPLU
           W0ITrJReOgo1cq9SbsxYawBgfp_gh6A5603k2-ZQwVK0JKSHuLFkuQ3U"
     }}
set dKey [::json::json2dict $key]

set dKeyBitLenDiv {n 8 p 16 q 16 dp 16 dq 16 qi 16 d 8}
if { ![dict exists $dKey kty] } {
    return -core error "Key type missing"
} elseif { [string toupper [dict get $dKey kty]] ne "RSA" } {
    return -core error "Wrong key type"
}
set dDec {}
foreach index [concat {e} [dict keys $dKeyBitLenDiv]] {
    if {![dict exists $dKey $index]} {
        return -code error "Key parameter '$index' missing"
    }
    dict set dDec $Index [BASE64URL decode [dict get $dKey $index]]
}
# Find key bit length
set bitLen [expr {[string length [dict get $dDec n]] * 8 }]
# The value may be shortended, if 0's are missing.
# I decided to fill to 128 bit pieces. This is an arbitrary value.
# ceil to next 128 to avoid any trincated key (0's not represented)
set remainder128 [expr {$BitLen % 128}]
if {$remainder128 != 0} { incr BitLen [expr {128 - $remainder128}] }

Now, we transform the key to MS-Win32 API key blob: [L7 ]

The header of the key blob is constant: [L8 ]

  • bType = 7: public/private key pair
  • bVersion = 2
  • Reserved: 0
  • aiKeyAlg: unsigned int: CALG_RSA_SIGN 0x00002400

As we generate a binary data structure, we only use binary commands.

set lKeyHeader {7 2 0 9216}
set KeyData [binary format ccsiu {*}$lKeyHeader]

Note: the lKeyHeader is identical to the twapi keyblob header. Due to that, we store it in a variable.

Now the RSA header: [L9 ]

# Magic: "RSA2" ("1" would be a public key)
append KeyData [binary format a4 "RSA2"]
# DWORD: bitlen
# DWORD: pubexp (e)
append KeyData [binary format iua4 $BitLen [string reverse [dict get $dDec e]]]

Note: The value "e" (public exponent) is in the example is unfortune to test the implementation. The given value is "AQAB". This are 3 bytes with values 1,0,1. The field is 4 bytes long. The input is big endian and Windows/Intel wants little Endian. In big endian, you fill with 0 on the left, little endian on the right. The current value of e is symetric on endianess and may not allow to detect endianness errors.

Now, we append the key parameters. Here is the mapping:

BYTE modulus[bitLen/8]; <-> n
BYTE prime1[bitLen/16]; <-> p
BYTE prime2[bitLen/16]; <-> q
BYTE exponent1[bitLen/16]; <-> dp
BYTE exponent2[bitLen/16]; <-> dq
BYTE coefficient[bitLen/16]; <-> qi
BYTE privateExponent[bitLen/8]; <-> d

Here is the code:

dict for {index bitDiv} $dKeyBitLenDiv {
    # Get the byte size
    set byteSize [expr {$bitLen / $bitDiv}]
    # Check correct input size
    # If smaller, it is filled with 0
    if {[string length [dict get $dDec $index]] > $byteSize} {
        return -code error "Key parameter '$index' with wrong length"
    }
    # Append to data (little endian)
    set dataCur [dict get $dDec $index]
    set dataCur [string reverse $dataCur]
    # This fills with 0 on the right. This is ok for little endian.
    append keyData [binary format "a$byteSize" $dataCur]
}
# To form a TWAPI KEYBLOB structure, this is appended as list element to the key header:
append lKeyHeader $keyData

Sign

Now, the data in *keyData* is the MS-Win32 API format. We can start signing.

We need a csp type of "prov_rsa_aes". The default does not support sha_256 with signing algorithms.

set hCrypt [twapi::crypt_acquire -create 1 -csptype prov_rsa_aes]
set hKey [twapi::crypt_import_key $hCrypt $lKeyHeader]
set hHash [twapi::capi_hash_create $hCrypt sha_256]
twapi::capi_hash_string $hHash $JWS utf-8
set sig [twapi::capi_hash_sign $hHash signature -nohashoid 0]
twapi::capi_hash_free $hHash
twapi::capi_key_free $hKey
twapi::crypt_free $hCrypt
# Big endian
set sig [string reverse $sig]
set sigEnc [toolBase64URL encode $sig]
append JWS "." $sigEnc

This is a sketch.

Verify

On verification, only the public key is required, thus only object "e" and "n" of the json object of the private key.

Thus, the key is:

set key {{"kty":"RSA",
      "n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddx
           HmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMs
           D1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSH
           SXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdV
           MTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8
           NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ",
      "e":"AQAB"
     }}
set dKey [::json::json2dict $key]

if { ![dict exists $dKey kty] } {
    return -core error "Key type missing"
} elseif { [string toupper [dict get $dKey kty]] ne "RSA" } {
    return -core error "Wrong key type"
}
set dDec {}
foreach index {e n} {
    if {![dict exists $dKey $index]} {
        return -code error "Key parameter '$index' missing"
    }
    dict set dDec $Index [BASE64URL decode [dict get $dKey $index]]
}
# Find key bit length
set bitLen [expr {[string length [dict get $dDec n]] * 8 }]
# The value may be shortended, if 0's are missing.
# I decided to fill to 128 bit pieces. This is an arbitrary value.
# ceil to next 128 to avoid any trincated key (0's not represented)
set remainder128 [expr {$BitLen % 128}]
if {$remainder128 != 0} { incr BitLen [expr {128 - $remainder128}] }

The header of the key blob is:

  • bType = 6: public key
  • bVersion = 2
  • Reserved: 0
  • aiKeyAlg: unsigned int: CALG_RSA_SIGN 0x00002400
set lKeyHeader {6 2 0 9216}
set KeyData [binary format ccsiu {*}$lKeyHeader]

Now the RSA header:

# Magic: "RSA1"
append KeyData [binary format a4 "RSA1"]
# DWORD: bitlen
# DWORD: pubexp (e)
append KeyData [binary format iua4 $BitLen [string reverse [dict get $dDec e]]]

Only the n parameter is appended:

set index n
# Get the byte size
set byteSize [expr {$bitLen / 8
# Check correct input size
# If smaller, it is filled with 0
if {[string length [dict get $dDec $index]] > $byteSize} {
    return -code error "Key parameter '$index' with wrong length"
}
# Append to data (little endian)
set dataCur [dict get $dDec $index]
set dataCur [string reverse $dataCur]
# This fills with 0 on the right. This is ok for little endian.
append keyData [binary format "a$byteSize" $dataCur]

# To form a TWAPI KEYBLOB structure, this is appended as list element to the key header:
append lKeyHeader $keyData

And now call verification

# Prepare little endian input signature
set sig [BASE64URL decode $sig]
set sig [string reverse $sig]

set hCrypt [twapi::crypt_acquire -create 1 -csptype prov_rsa_aes]
set hKey [twapi::crypt_import_key $hCrypt $lKeyHeader]
set hHash [twapi::capi_hash_create $hCrypt sha_256]
twapi::capi_hash_string $hHash $JWS utf-8
set fVerified [twapi::capi_hash_verify $hHash $sig $hKey -nohashoid 0]
twapi::capi_hash_free $hHash
twapi::capi_key_free $hKey
twapi::crypt_free $hCrypt

OpenSSL

I also tried a bit with OpenSSL, but not much. In OpenSSL, the parameters may be used as bignums. So, here is that transformation:

proc bin_d {d} {
    binary scan $d  cu* le
    set n 0
    set m 1
    foreach b [lreverse $le] {
        set n [expr {$n+$m*$b}]
        set m [expr {$m * 256}]
    }
    return $n
}
bin_d [dict get $dDec d]
2358310989939619510179986262349936882924652023566213765118606431955566700506538911356936879137503597382515919515633242482643314423192704128296593672966061810149316320617894021822784026407461403384065351821972350784300967610143459484324068427674639688405917977442472804943075439192026107319532117557545079086537982987982522396626690057355718157403493216553255260857777965627529169195827622139772389760130571754834678679842181142252489617665030109445573978012707793010592737640499220015083392425914877847840457278246402760955883376999951199827706285383471150643561410605789710883438795588594095047409018233862167884701


gold 01/30/2026. Added categories, so can find message in Wiki.