Interop between node-jose (js) and jwcrypto (python) using EC keys? - javascript

I'm struggling to produce a JWE in jwcrypto equivalent to that in node-jose with the same key. The goal is to produce a key in node-jose and export the pubkey to jwcrypto to encrypt a payload, which will then be consumed by node-jose and decrypted.
My test entirely in node-jose works fine:
var jose = require("node-jose")
var keyStore = jose.JWK.createKeyStore()
keyStore.generate('EC', 'P-521').then(function (result) {
// Use exported key to encrypt something (so we see the same thing jwcrypto does)
jose.JWK.asKey(result.toJSON()).then(function(result) {
jose.JWE.createEncrypt(result).update('this is a test payload').final().then(function (result) {
jose.JWE.createDecrypt(keyStore).decrypt(result).then(function (result) {
// Result is good
console.log(result)
})
})
})
However, when I do the same in python, node-jose produces a different JWE:
key = jwk.JWK(**json.loads(the_exported_key))
# This key looks exactly the same as the exported key in node-jose
print(key.export(private_key=False))
payload = "this is a test payload"
header = {
'alg': 'ECDH-ES',
'enc': 'A128CBC-HS256',
}
my_jwe = jwe.JWE(payload.encode('utf-8'), header)
my_jwe.add_recipient(key)
When node-jose tries to decrypt my_jwe, it fails with "Error: no key found". Strangely (or not, this is my first time using JWEs...), the two encryption results are (see examples below). I think I'm missing how to get jwcrypto to, like node-jose, not require 'header' values, but when I pull those it complains.
node-jose example (junk data):
{
ciphertext: "1e7YX6hNDJWJELhHTNXEOg",
iv: "oQZZq2smHX8u8MMwoC6NBA",
protected: "eyJhbGciOi".....(very long string),
tag: "3NfEqx9f2ivL8QodG5Duaw",
}
jwcrypto (junk data):
{
ciphertext: "7ldKnkcsLZUy-SXFRv_HpkWOsb-YUUlNFv-4M5yZhCA",
iv: "1uErMiK_RWcaPXPCPq12Uw",
header: {
alg: "ECDH-ES",
enc: "A128CBC-HS256",
epk: {
crv: "P-521",
kty: "EC",
x: different from the exported key, I assume this is expected 'epk',
y: different from the exported key, I assume this is expected 'epk',
},
kid: "JCU3sWKfirVybFbpy2NPOnq-4-43JiemRZLO5dmPMVo"
},
tag: "51AMFyCJld5uPyMFLLl-sw",
}

The results you got with jwcrypto or node-jose look compliant with the RFC7516.
The only difference is that node-jose set your header in the protected member (integrity protected header) whereas jwcrypto set it in the header member (per-recipient unprotected header).
My understanding is that node-jose throws an error because it cannot find the public key in the header (epk member). It only checks the protected member and not other headers (header and also unprotected members if present) which is not compliant with the RFC7516 section 2 paragraph 4.:
let the JOSE Header be the union of the members of the JWE Protected Header, the JWE Shared Unprotected Header and the corresponding JWE Per-Recipient Unprotected Header
From my point of view, when a JWE is created for only one recipient, there is no reason to set the epk member (as well as the alg and enc members) in an unprotected header. The presence of those unprotected headers will prevent you from using the JWE Compact Serialization. So the behaviour of jwcrypto should be changed.
I don't know how these two libraries work, however there are two ways to fix that issue:
Force jwcrypto to use the integrity protected header instead of the unprotected one (best).
Ask node.jose to take into account the other headers (good but may take some time)

Related

How to verify a JWT signature using Node-jose

I am trying to use node-jose to verify signatures of my JWTs. I know the secret, but am having trouble converting this secret into a JWK used for the verification.
Here is an example of how I am trying to create my key with my secret and verify my token. This results in Error: no key found.
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJpYWxfbnVtYmVyIjoiNWYxMGExNjMtMjk2OC00ZDZkLWIyZDgtOGQxNjQwMDNlMmQ0Iiwic2VxIjo1MTI4MTYsIm5hbWUiOiJOYW1lMSIsImlkIjo2NTQsImRlc2NyaXB0aW9uIjoiVGVzdCBEZWNvZGluZyJ9.ahLaTEhdgonxb8rfLG6NjcIg6rqbGzcHkwwFtvb9KTE"
let secret = "SuperSecretKey"
let props = {
kid: "test-key",
alg: "HS256",
use: "sig",
k: secret,
kty: "oct"
}
let key;
jose.JWK.asKey(props).then(function(result) {key = result})
jose.JWS.createVerify(key).verify(token).then(function(result){console.log(result)})
Do I need to modify my token to include the kid header somewhere? Am I generating the key correctly from the known secret for this library?
You have three problems with your code.
due to the asynchronous nature of the promises, key gets a value when the promise is fulfilled (in the .then part), but that happens after the next line gets called.
Place a console.log(key) directly after the line jose.JWK.asKey(... and you see you get "undefined" as a result. So there is actually no key.
the k value in a JWK is treated as a Base64Url encoded octet. When you sign the token, you have to use the base64url decoded value of k, but not k directly.
the secret "SuperSecretKey" is too short for node.jose. For the HS256 algorithm, the secret has to be 256 bits long. node.jose seems to be quite strict, compared to other libs.
To solve the first problem, you can either nest the calls (which quickly becomes hard to read, or use the async/await syntax like shown below:
var jose = require('node-jose')
async function tokenVerifyer()
{
let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJpYWxfbnVtYmVyIjoiNWYxMGExNjMtMjk2OC00ZDZkLWIyZDgtOGQxNjQwMDNlMmQ0Iiwic2VxIjo1MTI4MTYsIm5hbWUiOiJOYW1lMSIsImlkIjo2NTQsImRlc2NyaXB0aW9uIjoiVGVzdCBEZWNvZGluZyJ9.KK9F14mwi8amhsPT7ppqp_yCYwwOGcHculKByNPlDB8"
let secret = "SuperSecretKeyThatIsLongEnough!!" // A 32 character long secret to get 256 bits.
let props = {
kid: "test-key",
alg: "HS256",
use: "sig",
k: "cynZGe3BenRNOV2AY__-hwxraC9CkBoBMUdaDHgj5bQ",
//k : jose.util.base64url.encode(secret), // alternatively use above secret
kty: "oct"
}
let key = await jose.JWK.asKey(props)
let result = await jose.JWS.createVerify(key).verify(token)
}
tokenVerifyer()
In the above example, k is a key generated on https://mkjwk.org/ and the token was created with that key on https://jwt.io (check 'secret base64 encoded'). Alternatively, you can use your own secret, but have to make sure it's long enough.
Do I need to modify my token to include the kid header somewhere?
The small example above works without putting the kid in the token. For any real applications, you would usually add the kid into the token header. Your keystore could have more keys or rotating keys and the kidhelps to select the correct one.

Cardano-wallet malformed tx payload from Submit External Transaction

I’m trying to submit an already signed tx from cardano-cli using the cardano-wallet endpoint: https://localhost:8090/v2/proxy/transactions
the signed transaction look like this:
txBody = {
"type": "Tx MaryEra",
"description": "",
"cborHex": "83a400818258202d7928a59fcba5bf71c40fe6428a301ffda4d2fa681e5357051970436462b89400018282583900c0e88694ab569f42453eb950fb4ec14cb50f4d5d26ac83fdec2c505d818bcebf1df51c84239805b8a330d68fdbc3c047c12bb4c3172cb9391a002b335f825839003d2d9ceb1a47bc1b62b7498ca496b16a7b4bbcc6d97ede81ba8621ebd6d947875fcf4845ef3a5f08dd5522581cf6de7b9c065379cbb3754d1a001e8480021a00029361031a01672b7ea1008182582073dd733cc50d594cb89d3ac67287b02dae00982fc800e9c9c9d1bb282b56122558404d0cb4e4f1cc415ddcf546871f075d0ca6e0c2620cd784b06c21c9b86e4403cb7a115038487576dcb20e7820e9d0dc93ab2a737ed9d0a71a77bc1e12f7c4dd0ef6"
}
I just don’t know how to pass it to the endpoint using Content-Type application/octet-stream. The API doc says the payload should be:
string <binary>
Signed transaction message binary blob.
I’m using javascript for this and have tried passing the cborHex directly, using Buffer.from(txBody.cborHex).toString('base64') and the whole json Buffer.from(JSON.stringify(txBody)).toString('base64') but always got the same response:
{
"code": "malformed_tx_payload",
"message": "I couldn't verify that the payload has the c…node. Please check the format and try again."
}
Also I’ve noticed from the swagger specification that the endpoint support a JSON payload and taking a look to the cardano-wallet's source code here:
newtype PostExternalTransactionData = PostExternalTransactionData
{ payload :: ByteString
} deriving (Eq, Generic, Show)
I thought the structure should be some like this:
{
"payload": ?// some binary blob here that I can't find. I've tried with:
// Buffer.from(txBody.cborHex).toString('base64') and
// Buffer.from(JSON.stringify(txBody)).toString('base64')
}
Any idea how to construct the payload and pass the signed tx?
This code tells me that when decoding external (sealed) transaction the wallet tries Base 16 encoding first and if that fails then it tries Base 64:
instance FromJSON (ApiT SealedTx) where
parseJSON v = do
tx <- parseSealedTxBytes #'Base16 v <|> parseSealedTxBytes #'Base64 v
pure $ ApiT tx
After that BytesString is passed to this function https://github.com/input-output-hk/cardano-node/blob/5faa1d2bb85ae806ec51fa4c576dec2670c67c7a/cardano-api/src/Cardano/Api/SerialiseCBOR.hs#L32 together with the currentNodeEra that node is running.
(Each era has a different way of decoding)
It could be (I am not sure) that node is running say Alonzo but you're submitting a Mary-encoded Tx. In which case the decoding might fail.
I hope this helps.

How do I specify the passphrase for a private key in saml2-js?

I see the option to do this in saml-passport but I've already set things up using saml2-js. My key/cert has a passphrase that is required or I get a bad decrypt error. Is it possible to set this passphrase?
Here are the SP options
var sp_options = {
entity_id: "/startpoint",
private_key: fs.readFileSync(`${dir}src/certs/key.pem`).toString(),
certificate: fs.readFileSync(`${dir}src/certs/cert.pem`).toString(),
assert_endpoint: '/assert',
sign_get_request: true
}
var sp = new saml2.ServiceProvider(sp_options)
I would expect to have a key 'passphrase' under the private_key key but there is no key like that specified in the docs.
It seems that your saml module doesn't handle passphrase for crt/pem.
You can search another saml module on npm which can handle it, i didn't find one for the moment
You can probably manage the decryption of the pem by hand with an openssl wrapper / crypto wrapper but i might be quite complicated.
Otherwise, John Cruz seems the best option for no-code solution.
If you want to do it by hand, your encrypted PEM includes:
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,05E1DB4ACD187787
Which show you encrypting algorithm used, here : des-ede3-cbc, and the cipher IV in hex format, here 05E1DB4ACD187787
Then you have your base64 encrypted datas:
2rtyxqlZg/ROAHQRnYyHDpkdk9rgYVhsNrGdBzEySzUG+LRwTU/Z+ihSTKK0f2yj
Zpn/qOsXwq4IS6XOb+Q8M5AAbE7t3jKI14YDAvDK/jQpBLk907oxFqeNte3Qvmrm
OjzHJS/P1JXef4dByhrjlrdL/pNV9ov5dM8cyVcxRUbW6cNapXoSrlXrmNPM....
With node crypto module you can now handle the decryption:
const cipher = crypto.createDecipheriv('DES-EDE3-CBC', Buffer.from(secretKeyHex, "hex"), Buffer.from(ivHex, "hex"));
let c = cipher.update(encryptedPemBase64, 'base64','base64')
c += cipher.final('base64');
Note that the key must have a specific length, depending on the algorithm used
It's quite easy if your key is "static", can be tricky if you have to handle a lot of algorithm
By poking around the code in that library, it doesn't look like it has the logic to handle password-protected certs.
One option is to remove the passphrase by doing this:
openssl rsa -in key.pem -out key_nopass.pem
You'll be prompted to enter your password one more time. The newly-created file, key_nopass.pem won't require a password.

How does this API endpoint validate the request is legit?

I ran across a list of open API endpoints a while back, and took notice of this one because of the way it is called.
https://developer.marvel.com/documentation/authorization
Here are the rules for sending a request:
Authentication for Server-Side Applications
Server-side applications must pass two parameters in addition to the apikey parameter:
ts - a timestamp (or other long string which can change on a request-by-request basis)
hash - a md5 digest of the ts parameter, your private key and your public key (e.g. md5(ts+privateKey+publicKey)
For example, a user with a public key of "1234" and a private key of "abcd" could construct a valid call as follows:
http://gateway.marvel.com/v1/public/comics?ts=1&apikey=1234&hash=ffd275c5130566a2916217b101f26150
(the hash value is the md5 digest of 1abcd1234)
My Question
How does their server validate such a request? I am using a TIMESTAMP, but it is being hashed - how do they know my request is legit?
So, for fun and games, let's assume we are responsible for validating that call, in - let's say, Javascript (PsuedoCode):
/* Grab and Parse Querystring */
let ts = "1";
let publicKey = "1234"; // Public Key
let hash = "ffd275c5130566a2916217b101f26150"; //md5 ts+ privateKey + publicKey
// Ok. now what???
In this case, there is a "shared secret", and that is what they're calling the "private key".
Basically, they use the apikey parameter to look up what the secret is in their database. Then, they re-hash to ensure that the hash matches.
The reason for the timestamp is to prevent replay attacks. However, from what information you're saying, it doesn't appear that they're actually validating the request data itself (URL, parameters, etc.). So, someone who gets a request URL could make other requests with that same hash if they do so quickly. This is not a very good practice.
The timestamp and public key are both in the query parameters along with the hash. The server can look up the private key associated with the public key in the request, generate a hash, and compare that with the value you provided.
(The public key and private key are issued by the server as a pair, so it already knows the private key.)

Decryption Issue with Node Package "node-rsa"

I am attempting to implement simple public key cryptography with this library's RSA functions, but decryption seems to be broken.
I have two "users", Alice and Bob. Both Alice and Bob (code in separate files) create a new empty key via const key = new nodeRSA(). Then, they both generate a 2048 bit public and private key pair via the function key.generateKeyPair(2048). They both then give each other their public keys by exporting them from the key with key.exportKey('pkcs8-public-pem') and storing them into separate files and reading them in with fs. Alice then attempts to write a message to bob by passing both the string message and bob's public key into the function below
module.exports.writeMessage = (message, key) => {
const k = new rsa(key, 'pkcs8-public-pem')
const cipherText = k.encrypt(message, 'hex');
console.log('Saving "${cipherText}" to ctext.txt');
fs.writeFileSync('ctext.txt', cipherText);
};
Then, when bob goes to read the message, he passes in his full key and decodes the message from ctext.txt as shown in the function below
module.exports.readMessage = key => {
const encryptedMessage = fs.readFileSync('ctext.txt');
const message = key.decrypt(encryptedMessage, 'utf8');
return message;
};
Encryption works just fine, and Alice is able to send the ciphertext to ctext. The problem comes when bob calls the readMessage function and attempts to decipher the text. Both the Alice and Bob programs were activated and their keys remained unchanged throughout this process. The below error occurs on deciphering:
Error: Error during decryption (probably incorrect key). Original error: Error: Incorrect data or key
at NodeRSA.module.exports.NodeRSA.$$decryptKey (/Users/jisacf1/College/SeniorYear/Spring2019/CompSec/HW3/node_modules/node-rsa/src/NodeRSA.js:301:19)
at NodeRSA.module.exports.NodeRSA.decrypt (/Users/jisacf1/College/SeniorYear/Spring2019/CompSec/HW3/node_modules/node-rsa/src/NodeRSA.js:249:21)
at Object.module.exports.readMessage.key [as readMessage] (/Users/jisacf1/College/SeniorYear/Spring2019/CompSec/HW3/Part2/rsaReadWrite.js:7:25)
at inquirer.prompt.then (/Users/jisacf1/College/SeniorYear/Spring2019/CompSec/HW3/Part2/bob.js:42:43)
at processTicksAndRejections (internal/process/next_tick.js:81:5)
I really cannot see how the system thinks it is the incorrect key, since Alice encrypted the message using Bob's public key, and Bob is decoding the message using is private key. I've tried changing padding schemes to no avail as well. Any help would be appreciated greatly. For reference, the library's github is here: https://github.com/rzcoder/node-rsa
As mentioned by Maarten, the issue was that writeFileSync was encoding my cipher text in utf8 rather than the format the cipher text was in. This resulted in reading back incorrect cipher text, causing the key or data mismatch exception. Changing the default encoding for the function to hex solved the issue.

Categories