JOSE, Cryptography In Action
This blog post intends to complete my previous articles by providing more insights and clues to understand Cryptography topics in Authentication. It does not aim to deep dive and explain how Cryptography works but to focus on the issues to which it responds. Therefore I will sum up some core concepts first, and then play with the JOSE specification thanks to a widely used Java JOSE library namednimbus-jose-jwt
from Nimbus
. And last but not least, please note that to keep this article easy to understand, I will make some approximations.
Cryptography
To better understand the key concepts, I will refer to definitions extracted from glossary of the Computer Security Resource Center (CSRC) from the US National Institute of Standards and Technology (NIST) which is available here.
Cryptography: The discipline that embodies the principles, means, and methods for providing information security, including confidentiality, data integrity, source authentication, and non-repudiation.
In other words, Cryptography are methods used to protect information and communications. The NIST definition highlight four security properties.
Confidentiality: a property that data or information is not made available or disclosed to unauthorized persons or processes.
Integrity: a property whereby data has not been altered in an unauthorized manner since it was created, transmitted, or stored.
Authenticity: the property of being genuine and being able to be verified and trusted.
Non-Repudiation: provide assurance of the integrity and origin of data in such a way that the integrity and origin can be verified and validated by a third party
Methods to ensure CIAN security properties
Hashing for Integrity
Hashing is a process of generating a fixed-size byte array from an arbitrary input message (seen as an array of bytes). With a common hash function, an input parameter will always generate the same result. It is not reversible, meaning, even knowing which hashing method has been used, we can not get back the original input message from the hash result.
Therefore hashing is mainly used to check data integrity. There are several hashing algorithms such as MD5, and SHA1 (the one used in the example below), and SHA3 algorithms
Ex: when Maven (a dependency manager) pulls a jar library from a registry, it will download both the library and a file containing the hash result. Maven will then hash the jar file and compare the output result with the provided/expected hash result. If they match, that means the jar has not been corrupted while being downloaded, or at any other point in time.
# Hash nimbus-jose-jwt jar file using sha1 function
shasum ~/.m2/repository/com/nimbusds/nimbus-jose-jwt/9.31/nimbus-jose-jwt-9.31.jar
# Resulting in : 229ba7b31d1f886968896c48aeeba5a1586b00bc
# Display nimbus-jose-jwt (.sha1) file content provided by the Maven registry
cat ~/.m2/repository/com/nimbusds/nimbus-jose-jwt/9.31/nimbus-jose-jwt-9.31.jar.sha1
# Resulting in : 229ba7b31d1f886968896c48aeeba5a1586b00bc
# Values are the same, meaning integrity of the jar file has been proven.
It is important to keep in mind that hash functions are not bijective.
(A hash result is not uniquely paired with an input message.)
Since the output will result in a fixed-size byte array, the result range is finite, while the input message is infinite, so two distincts messages can produce the same output result. This is called hash collision.
Signing for Authenticity
A digital signature is a mathematical scheme for verifying the authenticity of a digital message or document.
Alice wants to send a message to Bob through the network.
Alice will sign her message with a key, attach the digital signature to the message and finally send the message to Bob.
Bob, thanks to a key, will be able to verify the signature and trust the message is indeed coming from Alice.
When combining hashing and digital signature, we ensure who has sent the message AND that the message has not been altered. aka Integrity and Authenticity
Furthermore, as hashing results is a fixed-size byte array, most of the time applying a signature to a hash value will be more cost effective than applying it to the whole message.
It is important to understand that the message is sent in clear along with the digital signature. Anyone who intercepts the message will be able to read the content. But if they try to alter it, we will be able to detect it.
Encryption for Confidentiality
In Cryptography, encryption is the process of encoding information.
We encode a plainText (the message to send) using an encryption key, resulting in a cipherText. The process can be reversed so that with the cipherText and the encryption key, we can get back the original plainText.
Alice wants to send a message to Bob through the network. But now expects that nobody but Bob can read the content.
Alice will encrypt her message with a key and send it to Bob.
Bob, with the proper key, will be able to decrypt the message and read it.
Asymmetric keys for Non-Repudiation
In both digital signature and encryption, the processes involves a key. Keys can be separated in two groups: symmetric & asymmetric.
Symmetric encryption key implies that the encrypt and decrypt tasks are performed with the same key, commonly named shared secret.
Here Alice and Bob are sharing the same key, thus, both can sign or encrypt a message. There is no way for a third party to ensure who, between Alice and Bob, has originally produced the message. That is why we can not ensure non-repudiation.
Asymmetric encryption keys are based on a key pair (public and private), obviously both having a different value.
For signature, we use the private key to sign, and the public key to verify.
While for encryption, we use the public key to encrypt, and the private key to decrypt.
Using asymmetric keys, Alice will sign her message using the private (secret) key.
She can expose the public key to anyone (so in particular to Bob), because the public key can only be used to verify a signature, but not to generate one.
Non-repudiation is reached here because the private key is hold exclusively by Alice, she is the only one able to produce a signature verified with the public key.
Should we always use asymmetric keys?
No, it will depends on your use cases:
Asymmetric keys ensure non-repudiation, so they are well designed for distributed systems as mentioned in a previous blog post here.
The Authorization Server generates tokens provided to a Client (application), which will use them to consume Resource Servers (APIs). In the case of JWT, in addition to Integrity and Authenticity, asymmetric keys will ensure to APIs non-repudiation. On the contrary, with symmetric keys, API having the key would be able to self generate tokens, breaking the CIAN security properties…
But asymmetric keys are more resource (cpu/memory) expensive compared to symmetric algorithms. Thus, a common practice, for large data, is to bypass this constraint by generating a symmetric secret to encrypt data, and then, encrypt that shared secret with an asymmetric key. The recipient will have to decrypt a small volume of data with its private key (the shared secret encrypted key), and then, use the result to decrypt the message.
That is currently the same with https website: SSL/TLS uses asymmetric encryption to establish a secure session between a client and a server, and symmetric encryption to exchange data within the secured session.
Just as importantly, current public-key cryptography are known to be “soon” deprecated, therefore unsafe, due to future quantum computers. Quantum computers could potentially crack the security used to protect privacy in the digital systems we rely on every day. That is why in 2016, NIST called upon the world’s cryptographers to design “quantum resistant” algorithms, revealing the first winners in July 2022.
On the other hand, symmetric key based encryption are safe regarding quantum computers. At least, as of today, cryptographers did not find any breach. They are also more efficient. That is why they are used to encrypt high amount of data. Ex: Mac Hard Drive encryption feature named FileVault
uses symmetric key encryption. They are also suitable when you do not need non-repudiation. Let’s take as an example, a SaaS website that gives to their user the ability to generate a token, to copy/paste it in their IDE such as Visual Studio Code. This token will be sent by the IDE to the SaaS API endpoints, therefore, the whole process involves a single party, the SaaS website is the only one who generates and verifies the token.
Illustrating CIAN properties with nimbus-jose-sdk
JOSE stands for Json Object Signature and Encryption. It’s a set of specifications which the best known is JWT (Json Web Tokens). But JOSE also defines how to handle Signature (JWS), Encryption (JWE), Keys (JWK) and some Algorithms (JWA) for digital signatures, content encryption and Key Management.
Playing with Java Shell Tool (JShell)
To illustrate how JOSE works, I will use JShell which is a Read-Eval-Print Loop (REPL) convenient to test a library. Regarding the library, as already mentioned earlier, I will use nimbus-jose-sdk which supports a wide range of algorithms defined in JOSE specifications. To do so, you need a Java runtime for JShell, and download nimbus-jose-sdk.
# Check you have jshell on your path
which jshell
# Launch jshell
jshell
# Add nimbus library in our classpath
/env --class-path ~/.m2/repository/com/nimbusds/nimbus-jose-jwt/9.31/nimbus-jose-jwt-9.31.jar
Having a look to Algorithms (JWA)
The main two classes are JWEAlgorithm and JWSAlgorithm. Within jshell you can type JWSAlgorithm.
then press tab to see all the supported algorithms, giving here : ES256K ES384 ES512 EdDSA HS256 HS384 HS512 NONE PS256 PS384 PS512 RS256 RS384 RS512
and Family
. I will ignore the NONE
value... Family
distributes all this algorithms in four groups: HSxxx are Hash-based Message Authentication Code (HMAC), ESXXX are Elliptic Curve Digital Signature Algorithm (ECDSA), EdDSA stands for Edwards-curve Digital Signature Algorithm. Except HMAC, they are all asymmetric key based algorithms. Now let’s take a look to the result of this command:
// Import main jose package
import com.nimbusds.jose.*;
// Have a look to Signature algorithms
JWSAlgorithm.Family.SIGNATURE.stream()
.map(Algorithm::toString)
.collect(Collectors.joining(","));
//> "RS256,RS384,RS512,PS256,PS384,PS512,ES256,ES256K,ES384,ES512,EdDSA"
You can see that HMAC based algorithms are excluded from the SIGNATURE list. I do consider this as a good thing because HMAC only demonstrates that whoever generated the MAC was in possession of the shared secret. Thus, if our intent is to prove the origin of the information between more than a single party, non-repudiation is mandatory, and so, HMAC is not relevant.
Now you can play with Encryption algorithms which are grouped in two families, symmetric and asymmetric.
// Have a look to Encryption Symmetric key based algorithms
JWEAlgorithm.Family.SYMMETRIC.stream()
.map(Algorithm::toString)
.collect(Collectors.joining(","));
//> "A128KW,A192KW,A256KW,A128GCMKW,A192GCMKW,A256GCMKW,dir"
// Have a look to Encryption Asymmetric key based algorithms
JWEAlgorithm.Family.ASYMMETRIC.stream()
.map(Algorithm::toString)
.collect(Collectors.joining(","));
//> "RSA1_5,RSA-OAEP,RSA-OAEP-256,RSA-OAEP-384,RSA-OAEP-512,
//> ECDH-ES,ECDH-ES+A128KW,ECDH-ES+A192KW,ECDH-ES+A256KW"
Keys represented as Json (JWK)
In this section, I will create an asymmetric key based on RSA. I will use this key later to sign content.
//In addition to previous example imports
import com.nimbusds.jose.jwk.*;
import com.nimbusds.jose.jwk.gen.*;
RSAKey rsaKey = new RSAKeyGenerator(2048)
.keyID("001")// ID is useful when we expose public keys
.keyUse(KeyUse.SIGNATURE) //Will use the key for Digital Signature
.algorithm(JWSAlgorithm.RS512)
.generate();
System.out.println(rsaKey);
/* Will result in something like
* {
* "kty":"RSA", "use":"sig", "kid":"001", "alg":"RS512",
* "e":"AQAB", "n":"...",
* "p":"...", "q":"...", "d":"...", "qi":"...", "dp":"...", "dq":"..."
* }
*/
System.out.println(rsaKey.toPublicJWK());
/* Will result in something like
* {
* "kty":"RSA","use":"sig","kid":"001","alg":"RS512",
* "e":"AQAB","n":"..."
* }
*/
System.out.println(rsaKey.getKeyType());// RSA
rsaKey.getAlgorithm();// RS512
System.out.println(rsaKey.getKeyUse());// sig
JWK represents a key in a set of claims depending of the key type. Some claims are common to all kinds of keys, defined by the JWK specification: the key type (kty
), use (use
), id (kid
) and algorithm (alg
).
In our current example, we additionally have the related RSA key claims. The public part has two numbers, exponent e
and modulus n.
The private part is composed of prime factors p
and q
and few more claims…
Each key type have their own claims representations as follow:
//JWK Common claims
KEY_TYPE_CLAIM = "kty";
KEY_USE_CLAIM = "use";
KEY_ID_CLAIM = "kid";
KEY_ALG_CLAIM = "alg";
//RSA claims
RSA_PUBLIC_N_CLAIM = "n";//public modulus
RSA_PUBLIC_E_CLAIM = "e";//public exponent
RSA_PRIVATE_P_CLAIM = "p";//private first prime factor
RSA_PRIVATE_Q_CLAIM = "q";//private second prime factor
RSA_PRIVATE_D_CLAIM = "d";//private exponent
RSA_PRIVATE_DP_CLAIM = "dp";//private first factor CRT exponent
RSA_PRIVATE_DQ_CLAIM = "dq";//private second factor CRT exponent
RSA_PRIVATE_QI_CLAIM = "qi";//private first CRT coefficient
//EC claims
EC_PUBLIC_CURVE_CLAIM = "crv";//public cryptographic curve
EC_PUBLIC_X_POINT_CLAIM = "x";//public x coordinate elliptic curve point
EC_PUBLIC_Y_POINT_CLAIM = "y";//public y coordinate elliptic curve point
EC_PRIVATE_D_POINT_CLAIM = "d";//private d coordinate elliptic curve point
//OKP claims
OKP_PUBLIC_CURVE_CLAIM = "crv";//public cryptographic curve
OKP_PUBLIC_X_POINT_CLAIM = "x";//public x coordinate elliptic curve point
OKP_PRIVATE_D_POINT_CLAIM = "d";//private d coordinate elliptic curve point
//OCT claims
OCT_SHARED_K_CLAIM = "k";//symmetric key value
Time to sign our first message (JWS)
//In addition to previous example imports
import com.nimbusds.jose.crypto.*;
String myMessage = "Hello I am Alexandre";
JWSObject jws = new JWSObject(
new JWSHeader.Builder((JWSAlgorithm)rsaKey.getAlgorithm())
.keyID(rsaKey.getKeyID())
.build(),
new Payload(myMessage)
);
jws.sign(new RSASSASigner(rsaKey.toPrivateKey()));
jws.getHeader();// Gives {"kid":"001","alg":"RS512"}
jws.getPayload(); // Gives "Hello I am Alexandre"
jws.getSignature(); // Gives a long string ;)
//It is a base64 String representation of the JWS
String jwsValue = jws.serialize(); // Gives something like "eyJr ... dAA"
To keep in mind, the JWT is composed of three parts:
1.Header: containing the JWK kid and algorithm used for signing
2.Payload: The original message (plain text)
3.Signature: The digital signature of the message.
The serialize method will generate a base64 encoded string representation of the JWS (all parts included). It is this serialized value that will be sent.
Do not mix encoding with encryption. A common beginner pitfall is to think the serialized value content is protected because it is not human-readable.
The process of encoding converts a source (the JWS header, payload and signature) into symbols for communication or storage. It can be reverted (decoding) without requiring any other parameter. Therefore, our current JWS Header and Payload content are still readable to anyone.
And now to verify the signed message (JWS)
/******************/
/* Recipient side */
/******************/
JWSObject toVerify = JWSObject.parse(jwsValue);
toVerify.verify(new RSASSAVerifier(rsaKey.toRSAPublicKey())); // Gives true
To verify the message, we need the public key (that can be exposed with no fear as it can be used only to verify and not to sign). In OpenID Connect solutions, it used to be exposed within the jwks_uri
endpoint available in the .well-known/openid-configuration
discovery endpoint. Ex: google public keys here. Obviously, even if the content is readable, you must not trust it until you verify it. Except… the key_id claim available in the header, which in case of multiple available public keys (as in the google jwks_uri
), will help you to load the good key.
Difference between JWS and JWT
//In addition to previous example imports
import com.nimbusds.jwt.*;
import java.time.Instant;
import java.util.Date;
// Generate a JWT
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("alexandre")
.expirationTime(Date.from(Instant.now().plusSeconds(3600)))
.build();
// Signed the JWT
SignedJWT signedJWT = new SignedJWT(
new JWSHeader.Builder((JWSAlgorithm)rsaKey.getAlgorithm())
.keyID(rsaKey.getKeyID())
.build(),
claimsSet
);
signedJWT.sign(new RSASSASigner(rsaKey.toPrivateKey()));
String jwtValue = signedJWT.serialize();
/******************/
/* Recipient side */
/******************/
// Verify
SignedJWT jwtToVerify = SignedJWT.parse(jwtValue);
jwtToVerify.verify(new RSASSAVerifier(rsaKey.toRSAPublicKey()));
Here I will not spend too much time, as you can see, the above source code is very similar to the previous JWS section. JWT is a signed token but with some expected registered claims such as the subject, audience, expiration time, and many more…
Confidentiality with JWE
In this example, I put in practice a common way to mix symmetric and asymmetric keys. The symmetric key named cek
standing for Content Encryption Key is used to encrypt the message content (our payload). This cek
can be generated “on the fly” each time we need to send a message.
Then we will encrypt the cek
with a public key that must be provided by the recipient of the message. Once received, the recipient will be able to decrypt the cek
, then decrypt and read the payload.
In that example, we ensure Integrity, Authenticity and Confidentiality.
//In addition to previous example imports
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
//Asymmetric Key
RSAKey rsaEncKey = new RSAKeyGenerator(2048)
.keyID("002")// ID is useful when we expose public keys
.keyUse(KeyUse.ENCRYPTION) //Will use the key for Digital Signature
.algorithm(JWEAlgorithm.RSA_OAEP_256)
.generate();
//Symmetric key
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(EncryptionMethod.A256GCM.cekBitLength());
SecretKey cek = keyGen.generateKey(); //CEK: Content Encryption Key
JWEObject jwe = new JWEObject(
new JWEHeader(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM),
new Payload("Hello, this expect to be a confidential message!")
);
//Encrypt
jwe.encrypt(new RSAEncrypter(rsaEncKey.toRSAPublicKey(), cek));
//It is a base64 encoded String of the JWE
String jweString = jwe.serialize();
/******************/
/* Recipient side */
/******************/
//Decrypt
JWEObject jweToDecrypt = JWEObject.parse(jweString);
jweToDecrypt.decrypt(new RSADecrypter(rsaEncKey.toPrivateKey()));
System.out.println(jweToDecrypt.getPayload().toString());
But we still do not warrant non-repudiation. The key used to encrypt the cek
is a public key, and must be shared from the recipient to the sender.
Meaning, it is at least known by two parties (the recipient and the sender).
Non-repudiation implies a key to be hold by a single party (aka private key).
To achieve CIAN properties, we need to nest signature and encryption.
As you can see, the private key used to sign the message is only hold by Alice. Likewise the private key used to decrypt the message is only hold by Bob. That means Bob is the only one able to decrypt the message. Bob can trust the message is coming from Alice, and Alice can not deny to have signed the message.
//In addition to previous example imports
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
//Asymmetric Key - will be used to sign a JWS
RSAKey rsaSignatureKey = new RSAKeyGenerator(2048)
.keyID("003")// ID is useful when we expose public keys
.keyUse(KeyUse.SIGNATURE) //Will use the key for Digital Signature
.algorithm(JWSAlgorithm.RS512)
.generate();
//Pointer just to enhance readability
PrivateKey signatureKey = rsaSignatureKey.toPrivateKey();
RSAPublicKey verifyKey = rsaSignatureKey.toRSAPublicKey();
JWSObject jws = new JWSObject(
new JWSHeader
.Builder((JWSAlgorithm)rsaSignatureKey.getAlgorithm())
.keyID(rsaSignatureKey.getKeyID())
.build(),
new Payload("Hello, I am an authenticated message")
);
jws.sign(new RSASSASigner(signatureKey));
//Asymmetric Key - Will be used to encrypt the JWS
RSAKey rsaEncKey = new RSAKeyGenerator(2048)
.keyID("004")// ID is useful when we expose public keys
.keyUse(KeyUse.ENCRYPTION) //Will use the key for Digital Signature
.algorithm(JWEAlgorithm.RSA_OAEP_256)
.generate();
//Pointer just to enhance readability
RSAKey encryptionKey = rsaEncKey.toPublicJWK();
PrivateKey decryptionKey = rsaEncKey.toPrivateKey();
JWEObject jwe = new JWEObject(
new JWEHeader
.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
.contentType("JWS")
.build(),
new Payload(jws)
);
jwe.encrypt(new RSAEncrypter(encryptionKey));
//It is a base64 encoded String of the JWE
String jweString = jwe.serialize();
/******************/
/* Recipient side */
/******************/
//Decrypt
JWEObject jweToDecrypt = JWEObject.parse(jweString);
jweToDecrypt.decrypt(new RSADecrypter(decryptionKey));
//Verify the content
JWSObject toVerify = jweToDecrypt.getPayload().toJWSObject();
toVerify.verify(new RSASSAVerifier(verifyKey)); // Gives true
//Once verified, we can trust the payload
System.out.println(toVerify.getPayload());
Here we finally comply with our `CIAN` four security principles.
We first signed a message using JWS and then encrypt it using JWE.
We can do the same using JWT instead of JWS. That is exactly what is being done when an OIDC Client applications have defined their id_token
and/or user_info
signed and encrypted algorithms claims as defined in the OpenID Connect Dynamic Client Registration specification.
Conclusion
The use of JOSE in this post was only motivated to illustrate how to comply with CIAN security principles. Obviously, it is not mandatory and you can achieve the same using plenty of other libraries. The most important being to understand those principles, and integrate them at the early stage of your software and architecture design. A last word regarding keys, a big part of this blog post speaks about them, but not how to handle them. Therefore, you should have a look to Key Management System (KMS) helping us to store, distribute, rotate, revoke keys… There you will learn new key concept named MEK, KEK and DEK, standing for Master|Key|Data Encryption Key…
How about you?
Do you have some additional recommendations? Let us know in the comments below!
🙏🏼 If you enjoy reading this article, please consider giving it a few 👏👏👏.
💌 Follow our latest posts on Twitter and LinkedIn and discover our latest stories on Medium 🚀
Who am I
Staff Engineer, working for Decathlon. I am more interested in architecture and backend development, especially on APIs and authentication/security topics. Sharing knowledge is what I prefer in my daily job.
Acknowledgments
I would like to thank Grégoire Waymel, Marco Mornati, Jérôme Molière and Boris Stoyanov-Brignoli for taking the time and effort to review this article. I sincerely appreciate all valuable comments and suggestions, which helped me to improve the quality of this article, but also for all our daily exchanges.