JWT
Amit Nadiger
Polyglot(Rust??, Move, C++, C, Kotlin, Java) Blockchain, Polkadot, UTXO, Substrate, Sui, Aptos, Wasm, Proxy-wasm,AndroidTV, Dvb, STB, Linux, Cas, Engineering management.
A JSON Web Token (JWT) is an open standard (RFC 7519 - JSON Web Token (JWT) (ietf.org)) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. It is often used for representing claims between a client and a server or between multiple services.
JWTs are commonly used for:
JWT Characteristics
JWTs have several characteristics that make them useful in various scenarios:
JWT Structure
JWTs consist of three parts: a header, a payload, and a signature. Each part is Base64Url-encoded and separated by periods ('.').
Header
The header typically consists of two parts:
Here's an example header:
{
"typ": "JWT",
"alg": "HS256"
}
What is the role of RSA and SHA256 in RS256 algorithm :
In the RS256 (RSA signature with SHA-256) algorithm, both RSA (Rivest–Shamir–Adleman) and SHA-256 (Secure Hash Algorithm 256-bit) play distinct roles in the signing and verification process of a JWT (JSON Web Token).
RS256 combines the strengths of RSA and SHA-256. RSA provides the asymmetric cryptography for secure key pair management, while SHA-256 provides a cryptographic hash function to ensure the integrity of the token's content. Together, they ensure that RS256-signed JWTs are both secure and tamper-evident.
Here's a breakdown of their roles:
BTW let me give very brief description of what Digest means :
Digest:
A "digest" refers to a fixed-length string of characters (usually in hexadecimal format) generated by applying a cryptographic hash function to a piece of data. A digest is also commonly referred to as a "hash" or "hash value."
A "digest algorithm" (or hash algorithm) is a mathematical function that takes an input (or "message") and produces a fixed-length string of characters, which is the digest. The key properties of a good digest algorithm include:
Digest algorithms are widely used in various aspects of computer security and cryptography, including:
Commonly used digest algorithms include SHA-256 (part of the SHA-2 family), SHA-1 (less secure and deprecated for many applications), and MD5 (generally considered weak and not recommended for security-critical purposes). The choice of digest algorithm depends on the specific security requirements of the application and the level of security needed.
let's go to original discussion of JWT structure and payload.
Payload
The payload contains claims. Claims are statements about an entity (typically, the user) and additional data. There are three types of claims:
Here's an example payload with some registered claims:
{
"sub": "Hello There",
"name": "Amit Nadiger",
"iat": 1516239022
...
}
Signature
The signature is used to verify that the sender of the JWT is who it says it is and to ensure that the message wasn't changed along the way. The signature is created by taking the encoded header, encoded payload, a secret (for HMAC-based algorithms), and the algorithm specified in the header. The result is a string of bytes, which is then Base64Url-encoded.
Here's an example signature:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
What is Base64?
Base64 is a binary-to-text encoding scheme that represents binary data in an ASCII string format. It's commonly used in computing and data transmission to ensure that data is transmitted and stored in a format that's both safe and efficient. Base64 encoding is designed to be machine-readable and human-readable, making it suitable for use in text-based protocols such as email, HTML, and URLs and of course JWT.
Here's how Base64 encoding works in detail:
Here's a simple example to illustrate the process:
Let's say you have a binary sequence: 01001001 01010011 00100000. To encode this binary data in Base64:
Group the binary data into sets of 6 bits each:
010010 010101 001100 100000
Convert each group of 6 bits to decimal:
18 21 12 32
Map the decimal values to the Base64 character set:
S V M g
Combine the Base64 characters into a string:
SVMg
So, the Base64-encoded representation of the binary data 01001001 01010011 00100000 is SVMg.
Below is base 64 mapping:
0 A 17 R 34 i 51 z
1 B 18 S 35 j 52 0
2 C 19 T 36 k 53 1
3 D 20 U 37 l 54 2
4 E 21 V 38 m 55 3
5 F 22 W 39 n 56 4
6 G 23 X 40 o 57 5
7 H 24 Y 41 p 58 6
8 I 25 Z 42 q 59 7
9 J 26 a 43 r 60 8
10 K 27 b 44 s 61 9
11 L 28 c 45 t 62 +
12 M 29 d 46 u 63 /
13 N 30 e 47 v
14 O 31 f 48 w
15 P 32 g 49 x
16 Q 33 h 50 y
Base64 is commonly used in various applications, including:
Please note that while Base64 encoding makes data more human-readable and suitable for text-based transmission, it does not provide encryption or security. It's not intended for securing sensitive data but for encoding it in a consistent and portable way.
Below base64_url_encode that takes a string as input which is already base64 encoded but it is not base64url compactable and this function performs Base64 URL encoding. Base64 URL encoding is a variant of Base64 encoding used for URL-safe transmission of data, and it replaces certain characters with others to make them safe for use in URLs.
fn main() {
let original_data = "Hello+World/123==";
let encoded_data = base64_url_encode(original_data);
println!("Encoded Data: {}", encoded_data);
}
fn base64_url_encode(data: &str) -> String {
let base64_url = data
.chars()
.map(|c| match c {
'+' => '-',
'/' => '_',
'=' => ' ', // Omit the '=' character
_ => c,
})
.collect::<String>()
.replace(" ", "");
// Remove any remaining spaces (due to omitted '=')
base64_url
}
/*
O/P
Encoded Data: Hello-World_123
*/
3. How to create JWTs
JWTs can indeed be created and validated using various algorithms, depending on the security requirements of the application. The choice of algorithm depends on factors like the level of security needed, the infrastructure in use, and whether we prioritize simplicity or complexity.
Here are some common JWT signing and verification algorithms and their roles:
What is Preferred Algorithm
The preferred algorithm depends on your specific use case and security requirements:
When choosing an algorithm, consider factors like the sensitivity of the data being transmitted, the potential impact of a security breach, the resources available for key management, and the compatibility of algorithms with your technology stack. Additionally, stay informed about best practices and security updates related to JWTs and the algorithms you use, as security is an evolving field.
There are several Rust crates available for creating JWTs using HMAC (Hash-based Message Authentication Code) algorithms, such as jsonwebtoken, jwt-compact, jwt, and jwt-simple. These crates provide straightforward ways to generate JWTs with HMAC, and they are well-documented and widely used in the Rust ecosystem.
Reference:
However, when it comes to creating JWTs using RSA (Rivest–Shamir–Adleman) algorithms in Rust, the landscape is a bit more complex. While Rust has a strong cryptography ecosystem, generating JWTs with RSA signing does require more involved setup due to the need for managing public and private keys, which are fundamental to RSA-based cryptography.
To create JWTs with RSA in Rust, we typically need to use the rsa - Rust (docs.rs) crate or a similar cryptography library. The rsa crate allows you to work with RSA keys and provides the tools necessary for signing and verifying JWTs. The process involves loading or generating RSA keys, defining JWT claims, signing the JWT with a private key, and then encoding the JWT.
Steps to create the JWT and sign using RSA :
I will take the below parameters as input :
// Keys
#[derive(Default, Clone)]
pub struct CustomKeys {
pub signing_key: Option<SigningKey<Sha256>>,
verifying_key: Option<VerifyingKey<Sha256>>
}
fn create_rsa_key(
algorithm: &str,
rsa_keys:&CustomKeys,
sub: &str,
iss: &str,
expires_in: i64,
aud: &str,
additional_claims: BTreeMap<String, String>) -> Result<String, Box<dyn std::error::Error>> {
Prepare Your Payload (Claims): Define the claims you want to include in your JWT. Claims can include the subject (sub), issuer (iss), expiration time (exp), audience (aud), and any custom claims specific to your application.
let time_options = TimeOptions::default();
let mut custom_claims = Claims::new(CustomClaims::new(sub, aud, iss,expires_in))
.set_duration_and_issuance(&time_options, Duration::seconds(expires_in))
.set_not_before(Utc::now() - Duration::hours(1));
// Add additional claims to the custom claims struct.
for (key, value) in additional_claims.iter() {
custom_claims.custom.add_claim(key, value);
}
Create a Payload JSON: Serialize the claims into a JSON string. This JSON string will become the payload of your JWT.
let payload = serde_json::to_string(&custom_claims).unwrap();
Create a Header: JWTs have a header that specifies the signing algorithm and token type. The header is also serialized into a JSON string.
let header = serde_json::json!({"alg": "RS256","typ": "JWT"}).to_string();
Base64-Encode Header and Payload: Encode the header and payload separately using Base64Url encoding. This creates two separate strings.
let encoded_header = base64url_encode(header.as_bytes());
let header_payload = format!("{}.{}", encoded_header, encoded_payload);
Combine Header and Payload: Combine the Base64-encoded header and payload with a period ('.') separator to form the unsigned token.
let header_payload = format!("{}.{}", encoded_header, encoded_payload);
Create the RsaPrivatekey using the private key and rsa crate.
pub rsa_keys: CustomKeys,
let rsa_result = RsaPrivateKey::from_pkcs1_pem(RSA_PKCS1_PRIVATE_KEY_2048);
rsa_keys.signing_key = match rsa_result {
Ok(rsa_signing_key) => {
Some(SigningKey::<Sha256>::new(rsa_signing_key))
},
Err(err) => {
return true;
},
}
Sign the Token: Use your RSA private key to sign the token. This involves applying a cryptographic signature algorithm (e.g., RS256) to the token. The signature is typically applied to the concatenated header and payload.
// Sign
let mut rng = rand::thread_rng();
let signature = rsa_keys.signing_key.as_ref().expect("Signing Key has not been created").sign_with_rng(&mut rng, header_payload.as_bytes());
assert_ne!(signature.to_bytes().as_ref(), header_payload.as_bytes());
Base64-Encode the Signature: Encode the signature generated in the previous step using Base64Url encoding.
let encoded_signature = base64url_encode(&signature.to_bytes().as_ref());
Combine Signature with Token: Append the Base64-encoded signature to the unsigned token, again separated by a period ('.').
let jwt = format!("{}.{}", header_payload, encoded_signature);
Our JWT is Ready: We now have a JWT consisting of three parts: the Base64Url-encoded header, the Base64Url-encoded payload, and the Base64Url-encoded signature. These three parts are separated by periods.
Here's a general overview of what a JWT might look like after these steps:
Base64Url(header).Base64Url(payload).Base64Url(signature)
Base64Url(header).Base64Url(payload).Base64Url(signature)
It's important to note that the header and payload are not encrypted; they are only Base64-encoded. The security of the JWT relies on the cryptographic signature, which can only be verified with the corresponding public key.
When validating a JWT, we'll need to:
JWT libraries in various programming languages simplify these steps and handle the details for you, but understanding the underlying process is important for implementing JWT-based authentication and security correctly.
Below is complete code which generates the JWT(Hmac and RSA) using Rust:
The above generates the jwt token as below :
hmac_token = eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2OTQzMzU2MDMsIm5iZiI6MTY5NDMyODQwMywiaWF0IjoxNjk0MzMyMDAzLCJzdWIiOiJUb2tlbkNyZWF0ZWlvbkRlbW8iLCJpc3MiOiJKYWlTaHJlZVJhbSIsImF1ZCI6IkphaUJhanJhbmdCYWxpIiwiZXhwIjozNjAwLCJHb2QxIjoiSmFpQmFqcmFuZ2JhbGkiLCJHb2QyIjoiSGFyZUtyaXNoYW5hIn0.mA8PtGur-Kp8RO4hGowRsOlMuBeZ8o6KbIDKJR_HSvw
For now RSA private key is hardcoaded
rsa_pkcs1_token = eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTQzMzU2MDMsIm5iZiI6MTY5NDMyODQwMywiaWF0IjoxNjk0MzMyMDAzLCJzdWIiOiJUb2tlbkNyZWF0ZWlvbkRlbW8iLCJpc3MiOiJKYWlTaHJlZVJhbSIsImF1ZCI6IkphaUJhanJhbmdCYWxpIiwiZXhwIjozNjAwLCJHb2QxIjoiSmFpQmFqcmFuZ2JhbGkiLCJHb2QyIjoiSGFyZUtyaXNoYW5hIn0.EKYiJ4WF668bFiVSB82yV5uonjERqOLaemoZGklLgFkK0ywAfO1dZBFXZi4GlJRaxa2y32OaKRsb7f5R4BucGQ7tkUEPjYYy3VXQDCD9b4mBLxZeUcdjoQ2hags3axGLrmYgGYDhZvBOiNewX8vxoWEOfW2xrmh7G5CXlSZ8QqNHfJ7G4D3Md3Y2YsmFcXAF0QKP0U3Id7sq62QHtJhPL6rc32MTxr8nSF16OU5BJWdVue4Wh0gEszAfrUMPy9bI0kEypcYzXVCGA9yuy2M-2UGJoUbnVERWF-C5F0UzE43OseHOujRirq4d41YqolGPivvdhwHiDtvDvDjFJkQglA
For now RSA private key is hardcoaded
rsa_pkcs8_token = eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTQzMzU2MDMsIm5iZiI6MTY5NDMyODQwMywiaWF0IjoxNjk0MzMyMDAzLCJzdWIiOiJUb2tlbkNyZWF0ZWlvbkRlbW8iLCJpc3MiOiJKYWlTaHJlZVJhbSIsImF1ZCI6IkphaUJhanJhbmdCYWxpIiwiZXhwIjozNjAwLCJHb2QxIjoiSmFpQmFqcmFuZ2JhbGkiLCJHb2QyIjoiSGFyZUtyaXNoYW5hIn0.EKYiJ4WF668bFiVSB82yV5uonjERqOLaemoZGklLgFkK0ywAfO1dZBFXZi4GlJRaxa2y32OaKRsb7f5R4BucGQ7tkUEPjYYy3VXQDCD9b4mBLxZeUcdjoQ2hags3axGLrmYgGYDhZvBOiNewX8vxoWEOfW2xrmh7G5CXlSZ8QqNHfJ7G4D3Md3Y2YsmFcXAF0QKP0U3Id7sq62QHtJhPL6rc32MTxr8nSF16OU5BJWdVue4Wh0gEszAfrUMPy9bI0kEypcYzXVCGA9yuy2M-2UGJoUbnVERWF-C5F0UzE43OseHOujRirq4d41YqolGPivvdhwHiDtvDvDjFJkQglA
amit:~/OmRustPractice/jwt-generator$
If you have seen the code I think you might have noticed there are different types of keys as PKCS1 and PKCS2.
The PKCS (Public-Key Cryptography Standards) family is a set of standards and specifications for various aspects of public-key cryptography. PKCS standards are developed and maintained by RSA Data Security, Inc., which is now part of the EMC Corporation. These standards cover a wide range of cryptographic operations, including key management, digital signatures, encryption, and more.
Two commonly referenced PKCS standards are PKCS #1 and PKCS #8, which serve different purposes in the context of public-key cryptography:
Both PKCS #1 and PKCS #8 are important standards in the field of public-key cryptography. PKCS #1 primarily deals with RSA-specific operations, while PKCS #8 provides a standardized way to represent private keys, making them interoperable across different cryptographic systems. These standards help ensure consistency and compatibility in cryptographic operations across various software and platforms.
Thanks for reading and please comment of you have any.