Compare commits

..

No commits in common. "d90bc4f741446012885640e93d45052fad4f7ea6" and "892dee6c1333fcc25c88333ee991f02830cb3c51" have entirely different histories.

9 changed files with 510 additions and 505 deletions

View File

@ -68,12 +68,6 @@ function extractParams(
} = oTel; } = oTel;
activeSpan?.addEvent('Arsenal:: entered Arsenal.auth.server.extractParams'); activeSpan?.addEvent('Arsenal:: entered Arsenal.auth.server.extractParams');
return tracer.startActiveSpan('Check validity of request parameters to authenticate using Arsenal', undefined, activeTracerContext, extractParamsSpan => { return tracer.startActiveSpan('Check validity of request parameters to authenticate using Arsenal', undefined, activeTracerContext, extractParamsSpan => {
extractParamsSpan.setAttributes({
'code.lineno': 75,
'code.filename': 'lib/auth/auth.ts',
'code.function': 'extractParams',
'code.url': 'https://github.com/scality/arsenal/blob/892dee6c1333fcc25c88333ee991f02830cb3c51/lib/auth/auth.ts',
});
log.trace('entered', { method: 'Arsenal.auth.server.extractParams' }); log.trace('entered', { method: 'Arsenal.auth.server.extractParams' });
const authHeader = request.headers.authorization; const authHeader = request.headers.authorization;
let version: 'v2' |'v4' | null = null; let version: 'v2' |'v4' | null = null;
@ -114,7 +108,8 @@ function extractParams(
activeSpan?.addEvent(`Arsenal:: Identified auth method version: ${version} and method: ${method}`); activeSpan?.addEvent(`Arsenal:: Identified auth method version: ${version} and method: ${method}`);
activeSpan?.addEvent('Arsenal:: Checking if valid request headers and query are used to make request to vault'); activeSpan?.addEvent('Arsenal:: Checking if valid request headers and query are used to make request to vault');
log.trace('identified auth method', { version, authMethod: method }); log.trace('identified auth method', { version, authMethod: method });
return checkFunctions[version][method](request, log, data, awsService, { activeSpan, extractParamsSpan, activeTracerContext, tracer }); extractParamsSpan.end();
return checkFunctions[version][method](request, log, data, awsService, { activeSpan, activeTracerContext, tracer });
} }
// no auth info identified // no auth info identified

View File

@ -6,97 +6,109 @@ import checkRequestExpiry from './checkRequestExpiry';
import algoCheck from './algoCheck'; import algoCheck from './algoCheck';
export function check(request: any, log: Logger, data: { [key: string]: string }, oTel: any) { export function check(request: any, log: Logger, data: { [key: string]: string }, oTel: any) {
const { activeSpan, extractParamsSpan, activeTracerContext, tracer } = oTel; const { activeSpan, activeTracerContext, tracer } = oTel;
activeSpan?.addEvent('Entered V2 header auth check'); activeSpan?.addEvent('Entered V2 header auth check');
log.trace('running header auth check'); return tracer.startActiveSpan('V2 Header Auth Check', undefined, activeTracerContext, authCheckSpan => {
activeSpan?.addEvent('Running header auth check'); log.trace('running header auth check');
const headers = request.headers; activeSpan?.addEvent('Running header auth check');
activeSpan?.addEvent('Extracting security token');
const token = headers['x-amz-security-token']; const headers = request.headers;
if (token && !constants.iamSecurityToken.pattern.test(token)) {
log.debug('invalid security token', { token }); activeSpan?.addEvent('Extracting security token');
activeSpan.recordException(errors.InvalidToken); const token = headers['x-amz-security-token'];
extractParamsSpan.end(); if (token && !constants.iamSecurityToken.pattern.test(token)) {
return { err: errors.InvalidToken }; log.debug('invalid security token', { token });
} activeSpan.recordException(errors.InvalidToken);
activeSpan?.addEvent('Extracted security token'); authCheckSpan.end();
activeSpan?.addEvent('Checking timestamp'); return { err: errors.InvalidToken };
// Check to make sure timestamp is within 15 minutes of current time }
let timestamp = headers['x-amz-date'] ? headers['x-amz-date'] : headers.date; activeSpan?.addEvent('Extracted security token');
timestamp = Date.parse(timestamp);
if (!timestamp) { activeSpan?.addEvent('Checking timestamp');
log.debug('missing or invalid date header', { method: 'auth/v2/headerAuthCheck.check' }); // Check to make sure timestamp is within 15 minutes of current time
activeSpan.recordException(errors.AccessDenied.customizeDescription('Authentication requires a valid Date or x-amz-date header')); let timestamp = headers['x-amz-date'] ? headers['x-amz-date'] : headers.date;
extractParamsSpan.end(); timestamp = Date.parse(timestamp);
return { err: errors.AccessDenied.customizeDescription('Authentication requires a valid Date or x-amz-date header') }; if (!timestamp) {
} log.debug('missing or invalid date header', { method: 'auth/v2/headerAuthCheck.check' });
activeSpan?.addEvent('Checked timestamp'); activeSpan.recordException(errors.AccessDenied.customizeDescription('Authentication requires a valid Date or x-amz-date header'));
activeSpan?.addEvent('Checking request expiry'); authCheckSpan.end();
const err = checkRequestExpiry(timestamp, log); return { err: errors.AccessDenied.customizeDescription('Authentication requires a valid Date or x-amz-date header') };
if (err) { }
activeSpan.recordException(err); activeSpan?.addEvent('Checked timestamp');
extractParamsSpan.end();
return { err }; activeSpan?.addEvent('Checking request expiry');
} const err = checkRequestExpiry(timestamp, log);
activeSpan?.addEvent('Checked request expiry'); if (err) {
activeSpan?.addEvent('Extracting authorization header'); activeSpan.recordException(err);
// Authorization Header should be in the format of 'AWS AccessKey:Signature' authCheckSpan.end();
const authInfo = headers.authorization; return { err };
activeSpan?.addEvent('Extracted authorization header'); }
if (!authInfo) { activeSpan?.addEvent('Checked request expiry');
log.debug('missing authorization security header');
activeSpan.recordException(errors.MissingSecurityHeader); activeSpan?.addEvent('Extracting authorization header');
extractParamsSpan.end(); // Authorization Header should be in the format of 'AWS AccessKey:Signature'
return { err: errors.MissingSecurityHeader }; const authInfo = headers.authorization;
} activeSpan?.addEvent('Extracted authorization header');
const semicolonIndex = authInfo.indexOf(':');
if (semicolonIndex < 0) { if (!authInfo) {
log.debug('invalid authorization header', { authInfo }); log.debug('missing authorization security header');
activeSpan.recordException(errors.InvalidArgument); activeSpan.recordException(errors.MissingSecurityHeader);
extractParamsSpan.end(); authCheckSpan.end();
return { err: errors.InvalidArgument }; return { err: errors.MissingSecurityHeader };
} }
const accessKey = semicolonIndex > 4 ? authInfo.substring(4, semicolonIndex).trim() : undefined; const semicolonIndex = authInfo.indexOf(':');
if (typeof accessKey !== 'string' || accessKey.length === 0) { if (semicolonIndex < 0) {
log.trace('invalid authorization header', { authInfo }); log.debug('invalid authorization header', { authInfo });
activeSpan.recordException(errors.MissingSecurityHeader); activeSpan.recordException(errors.InvalidArgument);
extractParamsSpan.end(); authCheckSpan.end();
return { err: errors.MissingSecurityHeader }; return { err: errors.InvalidArgument };
} }
// @ts-ignore const accessKey = semicolonIndex > 4 ? authInfo.substring(4, semicolonIndex).trim() : undefined;
log.addDefaultFields({ accessKey }); if (typeof accessKey !== 'string' || accessKey.length === 0) {
const signatureFromRequest = authInfo.substring(semicolonIndex + 1).trim(); log.trace('invalid authorization header', { authInfo });
log.trace('signature from request', { signatureFromRequest }); activeSpan.recordException(errors.MissingSecurityHeader);
activeSpan?.addEvent('Extracting signature from request'); authCheckSpan.end();
activeSpan?.addEvent('Constructing string to sign'); return { err: errors.MissingSecurityHeader };
const stringToSign = constructStringToSign(request, data, log); }
log.trace('constructed string to sign', { stringToSign }); // @ts-ignore
activeSpan?.addEvent('Constructed string to sign v2 headers'); log.addDefaultFields({ accessKey });
const algo = algoCheck(signatureFromRequest.length);
log.trace('algo for calculating signature', { algo }); const signatureFromRequest = authInfo.substring(semicolonIndex + 1).trim();
activeSpan?.addEvent('Checked algorithm for calculating signature'); log.trace('signature from request', { signatureFromRequest });
if (algo === undefined) { activeSpan?.addEvent('Extracting signature from request');
activeSpan.recordException(errors.InvalidArgument);
extractParamsSpan.end(); const stringToSign = constructStringToSign(request, data, log);
return { err: errors.InvalidArgument }; log.trace('constructed string to sign', { stringToSign });
} activeSpan?.addEvent('Constructed string to sign');
activeSpan?.addEvent('Exiting V2 header auth check');
extractParamsSpan.end(); const algo = algoCheck(signatureFromRequest.length);
return { log.trace('algo for calculating signature', { algo });
err: null, activeSpan?.addEvent('Checked algorithm for calculating signature');
params: {
version: 2, if (algo === undefined) {
data: { activeSpan.recordException(errors.InvalidArgument);
accessKey, authCheckSpan.end();
signatureFromRequest, return { err: errors.InvalidArgument };
stringToSign, }
algo,
authType: 'REST-HEADER', activeSpan?.addEvent('Exiting V2 header auth check');
signatureVersion: 'AWS', authCheckSpan.end();
signatureAge: Date.now() - timestamp, return {
securityToken: token, err: null,
params: {
version: 2,
data: {
accessKey,
signatureFromRequest,
stringToSign,
algo,
authType: 'REST-HEADER',
signatureVersion: 'AWS',
signatureAge: Date.now() - timestamp,
securityToken: token,
},
}, },
}, };
}; });
} }

View File

@ -5,98 +5,113 @@ import algoCheck from './algoCheck';
import constructStringToSign from './constructStringToSign'; import constructStringToSign from './constructStringToSign';
export function check(request: any, log: Logger, data: { [key: string]: string }, oTel: any) { export function check(request: any, log: Logger, data: { [key: string]: string }, oTel: any) {
const { activeSpan, extractParamsSpan, activeTracerContext, tracer } = oTel; const { activeSpan, activeTracerContext, tracer } = oTel;
activeSpan?.addEvent('Entered query auth check'); activeSpan?.addEvent('Entered query auth check');
log.trace('running query auth check'); return tracer.startActiveSpan('Query Auth Check', undefined, activeTracerContext, authCheckSpan => {
activeSpan?.addEvent('Running query auth check'); log.trace('running query auth check');
if (request.method === 'POST') { activeSpan?.addEvent('Running query auth check');
log.debug('query string auth not supported for post requests');
activeSpan.recordException(errors.NotImplemented); if (request.method === 'POST') {
extractParamsSpan.end(); log.debug('query string auth not supported for post requests');
return { err: errors.NotImplemented }; activeSpan.recordException(errors.NotImplemented);
} authCheckSpan.end();
const token = data.SecurityToken; return { err: errors.NotImplemented };
activeSpan?.addEvent('Extracting security token'); }
if (token && !constants.iamSecurityToken.pattern.test(token)) {
log.debug('invalid security token', { token }); const token = data.SecurityToken;
activeSpan.recordException(errors.InvalidToken); activeSpan?.addEvent('Extracting security token');
extractParamsSpan.end(); if (token && !constants.iamSecurityToken.pattern.test(token)) {
return { err: errors.InvalidToken }; log.debug('invalid security token', { token });
} activeSpan.recordException(errors.InvalidToken);
activeSpan?.addEvent('Extracted security token'); authCheckSpan.end();
/* return { err: errors.InvalidToken };
Check whether request has expired or if }
expires parameter is more than 604800000 milliseconds activeSpan?.addEvent('Extracted security token');
(7 days) in the future.
Expires time is provided in seconds so need to /*
multiply by 1000 to obtain Check whether request has expired or if
milliseconds to compare to Date.now() expires parameter is more than 604800000 milliseconds
*/ (7 days) in the future.
activeSpan?.addEvent('Checking expiration time'); Expires time is provided in seconds so need to
const expirationTime = parseInt(data.Expires, 10) * 1000; multiply by 1000 to obtain
if (Number.isNaN(expirationTime)) { milliseconds to compare to Date.now()
log.debug('invalid expires parameter', { expires: data.Expires }); */
activeSpan.recordException(errors.MissingSecurityHeader); activeSpan?.addEvent('Checking expiration time');
extractParamsSpan.end(); const expirationTime = parseInt(data.Expires, 10) * 1000;
return { err: errors.MissingSecurityHeader }; if (Number.isNaN(expirationTime)) {
} log.debug('invalid expires parameter', { expires: data.Expires });
activeSpan?.addEvent('Checked expiration time'); activeSpan.recordException(errors.MissingSecurityHeader);
const currentTime = Date.now(); authCheckSpan.end();
const preSignedURLExpiry = process.env.PRE_SIGN_URL_EXPIRY return { err: errors.MissingSecurityHeader };
&& !Number.isNaN(process.env.PRE_SIGN_URL_EXPIRY) }
? Number.parseInt(process.env.PRE_SIGN_URL_EXPIRY, 10) activeSpan?.addEvent('Checked expiration time');
: constants.defaultPreSignedURLExpiry * 1000;
if (expirationTime > currentTime + preSignedURLExpiry) { const currentTime = Date.now();
log.debug('expires parameter too far in future', { expires: request.query.Expires });
activeSpan.recordException(errors.AccessDenied); const preSignedURLExpiry = process.env.PRE_SIGN_URL_EXPIRY
extractParamsSpan.end(); && !Number.isNaN(process.env.PRE_SIGN_URL_EXPIRY)
return { err: errors.AccessDenied }; ? Number.parseInt(process.env.PRE_SIGN_URL_EXPIRY, 10)
} : constants.defaultPreSignedURLExpiry * 1000;
if (currentTime > expirationTime) {
log.debug('current time exceeds expires time', { expires: request.query.Expires }); if (expirationTime > currentTime + preSignedURLExpiry) {
activeSpan.recordException(errors.RequestTimeTooSkewed); log.debug('expires parameter too far in future', { expires: request.query.Expires });
extractParamsSpan.end(); activeSpan.recordException(errors.AccessDenied);
return { err: errors.RequestTimeTooSkewed }; authCheckSpan.end();
} return { err: errors.AccessDenied };
const accessKey = data.AWSAccessKeyId; }
// @ts-ignore if (currentTime > expirationTime) {
log.addDefaultFields({ accessKey }); log.debug('current time exceeds expires time', { expires: request.query.Expires });
const signatureFromRequest = decodeURIComponent(data.Signature); activeSpan.recordException(errors.RequestTimeTooSkewed);
log.trace('signature from request', { signatureFromRequest }); authCheckSpan.end();
activeSpan?.addEvent('Extracting signature from request'); return { err: errors.RequestTimeTooSkewed };
if (!accessKey || !signatureFromRequest) { }
log.debug('invalid access key/signature parameters');
activeSpan.recordException(errors.MissingSecurityHeader); const accessKey = data.AWSAccessKeyId;
extractParamsSpan.end(); // @ts-ignore
return { err: errors.MissingSecurityHeader }; log.addDefaultFields({ accessKey });
}
const stringToSign = constructStringToSign(request, data, log); const signatureFromRequest = decodeURIComponent(data.Signature);
log.trace('constructed string to sign', { stringToSign }); log.trace('signature from request', { signatureFromRequest });
activeSpan?.addEvent('Constructed string to sign v2 query'); activeSpan?.addEvent('Extracting signature from request');
const algo = algoCheck(signatureFromRequest.length);
log.trace('algo for calculating signature', { algo }); if (!accessKey || !signatureFromRequest) {
activeSpan?.addEvent('Checked algorithm for calculating signature'); log.debug('invalid access key/signature parameters');
if (algo === undefined) { activeSpan.recordException(errors.MissingSecurityHeader);
activeSpan.recordException(errors.InvalidArgument); authCheckSpan.end();
extractParamsSpan.end(); return { err: errors.MissingSecurityHeader };
return { err: errors.InvalidArgument }; }
}
activeSpan?.addEvent('Exiting query auth check'); const stringToSign = constructStringToSign(request, data, log);
extractParamsSpan.end(); log.trace('constructed string to sign', { stringToSign });
return { activeSpan?.addEvent('Constructed string to sign');
err: null,
params: { const algo = algoCheck(signatureFromRequest.length);
version: 2, log.trace('algo for calculating signature', { algo });
data: { activeSpan?.addEvent('Checked algorithm for calculating signature');
accessKey,
signatureFromRequest, if (algo === undefined) {
stringToSign, activeSpan.recordException(errors.InvalidArgument);
algo, authCheckSpan.end();
authType: 'REST-QUERY-STRING', return { err: errors.InvalidArgument };
signatureVersion: 'AWS', }
securityToken: token,
activeSpan?.addEvent('Exiting query auth check');
authCheckSpan.end();
return {
err: null,
params: {
version: 2,
data: {
accessKey,
signatureFromRequest,
stringToSign,
algo,
authType: 'REST-QUERY-STRING',
signatureVersion: 'AWS',
securityToken: token,
},
}, },
}, };
}; });
} }

View File

@ -17,7 +17,7 @@ export default function constructStringToSign(params: {
log?: Logger; log?: Logger;
proxyPath?: string; proxyPath?: string;
awsService: string; awsService: string;
}, oTel?: any,): string | Error { }): string | Error {
const { const {
request, request,
signedHeaders, signedHeaders,
@ -29,12 +29,7 @@ export default function constructStringToSign(params: {
proxyPath, proxyPath,
} = params; } = params;
const path = proxyPath || request.path; const path = proxyPath || request.path;
const {
activeSpan,
activeTracerContext,
tracer,
} = oTel;
activeSpan?.addEvent('Constructing canonical request for Authv4');
const canonicalReqResult = createCanonicalRequest({ const canonicalReqResult = createCanonicalRequest({
pHttpVerb: request.method, pHttpVerb: request.method,
pResource: path, pResource: path,
@ -43,7 +38,8 @@ export default function constructStringToSign(params: {
pSignedHeaders: signedHeaders, pSignedHeaders: signedHeaders,
payloadChecksum, payloadChecksum,
service: params.awsService, service: params.awsService,
}, oTel); });
// TODO Why that line? // TODO Why that line?
// @ts-ignore // @ts-ignore
if (canonicalReqResult instanceof Error) { if (canonicalReqResult instanceof Error) {
@ -55,13 +51,9 @@ export default function constructStringToSign(params: {
if (log) { if (log) {
log.debug('constructed canonicalRequest', { canonicalReqResult }); log.debug('constructed canonicalRequest', { canonicalReqResult });
} }
const createSignatureSpan = tracer.startSpan('Creating signature hash for AuthV4 using crypto sha256');
activeSpan?.addEvent('Creating signature hash for AuthV4 using crypto sha256');
const sha256 = crypto.createHash('sha256'); const sha256 = crypto.createHash('sha256');
const canonicalHex = sha256.update(canonicalReqResult, 'binary') const canonicalHex = sha256.update(canonicalReqResult, 'binary')
.digest('hex'); .digest('hex');
activeSpan?.addEvent('Created signature hash for AuthV4 using crypto sha256');
createSignatureSpan.end();
const stringToSign = `AWS4-HMAC-SHA256\n${timestamp}\n` + const stringToSign = `AWS4-HMAC-SHA256\n${timestamp}\n` +
`${credentialScope}\n${canonicalHex}`; `${credentialScope}\n${canonicalHex}`;
return stringToSign; return stringToSign;

View File

@ -19,16 +19,8 @@ export default function createCanonicalRequest(
pSignedHeaders: any; pSignedHeaders: any;
service: string; service: string;
payloadChecksum: string; payloadChecksum: string;
}, }
oTel?: any,
) { ) {
const {
activeSpan,
activeTracerContext,
tracer,
} = oTel;
activeSpan?.addEvent('Entered createCanonicalRequest');
const pHttpVerb = params.pHttpVerb; const pHttpVerb = params.pHttpVerb;
const pResource = params.pResource; const pResource = params.pResource;
const pQuery = params.pQuery; const pQuery = params.pQuery;
@ -36,34 +28,35 @@ export default function createCanonicalRequest(
const pSignedHeaders = params.pSignedHeaders; const pSignedHeaders = params.pSignedHeaders;
const service = params.service; const service = params.service;
let payloadChecksum = params.payloadChecksum; let payloadChecksum = params.payloadChecksum;
const payloadChecksumSpan = tracer.startSpan('ComputePayloadChecksum');
if (!payloadChecksum) { if (!payloadChecksum) {
if (pHttpVerb === 'GET') { if (pHttpVerb === 'GET') {
payloadChecksum = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b' + payloadChecksum = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b' +
'934ca495991b7852b855'; '934ca495991b7852b855';
} else if (pHttpVerb === 'POST') { } else if (pHttpVerb === 'POST') {
let notEncodeStar = false; let notEncodeStar = false;
// The java sdk does not encode the '*' parameter to compute the
// signature, if the user-agent is recognized, we need to keep
// the plain '*' as well.
if (/aws-sdk-java\/[0-9.]+/.test(pHeaders['user-agent'])) { if (/aws-sdk-java\/[0-9.]+/.test(pHeaders['user-agent'])) {
notEncodeStar = true; notEncodeStar = true;
} }
let payload = queryString.stringify(pQuery, undefined, undefined, { let payload = queryString.stringify(pQuery, undefined, undefined, {
encodeURIComponent: input => awsURIencode(input, true, notEncodeStar), encodeURIComponent: input => awsURIencode(input, true,
notEncodeStar),
}); });
payload = payload.replace(/%20/g, '+'); payload = payload.replace(/%20/g, '+');
payloadChecksum = crypto.createHash('sha256') payloadChecksum = crypto.createHash('sha256')
.update(payload, 'binary').digest('hex').toLowerCase(); .update(payload, 'binary').digest('hex').toLowerCase();
} }
} }
payloadChecksumSpan.end();
const canonicalURISpan = tracer.startSpan('ComputeCanonicalURI');
const canonicalURI = !!pResource ? awsURIencode(pResource, false) : '/'; const canonicalURI = !!pResource ? awsURIencode(pResource, false) : '/';
canonicalURISpan.end();
const canonicalQueryStrSpan = tracer.startSpan('ComputeCanonicalQueryStr'); // canonical query string
let canonicalQueryStr = ''; let canonicalQueryStr = '';
if (pQuery && !((service === 'iam' || service === 'ring' || service === 'sts') && pHttpVerb === 'POST')) { if (pQuery && !((service === 'iam' || service === 'ring' ||
service === 'sts') &&
pHttpVerb === 'POST')) {
const sortedQueryParams = Object.keys(pQuery).sort().map(key => { const sortedQueryParams = Object.keys(pQuery).sort().map(key => {
const encodedKey = awsURIencode(key); const encodedKey = awsURIencode(key);
const value = pQuery[key] ? awsURIencode(pQuery[key]) : ''; const value = pQuery[key] ? awsURIencode(pQuery[key]) : '';
@ -71,54 +64,32 @@ export default function createCanonicalRequest(
}); });
canonicalQueryStr = sortedQueryParams.join('&'); canonicalQueryStr = sortedQueryParams.join('&');
} }
canonicalQueryStrSpan.end();
const signedHeadersSpan = tracer.startSpan('SortSignedHeadersAlphabetically'); // signed headers
activeSpan?.addEvent('Splitting signed headers using deliminator: ;');
const signedHeadersList = pSignedHeaders.split(';'); const signedHeadersList = pSignedHeaders.split(';');
activeSpan?.addEvent('Split signed headers using ; as deliminator');
activeSpan?.addEvent('Sorting signed headers alphabetically');
signedHeadersList.sort((a: any, b: any) => a.localeCompare(b)); signedHeadersList.sort((a: any, b: any) => a.localeCompare(b));
activeSpan?.addEvent('Sorted signed headers alphabetically');
activeSpan?.addEvent('Joining signed headers using deliminator: ;');
const signedHeaders = signedHeadersList.join(';'); const signedHeaders = signedHeadersList.join(';');
activeSpan?.addEvent('Joined signed headers using ; as deliminator');
activeSpan.setAttributes({
'signedHeaders.request': pSignedHeaders,
'signedHeaders.request.authv4': signedHeaders,
});
signedHeadersSpan.setAttributes({
'signedHeaders.request': pSignedHeaders,
'signedHeaders.request.authv4': signedHeaders,
'code.url': 'https://github.com/scality/arsenal/blob/c6bb489adeb7419fdbcdf01db2b46a593747530d/lib/auth/v4/createCanonicalRequest.ts#L76',
'code.function': 'createCanonicalRequest',
'code.lineno': 76,
'code.filename': 'lib/auth/v4/createCanonicalRequest.ts',
});
signedHeadersSpan.end();
const canonicalHeadersListSpan = tracer.startSpan('FormatHeadersToMatch CanonicalHeadersList'); // canonical headers
const canonicalHeadersList = signedHeadersList.map((signedHeader: any) => { const canonicalHeadersList = signedHeadersList.map((signedHeader: any) => {
if (pHeaders[signedHeader] !== undefined) { if (pHeaders[signedHeader] !== undefined) {
const trimmedHeader = pHeaders[signedHeader] const trimmedHeader = pHeaders[signedHeader]
.trim().replace(/\s+/g, ' '); .trim().replace(/\s+/g, ' ');
return `${signedHeader}:${trimmedHeader}\n`; return `${signedHeader}:${trimmedHeader}\n`;
} }
// nginx will strip the actual expect header so add value of
// header back here if it was included as a signed header
if (signedHeader === 'expect') { if (signedHeader === 'expect') {
return `${signedHeader}:100-continue\n`; return `${signedHeader}:100-continue\n`;
} }
// handle case where signed 'header' is actually query param
return `${signedHeader}:${pQuery[signedHeader]}\n`; return `${signedHeader}:${pQuery[signedHeader]}\n`;
}); });
canonicalHeadersListSpan.end();
const canonicalHeadersSpan = tracer.startSpan('JoinAllCanonicalHeaders using no deliminator');
const canonicalHeaders = canonicalHeadersList.join(''); const canonicalHeaders = canonicalHeadersList.join('');
canonicalHeadersSpan.end();
const canonicalRequestSpan = tracer.startSpan('ConstructCanonicalRequest');
const canonicalRequest = `${pHttpVerb}\n${canonicalURI}\n` + const canonicalRequest = `${pHttpVerb}\n${canonicalURI}\n` +
`${canonicalQueryStr}\n${canonicalHeaders}\n` + `${canonicalQueryStr}\n${canonicalHeaders}\n` +
`${signedHeaders}\n${payloadChecksum}`; `${signedHeaders}\n${payloadChecksum}`;
canonicalRequestSpan.end();
return canonicalRequest; return canonicalRequest;
} }

View File

@ -22,165 +22,181 @@ import {
* @param awsService - Aws service ('iam' or 's3') * @param awsService - Aws service ('iam' or 's3')
*/ */
export function check(request: any, log: Logger, data: { [key: string]: string }, awsService: string, oTel: any) { export function check(request: any, log: Logger, data: { [key: string]: string }, awsService: string, oTel: any) {
const { activeSpan, extractParamsSpan, activeTracerContext, tracer } = oTel; const { activeSpan, activeTracerContext, tracer } = oTel;
activeSpan?.addEvent('Entered V4 header auth check'); activeSpan?.addEvent('Entered V4 header auth check');
log.trace('running header auth check'); return tracer.startActiveSpan('V4 Header Auth Check', undefined, activeTracerContext, authCheckSpan => {
activeSpan?.addEvent('Extracting security token'); log.trace('running header auth check');
const token = request.headers['x-amz-security-token'];
if (token && !constants.iamSecurityToken.pattern.test(token)) { activeSpan?.addEvent('Extracting security token');
log.debug('invalid security token', { token }); const token = request.headers['x-amz-security-token'];
activeSpan.recordException(errors.InvalidToken); if (token && !constants.iamSecurityToken.pattern.test(token)) {
extractParamsSpan.end(); log.debug('invalid security token', { token });
return { err: errors.InvalidToken }; activeSpan.recordException(errors.InvalidToken);
} authCheckSpan.end();
activeSpan?.addEvent('Extracted security token'); return { err: errors.InvalidToken };
activeSpan?.addEvent('Extracting authorization header');
const authHeader = request.headers.authorization;
if (!authHeader) {
log.debug('missing authorization header');
activeSpan.recordException(errors.MissingSecurityHeader);
extractParamsSpan.end();
return { err: errors.MissingSecurityHeader };
}
activeSpan?.addEvent('Extracted authorization header');
activeSpan?.addEvent('Extracting auth header items');
const authHeaderItems = extractAuthItems(authHeader, log);
if (Object.keys(authHeaderItems).length < 3) {
log.debug('invalid authorization header', { authHeader });
activeSpan.recordException(errors.InvalidArgument);
extractParamsSpan.end();
return { err: errors.InvalidArgument };
}
activeSpan?.addEvent('Extracted auth header items');
const payloadChecksum = request.headers['x-amz-content-sha256'];
if (!payloadChecksum && awsService !== 'iam') {
log.debug('missing payload checksum');
activeSpan.recordException(errors.MissingSecurityHeader);
extractParamsSpan.end();
return { err: errors.MissingSecurityHeader };
}
if (payloadChecksum === 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD') {
log.trace('requesting streaming v4 auth');
if (request.method !== 'PUT') {
log.debug('streaming v4 auth for put only', { method: 'auth/v4/headerAuthCheck.check' });
activeSpan.recordException(errors.InvalidArgument);
extractParamsSpan.end();
return { err: errors.InvalidArgument };
} }
if (!request.headers['x-amz-decoded-content-length']) { activeSpan?.addEvent('Extracted security token');
activeSpan?.addEvent('Extracting authorization header');
const authHeader = request.headers.authorization;
if (!authHeader) {
log.debug('missing authorization header');
activeSpan.recordException(errors.MissingSecurityHeader); activeSpan.recordException(errors.MissingSecurityHeader);
extractParamsSpan.end(); authCheckSpan.end();
return { err: errors.MissingSecurityHeader }; return { err: errors.MissingSecurityHeader };
} }
} activeSpan?.addEvent('Extracted authorization header');
log.trace('authorization header from request', { authHeader });
const signatureFromRequest = authHeaderItems.signatureFromRequest!; activeSpan?.addEvent('Extracting auth header items');
const credentialsArr = authHeaderItems.credentialsArr!; const authHeaderItems = extractAuthItems(authHeader, log);
const signedHeaders = authHeaderItems.signedHeaders!; if (Object.keys(authHeaderItems).length < 3) {
activeSpan.addEvent('Checking if signed headers are complete'); log.debug('invalid authorization header', { authHeader });
if (!areSignedHeadersComplete(signedHeaders, request.headers)) { activeSpan.recordException(errors.InvalidArgument);
log.debug('signedHeaders are incomplete', { signedHeaders }); authCheckSpan.end();
activeSpan.recordException(errors.AccessDenied); return { err: errors.InvalidArgument };
extractParamsSpan.end();
return { err: errors.AccessDenied };
}
activeSpan.addEvent('Signed headers are complete');
let timestamp: string | undefined;
// check request timestamp
activeSpan.addEvent('Checking request timestamp');
const xAmzDate = request.headers['x-amz-date'];
if (xAmzDate) {
const xAmzDateArr = xAmzDate.split('T');
// check that x-amz- date has the correct format and after epochTime
if (xAmzDateArr.length === 2 && xAmzDateArr[0].length === 8 && xAmzDateArr[1].length === 7 && Number.parseInt(xAmzDateArr[0], 10) > 19700101) {
// format of x-amz- date is ISO 8601: YYYYMMDDTHHMMSSZ
timestamp = request.headers['x-amz-date'];
} }
} else if (request.headers.date) { activeSpan?.addEvent('Extracted auth header items');
timestamp = convertUTCtoISO8601(request.headers.date);
} const payloadChecksum = request.headers['x-amz-content-sha256'];
if (!timestamp) { if (!payloadChecksum && awsService !== 'iam') {
log.debug('missing or invalid date header', { method: 'auth/v4/headerAuthCheck.check' }); log.debug('missing payload checksum');
activeSpan.recordException(errors.AccessDenied.customizeDescription('Authentication requires a valid Date or x-amz-date header')); activeSpan.recordException(errors.MissingSecurityHeader);
extractParamsSpan.end(); authCheckSpan.end();
return { err: errors.AccessDenied.customizeDescription('Authentication requires a valid Date or x-amz-date header') }; return { err: errors.MissingSecurityHeader };
} }
activeSpan.addEvent('Request timestamp is valid'); if (payloadChecksum === 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD') {
activeSpan.addEvent('Validating credentials'); log.trace('requesting streaming v4 auth');
const validationResult = validateCredentials(credentialsArr, timestamp, log); if (request.method !== 'PUT') {
if (validationResult instanceof Error) { log.debug('streaming v4 auth for put only', { method: 'auth/v4/headerAuthCheck.check' });
log.debug('credentials in improper format', { credentialsArr, timestamp, validationResult }); activeSpan.recordException(errors.InvalidArgument);
activeSpan.recordException(validationResult); authCheckSpan.end();
extractParamsSpan.end(); return { err: errors.InvalidArgument };
return { err: validationResult }; }
} if (!request.headers['x-amz-decoded-content-length']) {
activeSpan.addEvent('Credentials are valid'); activeSpan.recordException(errors.MissingSecurityHeader);
// credentialsArr is [accessKey, date, region, aws-service, aws4_request] authCheckSpan.end();
const scopeDate = credentialsArr[1]; return { err: errors.MissingSecurityHeader };
const region = credentialsArr[2]; }
const service = credentialsArr[3]; }
const accessKey = credentialsArr.shift();
const credentialScope = credentialsArr.join('/'); log.trace('authorization header from request', { authHeader });
// In AWS Signature Version 4, the signing key is valid for up to seven days
// (see Introduction to Signing Requests. const signatureFromRequest = authHeaderItems.signatureFromRequest!;
// Therefore, a signature is also valid for up to seven days or const credentialsArr = authHeaderItems.credentialsArr!;
// less if specified by a bucket policy. const signedHeaders = authHeaderItems.signedHeaders!;
// Since policies are not yet implemented, we will have a 15
// minute default like in v2 Auth. activeSpan.addEvent('Checking if signed headers are complete');
// See http://docs.aws.amazon.com/AmazonS3/latest/API/ if (!areSignedHeadersComplete(signedHeaders, request.headers)) {
// bucket-policy-s3-sigv4-conditions.html log.debug('signedHeaders are incomplete', { signedHeaders });
// TODO: When implementing bucket policies, activeSpan.recordException(errors.AccessDenied);
// note that expiration can be shortened so authCheckSpan.end();
// expiry is as set out in the policy. return { err: errors.AccessDenied };
// 15 minutes in seconds }
activeSpan.addEvent('checking if signature is expired') activeSpan.addEvent('Signed headers are complete');
const expiry = (15 * 60);
const isTimeSkewed = checkTimeSkew(timestamp, expiry, log); let timestamp: string | undefined;
if (isTimeSkewed) { // check request timestamp
activeSpan.recordException(errors.RequestTimeTooSkewed); activeSpan.addEvent('Checking request timestamp');
extractParamsSpan.end(); const xAmzDate = request.headers['x-amz-date'];
return { err: errors.RequestTimeTooSkewed }; if (xAmzDate) {
} const xAmzDateArr = xAmzDate.split('T');
activeSpan.addEvent('signature is not expired'); // check that x-amz- date has the correct format and after epochTime
activeSpan.addEvent('Constructing string to sign'); if (xAmzDateArr.length === 2 && xAmzDateArr[0].length === 8 && xAmzDateArr[1].length === 7 && Number.parseInt(xAmzDateArr[0], 10) > 19700101) {
const stringToSign = constructStringToSign({ // format of x-amz- date is ISO 8601: YYYYMMDDTHHMMSSZ
log, timestamp = request.headers['x-amz-date'];
request, }
query: data, } else if (request.headers.date) {
signedHeaders, timestamp = convertUTCtoISO8601(request.headers.date);
credentialScope, }
timestamp, if (!timestamp) {
payloadChecksum, log.debug('missing or invalid date header', { method: 'auth/v4/headerAuthCheck.check' });
awsService: service, activeSpan.recordException(errors.AccessDenied.customizeDescription('Authentication requires a valid Date or x-amz-date header'));
}, oTel); authCheckSpan.end();
log.trace('constructed stringToSign', { stringToSign }); return { err: errors.AccessDenied.customizeDescription('Authentication requires a valid Date or x-amz-date header') };
if (stringToSign instanceof Error) { }
activeSpan.recordException(stringToSign); activeSpan.addEvent('Request timestamp is valid');
extractParamsSpan.end();
return { err: stringToSign }; activeSpan.addEvent('Validating credentials');
} const validationResult = validateCredentials(credentialsArr, timestamp, log);
activeSpan.addEvent('Constructed string to sign v4 headers'); if (validationResult instanceof Error) {
activeSpan.addEvent('Exiting V4 header auth check'); log.debug('credentials in improper format', { credentialsArr, timestamp, validationResult });
extractParamsSpan.end(); activeSpan.recordException(validationResult);
return { authCheckSpan.end();
err: null, return { err: validationResult };
params: { }
version: 4, activeSpan.addEvent('Credentials are valid');
data: { // credentialsArr is [accessKey, date, region, aws-service, aws4_request]
accessKey, const scopeDate = credentialsArr[1];
signatureFromRequest, const region = credentialsArr[2];
region, const service = credentialsArr[3];
service, const accessKey = credentialsArr.shift();
scopeDate, const credentialScope = credentialsArr.join('/');
stringToSign,
authType: 'REST-HEADER', // In AWS Signature Version 4, the signing key is valid for up to seven days
signatureVersion: 'AWS4-HMAC-SHA256', // (see Introduction to Signing Requests.
signatureAge: Date.now() - convertAmzTimeToMs(timestamp), // Therefore, a signature is also valid for up to seven days or
credentialScope, // less if specified by a bucket policy.
timestamp, // Since policies are not yet implemented, we will have a 15
securityToken: token, // minute default like in v2 Auth.
// See http://docs.aws.amazon.com/AmazonS3/latest/API/
// bucket-policy-s3-sigv4-conditions.html
// TODO: When implementing bucket policies,
// note that expiration can be shortened so
// expiry is as set out in the policy.
// 15 minutes in seconds
activeSpan.addEvent('checking if signature is expired')
const expiry = (15 * 60);
const isTimeSkewed = checkTimeSkew(timestamp, expiry, log);
if (isTimeSkewed) {
activeSpan.recordException(errors.RequestTimeTooSkewed);
authCheckSpan.end();
return { err: errors.RequestTimeTooSkewed };
}
activeSpan.addEvent('signature is not expired');
activeSpan.addEvent('Constructing string to sign');
const stringToSign = constructStringToSign({
log,
request,
query: data,
signedHeaders,
credentialScope,
timestamp,
payloadChecksum,
awsService: service,
});
log.trace('constructed stringToSign', { stringToSign });
if (stringToSign instanceof Error) {
activeSpan.recordException(stringToSign);
authCheckSpan.end();
return { err: stringToSign };
}
activeSpan.addEvent('Constructed string to sign');
activeSpan.addEvent('Exiting V4 header auth check');
authCheckSpan.end();
return {
err: null,
params: {
version: 4,
data: {
accessKey,
signatureFromRequest,
region,
service,
scopeDate,
stringToSign,
authType: 'REST-HEADER',
signatureVersion: 'AWS4-HMAC-SHA256',
signatureAge: Date.now() - convertAmzTimeToMs(timestamp),
credentialScope,
timestamp,
securityToken: token,
},
}, },
}, };
}; });
} }

View File

@ -15,116 +15,115 @@ import { areSignedHeadersComplete } from './validateInputs';
export function check(request: any, log: Logger, data: { [key: string]: string }, oTel: any) { export function check(request: any, log: Logger, data: { [key: string]: string }, oTel: any) {
const { const {
activeSpan, activeSpan,
extractParamsSpan,
activeTracerContext, activeTracerContext,
tracer, tracer,
} = oTel; } = oTel;
activeSpan?.addEvent('Arsenal:: entered Arsenal.auth.v4.queryAuthCheck'); activeSpan?.addEvent('Arsenal:: entered Arsenal.auth.v4.queryAuthCheck');
activeSpan?.addEvent('Arsenal:: extracting query parameters') return tracer.startActiveSpan('Arsenal::Arsenal.auth.v4.queryAuthCheck', undefined, activeTracerContext, authCheckSpan => {
const authParams = extractQueryParams(data, log); activeSpan?.addEvent('Arsenal:: extracting query parameters')
activeSpan?.addEvent('Arsenal:: extracting query params'); const authParams = extractQueryParams(data, log);
if (Object.keys(authParams).length !== 5) { activeSpan?.addEvent('Arsenal:: extracting query params');
activeSpan.recordException(errors.InvalidArgument); if (Object.keys(authParams).length !== 5) {
extractParamsSpan.end(); activeSpan.recordException(errors.InvalidArgument);
return { err: errors.InvalidArgument }; authCheckSpan.end();
} return { err: errors.InvalidArgument };
}
// Query params are not specified in AWS documentation as case-insensitive,
// so we use case-sensitive // Query params are not specified in AWS documentation as case-insensitive,
const token = data['X-Amz-Security-Token']; // so we use case-sensitive
if (token && !constants.iamSecurityToken.pattern.test(token)) { const token = data['X-Amz-Security-Token'];
log.debug('invalid security token', { token }); if (token && !constants.iamSecurityToken.pattern.test(token)) {
activeSpan.recordException(errors.InvalidToken); log.debug('invalid security token', { token });
extractParamsSpan.end(); activeSpan.recordException(errors.InvalidToken);
return { err: errors.InvalidToken }; authCheckSpan.end();
} return { err: errors.InvalidToken };
}
const signedHeaders = authParams.signedHeaders!;
const signatureFromRequest = authParams.signatureFromRequest!; const signedHeaders = authParams.signedHeaders!;
const timestamp = authParams.timestamp!; const signatureFromRequest = authParams.signatureFromRequest!;
const expiry = authParams.expiry!; const timestamp = authParams.timestamp!;
const credential = authParams.credential!; const expiry = authParams.expiry!;
const credential = authParams.credential!;
if (!areSignedHeadersComplete(signedHeaders, request.headers)) {
log.debug('signedHeaders are incomplete', { signedHeaders }); if (!areSignedHeadersComplete(signedHeaders, request.headers)) {
activeSpan.recordException(errors.AccessDenied); log.debug('signedHeaders are incomplete', { signedHeaders });
extractParamsSpan.end(); activeSpan.recordException(errors.AccessDenied);
return { err: errors.AccessDenied }; authCheckSpan.end();
} return { err: errors.AccessDenied };
}
const validationResult = validateCredentials(credential, timestamp,
log); const validationResult = validateCredentials(credential, timestamp,
if (validationResult instanceof Error) { log);
log.debug('credentials in improper format', { credential, if (validationResult instanceof Error) {
timestamp, validationResult }); log.debug('credentials in improper format', { credential,
activeSpan.recordException(validationResult); timestamp, validationResult });
extractParamsSpan.end(); activeSpan.recordException(validationResult);
return { err: validationResult }; authCheckSpan.end();
} return { err: validationResult };
const accessKey = credential[0]; }
const scopeDate = credential[1]; const accessKey = credential[0];
const region = credential[2]; const scopeDate = credential[1];
const service = credential[3]; const region = credential[2];
const requestType = credential[4]; const service = credential[3];
const requestType = credential[4];
const isTimeSkewed = checkTimeSkew(timestamp, expiry, log);
if (isTimeSkewed) { const isTimeSkewed = checkTimeSkew(timestamp, expiry, log);
activeSpan.recordException(errors.RequestTimeTooSkewed); if (isTimeSkewed) {
extractParamsSpan.end(); activeSpan.recordException(errors.RequestTimeTooSkewed);
return { err: errors.RequestTimeTooSkewed }; authCheckSpan.end();
} return { err: errors.RequestTimeTooSkewed };
}
// In query v4 auth, the canonical request needs
// to include the query params OTHER THAN // In query v4 auth, the canonical request needs
// the signature so create a // to include the query params OTHER THAN
// copy of the query object and remove // the signature so create a
// the X-Amz-Signature property. // copy of the query object and remove
const queryWithoutSignature = Object.assign({}, data); // the X-Amz-Signature property.
delete queryWithoutSignature['X-Amz-Signature']; const queryWithoutSignature = Object.assign({}, data);
delete queryWithoutSignature['X-Amz-Signature'];
// For query auth, instead of a
// checksum of the contents, the // For query auth, instead of a
// string 'UNSIGNED-PAYLOAD' should be // checksum of the contents, the
// added to the canonicalRequest in // string 'UNSIGNED-PAYLOAD' should be
// building string to sign // added to the canonicalRequest in
const payloadChecksum = 'UNSIGNED-PAYLOAD'; // building string to sign
const payloadChecksum = 'UNSIGNED-PAYLOAD';
activeSpan?.addEvent('Constructing string to sign');
const stringToSign = constructStringToSign({ const stringToSign = constructStringToSign({
log, log,
request, request,
query: queryWithoutSignature, query: queryWithoutSignature,
signedHeaders, signedHeaders,
payloadChecksum, payloadChecksum,
timestamp, timestamp,
credentialScope: credentialScope:
`${scopeDate}/${region}/${service}/${requestType}`, `${scopeDate}/${region}/${service}/${requestType}`,
awsService: service, awsService: service,
}, oTel); });
activeSpan?.addEvent('Constructed string to sign v4 query'); if (stringToSign instanceof Error) {
if (stringToSign instanceof Error) { activeSpan.recordException(stringToSign);
activeSpan.recordException(stringToSign); authCheckSpan.end();
extractParamsSpan.end(); return { err: stringToSign };
return { err: stringToSign }; }
} log.trace('constructed stringToSign', { stringToSign });
log.trace('constructed stringToSign', { stringToSign }); activeSpan.addEvent('Arsenal:: exiting Arsenal.auth.v4.queryAuthCheck');
activeSpan.addEvent('Arsenal:: exiting Arsenal.auth.v4.queryAuthCheck'); authCheckSpan.end();
extractParamsSpan.end(); return {
return { err: null,
err: null, params: {
params: { version: 4,
version: 4, data: {
data: { accessKey,
accessKey, signatureFromRequest,
signatureFromRequest, region,
region, scopeDate,
scopeDate, stringToSign,
stringToSign, authType: 'REST-QUERY-STRING',
authType: 'REST-QUERY-STRING', signatureVersion: 'AWS4-HMAC-SHA256',
signatureVersion: 'AWS4-HMAC-SHA256', signatureAge: Date.now() - convertAmzTimeToMs(timestamp),
signatureAge: Date.now() - convertAmzTimeToMs(timestamp), securityToken: token,
securityToken: token, },
}, },
}, };
}; });
} }

View File

@ -23,6 +23,11 @@ export default function routerGET(
} = dataRetrievalParams; } = dataRetrievalParams;
return tracer.startActiveSpan('Arsenal:: Performing Get API related operations using Cloudserver, Vault and Metadata', undefined, activeTracerContext, cloudserverApiSpan => { return tracer.startActiveSpan('Arsenal:: Performing Get API related operations using Cloudserver, Vault and Metadata', undefined, activeTracerContext, cloudserverApiSpan => {
activeSpan.addEvent('Request validated, routing request using routeGET() in arsenal'); activeSpan.addEvent('Request validated, routing request using routeGET() in arsenal');
cloudserverApiSpan.setAttributes({
'code.lineno': 9,
'code.filename': 'lib/s3routes/routes/routeGET.ts',
'code.function': 'routerGET()',
})
activeSpan.addEvent('Detecting which API to route to using arsenal routeGET()') activeSpan.addEvent('Detecting which API to route to using arsenal routeGET()')
log.debug('routing request', { method: 'routerGET' }); log.debug('routing request', { method: 'routerGET' });

View File

@ -638,7 +638,7 @@ export function responseStreamData(
}, },
} = retrieveDataParams; } = retrieveDataParams;
activeSpan.addEvent('Request processed, getting Data from sproxyd'); activeSpan.addEvent('Request processed, getting Data from sproxyd');
return tracer.startActiveSpan('Getting Object Data from RING', undefined, activeTracerContext, sproxydSpan => { return tracer.startActiveSpan('Getting Data From RING', undefined, activeTracerContext, sproxydSpan => {
sproxydSpan.setAttributes({ sproxydSpan.setAttributes({
'code.function': 'Arsenal:: responseStreamData()', 'code.function': 'Arsenal:: responseStreamData()',
'code.filepath': 'lib/s3routes/routesUtils.js', 'code.filepath': 'lib/s3routes/routesUtils.js',