Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/IAMClient.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion lib/IAMClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -1134,7 +1134,12 @@ class VaultClient {
sha256: Sha256,
host: this._host ? this._host : options.host,
});
const signedReq = await signer.sign(options);
// Sign with the original path argument (e.g. '/'), not
// options.path which may include a proxyPath prefix
// (e.g. '/_/backbeat/iam'). The proxyPath is only meant
// for HTTP routing through nginx.
// The signature must use the canonical IAM path that Vault verifies against.
const signedReq = await signer.sign({ ...options, path });
Object.keys(signedReq.headers).forEach(key => {
req.setHeader(key, signedReq.headers[key]);
});
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"engines": {
"node": ">=20"
},
"version": "8.5.3",
"version": "8.5.4",
"description": "Client library and binary for Vault, the user directory and key management service",
"main": "index.js",
"repository": "scality/vaultclient",
Expand Down
109 changes: 109 additions & 0 deletions tests/unit/pathPrefix.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const assert = require('assert');
const { createHmac, createHash } = require('crypto');
const http = require('http');
const IAMClient = require('../../lib/IAMClient');

Expand Down Expand Up @@ -40,3 +41,111 @@ describe('path prefix test with path parameter set', () => {
});
});
});

// When a proxyPath is set (e.g. '/_/backbeat/iam'), the HTTP request
// is sent to that path for nginx routing, but the V4 signature must
// be computed with the IAM canonical path '/' — which is what Vault
// verifies against. A mismatch causes InvalidAccessKeyId (VLTCLT-37).
describe('V4 signature with proxyPath must use IAM canonical path', () => {
const proxyPath = '/_/backbeat/iam';
const iamCanonicalPath = '/';
const accessKey = 'TESTACCESSKEY00000001';
const secretKey = 'testsecretkey000000000000000000000000000001';
let server;
let client;

function sha256(data) {
return createHash('sha256').update(data, 'utf8').digest('hex');
}

function hmac(data, key) {
return createHmac('sha256', key).update(data, 'utf8').digest();
}

// Simplified AWS Signature V4 verification. Recomputes the expected
// signature using the given canonicalPath.
// Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv.html
function computeExpectedSignature(req, canonicalPath) {
const authHeader = req.headers.authorization;
assert(authHeader,
'request must include Authorization header (admin credentials)');

const credMatch = authHeader.match(
/Credential=([^/]+)\/(\d{8})\/([^/]+)\/(\w+)\/aws4_request/);
assert(credMatch, 'Authorization header must contain valid Credential');
const [, , date, region, service] = credMatch;

const signedHeaders = authHeader
.match(/SignedHeaders=([^,]+)/)[1].split(';');

const headersStr = signedHeaders
.map(h => `${h}:${req.headers[h]}`).join('\n');
const payloadHash = req.headers['x-amz-content-sha256'];

const canonicalReq = [
req.method, canonicalPath, '',
`${headersStr}\n`,
signedHeaders.join(';'),
payloadHash,
].join('\n');

const stringToSign = [
'AWS4-HMAC-SHA256',
req.headers['x-amz-date'],
`${date}/${region}/${service}/aws4_request`,
sha256(canonicalReq),
].join('\n');

const kDate = hmac(date, `AWS4${secretKey}`);
const kRegion = hmac(region, kDate);
const kService = hmac(service, kRegion);
const kSigning = hmac('aws4_request', kService);
return createHmac('sha256', kSigning)
.update(stringToSign, 'utf8').digest('hex');
}

function getClientSignature(req) {
const match = req.headers.authorization.match(
/Signature=([a-f0-9]+)/);
assert(match, 'Authorization header must contain Signature');
return match[1];
}

beforeEach('start server', done => {
server = http.createServer((req, res) => {
req.on('data', () => {});
req.on('end', () => {
const clientSig = getClientSignature(req);
const expectedSig =
computeExpectedSignature(req, iamCanonicalPath);
if (clientSig !== expectedSig) {
res.writeHead(403);
res.end(JSON.stringify({
ErrorResponse: { Error: {
Code: 'InvalidAccessKeyId',
Message: 'Signature mismatch',
} },
}));
return;
}
res.writeHead(200);
res.end(JSON.stringify({ accounts: [] }));
});
}).listen(8500, () => {
client = new IAMClient('127.0.0.1', 8500,
undefined, undefined, undefined, undefined, undefined,
accessKey, secretKey, undefined, proxyPath);
done();
}).on('error', done);
});

afterEach('stop server', () => server.close());

it('should sign with "/" so Vault signature check succeeds', done => {
client.listAccounts({}, (err, data) => {
assert.ifError(err);
assert(data, 'should return response data');
done();
});
});
});