Skip to content

Commit b351a46

Browse files
debugging signing with SigV4 - found IBM's signing code - adapting
1 parent 2e4130d commit b351a46

6 files changed

Lines changed: 231 additions & 5 deletions

File tree

src/bluemix/README.MD

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22

33

44
## Notes
5-
Cloud functions not currently supported in au-syd.
5+
Cloud functions not currently supported in au-syd.
6+
For Cloud Storage to generate Presigned URLS you need a credential with HMAC enabled.

src/bluemix/experiments/get.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,42 @@
1-
// Example with signed URL https://github.com/ibm-functions/template-cloud-object-storage/blob/master/runtimes/nodejs/actions/app.js
1+
'use strict';
2+
3+
const Storage = require('ibm-cos-sdk');
4+
5+
const config = require('./../config.json');
6+
const key = require('./../keys/ibm_storage.json');
7+
8+
const s3Config = {
9+
endpoint: 's3.au-syd.cloud-object-storage.appdomain.cloud',
10+
apiKeyId: key.apikey,
11+
ibmAuthEndpoint: 'https://iam.ng.bluemix.net/oidc/token',
12+
serviceInstanceId: key.resource_instance_id,
13+
};
14+
15+
let storage = new Storage.S3(s3Config);
16+
17+
/**
18+
* Returns a Signed URL to access the uploaded file
19+
* TODO: set expiry date to match the bucket's expiry rules for files
20+
*/
21+
function getURL(fileName, bucketName) {
22+
let params = { Bucket: bucketName, Key: fileName, Expires: 600 };
23+
return new Promise((resolve, reject) => {
24+
storage.getSignedUrl('getObject', params, function(err, url) {
25+
if (err) reject(err);
26+
else resolve(url);
27+
});
28+
});
29+
}
30+
31+
exports.get = function (fileName) {
32+
return getURL(fileName, config.bucket_name);
33+
};
34+
35+
36+
/*
37+
HMAC Requirements:
38+
- All requests must have an x-amz-date header with the date in %Y%m%dT%H%M%SZ format.
39+
- Any request that has a payload (object uploads, deleting multiple objects, etc.) must provide a x-amz-content-sha256 header with a SHA256 hash of the payload contents.
40+
- ACLs (other than public-read) are unsupported.
41+
42+
*/

src/bluemix/experiments/signing.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
'use strict';
2+
3+
// Initial template modifier from https://console.bluemix.net/docs/services/cloud-object-storage/hmac/presigned-urls.html#create-a-presigned-url
4+
5+
const crypto = require('crypto');
6+
const moment = require('moment');
7+
const https = require('https');
8+
9+
const config = require('./../config.json');
10+
const key = require('./../keys/ibm_storage.json');
11+
12+
const accessKey = key.cos_hmac_keys.access_key_id;
13+
const secretKey = key.cos_hmac_keys.secret_access_key;
14+
const httpMethod = 'GET';
15+
const host = '{endpoint}';
16+
const region = '';
17+
const endpoint = 'https://' + host;
18+
const bucket = 'example-bucket';
19+
const objectKey = 'example-object'
20+
const expiration = 86400 // time in seconds
21+
22+
// hashing and signing methods
23+
function hash(key, msg) {
24+
var hmac = crypto.createHmac('sha256', key);
25+
hmac.update(msg, 'utf8');
26+
return hmac.digest();
27+
}
28+
29+
function hmacHex(key, msg) {
30+
var hmac = crypto.createHmac('sha256', key);
31+
hmac.update(msg, 'utf8');
32+
return hmac.digest('hex');
33+
}
34+
35+
function hashHex(msg) {
36+
var hash = crypto.createHash('sha256');
37+
hash.update(msg);
38+
return hash.digest('hex');
39+
}
40+
41+
// region is a wildcard value that takes the place of the AWS region value
42+
// as COS doesn't use the same conventions for regions, this parameter can accept any string
43+
function createSignatureKey(key, datestamp, region, service) {
44+
keyDate = hash(('AWS4' + key), datestamp);
45+
keyString = hash(keyDate, region);
46+
keyService = hash(keyString, service);
47+
keySigning = hash(keyService, 'aws4_request');
48+
return keySigning;
49+
}
50+
51+
function createHexSignatureKey(key, datestamp, region, service) {
52+
keyDate = hashHex(('AWS4' + key), datestamp);
53+
keyString = hashHex(keyDate, region);
54+
keyService = hashHex(keyString, service);
55+
keySigning = hashHex(keyService, 'aws4_request');
56+
return keySigning;
57+
}
58+
59+
function printDebug() {
60+
// this information can be helpful in troubleshooting, or to better
61+
// understand what goes into signature creation
62+
console.log('These are the values used to construct this request.');
63+
console.log('Request details -----------------------------------------');
64+
console.log(`httpMethod: ${httpMethod}`);
65+
console.log(`host: ${host}`);
66+
console.log(`region: ${region}`);
67+
console.log(`endpoint: ${endpoint}`);
68+
console.log(`bucket: ${bucket}`);
69+
console.log(`objectKey: ${objectKey}`);
70+
console.log(`timestamp: ${timestamp}`);
71+
console.log(`datestamp: ${datestamp}`);
72+
73+
console.log('Standardized request details ----------------------------');
74+
console.log(`standardizedResource: ${standardizedResource}`);
75+
console.log(`standardizedQuerystring: ${standardizedQuerystring}`);
76+
console.log(`standardizedHeaders: ${standardizedHeaders}`);
77+
console.log(`signedHeaders: ${signedHeaders}`);
78+
console.log(`payloadHash: ${payloadHash}`);
79+
console.log(`standardizedRequest: ${standardizedRequest}`);
80+
81+
console.log('String-to-sign details ----------------------------------');
82+
console.log(`credentialScope: ${credentialScope}`);
83+
console.log(`string-to-sign: ${sts}`);
84+
console.log(`signatureKey: ${signatureKey}`);
85+
console.log(`signature: ${signature}`);
86+
87+
console.log('Because the signature key has non-ASCII characters, it is');
88+
console.log('necessary to create a hexadecimal digest for the purposes');
89+
console.log('of checking against this example.');
90+
91+
signatureKeyHex = createHexSignatureKey(secretKey, datestamp, region, 's3')
92+
93+
console.log(`signatureKey (hexidecimal): ${signatureKeyHex}`);
94+
95+
console.log('Header details ------------------------------------------');
96+
console.log(`pre-signed url: ${requestUrl}`);
97+
}
98+
99+
// assemble the standardized request
100+
var time = moment().utc();
101+
var timestamp = time.format('YYYYMMDDTHHmmss') + 'Z';
102+
var datestamp = time.format('YYYYMMDD');
103+
104+
var standardizedQuerystring = 'X-Amz-Algorithm=AWS4-HMAC-SHA256' +
105+
'&X-Amz-Credential=' + encodeURIComponent(accessKey + '/' + datestamp + '/' + region + '/s3/aws4_request') +
106+
'&X-Amz-Date=' + timestamp +
107+
'&X-Amz-Expires=' + expiration.toString() +
108+
'&X-Amz-SignedHeaders=host';
109+
110+
var standardizedResource = '/' + bucket + '/' + objectKey;
111+
112+
var payloadHash = 'UNSIGNED-PAYLOAD';
113+
var standardizedHeaders = 'host:' + host;
114+
var signedHeaders = 'host';
115+
116+
var standardizedRequest = httpMethod + '\n' +
117+
standardizedResource + '\n' +
118+
standardizedQuerystring + '\n' +
119+
standardizedHeaders + '\n' +
120+
'\n' +
121+
signedHeaders + '\n' +
122+
payloadHash;
123+
124+
// assemble string-to-sign
125+
var hashingAlgorithm = 'AWS4-HMAC-SHA256';
126+
var credentialScope = datestamp + '/' + region + '/' + 's3' + '/' + 'aws4_request';
127+
var sts = hashingAlgorithm + '\n' +
128+
timestamp + '\n' +
129+
credentialScope + '\n' +
130+
hashHex(standardizedRequest);
131+
132+
// generate the signature
133+
signatureKey = createSignatureKey(secretKey, datestamp, region, 's3');
134+
signature = hmacHex(signatureKey, sts);
135+
136+
// create and send the request
137+
// the 'requests' package autmatically adds the required 'host' header
138+
var requestUrl = endpoint + '/' +
139+
bucket + '/' +
140+
objectKey + '?' +
141+
standardizedQuerystring +
142+
'&X-Amz-Signature=' +
143+
signature;
144+
145+
console.log(`requestUrl: ${requestUrl}`);
146+
147+
console.log(`\nSending ${httpMethod} request to IBM COS -----------------------`);
148+
console.log('Request URL = ' + requestUrl);
149+
150+
// create and send the request
151+
console.log(`\nSending ${httpMethod} request to IBM COS -----------------------`);
152+
console.log('Request URL = ' + requestUrl);
153+
154+
var request = https.get(requestUrl, function (response) {
155+
console.log('\nResponse from IBM COS ----------------------------------');
156+
console.log(`Response code: ${response.statusCode}\n`);
157+
158+
response.on('data', function (chunk) {
159+
console.log('Response: ' + chunk);
160+
printDebug();
161+
});
162+
});
163+
164+
request.end();

src/bluemix/experiments/upload.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
'use strict';
22

3-
//https://github.com/IBM-Cloud/node-file-upload-S3/blob/master/server.js
4-
//https://github.com/balderdashy/skipper-s3/blob/master/index.js
5-
63
const request = require('request');
74
const path = require('path');
85
const Storage = require('ibm-cos-sdk');
@@ -73,4 +70,9 @@ exports.upload = function (url) {
7370
});//œ
7471
7572
}//ƒ
73+
74+
//for future exploration:
75+
76+
//https://github.com/IBM-Cloud/node-file-upload-S3/blob/master/server.js
77+
//https://github.com/balderdashy/skipper-s3/blob/master/index.js
7678
*/

src/bluemix/package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/bluemix/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
"author": "Owen Smith",
1919
"license": "MIT",
2020
"dependencies": {
21+
"crypto": "^1.0.1",
22+
"https": "^1.0.0",
2123
"ibm-cos-sdk": "^1.4.1",
24+
"moment": "^2.24.0",
2225
"request": "^2.88.0",
2326
"uuidv4": "^2.0.0"
2427
},

0 commit comments

Comments
 (0)