I'm trying to reproduce a decoding of a JWE starting from jwt.io as an example and translating into code by using library jose4j
From site jwt.io I have the following:
HEADER:
{
"alg": "HS256"
}
PAYLOAD:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
VERIFY SIGNATURE:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
Fdh9u8rINxfivbrianbbVT1u232VQBZYKx1HGAGPt2I
)
the secret base64 is not encoded.
Now I try to reproduce the situation with jose4j and then having as a result the same value on the encoded field, which is:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.jOJ7G4oijaDk9Tr4ntAXczd6PlI4oVvBU0_5cf7oaz4
Then:
Key key = new HmacKey("Fdh9u8rINxfivbrianbbVT1u232VQBZYKx1HGAGPt2I".getBytes(StandardCharsets.UTF_8));
JsonWebEncryption jwe = new JsonWebEncryption();
String payload = Json.createObjectBuilder()
.add("sub", "1234567890")
.add("name", "John Doe")
.add("iat", "1516239022")
.build()
.toString();
jwe.setPayload(payload);
jwe.setHeader("alg", "HS256");
jwe.setKey(key);
String serializedJwe = jwe.getCompactSerialization();
System.out.println("Serialized Encrypted JWE: " + serializedJwe);
However I get this error:
org.jose4j.lang.InvalidAlgorithmException: HS256 is an unknown, unsupported or unavailable alg algorithm (not one of [RSA1_5, RSA-OAEP, RSA-OAEP-256, dir, A128KW, A192KW, A256KW, ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW, PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW, A128GCMKW, A192GCMKW, A256GCMKW]).
HS256 is a JWS algorithm so you'd need to use JsonWebSignature rather than JsonWebEncryption to accomplish what it looks like you're trying to do.
Related
I am using Scala to generate JWT using RS256 algorithm and private keys:
val jwtPayload = s"""{
| "exp": $time,
| "iss": "$orgId",
| "sub": "$technicalAccountId",
| "aud": "${imsExp}/c/${clientId}",
| "${imsExp}/s/${metaScope}": true
|}""".stripMargin
println(jwtPayload)
val token = Jwts.builder()
.setPayload(jwtPayload)
.signWith(SignatureAlgorithm.RS256,privateKey.getBytes("UTF-8"))
But this fails with the error:
Key bytes may only be specified for HMAC signatures. If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead.
at io.jsonwebtoken.lang.Assert.isTrue(Assert.java:38)
But the same code works well in javascript:
const jwtPayload = {
exp: Math.round(300 + Date.now() / 1000),
iss: secrets.org,
sub: secrets.id,
aud: `${secrets.imsEndpoint}/c/${secrets.technicalAccount.clientId}`,
[`${secrets.imsEndpoint}/s/${secrets.metascopes}`]: true
};
let token;
try {
token = jwt.sign(
jwtPayload,
{ key: secrets.privateKey},
{ algorithm: 'RS256' }
);
console.log(token);
} catch (tokenError) {
return Promise.reject(tokenError);
}
I am unable to identify two things:
How to pass passphrase?
How to get rid of below error:
Key bytes may only be specified for HMAC signatures. If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead.
at io.jsonwebtoken.lang.Assert.isTrue(Assert.java:38)
When I remove .getBytes method, I recieve a new error:
Exception in thread "main" java.lang.IllegalArgumentException: Base64-encoded key bytes may only be specified for HMAC signatures. If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead.
I have a quarkus app which does not generate jwt tokens itself but possesses a secret key of HS256-signed tokens (qwertyuiopasdfghjklzxcvbnm123456). I need to verify tokens of the incoming network requests, but for every request I get the error:
io.smallrye.jwt.auth.principal.ParseException: SRJWT07000: Failed to verify a token
...
Caused by: org.jose4j.jwt.consumer.InvalidJwtSignatureException: JWT rejected due to invalid signature. Additional details: [[9] Invalid JWS Signature: JsonWebSignature{"typ":"JWT","alg":"HS256"}->eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NjczODI2NzIsImV4cCI6MTY5ODkxODY3MiwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJSb2xlIjpbIk1hbmFnZXIiLCJQcm9qZWN0IEFkbWluaXN0cmF0b3IiXX0.5vBHzbTKjLnAkAIYuA3c50nWV--o9jIWV2i0GZI-aw4]
My application.properties config:
smallrye.jwt.verify.key-format=JWK
smallrye.jwt.verify.key.location=JWTSecret.jwk
smallrye.jwt.verify.algorithm=HS256
quarkus.native.resources.includes=JWTSecret.jwk
JWTSecret.jwk
{
"kty": "oct",
"k": "qwertyuiopasdfghjklzxcvbnm123456",
"alg": "HS256"
}
I tried to verify the signature of the token with jwt.io using secret key above (and it verified the signature just fine), so my guess there's something wrong with my JWK file or application.properties configuration. I also tried RS256 verification algorithm (with public/private pem keys) and it worked fine, but unfortunately I need it to work with HS256.
Below the code, but it should be ok since it works fine with other verification algorithms.
package co.ogram.domain
import org.eclipse.microprofile.jwt.JsonWebToken
import javax.annotation.security.RolesAllowed
import javax.enterprise.inject.Default
import javax.inject.Inject
import javax.ws.rs.*
import javax.ws.rs.core.Context
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.SecurityContext
#Path("/secured")
class TokenSecuredResource {
#Inject
#field:Default
var jwt: JsonWebToken? = null
#GET
#Path("/roles-allowed")
#RolesAllowed("Admin")
#Produces(MediaType.TEXT_PLAIN)
fun helloRolesAllowed(#Context ctx: SecurityContext): String? {
return getResponseString(ctx!!)
}
private fun getResponseString(ctx: SecurityContext): String {
val name: String
name = if (ctx.userPrincipal == null) {
"anonymous"
} else if (ctx.userPrincipal.name != jwt!!.name) {
throw InternalServerErrorException("Principal and JsonWebToken names do not match")
} else {
ctx.userPrincipal.name
}
val type = jwt!!.getClaim<Int>("type")
return String.format(
"hello + %s,"
+ " isHttps: %s,"
+ " authScheme: %s,"
+ " type: %s,"
+ " hasJWT: %s",
name, ctx.isSecure, ctx.authenticationScheme, type, hasJwt()
)
}
private fun hasJwt(): Boolean {
return jwt!!.claimNames != null
}
}
The jose4j package does the correct verification given the JWK as an input.
Your JWT is signed with the actual octets of jwk.k ("qwertyuiopasdfghjklzxcvbnm123456").
In reality you should base64url decode the k to get a buffer to use as the HS256 secret to sign. This will align with what the jose4j package does (which is correct).
I have a DockerFile based on Varnish 7.0 alpine, I have a custom vcl file to handle JWT authentication. We pass the JWT as a Bearer in the header.
I am based on this example: https://feryn.eu/blog/validating-json-web-tokens-in-varnish/
set req.http.tmpPayload = regsub(req.http.x-token,"[^\.]+\.([^\.]+)\.[^\.]+$","\1");
set req.http.tmpHeader = regsub(req.http.x-token,"([^\.]+)\.[^\.]+\.[^\.]+","\1");
set req.http.tmpRequestSig = regsub(req.http.x-token,"^[^\.]+\.[^\.]+\.([^\.]+)$","\1");
set req.http.tmpCorrectSig = digest.base64url_nopad_hex(digest.hmac_sha256(std.fileread("/jwt/privateKey.pem"), req.http.tmpHeader + "." + req.http.tmpPayload));
std.log("req sign " + req.http.tmpRequestSig);
std.log("calc sign " + req.http.tmpCorrectSig);
if(req.http.tmpRequestSig != req.http.tmpCorrectSig) {
std.log("invalid signature match");
return(synth(403, "Invalid JWT signature"));
}
My problem is that tmpCorrectSig is empty, I don't know if I can load from a file, since my file contains new lines and other caracteres ?
For information, this Vmod is doing what I want: https://code.uplex.de/uplex-varnish/libvmod-crypto, but I can't install it on my Arm M1 pro architecture, I spent so much time trying...
Can I achieve what I want?
I have a valid solution that leverages the libvmod-crypto. The VCL supports both HS256 and RS256.
These are the commands I used to generated the certificates:
cd /etc/varnish
ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key
openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub
I use https://jwt.io/ to generate a token and paste in the values from my certificates to encrypt the signature.
The VCL code
This is the VCL code that will extract the JWT from the token cookie:
vcl 4.1;
import blob;
import digest;
import crypto;
import std;
sub vcl_init {
new v = crypto.verifier(sha256,std.fileread("/etc/varnish/jwtRS256.key.pub"));
}
sub vcl_recv {
call jwt;
}
sub jwt {
if(req.http.cookie ~ "^([^;]+;[ ]*)*token=[^\.]+\.[^\.]+\.[^\.]+([ ]*;[^;]+)*$") {
set req.http.x-token = ";" + req.http.Cookie;
set req.http.x-token = regsuball(req.http.x-token, "; +", ";");
set req.http.x-token = regsuball(req.http.x-token, ";(token)=","; \1=");
set req.http.x-token = regsuball(req.http.x-token, ";[^ ][^;]*", "");
set req.http.x-token = regsuball(req.http.x-token, "^[; ]+|[; ]+$", "");
set req.http.tmpHeader = regsub(req.http.x-token,"token=([^\.]+)\.[^\.]+\.[^\.]+","\1");
set req.http.tmpTyp = regsub(digest.base64url_decode(req.http.tmpHeader),{"^.*?"typ"\s*:\s*"(\w+)".*?$"},"\1");
set req.http.tmpAlg = regsub(digest.base64url_decode(req.http.tmpHeader),{"^.*?"alg"\s*:\s*"(\w+)".*?$"},"\1");
if(req.http.tmpTyp != "JWT") {
return(synth(400, "Token is not a JWT: " + req.http.tmpHeader));
}
if(req.http.tmpAlg != "HS256" && req.http.tmpAlg != "RS256") {
return(synth(400, "Token does not use a HS256 or RS256 algorithm"));
}
set req.http.tmpPayload = regsub(req.http.x-token,"token=[^\.]+\.([^\.]+)\.[^\.]+$","\1");
set req.http.tmpRequestSig = regsub(req.http.x-token,"^[^\.]+\.[^\.]+\.([^\.]+)$","\1");
if(req.http.tempAlg == "HS256") {
set req.http.tmpCorrectSig = digest.base64url_nopad_hex(digest.hmac_sha256("SlowWebSitesSuck",req.http.tmpHeader + "." + req.http.tmpPayload));
if(req.http.tmpRequestSig != req.http.tmpCorrectSig) {
return(synth(403, "Invalid HS256 JWT signature"));
}
} else {
if (! v.update(req.http.tmpHeader + "." + req.http.tmpPayload)) {
return (synth(500, "vmod_crypto error"));
}
if (! v.valid(blob.decode(decoding=BASE64URLNOPAD, encoded=req.http.tmpRequestSig))) {
return(synth(403, "Invalid RS256 JWT signature"));
}
}
set req.http.tmpPayload = digest.base64url_decode(req.http.tmpPayload);
set req.http.X-Login = regsub(req.http.tmpPayload,{"^.*?"login"\s*:\s*(\w+).*?$"},"\1");
set req.http.X-Username = regsub(req.http.tmpPayload,{"^.*?"sub"\s*:\s*"(\w+)".*?$"},"\1");
unset req.http.tmpHeader;
unset req.http.tmpTyp;
unset req.http.tmpAlg;
unset req.http.tmpPayload;
unset req.http.tmpRequestSig;
unset req.http.tmpCorrectSig;
unset req.http.tmpPayload;
}
}
Installing libvmod-crypto
libvmod-crypto is required to use RS256, which is not supported by libvmod-digest.
Unfortunately I'm getting an error when running the ./configure script:
./configure: line 12829: syntax error: unexpected newline (expecting ")")
I'll talk to the maintainer of the VMOD and see if we can figure out someway to fix this. If this is an urgent matter, I suggest you use a non-Alpine Docker container for the time being.
Firstly, the configure error was caused by a missing -dev package, see the gitlab issue (the reference is in a comment, but I think it should be more prominent).
The main issue in the original question is that digest.hmac_sha256() can not be used to verify RS256 signatures. A JWT RS256 signature is a SHA256 hash of the subject encrypted with an RSA private key, which can then be verified by decrypting with the RSA public key and checking the signature. This is what crypto.verifier(sha256, ...) does.
In this regard, Thijs' previous answer is already correct.
Yet the code which is circulating and has been referenced here it nothing I would endorse. Among other issues, a fundamental problem is that regular expressions are used to (pretend to) parse JSON, which is simply not correct.
I use a better implementation for long, but just did not get around to publishing it. So now is the time, I guess.
I have just added VCL snippets from production code for JWT parsing and validation.
The example is used like so with the jwt directory in vcl_path:
include "jwt/jwt.vcl";
include "jwt/rsa_keys.vcl";
sub vcl_recv {
jwt.set(YOUR_JWT); # replace YOUR_JWT with an actual variable/header/function
call recv_jwt_validate;
# do things with jwt_payload.extract(".scope")
}
Here, the scope claim contains the data that we are actually interested in for further processing, if you want to use other claims, just rename .scope or add another jwt_payload.expect(CLAIM, ...) and then use jwt_payload.extract(CLAIM).
This example uses some vmods, which we developed and maintain in particular with JWT in mind, though not exclusively:
crypto (use gitlab mirror for issues) for RS signatures (mostly RS256)
frozen (use gitlab mirror for issues) for JSON parsing
Additionally, we use
re2 (use gitlab mirror for issues) to efficiently split the JWT into the three parts (header, payload, signature)
and taskvar from objvar (gitlab) for proper variables.
One could do without these two vmods (re2 could be replaced by the re vmod or even regsub and taskvar with headers), but they make the code more efficient and cleaner.
blobdigest (gitlab) is not contained in the example, but can be used to validate HS signtures (e.g. HS256).
The RFC7518 has a list of algorithms values used in JWT. However there is no value for EdDSA, such as Ed25519. Also Ed25519 is not accepted as a valid value when verifying in Jose. What is the correct alg value for Ed25519?
ED25519 is a EdDSA (Edwards-curve DSA) signature scheme. See also RFC8037 and RFC8032.
According to the jose documentation alg needs to be set to EdDSA:
JWS Algorithm: Edwards-curve DSA
alg: EdDSA
EdDSAis also listed in the section JSON Web Signature and Encryption Algorithms of the IANA Registry (thanks #Florent Morselli for the hint)
Here I show an example how to generate a ed25519 keypair and a signed token in Node.js with jose and crypto and then verify the token with the public key in Python:
Generate key pair and token:
const { SignJWT } = require('jose/jwt/sign')
const { generateKeyPairSync } = require('crypto')
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
console.log(publicKey.export({format:'pem',type:'spki'}))
console.log(privateKey.export({format:'pem',type:'pkcs8'}))
const jwt = await new SignJWT({ 'id': 1 })
.setProtectedHeader({ alg: 'EdDSA' })
.setExpirationTime('2h')
.sign(privateKey)
console.log(jwt)
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA7fySb/9h7hVH8j1paD5IoLfXj4prjfNLwOPUYKvsTOc=
-----END PUBLIC KEY-----
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIIJtJBnTuKbIy5YjoNiH95ky3DcA3kRB0I2i7DkVM6Cf
-----END PRIVATE KEY-----
eyJhbGciOiJFZERTQSJ9.eyJpZCI6MX0.RAxBAQPFOxrCfgqb56eaAz9u2lByj-WEO-
JWgJH3Cyx1o1Hwjn1pA2M4NgJeob9vb2Oaw4FOeYFr6_33XMTnAQ
the decoded token header:
{
"alg": "EdDSA"
}
Verify the token in Python with PyJWT:
import jwt
public_key = """-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA7fySb/9h7hVH8j1paD5IoLfXj4prjfNLwOPUYKvsTOc=
-----END PUBLIC KEY-----"""
token = 'eyJhbGciOiJFZERTQSJ9.eyJpZCI6MX0.RAxBAQPFOxrCfgqb56eaAz9u2lByj-WEO-JWgJH3Cyx1o1Hwjn1pA2M4NgJeob9vb2Oaw4FOeYFr6_33XMTnAQ'
decoded = jwt.decode(token, public_key, algorithms='EdDSA', verify=True)
print(decoded)
Note: jwt.io currently doesn't support the EdDSA/ED25519 algorithm, so you can't verify the token on that site. I also don't know about any other JWT website that can verify EdDSA signed tokens.
I am stuck, I am trying to sign a Json Web Token for Docusign. https://developers.docusign.com/esign-rest-api/guides/authentication/oauth2-jsonwebtoken Docusign just provides a RSA private and public key hash. That's it. The JWT must signed using RS256.
I found a JWT module https://www.powershellgallery.com/packages/JWT/1.1.0 but that requires that I have the certificate installed. But all I have is the key hash.
Levering some other code I found I was able to create a JWT token , although with the wrong algorithm. https://www.reddit.com/r/PowerShell/comments/8bc3rb/generate_jwt_json_web_token_in_powershell/
I've been trying modify it to use the RSACryto provider by creating a new object but ive been unsuccessful.
I tried to create a new object and see if I can some how import the key so that I can sign the token. But I cant seem to be able to do that.
$rsa = New-Object -TypeName System.Security.Cryptography.RSACryptoServiceProvider
$keyhash = "-----BEGIN RSA PRIVATE KEY-----
XXXXXX
-----END RSA PRIVATE KEY-----"
$blob = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($keyhash))
I tried to use the ImportCspBlob method but it requires that the string is converted to bytes but I cant seem to do that either.
Im not sure if I am even approaching this int he correct fashion. Ive been getting errors of either
Exception calling "ImportCspBlob" with "1" argument(s): "Bad Version of provider.
or
Cannot convert value to type "System.Byte". Error: "Input string was not in a correct format."
EDIT:
I have a work around using Node.js, although id still like to see if it is possible to do what I am trying to do natively in Powershell . This work aroun might be useful for some as there does not seem to be many references for using Powershell and Docusign API.
I found a node.JS script here that creates a JWT Token using the RS256 algorithm. https://github.com/BlitzkriegSoftware/NodejwtRSA ,
I stripped out all the extra stuff so the output to the console is only the token, and added the relevant scope to the "payload data" and under the sign options updated the sub, aud, and iss. The my RSA Private key was stored locally on the system in a file.
nodejs script - My modified version below
'use strict';
const path = require('path');
const fs = require('fs');
// https://github.com/auth0/node-jsonwebtoken
var jwt = require('jsonwebtoken');
// Private Key (must read as utf8)
var privateKey = fs.readFileSync('./rsatest.pk','utf8');
// Sample claims payload with user defined fields (this can be anything, but briefer is better):
var payload = { };
// Populate with fields and data
payload.scope = "signature impersonation";
// Values for the rfc7519 fields
var iss = "XXXXX-XXXX-XXX-XXX-XXX";
var sub = "XXXXX-XXXX-XXX-XXX-XXX";
var aud = "account-d.docusign.com";
// Expiration timespan: https://github.com/auth0/node-jsonwebtoken#token-expiration-exp-claim
var exp = "1h";
// JWT Token Options, see: https://tools.ietf.org/html/rfc7519#section-4.1 for the meaning of these
// Notice the `algorithm: "RS256"` which goes with public/private keys
var signOptions = {
issuer : iss,
subject: sub,
audience: aud,
expiresIn: exp,
algorithm: "RS256"
};
var token = jwt.sign(payload, privateKey, signOptions);
console.log(token)
process.exitCode = 0;
I called it from Powershell and feed the access token back into my script so i can then get my access token and start making my API calls.
#get the JWT token
$token = & node C:\temp\nodejwt.js
# Generate Header for API calls.
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Content-Type", "application/x-www-form-urlencoded")
$body ="grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=$token"
$authuri = "https://account-d.docusign.com/oauth/token"
#send the JWT and get the access token
$accesstoken = Invoke-RestMethod -Method post -Uri $authuri -Headers $headers -Body $body -Verbose
$getheaders = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$getheaders.Add("Content-Type", "application/json")
$getheaders.Add('Authorization','Bearer ' + $accesstoken.access_token)
$geturi = "https://account-d.docusign.com/oauth/userinfo"
#use the access token to make api calls
Invoke-RestMethod -Method get -Uri $geturi -Headers $getheaders
I also had the same problem and was stuck.I used Python to get JWT. Be sure to install PyJWT and cryptography library.
$ pip install PyJWT
$ pip install pyjwt[crypto]
import time
import jwt
from datetime import datetime, timedelta
private_key= b'-----BEGIN PRIVATE KEY-----
`-----END PRIVATE KEY-----\n'
encoded_jwt = jwt.encode({
"iss":"<service account e-mail>",
"scope":"",
"aud":"",
"exp":int(time.time())+3600,
"iat":int(time.time())
}, private_key, algorithm='RS256')
encoded_jwt