I'm trying to sign a payload and recreate the expected signature in the documentation of a service documented here: https://paybis.readme.io/reference/partner-api#signing-requests
My implementation of the signature is:
const signRequest = (privateKey: Buffer) => {
const verifiableData = '{"event":"VERIFICATION_STATUS_UPDATED","data":{"partnerUserId":"e18fb964-fd9a-4de7-96c4-1lclszzd","status":"started"},"timestamp":1654073212}'
const signature = crypto.sign(
'sha512',
Buffer.from(verifiableData),
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
},
);
return signature.toString("base64");
}
And my test:
const privateKey = fs.readFileSync(path.resolve(__dirname, './private.key'));
const signature = signRequest(privateKey);
const signature2 = signRequest(privateKey);
expect(signature).to.equal(signature2); //FAILS
Where the private key is formatted as such:
-----BEGIN RSA PRIVATE KEY-----
MIIJKQIBAAKCAgEAtJWQWnZJqbbxz1wNr3Dn/9I43z4Ddm/jd4G+PCkNGYXcqVqX
...
ukzH0Cx/iuONcUrYtpirM9ZMotfyyl4xO0Hc9bD/I97xn93GOvKFBkV9l7hW
-----END RSA PRIVATE KEY-----
No matter what I do, the signature is not consistent (aka. varies with every attempt)
Can anyone pinpoint what I might be doing wrong?
I tried using a string as the verifiable body, saving the key as a .pem file, instantiating the private key as a string, and passing it as both Buffer and string to the function.
As stated in one of the comments, the answer is that RSA-PSS is non-deterministic, so the signature will not be the same by design.
However, when trying to verify any of these different correct signatures with crypto.verify, it does verify as expected:
it(`Can create correctly encrypt body`, async function () {
const privateKey = process.env.PAYBIS_RSA_PRIVATE_KEY!;
const publicKey = KEY1;
const wrongPublicKey = KEY2;
const requestBody = { ... };
const signature = signRequest(requestBody, privateKey); // Signature here is always different
const verified = verifySignature(requestBody, signature, publicKey);
const verifiedFalse = verifySignature(requestBody, signature, wrongPublicKey);
expect(verified).to.equal(true, 'Could not verify correct signature'); //Correctly outputs true
expect(verifiedFalse).to.equal(false, 'Verified incorrect signature'); //Correctly outputs false
});
Related
I have been trying to import my PKCS#8 private key from Apple (used for signing JWTs for their Web services/APIs), for use with the browser's Subtle Crypto API (https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#pkcs_8_import). When I try with my private key it fails saying "Data provided to an operation does not meet requirements" but when I use the private key used in the example, it works. The code I have been trying with is from the example on the above URL.
I need a solution that works with vanilla JS, so I can't use npm packages.
The code I use for importing is this:
async function getPrivateKey() {
try {
const privateKey = await importPrivateKey(failingPemEncodedKey);
//const privateKey = await importPrivateKey(succeedingPemEncodedKey);
console.log('privateKey', privateKey);
} catch (error) {
console.log('ERROR', error.message);
}
return privateKey;
}
const failingPemEncodedKey = `-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg/zwtY1nn6uLpYyukIMgEX4pNgp7GQFevZSr9jkMFSR6gCgYIKoZIzj0DAQehRANCAASC0fq/OiBuA2UpHukZHxj88cZJXoesjhxBfO/XAKGlnRFM0QomoF1aoFlkFGvHU/3h53J/dxAwRzNSy++6CyEu
-----END PRIVATE KEY-----`;
const succeedingPemEncodedKey = `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDD0tPV/du2vftjvXj1t/gXTK39sNBVrOAEb/jKzXae+Xa0H+3LhZaQIQNMfACiBSgIfZUvEGb+7TqXWQpoLoFR/R7MvGWcSk98JyrVtveD8ZmZYyItSY7m2hcasqAFiKyOouV5vzyRe87/lEyzzBpF3bQQ4IDaQu+K9Hj5fKuU6rrOeOhsdnJc+VdDQLScHxvMoLZ9Vtt+oK9J4/tOLwr4CG8khDlBURcBY6gPcLo3dPU09SW+6ctX2cX4mkXx6O/0mmdTmacr/vu50KdRMleFeZYOWPAEhhMfywybTuzBiPVIZVP8WFCSKNMbfi1S9A9PdBqnebwwHhX3/hsEBt2BAgMBAAECggEABEI1P6nf6Zs7mJlyBDv+Pfl5rjL2cOqLy6TovvZVblMkCPpJyFuNIPDK2tK2i897ZaXfhPDBIKmllM2Hq6jZQKB110OAnTPDg0JxzMiIHPs32S1d/KilHjGff4Hjd4NXp1l1Dp8BUPOllorR2TYm2x6dcCGFw9lhTr8O03Qp4hjn84VjGIWADYCk83mgS4nRsnHkdiqYnWx1AjKlY51yEK6RcrDMi0Th2RXrrINoC35sVv+APt2rkoMGi52RwTEseA1KZGFrxjq61ReJif6p2VXEcvHeX6CWLx014LGk43z6Q28P6HgeEVEfIjyqCUea5Du/mYb/QsRSCosXLxBqwQKBgQD1+fdC9ZiMrVI+km7Nx2CKBn8rJrDmUh5SbXn2MYJdrUd8bYNnZkCgKMgxVXsvJrbmVOrby2txOiqudZkk5mD3E5O/QZWPWQLgRu8ueYNpobAX9NRgNfZ7rZD+81vh5MfZiXfuZOuzv29iZhU0oqyZ9y75eHkLdrerNkwYOe5aUQKBgQDLzapDi1NxkBgsj9iiO4KUa7jvD4JjRqFy4Zhj/jbQvlvM0F/uFp7sxVcHGx4r11C+6iCbhX4u+Zuu0HGjT4d+hNXmgGyxR8fIUVxOlOtDkVJa5sOBZK73/9/MBeKusdmJPRhalZQfMUJRWIoEVDMhfg3tW/rBj5RYAtP2dTVUMQKBgDs8yr52dRmT+BWXoFWwaWB0NhYHSFz/c8v4D4Ip5DJ5M5kUqquxJWksySGQa40sbqnD05fBQovPLU48hfgr/zghn9hUjBcsoZOvoZR4sRw0UztBvA+7jzOz1hKAOyWIulR6Vca0yUrNlJ6G5R56+sRNkiOETupi2dLCzcqb0PoxAoGAZyNHvTLvIZN4iGSrjz5qkM4LIwBIThFadxbv1fq6pt0O/BGf2o+cEdq0diYlGK64cEVwBwSBnSg4vzlBqRIAUejLjwEDAJyA4EE8Y5A9l04dzV7nJb5cRak6CrgXxay/mBJRFtaHxVlaZGxYPGSYE6UFS0+3EOmmevvDZQBf4qECgYEA0ZF6Vavz28+8wLO6SP3w8NmpHk7K9tGEvUfQ30SgDx4G7qPIgfPrbB4OP/E0qCfsIImi3sCPpjvUMQdVVZyPOIMuB+rV3ZOxkrzxEUOrpOpR48FZbL7RN90yRQsAsrp9e4iv8QwB3VxLe7X0TDqqnRyqrc/osGzuS2ZcHOKmCU8=
-----END PRIVATE KEY-----`;
async function importPrivateKey(pem) {
const pemHeader = '-----BEGIN PRIVATE KEY-----';
const pemFooter = '-----END PRIVATE KEY-----';
const pemContents = pem.substring(
pemHeader.length,
pem.length - pemFooter.length
);
const binaryDerString = window.atob(pemContents);
const binaryDer = str2ab(binaryDerString);
const privateKey = await window.crypto.subtle.importKey(
'pkcs8',
binaryDer,
{
name: 'RSA-PSS',
hash: 'SHA-256',
},
true,
['sign']
);
return privateKey;
}
I have tried the code in a StackBlitz but it is the same result. Does it make sense to someone, that can explain what I am doing wrong?
https://stackblitz.com/edit/js-kg244r?file=index.js
I am trying to verify a HMAC signature using the SubtleCrypto API. The whole thing is supposed to run in Cloudflare Workers and I am testing it locally using their wrangler tool.
This is my code so far, but it generates the wrong signature.
const message = "(query params from an url)";
const given_signature = "(extracted from the query params)";
const SECRET = "...";
const algorithm = { name: 'HMAC', hash: 'SHA-256' };
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(SECRET),
algorithm,
false,
['sign', 'verify']
);
const signature = await crypto.subtle.sign(
algorithm.name,
key,
encoder.encode(message)
);
const digest = btoa(String.fromCharCode(...new Uint8Array(signature)));
// The digest does not match the signature extracted from the query params
// If I, for example, want to verify the signature directly, the result is still false.
const verify = await crypto.subtle.verify(
algorithm.name,
key,
encoder.encode(given_signature),
encoder.encode(message)
);
If I am using the same secret and message in online HMAC testing tools, I am getting the correct results, so I am certain that there must be a bug in my code.
What I find interesting, is that the signature generated by my code is much shorter than the given one (e.g. 3fn0mhrebHTJMhtOyvRP5nZIhogX/M1OKQ5GojniZTM= vs ddf9f49a1ade6c74c9321b4ecaf44fe67648868817fccd4e290e46a239e26533).
Does anyone have an idea where I am going wrong?
Thanks to the helpful comments! The problem in a nutshell was that the provided signature was encoded as a HEX string, while the generated signature was a base64-encoded string.
To keep things clean, here is a working version that uses the crypto.subtle.verify function:
const message = "(query params from an url w/o the hmac signature)";
const given_signature = "(the given hmac signature extracted from the query params)";
const SECRET = "(my secret key)";
const hexToBuffer = (hex: string) => {
const matches = hex.match(/[\da-f]{2}/gi) ?? [];
const typedArray = new Uint8Array(
matches.map(function (h) {
return parseInt(h, 16);
})
);
return typedArray.buffer;
};
const algorithm = { name: "HMAC", hash: "SHA-256" };
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(SECRET),
algorithm,
false,
["sign", "verify"]
);
const result: boolean = await crypto.subtle.verify(
algorithm.name,
key,
hexToBuffer(given_signature),
encoder.encode(message)
);
I need to sign a message with crypto.sign() function in NodeJS to get a valid JWT.
I have a private key (base 64) like this:
Dm2xriMD6riJagld4WCA6zWqtuWh40UzT/ZKO0pZgtHATOt0pGw90jG8BQHCE3EOjiCkFR2/gaW6JWi+3nZp8A==
And I tried to get a signature:
const getJWT = () => {
const privateKey =
"Dm2xriMD6riJagld4WCA6zWqtuWh40UzT/ZKO0pZgtHATOt0pGw90jG8BQHCE3EOjiCkFR2/gaW6JWi+3nZp8A==";
const payload = {
iss: "test",
aud: "test.com",
iat: 1650101178,
exp: 1650101278,
sub: "12345678-1234-1234-1234-123456789123"
};
const token = encode(payload, privateKey);
return token
};
const encode = (payload, key) => {
const header = {
typ: "JWT",
alg: "EdDSA"
};
const headerBase64URL = base64url(JSON.stringify(header));
const payloadBase64URL = base64url(JSON.stringify(payload));
const msg = Buffer.from(`${headerBase64URL}.${payloadBase64URL}`);
const keyDecoded = Buffer.from(key, "base64");
const signature = crypto.sign("Ed25519", msg, keyDecoded); //Here is the problem
const signatureBase64url = base64url(Buffer.from(signature));
return `${msg}.${signatureBase64url}`;
};
I received this error:
internal/crypto/sig.js:142
return _signOneShot(keyData, keyFormat, keyType, keyPassphrase, data,
^
Error: error:0909006C:PEM routines:get_name:no start line
library: 'PEM routines',
function: 'get_name',
reason: 'no start line',
code: 'ERR_OSSL_PEM_NO_START_LINE'
How can I adapt my private key to a valid format?
The crypto.sign() method requires for Ed25519 a private key in PKCS#8 format. Your key is a raw key consisting of the concatenation of the raw private 32 bytes key and the raw public 32 bytes, base64 encoded. A DER encoded PKCS#8 key can be derived and imported as follows:
Base64 decode your key. Use the first 32 bytes of your raw 64 bytes key (i.e. the raw private key).
Concat the following prefix for a private Ed25519 key (hex): 302e020100300506032b657004220420
Import that DER encoded PKCS#8 key.
Accordingly, the key import in getJWT() must be changed as follows:
const privateKey = toPkcs8der('Dm2xriMD6riJagld4WCA6zWqtuWh40UzT/ZKO0pZgtHATOt0pGw90jG8BQHCE3EOjiCkFR2/gaW6JWi+3nZp8A==');
with
const toPkcs8der = (rawB64) => {
var rawPrivate = Buffer.from(rawB64, 'base64').subarray(0, 32);
var prefixPrivateEd25519 = Buffer.from('302e020100300506032b657004220420','hex');
var der = Buffer.concat([prefixPrivateEd25519, rawPrivate]);
return crypto.createPrivateKey({key: der, format: "der", type: "pkcs8"})
}
Furthermore, in the encode() function:
Remove the line const keyDecoded = Buffer.from(key, "base64")
Create the signature with
const signature = crypto.sign(null, msg, key)
Note that for Ed25519, a null must be passed as first parameter in the sign() call. The algorithm comes from the key.
With these changes, the NodeJS code returns the following JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJ0ZXN0IiwiYXVkIjoidGVzdC5jb20iLCJpYXQiOjE2NTAxMDExNzgsImV4cCI6MTY1MDEwMTI3OCwic3ViIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MTIzIn0.f7WG_02UKljrMeVVOTNNBAGxtLXJUT_8QAnujNhomV18Pn5cU-0lHRgVlmRttOlqI7Iol_fHut3C4AOXxDGnAQ
that matches the expected JWT.
I'm using openpgp.js version 2.2.1.
So I've managed to encrypt a message just fine
const options = {
data: voteObject.option, // input as Message object
publicKeys: (pgp.key.readArmored(pubkey)).keys, // for encryption
};
pgp.encrypt(options).then(function(ciphertext) {
console.log(ciphertext.data);
});
This logs the encrypted message. The problem I'm now having is that I can't decrypt it. I'm at a complete loss at this point and to be honest I've tried everything to the point I don't know what I'm doing anymore. I know this isn't much to work with but I don't really have anything else to give.
Any suggestions at all would be a huge help!
I think you are mixing up the passphrase for a key and the password for "simply" encrypting a string.
Usually, in PGP a sender is encrypting a message with the receiver's public key. The receiver of the message can then decrypt his private key with his secret passphrase and with the resulting decrpyted private key he can decrypt the message.
I added a working example below:
Encryption
const receiverPublicKey = ...;
let publicKeys = (await openpgp.key.readArmored(receiverPublicKey)).keys;
let options = {
data: 'Hello, World!',
publicKeys: publicKeys
};
return openpgp.encrypt(options)
.then((encryptedMessageObject) => {
return encryptedMessageObject.data; // -----BEGIN PGP MESSAGE----- ... wcBMA0rHUQJA4dCdAQg...
});
Decryption
const receiverPrivateKey = ...;
const receiverPassphrase = 'secret';
const encryptedMessage = '-----BEGIN PGP MESSAGE----- ... wcBMA0rHUQJA4dCdAQg...';
let privKeyObj = (await openpgp.key.readArmored(receiverPrivateKey)).keys[0];
await privKeyObj.decrypt(receiverPassphrase);
let options = {
message: await openpgp.message.readArmored(encryptedMessage),
privateKey: privKeyObj
};
return openpgp.decrypt(options)
.then((plaintextObject) => {
return plaintextObject.data; // Hello, World!
});
This is the usual process of using PGP with one sender and one receiver (note that the signing of the message and checking the signature is missing).
Now there's also password in the decrypt options.
For that, see the example from the docs:
var options, encrypted;
options = {
data: 'Hello, World!', // input as String
passwords: ['secret stuff'] // multiple passwords possible
};
openpgp.encrypt(options).then(function(ciphertext) {
encrypted = ciphertext.data; // '-----BEGIN PGP MESSAGE ... END PGP MESSAGE-----'
});
options = {
message: openpgp.message.readArmored(encrypted), // parse armored message
password: 'secret stuff' // decrypt with password
};
openpgp.decrypt(options).then(function(plaintext) {
return plaintext.data; // 'Hello, World!'
});
In this case, a password is used to encrypt and decrypt a message - no public or private key at all.
I hope that helps!
Golang code is as below
func GenerateClientToken(secret, user, timestamp, info string) string {
token := hmac.New(sha256.New, []byte(secret))
token.Write([]byte(user))
token.Write([]byte(timestamp))
token.Write([]byte(info))
return hex.EncodeToString(token.Sum(nil))
}
How can I convert from this to reactjs code.
I am trying like this
import CryptoJS from 'crypto-js'
generateClientToken(secret, user, timestamp, info) {
var hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, secret);
hmac.update(user);
hmac.update(timestamp);
hmac.update(info);
var hash = hmac.finalize();
console.log("hmac: ", hash.toString(CryptoJS.enc.Base64))
console.log("hmac: ", hash.toString(CryptoJS.enc.Hex))
}
but result is not same with golang result. What am I wrong? and How will I do?
Go code: https://play.golang.org/p/7pXgn5GPQm
React:
Package used: "crypto-js": "^3.1.9-1"
React v15.4.2
Inside a React Component, a function:
generateClientToken(secret, user, timestamp, info) {
let hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, secret);
hmac.update(user);
hmac.update(timestamp);
hmac.update(info);
let hash = hmac.finalize();
console.log("hmac: ", hash.toString(CryptoJS.enc.Hex))
}
Inside render()
const secret = "test";
const user = "Dennis";
const timestamp = "1";
const info = "qwerty";
this.generateClientToken(secret, user, timestamp, info);