Node.js / SAML: How to decrypt contents of RequestedSecurityToken - javascript

I'm using the passport-wsfed-saml2, the passport strategy for both WS-fed and the SAML2 protocol.
The WS-fed part of the logic does not seem to handle RequestedSecurityToken elements whose content is an <xenc:EncryptedData> element.
This makes the strategy incompatible with ADFS 2.0 relying parties that have an encryption certificate specified.
I'd like to monkey-patch the strategy's WsFederation.extractToken method with some decryption logic.
Below is a sample of the RequestSecurityTokenResponse xml that I would like to preprocess. How should I go about decrypting the token? Specifically, how do I use the information provided in the <KeyInfo> element in conjunction with the <xenc:CipherData> element to access the plaintext token data.
<?xml version="1.0" encoding="UTF-8"?>
<t:RequestSecurityTokenResponse xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
<t:Lifetime>
<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2017-01-05T21:02:07.193Z</wsu:Created>
<wsu:Expires xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2017-01-05T22:02:07.193Z</wsu:Expires>
</t:Lifetime>
<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
<wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
<wsa:Address>https://localhost</wsa:Address>
</wsa:EndpointReference>
</wsp:AppliesTo>
<t:RequestedSecurityToken>
<xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Type="http://www.w3.org/2001/04/xmlenc#Element">
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc" />
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<e:EncryptedKey xmlns:e="http://www.w3.org/2001/04/xmlenc#">
<e:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p">
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
</e:EncryptionMethod>
<KeyInfo>
<o:SecurityTokenReference xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<X509Data>
<X509IssuerSerial>
<X509IssuerName>CN=token-signing, OU=SomeOrg, O=EvilCorp, L=Williston, S=VT, C=US</X509IssuerName>
<X509SerialNumber>13135613350938963680</X509SerialNumber>
</X509IssuerSerial>
</X509Data>
</o:SecurityTokenReference>
</KeyInfo>
<e:CipherData>
<e:CipherValue>v6ueRi+G+s31b9RZxE1X8gfNWk6qC9EWimhmDQzLOl/9HQrToqcLRNVqpdocfAgAGp3RkyR9IcwED7PZkreNNzEYMN3pntqS1372Nk6EEYwJSVmWkXmsv4m+xeJvGPQrDIZOwlq22OBt0EAwXoq7LvkmF0s/uhB4TItD47iAsDOFThMpuPoYo0EDLgPWzHtrZqTsC33c10zKKgyynSJPAKaC/+a9mSc4uxq55njU4GLVP/p4FvubPF2U1j4I7ozRGGWsAD5iTGwIOIF7H/ftKoRGIoFen29Ud87mm00BrF0GSUzcxTX+isMfI+HWp8u9zaO1ZLge5+x12BJcVWOYwblTQ7IPWyCMmaUscGgQPZ82ROrMCbX2f6HcGHtl8rwzXbz/VfAZkkxXZAfq9NRjSIRcmVtwC4cjwPAAcwE6V8+lvFn/2dUgzSz9y5K4HpzWZc2jg91oyzhFV+5luC+NV2HPAtTshjWOWhAcVuZYdINfcU1rSHKirBtDPQjxEWcyxkGyrl6UfWq1sEDuaXBPVNWT9/jyjuf7Rzyxnype8SleTK197FnD+rq6NzG9H4MpTFwhgokiPx4/RONjog7I1qnNM5wFybJ5WvkSh+x1w1w7/CNGipJSXCy3swGuSgSF3LI1bUZSzL+JqhUmYq8EVxW31TPe7JbBwMdvnGl7e6Q=</e:CipherValue>
</e:CipherData>
</e:EncryptedKey>
</KeyInfo>
<xenc:CipherData>
<xenc:CipherValue>.... whole bunch of base64 encoded data ....</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedData>
</t:RequestedSecurityToken>
<t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>
<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
<t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>
</t:RequestSecurityTokenResponse>

The <RequestedSecurityToken> element contains an <EncryptedData> element. The EncryptedData element is comprised of three parts:
1. EncryptionMethod, in my case it's aes256-cbc:
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc" />
2. KeyInfo, in my case this is the AES encryption key, but the key has been encrypted using the encryption certificate configured in ADFS. We'll need to decrypt the key using the RSA private key of the encryption certificate, giving us the AES encryption key/password that can be used to decrypt the SAML security token:
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<e:EncryptedKey xmlns:e="http://www.w3.org/2001/04/xmlenc#">
<e:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p">
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
</e:EncryptionMethod>
<KeyInfo>
<o:SecurityTokenReference xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<X509Data>
<X509IssuerSerial>
<X509IssuerName>CN=token-signing, OU=SomeOrg, O=EvilCorp, L=Williston, S=VT, C=US</X509IssuerName>
<X509SerialNumber>13135613350938963680</X509SerialNumber>
</X509IssuerSerial>
</X509Data>
</o:SecurityTokenReference>
</KeyInfo>
<e:CipherData>
<e:CipherValue>v6ueRi+G+s31b9RZxE1X8gfNWk6qC9EWimhmDQzLOl/9HQrToqcLRNVqpdocfAgAGp3RkyR9IcwED7PZkreNNzEYMN3pntqS1372Nk6EEYwJSVmWkXmsv4m+xeJvGPQrDIZOwlq22OBt0EAwXoq7LvkmF0s/uhB4TItD47iAsDOFThMpuPoYo0EDLgPWzHtrZqTsC33c10zKKgyynSJPAKaC/+a9mSc4uxq55njU4GLVP/p4FvubPF2U1j4I7ozRGGWsAD5iTGwIOIF7H/ftKoRGIoFen29Ud87mm00BrF0GSUzcxTX+isMfI+HWp8u9zaO1ZLge5+x12BJcVWOYwblTQ7IPWyCMmaUscGgQPZ82ROrMCbX2f6HcGHtl8rwzXbz/VfAZkkxXZAfq9NRjSIRcmVtwC4cjwPAAcwE6V8+lvFn/2dUgzSz9y5K4HpzWZc2jg91oyzhFV+5luC+NV2HPAtTshjWOWhAcVuZYdINfcU1rSHKirBtDPQjxEWcyxkGyrl6UfWq1sEDuaXBPVNWT9/jyjuf7Rzyxnype8SleTK197FnD+rq6NzG9H4MpTFwhgokiPx4/RONjog7I1qnNM5wFybJ5WvkSh+x1w1w7/CNGipJSXCy3swGuSgSF3LI1bUZSzL+JqhUmYq8EVxW31TPe7JbBwMdvnGl7e6Q=</e:CipherValue>
</e:CipherData>
</e:EncryptedKey>
</KeyInfo>
3. CipherData: the SAML security token, encrypted using AES.
<xenc:CipherData>
<xenc:CipherValue>.... whole bunch of base64 encoded data ....</xenc:CipherValue>
</xenc:CipherData>
Solution
Here's the code to monkey patch the passport-wsfed-saml2 library's WsFederation.extractToken method:
import WsFederation = require('passport-wsfed-saml2/lib/passport-wsfed-saml2/wsfederation');
import { createPrivateKey } from 'ursa-purejs';
import { createDecipheriv, randomBytes } from 'crypto';
import { DOMParser } from 'xmldom';
import { readFileSync } from 'fs';
const tokenSigningKey = createPrivateKey(readFileSync('./certs/token-signing.pem'));
const parser = new DOMParser();
WsFederation.prototype.standardExtractToken = WsFederation.prototype.extractToken;
WsFederation.prototype.extractToken = function (this: any, req: string) {
const token: Element | null = this.standardExtractToken(req);
// Is the SAML token encrypted?
if (!token || token.nodeName !== 'xenc:EncryptedData') {
// no. return it.
return token;
}
// We need to decrypt the SAML token...
// Grab the CipherValue elements. There will be two:
// 0. The encryption key for the SAML token, encrypted by ADFS using the rsa-oaep-mgf1p
// algo and the public key of the encryption certificate configured in the relying party.
// 1. The SAML token, encrypted using the aes-256-cbc algo with the key from #0 ^^^
const ciphers = token.getElementsByTagNameNS('http://www.w3.org/2001/04/xmlenc#', 'CipherValue');
const aesPasswordCipher = <string>ciphers[0].textContent;
const samlTokenCipher = <string>ciphers[1].textContent;
// Decrypt the password for the SAML token.
const aesPassword = tokenSigningKey.decrypt(aesPasswordCipher, 'base64');
// Decrypt the SAML token.
const decipher = createDecipheriv('aes-256-cbc', aesPassword, randomBytes(16));
let saml = decipher.update(new Buffer(samlTokenCipher, 'base64'), 'binary', 'utf8');
saml += decipher.final('utf8');
// Parse the XML and return the token.
return parser.parseFromString(saml);
};
Useful link: https://coolaj86.com/articles/asymmetric-public--private-key-encryption-in-node-js/

Related

Is secure to use "createCipheriv" from "crypto" to ensure authenticity on a API call?

Context:
To contextualize I have an app (React Native) that will need to use a third-party login API (A E-commerce SASS platform), the said API will not allow me to use the returns from other than the same subdomain to redirect, my idea is to create a node.js service inside the platform with that I make my API endpoint check for a token generated from a unique key using createCipheriv, if it all matches return to the correct URL, note that this will not interfere in the login process, I just wish to secure the origin of the requests.
The flow would be like so:
1. App + "?returnUrl={API}&token={...}"
2. Login + "&token={...}" + Redirect
3. API Check origin + Redirect
4. Return to App
Just an example of the process:
Service:
const { createCipheriv, randomBytes, createDecipheriv } = require('crypto');
/// Cipher
const message = "USER_EMAIL"; // Shared Info
const key = "SECRET_KEY"; // Shared secret
const iv = randomBytes(16); // Shared IV
const cipher = createCipheriv('aes256', key, iv);
/// Encrypt
const encryptedMessage = cipher.update(message, 'utf8', 'hex') + cipher.final('hex');
Payload:
{
"token": "...",
"iv": "..."
}
RN App:
const { token, iv } = payload
const key = "SECRET_KEY"; // Shared secret
/// Decrypt
const decipher = createDecipheriv('aes256', key, iv);
const decryptedMessage = decipher.update(encryptedMessage, 'hex', 'utf-8') + decipher.final('utf8');
console.log(`Deciphered: ${decryptedMessage.toString('utf-8')}`);

AWS Signature V2: generate a signature for a Query request in Javascript

i am trying to re-create AWS signature version 2 authentication on javascript, what i have right now is
String.prototype.getBytes = () => {
return this.toString()
.split('')
.map((i) => i.charCodeAt(0));
};
let key = 'redacted_access_key_id';
const bytes = key.getBytes();
let signingKey = crypto.HmacSHA256(bytes, key);
let data = JSON.stringify({ lang: 'en', pageNumber: 0, pageSize: 20 });
const contentMd5 = crypto.MD5(data).toString();
data = data.getBytes();
signingKey = crypto.HmacSHA256(data, key);
const result = Buffer.from(signingKey.toString()).toString('base64');
Which outputs something like
ZGY0MmI3MDVjNmJlNzY5ZWYwZjU1ZTc5MDhhOGNkYzI3ZWVjYzQ5ODBmY2M1NGI5NTc2MmVmNTY1NzEwNjhhMA== 
which is incorrect, because the hash should be exactly 28 characters in length. Now the AWS signature version 2 auth docs show how it is being made, but only in java
import java.security.SignatureException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import com.amazonaws.util.*;
/**
* This class defines common routines for generating
* authentication signatures for AWS Platform requests.
*/
public class Signature {
private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
public static String calculateRFC2104HMAC(String data, String key)
throws java.security.SignatureException
{
String result;
try {
// Get an hmac_sha256 key from the raw key bytes.
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes("UTF-8"), HMAC_SHA256_ALGORITHM);
// Get an hmac_sha256 Mac instance and initialize with the signing key.
Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
mac.init(signingKey);
// Compute the hmac on input data bytes.
byte[] rawHmac = mac.doFinal(data.getBytes("UTF-8"));
// Base64-encode the hmac by using the utility in the SDK
result = BinaryUtils.toBase64(rawHmac);
} catch (Exception e) {
throw new SignatureException("Failed to generate HMAC : " + e.getMessage());
}
return result;
}
}
I am trying to recreate this exact same code in javascript but something is wrong. Can someone please help me with this, i cant find any examples in javascript.
Thank you.
The following code is the equivalent of the Java version of calculateRFC2104HMAC in JS.
const CryptoJS = require('crypto-js');
const calculateRFC2104HMAC = (data, key) => {
const rawHmac = CryptoJS.HmacSHA256(CryptoJS.enc.Utf8.parse(data), CryptoJS.enc.Utf8.parse(key));
return CryptoJS.enc.Base64.stringify(rawHmac);
}
Sample usage based on the example on AWS Signature V2 page
const urlSafeSignature = (data, key) => encodeURIComponent(calculateRFC2104HMAC(data, key));
const data =
`GET
elasticmapreduce.amazonaws.com
/
AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Action=DescribeJobFlows&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2011-10-03T15%3A19%3A30&Version=2009-03-31`
const key = `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY`
console.log(urlSafeSignature(data, key));
The documentation advises to use AWS Signature V4 which has a AWS published library on NPM here. The AWS signed requests are for AWS Services and the signature in the request helps validating the request, prevents replay attacks. I'm not sure what you are trying to send in the following code and for which AWS service.
let data = JSON.stringify({ lang: 'en', pageNumber: 0, pageSize: 20 });
You must provide all details required to sign a request as per the AWS documentation.

How to decode in PHP RSA OAEP encoded in Javascript

So, I have some encryption/decryption issues …
I encrypt data in javascript thanks to node-forge and I try to decrypt it into PHP thanks to openssl_private_decrypt.
On the PHP side, I use the «OPENSSL_PKCS1_OAEP_PADDING» padding. So, on the javascript side I tried to configure forge to encrypt data with RSA-OAEP.
And when I try to decsypt the message on the PHP side, I have these errors :
error:04099079:rsa routines:RSA_padding_check_PKCS1_OAEP_mgf1:oaep decoding error
error:04065072:rsa routines:rsa_ossl_private_decrypt:padding check failed
I've tried to configure the encryption with sha1 message digest and sha1 for mgf1 option. I've also tried without any option (if I remember, forge use SHA256 by default). But there is nothing to do, I always have the same error …
javascript
const pubkey = `-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
`;
const privkey = `-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
`;
let publicKey = forge.pki.publicKeyFromPem(pubkey);
let key = forge.random.getBytesSync(32);
let encKey = publicKey.encrypt(key, 'RSA-OAEP', {
md: forge.md.sha1.create(),
mgf1: {
md: forge.md.sha1.create()
}
});
let b64Key = encodeURIComponent(btoa(enckey));
Next, I send the key in url with the "xcem" param, thanks to HttpClient. And I receive it in PHP.
php
$privKey = "";
$b64Key = urldecode($_GET['xcem']);
$encKey = base64_decode($b64Key);
$key = null;
if (!openssl_private_decrypt($encKey, $key, file_get_contents('/keys/openssl_private.key'), OPENSSL_PKCS1_OAEP_PADDING))
{
$errorssl = [];
while ($error = openssl_error_string()) {
$errorssl[] = $error;
}
throw new Exception("Erreur lors du décryptage du message ! " . json_encode($errorssl));
}
When I send Data between 2 PHP servers, there is no problem …
But I can't make it work between JS and PHP … I need some help ^^
Sooo …
After some tests, and headaches I found the problem … In my original code (not the one here) I sent the key, the init vector, the signature and the encrypted data …
But in my PHP, I tried to decode the signature … I didn't send data in the right order …
My bad …

Java sign/verify keys and Javascript WebCrypto verification fails

I'm trying to
generate sign/verification keys (RSA)
sign a value (using those keys) on a Java web application (lets call server-side)
in order to a web client to verify - public-key imported as RSASSA-PKCS1-v1_5 + SHA-256, (in a browser, using WebCrypto API / client-side)
I'm having problems verifying the signed value (signed in the Java server-side) even though the public sign/verify key is successfully imported as a JWK in the client side.
I was wondering if there is any algorithm compatibility issue in any of the steps (OpenSSL, Java or Javascript) that I may be encountering.
The OpenSSL commands used to generate the keys
openssl genrsa -out privatekey.pem 2048
openssl rsa -in privatekey.pem -pubout > publickey.pub
openssl pkcs8 -topk8 -inform PEM -outform DER -in privatekey.pem -out privatekey-pkcs8.pem
Importing keys with Java (server-side)
public static KeyPair generateSignKeyPair() throws ... {
byte[] privBytes = b64ToByteArray(PRIVATE_KEY_PEM_VALUE);
byte[] pubBytes = b64ToByteArray(PUBLIC_KEY_PEM_VALUE);
// private key
KeySpec keySpec = new PKCS8EncodedKeySpec(privBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
// public key (javaPubSignKey)
X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(pubBytes);
PublicKey publicKey = keyFactory.generatePublic(X509publicKey);
return new KeyPair(publicKey, privateKey);
}
Signing a value with Java (server-side)
public static byte[] generateSignature(PrivateKey signPrivateKey, byte[] data) throws ... {
Signature dsa = Signature.getInstance("SHA256withRSA");
dsa.initSign(signPrivateKey);
dsa.update(data);
return dsa.sign();
}
Send them to a web-app for the WebCrypto API to verify as a client/browser (the client is aware of the publicKey generated in the first step).
// Import public sign/verify key (javaPubSignVerifyKey)
var signatureAlgorithm = {
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {
name: 'SHA-256'
}
};
// JWK format (1)
crypto.subtle.importKey(
'jwk', javaPubSignVerifyKey, signatureAlgorithm, false, ['verify']
).then(success, error);
function success(key) {
signatureVerifyPublicKey = key;
}
Note (1): on the Java side, I'm using com.nimbusds.jose.jwk.JWK to export the publicKey to JWK format.
The sign key is successfully imported by WebCrypto. But when it comes to the verification, it fails (the verification boolean is false).
crypto.subtle.verify(
signatureAlgorithm,
signatureVerifyPublicKey,
signature, // bytes in Int8Array format (2)
data // bytes in Int8Array format
).then(
function (valid) {
// valid === false
}
)
Note (2): also note that every example I found on WebCrypto used Uint8Array to represent byte arrays, but since Java generates signed byte-arrays I need to use Int8Array so that the signature values are not contaminated (maybe this is an issue aswell).
EDIT: for reference, it turned out to be another unrelated issue - I was converting the expected data from base64 twice in Javascript without noticing it; naturally the verification failed.
Please, check this simple code based on yours to import a RSA public key (spki) and verify a signature. I have generated the keys and signature using similar Java code
var publicKeyB64 = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVdZDEs6htb3oxWstz7q+e5IwIRcptMNJiyemuoNyyjtiOy+0tEodjgo7RVoyUcGU3MysEivqvKdswQZ4KfwQCBLAR8DRzp3biAge5utZcKsQoQaC1rCEplfmzEo5ovIlBcMq5x1BxnrnlwEPRmM7MefRa+OeAOQJcstHcrJFO7QIDAQAB";
var dataB64 = "aGVsbG8=";
var signatureB64 = "aEOmUA7YC5gvF6QgH+TMg0erY5pzr83nykZGFtyGOOe+6ld+MC4/Qdb608XiNud+pBpzh0wqd6aajOtJim5XEfCH8vUPsv45aSPtukUIQTX00Oc1frIFDQI6jGJ4Q8dQYIwpqsyE2rkGwTDzt1fTTGiw54pLsJXjtL/D5hUEKL8=";
var signatureAlgorithm = {name: 'RSASSA-PKCS1-v1_5',modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]),hash: { name: 'SHA-256' }};
//convert public key, data and signature to ArrayBuffer.
var publicKey = str2ab(atob(publicKeyB64));
var data = str2ab(atob(dataB64));
var signature = str2ab(atob(signatureB64));
crypto.subtle.importKey("spki", publicKey, signatureAlgorithm, false,["verify"]).
then(function(key){
console.log(key);
return crypto.subtle.verify( signatureAlgorithm, key, signature, data);
}).then( function (valid) {
console.log("Signature valid: "+valid);
}).catch(function(err) {
alert("Verification failed " + err );
});
I could not reproduce exactly the issue. Using the str2ab utility function you have linked, the code works perfectly.
//Utility function
function str2ab(str) {
var arrBuff = new ArrayBuffer(str.length);
var bytes = new Uint8Array(arrBuff);
for (var iii = 0; iii < str.length; iii++) {
bytes[iii] = str.charCodeAt(iii);
}
return bytes;
}
I suggest to compare both codes to find the differences

Web Crypto API JWK usage in Python

I am developing a P2P Infrastructure that will have data from a set of different applications, distributed through the network. This P2P overlay is composed by a set of Python Twisted Servers.
I need to guarantee the security and privacy of the stored data, for each user of each application. Consequently, I am generating pairs of RSA keys in the client side of the web app, using the Web Crypto API. The RSA key pairs will be stored in the P2P overlay as well. So, I cipher on the client side, the private keys with a derivation of the user password.
In addition, I am using jwk to pem module to convert the JWK public key into a PEM key, to be used in the Python Cryptography library (PyCrypt or m2Crypto).
Finally, I have to guarantee that the message containing those credentials, as well as the user data , maintain its integrity. Therefore, in the client side, I am signing this data with the user's private key.
I send the data, as well as the signature, both in ArrayBuffer type to the server, encoded in base64.
function signData(private_key, data, callback){
var dataForHash = str2ab(JSON.stringify(sortObject(data)));
computeSHA(dataForHash, "SHA-256", function(hash){
signRSA(private_key, hash, function(data){
callback(data.buffer.b64encode(), dataForHash.b64encode());
});
});
}
function computeSHA(data, mode, callback){
window.crypto.subtle.digest(
{
name: mode,
},
data
)
.then(function(hash){
callback(new Uint8Array(hash).buffer);
})
.catch(function(err){
console.error(err);
});
}
function signRSA(private_key, data, callback){
window.crypto.subtle.sign(
{
name: "RSASSA-PKCS1-v1_5",
},
private_key,
data
)
.then(function(signature){
callback(new Uint8Array(signature));
})
.catch(function(err){
console.error(err);
});
}
ArrayBuffer.prototype.b64encode = function(){
return btoa(String.fromCharCode.apply(null, new Uint8Array(this)));
};
Afterwards, when the Python Server receives this http request, it decodes data and signature from base64.
dataForHash = base64.b64decode(dataReceived['data'])
signature = base64.b64decode(dataReceived['signature'])
For validating the signature, the public key is needed. Consequently:
data = utils.byteify(json.loads(dataForHash.decode("utf-16")))
pub_key = base64.b64decode(data['pub_key']) # Get PEM Public Key
(utils.byteify() converts unicode string to regular strings)
Verifying signature:
Authentication.verifySignature(signature, dataForHash, pub_key)
Method definition:
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
def verifySignature(signature, data, pub_key):
key = RSA.importKey(pub_key)
h = SHA256.new(data)
verifier = PKCS1_v1_5.new(key)
return verifier.verify(h, signature)
However, the signature verification returns False. I have also tried to use the m2crypto library, but it returns 0.
I managed to find the problem.
Although in Python (PyCrypto) the sign function should receive the hash of the data to sign, using the Web Cryptography API, the sign method applies a hash function to the received data before signing it.
Consequently, the data in JS was being hashed twice, one before invoking the sign method and one in the sign method, before creating the signature.
function signData(private_key, data, callback){
var dataForHash = str2ab(JSON.stringify(sortObject(data)));
signRSA(private_key, dataForHash, function(data){
callback(data.buffer.b64encode(), dataForHash.b64encode());
});
}
ArrayBuffer.prototype.b64encode = function(){
return btoa(String.fromCharCode.apply(null, new Uint8Array(this)));
};
String.prototype.b64decode = function(){
var binary_string = window.atob(this);
var len = binary_string.length;
var bytes = new Uint8Array(new ArrayBuffer(len));
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes;
};
With this modification, the verification in python returns True now.

Categories