DSig Part 2: JSON Web Signature (JWS)

This post was originally published as “DSig Part 2: JSON Web Signature (JWS)” on the Levvel Blog.

This is the second in a three post series about digital signatures. The first post explored the use of the XML Digital Signature specification. This post will explore the JSON Web Signature specification (JWS); JWS defines a standard way to apply digital signatures to JSON data structures. This is analogous to how the XML Digital Signature spec defines how to apply digital signatures to XML documents. The last post will compare these two specifications.

How Does It Work?

The JSON Web Signature specification provides a standard, general-purpose mechanism for generating and representing a digital signature on a JSON data structure. From the spec, “JWS represents digitally signed or MACed content using JSON data structures and base64URL encoding”. JSON (Javascript Object Notation) was introduced as part of the JavaScript programming language for representing objects and it got popular on the API scene. With popularity comes maturity. Part of the maturing process is the establishment of industry specs around things like digital signatures. The industry saw the same thing happen with XML fifteen years ago.

Signature Generation

The first step in generating a digital signature is to create or obtain the JSON data structure that must be signed. This must be valid JSON per the JSON spec.

Next, the JSON data structure is converted to a string (UTF8 character encoding). Whatever language and JSON library you are working with will provide a mechanism for doing this correctly (yes, I am waving my hands at this detail).

Then, the JWS Payload UTF8 string representation is converted to a Base64-URL encoding. This base64-URL encoding is very similar to normal Base64-encoding, but there are a couple of differences that make it safe to use with URLs. The details are laid out here.

Now, create the JOSE Header that contains the set of parameters that describe the cryptographic operations employed. This can be comprised of a JWS Protected Header (protected by the digital signature) and a JWS Unprotected Header (not protected by a JWS digital signature). In the JWS Compact Serialization, the entire JOSE Header must be a JWS Protected Header (meaning the whole header is digitally signed). The header parameters describe the JWS digital signature or MAC on the header and payload. The parameter names can be Registered Header Parameter (defined in the JWS spec) names, Public Header Parameter (registered with IANA registry, but not defined by the JWS spec) names, or Private Header parameter (agreed to by the JWS producer and consumer) names. The Registered Header Parameter can be found in the spec. The only one that is required is the “alg” parameter that describes what signing or MAC algorithm was used.

The parameter names can be Registered Header Parameter (defined in the JWS spec) names, Public Header Parameter (registered with IANA registry, but not defined by the JWS spec) names, or Private Header parameter (agreed to by the JWS producer and consumer) names.  The Registered Header Parameter names are:

Parameter Name Description Required Must be Understood
alg A string that describes the cryptographic algorithm that was used to secure the JWS.  Valid values are defined in the JWA (JSON Web Algorithms) spec.  Yes  Yes
jku A string that contains the JWK (JSON Web Key) set URL.  This is a location that contains information about a set of keys that were used for digital signatures in the JWK format.  No  No
jwk  A JSON data structure (defined in the JWK spec) that describes a public key corresponding to the private key used to generate the JWS.  No  No
kid  A string that contains a “hint” about which key was used to generate the JWS.  It’s form is unspecified.  No  No
x5u  A string that contains a URL pointing at an X509 representation (think PEM format) of the public certificate corresponding to the private key used to generate the JWS.  No  No
x5c  An array of strings containing the public certificate corresponding to the private key used to generate the JWS and its trust chain. No  No
x5t  A string containing the SHA1 thumb print of the public certificate corresponding to the private key used to generate the JWS.  No  No
x5t#256  A string containing the SHA256 thumb print of the public certificate corresponding to the private key used to generate the JWS.  No  No
typ  A string containing the Media Type or MIME type of the complete JWS.  No  No
cty  A string containing the Media Type or MIME type of the JWS Payload.  No  No
crit  An array of strings containing the names of other Header Parameter names that are extensions to the JWS or JWA specs that must be understood.  No  Yes

More information about these headers can be found in the JWS specification.

You will notice that there are several different ways to represent or reference the signature key in the Header.  Pick one that works for you or use what your Identity Provider supports.

Once the JWS Protected Header has been generated, it must be converted to UTF8 and Base64-URL encoded the same way that the Payload was above.

Next, the digital signature (using the algorithm corresponding to the value in the JWS Protected Header “alg” parameter) of the following structure must be computed:

ASCII(BASE64URL(UTF8(JWS Protected Header)) || ‘.’ || BASE64URL(JWS Payload))

I borrowed this representation from the JWS spec. The ASCII() function listed here means that the resulting string should be using ASCII character encoding. Most base64-encoding libraries (or Base64-URL encoding libraries) would generate this by default. The rest of it is simply a concatenation of the values we computed above separated by a ‘.’. The details of what this looks depends on the algorithm being used. It will involve either an X509 Private/Public key pair (asymmetric key) or a shared key (symmetric key). For the RS256 or RSA-SHA256 signing algorithm, this corresponds to the well-known RSASSA-PKCS1-v1_5 using SHA-256 algorithm. If you checked out the JWA spec earlier, you may have noticed that the RS256 algorithm is only recommended. The only signing algorithms that JWS implementations must support is HS256 (HMAC using SHA256). Any commercially viable implementation would support more — likely most of the ones in the JWA spec list.

Next, compute the Base64-URL encoding of the JWS-Signature. The details are the same as for the JWS Header and Payload described earlier.

To generate the JWS Compact Serialization of JSON Web Signature assemble the pieces as:

BASE64URL(UTF8(JWS Protected Header)) || ‘.’ || BASE64URL(JWS Payload) || ‘.’ || BASE64URL(JWS Signature)

Signature Validation

The process of validating the JWS is described here. Read the spec if you need the details. It is similar to other signature validation steps (like what I described in DSig Part 1).

JSON Web Signature: An Example

Let’s take a take a basic JSON data structure:

{ ‘a’:’b’,
‘c’:’d’,
‘e’: 1.0
}

Let’s assume that we want to use the RSA-256 algorithm to generate a digital signature on this JSON structure. So, we will have a minimum JWS Protected Header of:

{ alg: ‘RS256’ }

For anyone familiar with JSON Web Tokens (JWTs), this will look very familiar. The header.payload.signature structure of a JWT token is defined by the JWS spec.

Now, we need an X509 Private/Public Key Pair to generate the digital signature. I created these artifacts following the instructions from a ThinkMiddleware.com blog post I wrote a while ago. This gives us the following public certificate:

-----BEGIN CERTIFICATE-----
     MIIDpDCCAoygAwIBAgIBATANBgkqhkiG9w0BAQsFADBgMQswCQYDVQQGEwJVUzEL
    MAkGA1UECAwCV0ExEjAQBgNVBAcMCVZhbmNvdXZlcjEPMA0GA1UECgwGTGV2dmVs
    MQ0wCwYDVQQLDARCbG9nMRAwDgYDVQQDDAdEU2lnLUNBMB4XDTE2MDMyODE5MTc0
    MloXDTE3MDMyODE5MTc0MlowTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMQ8w
    DQYDVQQKDAZMZXZ2ZWwxDTALBgNVBAsMBEJsb2cxEjAQBgNVBAMMCURTaWctQ2Vy
    dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMeXtfr5CMQav+1sKeZ3
    N7GgsvAhZLNSMu4MN/1tQ5ehumtOYrqlnOND5lP4aPYiWE0/egf2xjIAJO68ql3/
    DaL6893uM+XxR9097yEN7I7O0Xq1AQCS31jpq5Zi9cCAavvbKJZjxk7+JIuaREdn
    1nzCJymLVzahJ6miS4+Q1A6jCYmFf8+MTdmaRha4faRo9iinQs8ovr8kxzzsW3NZ
    xt/R37g2iK4XzQZ9RuQxZ4zwE43nwGQ46MdDaWvIlg/swAgN07gwCgUdlA9lcjgB
    4Sg8K7ZWNgD2BqzBIoC9oEdq0Xo5s93GPAKbK9Xv5D5bCHoIBS4PsUIdfPBQjcO4
    0DECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBH
    ZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFAclkkcBT1nwsYeJoAprqSSa
    gaejMB8GA1UdIwQYMBaAFDCL/Me6ZSbqOiEQSfRYpgGcPNXVMA0GCSqGSIb3DQEB
    CwUAA4IBAQC3+sVnFFUc3rGED2661Yf4DEaqV0+3T/MahQ4FL64M66UfA6cSrYHB
    0zehgoAOytxomurVBxj3Z6+cNN41zPGRB1xrOH/psEqZTiLgiG9hS9zGWxZDeB/G
    E/rvoY75DnKtzHGebDlGPoHV4Nzg6DOMRbkLwpdO9HRXT60tBrrX3WsigZRmfiaG
    V/NtKtvKSNNDhh3/oUoRwOH1ksbkJ4W41s7yfTNL5L8TqyvzyXsVCyaHKMd0hsE2
    8OXfmOCXB5YYLjZFs+VXmvshj9hZ7K3BrAy4p5x8+4fWrElyPmWhAp4dOsMNyH7j
    Uy+5Jn1Qst4rGHrb4igpiX6GrEeV8RGv
    -----END CERTIFICATE-----

And, Private Key:

-----BEGIN RSA PRIVATE KEY-----
    MIIEpAIBAAKCAQEAx5e1+vkIxBq/7Wwp5nc3saCy8CFks1Iy7gw3/W1Dl6G6a05i
    uqWc40PmU/ho9iJYTT96B/bGMgAk7ryqXf8Novrz3e4z5fFH3T3vIQ3sjs7RerUB
    AJLfWOmrlmL1wIBq+9solmPGTv4ki5pER2fWfMInKYtXNqEnqaJLj5DUDqMJiYV/
    z4xN2ZpGFrh9pGj2KKdCzyi+vyTHPOxbc1nG39HfuDaIrhfNBn1G5DFnjPATjefA
    ZDjox0Npa8iWD+zACA3TuDAKBR2UD2VyOAHhKDwrtlY2APYGrMEigL2gR2rRejmz
    3cY8Apsr1e/kPlsIeggFLg+xQh188FCNw7jQMQIDAQABAoIBAElSxpvomPvSB+gk
    8E+mRGOQ8aud2Oy3qdYhzv/fABHYbC+3oDWJWdVzwE3g2E5z15SpWR9L3QvJWcgK
    r1vQsyXIo4ZIV/CFby0r74lpIBpmiDZYAIJBcqOrVEnvGPEbPDJCFOsKxHOIkcxn
    Y+YHk5hJizGL6wI6ueNrp+6Z+g1V2IsO+e4c1EjXf3hGNgqEOS7lBTe9rnjrVg+O
    sD3omq++ukyAtPHGdoRBIKV64oyXMJvKewl7rLiCjiqJSTSuWl4S1MiFTqmyVH4S
    GJKSzwp+qT57iAi3ScXfLpXAmnXBj+mTk72S8adxaIiB8rROGILquC7CZLC/668/
    htAxOAECgYEA/qvhfMLG2EoYt5Dzvo294Mt+QW6e7nzSfQhBgmEWUthQfO6n1tqc
    49zRQa2Mr0WFYHWleY1DNEFql0eZbLkSdiqMshO/F4CZlMNNdi2nUuMMhYp7f1RP
    yUskjUQsfgtYS/1B97/8HO4wT+ODv73ATL+b89scaeuoX3/zcAY2mqECgYEAyKJF
    aQeuQPkoaAiUPECfj3uMD6jlndS9ZXAefpl2sqzazxT6KFYaJb3xNdehyOogK7nu
    waFBbmK5JgubR9BUJxyxtxBoXVN3NuHdIntz7h+cvxU4FkifEWQRf57TDljdw5gQ
    5lagUhe+sKEISS76H7XLWVCgTftY9EbcZHlUW5ECgYEA7WIJrOFht/pZT8ItcVFt
    zDviU9tpCaQQD7vCVGVrCY3YeJt8lyjvVPZfl3jNyhJjYKZIuCSUoADZ+mki+dUJ
    AFxpCRSe6qlUfvYNHjsv3HMHGPMcanOMa3U/fO4vCs5IX4ySEP1DYzQilFTeWbvl
    o6R3GbcDpTekmmAJ6kHgZAECgYBEP1anDJLMzTRedxSdjkn4j8ksBoLc9mHaoJHu
    9Jr42m2v5W3WzLsmPX9QlmIVKvb4iM3bldPhDbn3ZBlVC1uk1lDpea5WmK7Dv37u
    XNoplR1HrpsekWNykfyV0dvvVHFTOGX/RFZStnsKMCzWYCF7KebbId71x9sRdQ1B
    LDhy0QKBgQC/YlztESpAMhMLpM9vLQWjc7anrDKgugJXu3W8n+s+DyY2AUZ5PkpW
    KvgHhY3qTl/V1Ivfu8PZLpJnE3B1xzbR+FzWwbXVhxTOn/RU4d4bhg6tBfoV0juw
    qTf0984Scy43UjT2+LRbpk0Koi1gO6V1Ieg3+g8UOgb7L7TX2bNuXA==
    -----END RSA PRIVATE KEY-----

The details of this X509 Public Certificate are:

 broeckel@host jws]$ openssl x509 -in dsig-cert.pem -text
    Certificate:
    Data:
    Version: 3 (0x2)
    Serial Number: 1 (0x1)
    Signature Algorithm: sha256WithRSAEncryption
    Issuer: C=US, ST=WA, L=Vancouver, O=Levvel, OU=Blog, CN=DSig-CA
    Validity
    Not Before: Mar 28 19:17:42 2016 GMT
    Not After : Mar 28 19:17:42 2017 GMT
    Subject: C=US, ST=WA, O=Levvel, OU=Blog, CN=DSig-Cert
    Subject Public Key Info:
    Public Key Algorithm: rsaEncryption
    Public-Key: (2048 bit)
    Modulus:
    00:c7:97:b5:fa:f9:08:c4:1a:bf:ed:6c:29:e6:77:
    37:b1:a0:b2:f0:21:64:b3:52:32:ee:0c:37:fd:6d:
    43:97:a1:ba:6b:4e:62:ba:a5:9c:e3:43:e6:53:f8:
    68:f6:22:58:4d:3f:7a:07:f6:c6:32:00:24:ee:bc:
    aa:5d:ff:0d:a2:fa:f3:dd:ee:33:e5:f1:47:dd:3d:
    ef:21:0d:ec:8e:ce:d1:7a:b5:01:00:92:df:58:e9:
    ab:96:62:f5:c0:80:6a:fb:db:28:96:63:c6:4e:fe:
    24:8b:9a:44:47:67:d6:7c:c2:27:29:8b:57:36:a1:
    27:a9:a2:4b:8f:90:d4:0e:a3:09:89:85:7f:cf:8c:
    4d:d9:9a:46:16:b8:7d:a4:68:f6:28:a7:42:cf:28:
    be:bf:24:c7:3c:ec:5b:73:59:c6:df:d1:df:b8:36:
    88:ae:17:cd:06:7d:46:e4:31:67:8c:f0:13:8d:e7:
    c0:64:38:e8:c7:43:69:6b:c8:96:0f:ec:c0:08:0d:
    d3:b8:30:0a:05:1d:94:0f:65:72:38:01:e1:28:3c:
    2b:b6:56:36:00:f6:06:ac:c1:22:80:bd:a0:47:6a:
    d1:7a:39:b3:dd:c6:3c:02:9b:2b:d5:ef:e4:3e:5b:
    08:7a:08:05:2e:0f:b1:42:1d:7c:f0:50:8d:c3:b8:
    d0:31
    Exponent: 65537 (0x10001)
    X509v3 extensions:
    X509v3 Basic Constraints:
    CA:FALSE
    Netscape Comment:
    OpenSSL Generated Certificate
    X509v3 Subject Key Identifier:
    07:25:92:47:01:4F:59:F0:B1:87:89:A0:0A:6B:A9:24:9A:81:A7:A3
    X509v3 Authority Key Identifier:
    keyid:30:8B:FC:C7:BA:65:26:EA:3A:21:10:49:F4:58:A6:01:9C:3C:D5:D5
    Signature Algorithm: sha256WithRSAEncryption
    b7:fa:c5:67:14:55:1c:de:b1:84:0f:6e:ba:d5:87:f8:0c:46:
    aa:57:4f:b7:4f:f3:1a:85:0e:05:2f:ae:0c:eb:a5:1f:03:a7:
    12:ad:81:c1:d3:37:a1:82:80:0e:ca:dc:68:9a:ea:d5:07:18:
    f7:67:af:9c:34:de:35:cc:f1:91:07:5c:6b:38:7f:e9:b0:4a:
    99:4e:22:e0:88:6f:61:4b:dc:c6:5b:16:43:78:1f:c6:13:fa:
    ef:a1:8e:f9:0e:72:ad:cc:71:9e:6c:39:46:3e:81:d5:e0:dc:
    e0:e8:33:8c:45:b9:0b:c2:97:4e:f4:74:57:4f:ad:2d:06:ba:
    d7:dd:6b:22:81:94:66:7e:26:86:57:f3:6d:2a:db:ca:48:d3:
    43:86:1d:ff:a1:4a:11:c0:e1:f5:92:c6:e4:27:85:b8:d6:ce:
    f2:7d:33:4b:e4:bf:13:ab:2b:f3:c9:7b:15:0b:26:87:28:c7:
    74:86:c1:36:f0:e5:df:98:e0:97:07:96:18:2e:36:45:b3:e5:
    57:9a:fb:21:8f:d8:59:ec:ad:c1:ac:0c:b8:a7:9c:7c:fb:87:
    d6:ac:49:72:3e:65:a1:02:9e:1d:3a:c3:0d:c8:7e:e3:53:2f:
    b9:26:7d:50:b2:de:2b:18:7a:db:e2:28:29:89:7e:86:ac:47:
    95:f1:11:af

The Base64-URL encoded representation of the Secure Header (converted to UTF8) is:

 

eyJhbGciOiJSUzI1NiJ9

 

Since ASCII and UTF8 are compatible with one another for regular characters, the UTF8 representation of the header looks the same as in the ASCII representation.

The Base64-URL encoded representation of the Payload (converted to UTF8) is:

 

eyJhIjoiYiIsImMiOiJkIiwiZSI6MX0

 

The same commentary about UTF8 encoding applies here as well. You can verify both of these encoded values look like the original by doing the following:

  • replace all instances of ‘-’ with ‘+’
  • replace all instances of ‘_’ with ‘/’
  • add some number of equal signs for padding at the end (L(s) % 4 == 0, add 0 ‘=’; L(s)%4 == 1, bad input; L(s)%4 == 2, add 1 ‘=’; L(s)%4==2, add 2 ‘=’)
  • go here
  • copy and paste the string into the field.
  • click

The original JSON structure in string format will fall out.

The signature that is generated from the combined “payload.header” structure is:

CNMaYaDGU3ZhFV1ve6p3sAdYXhEklej8DVIAMqIWCkpNmT6Jp7iigcndXwH5q3WQFHiswgIQU5-_-4rV3jKGptCROmEyWPW8_elhYH1apzAyjOjyZ55ygv37xKHzIFhixzAwmXlAv4pfD4lVelYWVNOSN7REA0QJeCy2vKdqZ5cjqCXQ1lkQUlzOE7dpuNoAkhAhAJJ8HaamFKy7Gl7uwmqbIr-dVYv21d_9O7mO26n0gy3zWXD2nJDxU5Mzl2pZd8-sFvUr9Kmp_YkeRMh4bSe0fr1Uc_YgkjpmYUyu7kaxRWTbAdJ3GwqWFMUDiyfhHdzvZPZyU4VkWreimoydMA

Assembling all of these together as payload.header.signature yields:

eyJhbGciOiJSUzI1NiJ9.eyJhIjoiYiIsImMiOiJkIiwiZSI6MX0.CNMaYaDGU3ZhFV1ve6p3sAdYXhEklej8DVIAMqIWCkpNmT6Jp7iigcndXwH5q3WQFHiswgIQU5-_-4rV3jKGptCROmEyWPW8_elhYH1apzAyjOjyZ55ygv37xKHzIFhixzAwmXlAv4pfD4lVelYWVNOSN7REA0QJeCy2vKdqZ5cjqCXQ1lkQUlzOE7dpuNoAkhAhAJJ8HaamFKy7Gl7uwmqbIr-dVYv21d_9O7mO26n0gy3zWXD2nJDxU5Mzl2pZd8-sFvUr9Kmp_YkeRMh4bSe0fr1Uc_YgkjpmYUyu7kaxRWTbAdJ3GwqWFMUDiyfhHdzvZPZyU4VkWreimoydMA

Now, while this isn’t strictly a JSON Web Token (JWT), this site is helpful in validating digital signatures. You can load the X509 Private Key and Public Cert provided above into this site to validate the digital signature. If you are reading this blog post more than a year after it was published, the certificate will have expired. I’m not sure if http://jwt.io will work with an expired certificate (haven’t tried it). But, a link to an older blog post that covers how to generate your own private key and public certificate is provided earlier in this example. Note, that this site currently only works with the RS256 and HS256 algorithms.

Some Code

While generating the example above, I wrote some simple node.js code that uses the jsjws node module to create the JWS.

Here is the code.

var assert = require(‘assert’);
var jsjws = require(‘jsjws’);
var fs = require(‘fs’);
var privKeyFile = fs.readFileSync(‘. / dsig - key.pem’);
console.log(“privKeyFile: “+privKeyFile);
var priv_pem = jsjws.createPrivateKey(privKeyFile, ‘changeit’, ’utf8 '); 
var pubCertFile = fs.readFileSync(‘. / dsig - cert.pem’); console.log(“pubKeyFile: “+pubCertFile);
var pub_pem = jsjws.X509.getPublicKeyFromCertPEM(pubCertFile.toString()) var header = {
            alg: ‘RS256’
        }; 
console.log(“Header: “+JSON.stringify(header));
var payload = {‘
            a’: ’b’,
            ‘c’: ’d’,
            ‘e’: 1.0
        }; console.log(“Payload: “+JSON.stringify(payload));
var sig = new jsjws.JWS().generateJWSByKey(header, payload, priv_pem); 
console.log(“Signature: “+sig);
var jws = new jsjws.JWS(); 
assert(jws.verifyJWSByKey(sig, pub_pem, [‘RS256’])); 
assert.deepEqual(jws.getParsedHeader(), header); 
assert.equal(jws.getUnparsedPayload(), JSON.stringify(payload)); 
console.log(“UnparsedHeader: “+jws.getUnparsedHeader()); 
console.log(“UnparsedPayload: “+jws.getUnparsedPayload());

Save this code as jws.js. Save the private key given above in a file called dsig-key.pem and the public certificate in a file called dsig-cert.pem. Install node. Install the jsjws and assert node modules. Then, run it. I ran this on a Fedora Core Linux install running the latest node version on that platform.

In the next and final part of this series, we will compare the XML Digital Signature spec to the JSON Web Signature spec.