I'm working on authentication for my JSON-RPC API and my current working strategy is using signed requests sent via POST over SSL.
I'm wondering if anyone can see any vulnerabilities that I haven't taken into consideration with the following signature method.
All communication between the client and the server is done via POST requests sent over SSL. Insecure http requests are denied outright by the API server.
Dependencies
var uuid = require('node-uuid');
var crypto = require('crypto');
var moment = require('moment');
var MyAPI = require('request-json').newClient('https://api.myappdomain.com');
Dependency Links: node-uuid, crypto, moment, request-json
Vars
var apiVersion = '1.0';
var publicKey = 'MY_PUBLIC_KEY_UUID';
var secretKey = 'MY_SECRET_KEY_UUID';
Request Object
var request = {
requestID : uuid.v4(),
apiVersion : apiVersion,
nonce : uuid.v4(),
timestamp : moment.utc( new Date() ),
params : params
}
Signature
var signature = crypto.createHmac('sha512',secretKey).update(JSON.stringify(request)).digest('hex');
Payload Packaging (Sent as cleartext via POST over SSL)
var payload = {
request: request,
publicKey : publicKey,
signature : signature
}
Resultant Payload JSON Document
{
"request" : {
"requestID" : "687de6b4-bb02-4d2c-8d3a-adeacd2d183e",
"apiVersion" : "1.0",
"nonce" : "eb7e4171-9e23-408a-aa2b-cd437a78af22",
"timestamp" : "2014-05-23T01:36:52.225Z",
"params" : {
"class" : "User"
"method" : "getProfile",
"data" : {
"id" : "SOME_USER_ID"
}
}
},
"publicKey" : "PUBLIC_KEY",
"signature" : "7e0a06b560220c24f8eefda1fda792e428abb0057998d5925cf77563a20ec7b645dacdf96da3fc57e1918950719a7da70a042b44eb27eabc889adef95ea994d1",
}
POST Request
MyAPI.post('/', payload, function(response){
/// Handle any errors ...
/// Do something with the result ...
/// Inspect the request you sent ...
});
Server-Side
And then on the server-side the following occurs to authenticate the request:
PUBLIC_KEY is used to lookup the SECRET_KEY in the DB.
SECRET_KEY is used to create an HMAC of the request object from the payload.
The signature hash sent in the payload is compared to the hash of the request object created on the server. If they match, we move on to authenticating the timestamp.
Given that we can now trust the timestamp sent in the cleartext request object since it was included in the signature hash sent from the client, the timestamp is evaluated and the authentication is rejected if the request is too old. Otherwise, the request is authenticated.
So far as I understand, this is a secure method for signing and authentication requests sent over SSL. Is this correct?
Thanks in advance for any help.
Update on JSON Property Order
The order of properties when using JSON.stringify is essentially random, which could cause signature mis-matches.
Using this signing process over the past few weeks I haven't run into any hash mis-match issues due to the order of the properties in the JSON request object. I believe it's because I only stringify the request object literal once, right before the client-side hash is calculated. Then, the request object is in JSON format as part of the payload. Once received by the server, the hash is created directly from the JSON object received in the payload, there's no second JSON.stringify method invoked, so the signature always matches because the order of the properties is determined once, by the client. I'll keep looking into this though as it seems like a weak point, if not a security concern.
JSON.stringify does not guarantee order of properties. For example, object
{
a: 1,
b: 2
}
could be serialized in two ways: {"a":1,"b":2} or {"b":2,"a":1}. They are the same from JSON point of view but they will result it different HMACs.
Imaging, that for signings your JSON.stringify produced first form, but for checking signature second one. Your signature check will fail although signature was valid.
The only fishy thing I see here would be the JSON.stringify as posted in other comments, but you can use:
https://www.npmjs.com/package/json-stable-stringify
That way you can have a deterministic hash for you signs.
Related
I am using DocusSign connect webhook service and want to use HMAC Security to validate the request. To do this I have followed the instructions mentioned in
https://developers.docusign.com/esign-rest-api/guides/connect-hmac that is:
On our account on DocuSign, I have set for Connect the Include HMAC Signature and created a Connect Authentication Key.
Received the Connect message from Docusign connect containing the header with the data hashed with the application’s defined HMAC keys.
But facing the issue in 3rd step i.e. validating the HMAC signature using below code -
// x-docusign-signature headers
String headerSign = request.getHeader("X-DocuSign-Signature-1");
String secret = "....";
-------
public static boolean HashIsValid(String secret, String payload,
String headerSign)
throws InvalidKeyException, NoSuchAlgorithmException,
UnsupportedEncodingException {
String computedHash = ComputeHash(secret, payload);
boolean isEqual =
MessageDigest.isEqual(computedHash.getBytes("UTF-8"),
headerSign.getBytes("UTF-8"));
return isEqual;
}
------
public static String ComputeHash(String secret, String payload)
throws InvalidKeyException, NoSuchAlgorithmException {
String digest = "HmacSHA256";
Mac mac = Mac.getInstance(digest);
mac.init(new SecretKeySpec(secret.getBytes(), digest));
String base64Hash = new String(
Base64.getEncoder().encode(mac.doFinal(payload.getBytes())));
return base64Hash;
}
But it always returns false.
Anyone who has any idea why my hash code is different from the one received from DocuSign?
Either your comparison test is wrong or your payload variable is including too much or too little.
To test your comparison, print out computedHash and headerSign.
To test your payload value, print it out and check that it is the entire body of the POST request to your listener (your server).
Also check that you have exactly one X-DocuSign-Signature header. One way is to confirm that there is no value for header X-DocuSign-Signature-2
I've filed internal bug report DEVDOCS-4874 since the Java example has a bug.
I am using Transloadit API to merge audio file and series of images.
At some point, I need to retrieve list of assemblies (videos generated till now) for which transloadit provides a get API endpoint but that endpoint accepts two query strings, signature and params(to configure the list)
I am generating signature of the same params which is being sent as query string to the API along with it's signature but it is returning an error that signature doesn't match.
Transloadit have proper docs of how to create signature for each major language here https://transloadit.com/docs/#signature-authentication
Also the docs (https://transloadit.com/docs/api/#assemblies-get) doesn't state whether the signature will be generated of the same params or not.
Please help if anyone have used transloadit and had a same problem before and solved it now
I believe what your problem may be is that you're not URL encoding the JSON before passing it in your GET request. Here's a small snippet in Python showing how to turn a dictionary of values into JSON to generate a signature, and then into a URL encoded object for the GET request.
params = {
'auth': {
'key': auth_key,
'expires': expires
},
'template_id': template_id
}
# Converts the dictionary into JSON
message = json.dumps(params, separators=(',', ':'))
signature = hmac.new(auth_secret.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha1).hexdigest()
# URL encodes it
params_encoded = urllib.parse.quote_plus(message)
url = f'https://api2.transloadit.com/assemblies?signature={signature}¶ms={params_encoded}'
response = requests.get(url)
I need to validate Xero webhook in my node js project. This is Xero documentation steps to validate: https://developer.xero.com/documentation/webhooks/creating-webhooks#STATUS
var crypto = require("crypto")
function getHmacSha256(message, secret) {
return crypto.createHmac("sha256", secret).update(message).digest("base64")
}
// webhookPayload and signature get from webhook body and header
const webhookPayload = {
events: [],
firstEventSequence: 0,
lastEventSequence: 0,
entropy: 'OSHPXTUSXASRFBBCJFEN'
}
const signature = "OXLaeyZanKI5QDnLkXIVB35XrZygYsPMeK8WfoXUMU8="
const myKey = "1y5VYfv7WbimUQIMXiQCB6W6TKIp+5ZZJNjn3Fsa/veK5X/C8BZ4yzvPkmr7LvuL+yfKwm4imnfAB5tEoJfc4A=="
var hash = getHmacSha256(JSON.stringify(webhookPayload), myKey)
//If the payload is hashed using HMACSHA256 with your webhook signing key and base64 encoded, it should match the signature in the header.
if (signature === hash) {
return res.status(200).end()
}else{
return res.status(401).end()
}
Every time my signature and hash are different so it returns with 401 every time.
So I failed to complete Intent to receive
From what you're describing, my guess is you are unintentionally modifying the request body. You need to accept the raw request body from the webhook event without modification. If this body is modified at all, your code will fail to verify the signature and will fail Xero’s “Intent to receive” validation. Check out this blog post for details.
I would like to send a string (JSON formatted) to my webservice instead using a DTO.
var client = new JsonServiceClient(absoluteUrl);
client.Post<T>(absoluteUrl, data);
But, after to do change my data (DTO object) to a JSON string, I'm getting a ServiceStack Exception: Internal Server Error. Looks like the ServiceStack Post/Send method changes my JSON request.
Is it possible or is there any way to avoid it? Someone else had the same issue?
UPDATE 1: I'm using this approach because of the OAuth authentication. I'm generating a HMAC by request.
Thanks.
You can use HTTP Utils to send raw JSON, e.g:
var response = absoluteUrl.PostJsonToUrl(data)
.FromJson<T>();
ServiceStack's .NET Service Clients provide Typed API's to send Typed Request DTO's, it's not meant for POST'ing raw strings but I've just added support for sending raw string, byte[] and Stream in this commit so now you can send raw data with:
var requestPath = request.ToPostUrl();
string json = request.ToJson();
var response = client.Post<GetCustomerResponse>(requestPath, json);
byte[] bytes = json.ToUtf8Bytes();
response = client.Post<GetCustomerResponse>(requestPath, bytes);
Stream ms = new MemoryStream(bytes);
response = client.Post<GetCustomerResponse>(requestPath, ms);
This change is available from v4.0.43+ that's now available on MyGet.
Sharing Cookies with HttpWebRequest
To have different HttpWebRequests share the same "Session" you just need to share the clients Cookies, e.g. after authenticating with a JsonServiceClient you can share the cookies with HTTP Utils by assigning it's CookieContainer, e.g:
var response = absoluteUrl.PostJsonToUrl(data,
requestFilter: req => req.CookieContainer = client.CookieContainer)
.FromJson<T>();
After following the guide for creating a custom identity provider for azure mobile services I can easily generate the appropriate tokens. The code is pretty simple and looks like this:
var userAuth = {
user: { userId : userId },
token: zumoJwt(expiry, aud, userId, masterKey)
}
response.send(200, userAuth);
The definitions for the parameters and code for zumoJwt are located at the link. Azure automatically decodes the token and populates the user on the request object which is what I'd like to simulate.
Basically I'd like to to decrypt the token on the serverside via Node (not .net).
What I ended up doing to validate the token is the following (boiled down). This seems to be about what the azure mobile services is doing on routes that require authorization.
var jws = require('jsw'); // https://github.com/brianloveswords/node-jws
function userAuth() {
var token = ''; // get token as header or whatever
var key = crypto.createHash('sha256').update(global.masterKey + "JWTSig").digest('binary');
if (!jws.verify(token,key)) {
// invalid token logic
} else {
var decode = jws.decode(token)
req.user = {
userId: decode.payload.uid.split(';')[0].split('::')[0]
};
next();
}
}
app.use(authChecker);
The tokens aren't really encrypted - they're just signed. The tokens have the JWT format (line breaks added for clarity):
<header>, base64-encoded
"."
<envelope>, base64-encoded
"."
<signature>, base64-encoded
If you want to decode (not decrypt) the token in node, you can split the value in the . character, take the first two members, base64-decode them (var buffer = new Buffer(part, 'base64')), and convert the buffer to string (buffer.toString('utf-8')).
If you want to validate the token, just follow the same steps you need to re-sign the first two parts of the token (header + '.' + envelope) with the master key, in the same way that the token was created, and compare it with the signature you received on the original token.