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;
activeSpan?.addEvent('Arsenal:: entered Arsenal.auth.server.extractParams');
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' });
const authHeader = request.headers.authorization;
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:: Checking if valid request headers and query are used to make request to vault');
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

View File

@ -6,97 +6,109 @@ import checkRequestExpiry from './checkRequestExpiry';
import algoCheck from './algoCheck';
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');
log.trace('running header auth check');
activeSpan?.addEvent('Running header auth check');
const headers = request.headers;
activeSpan?.addEvent('Extracting security token');
const token = headers['x-amz-security-token'];
if (token && !constants.iamSecurityToken.pattern.test(token)) {
log.debug('invalid security token', { token });
activeSpan.recordException(errors.InvalidToken);
extractParamsSpan.end();
return { err: errors.InvalidToken };
}
activeSpan?.addEvent('Extracted security token');
activeSpan?.addEvent('Checking timestamp');
// Check to make sure timestamp is within 15 minutes of current time
let timestamp = headers['x-amz-date'] ? headers['x-amz-date'] : headers.date;
timestamp = Date.parse(timestamp);
if (!timestamp) {
log.debug('missing or invalid date header', { method: 'auth/v2/headerAuthCheck.check' });
activeSpan.recordException(errors.AccessDenied.customizeDescription('Authentication requires a valid Date or x-amz-date header'));
extractParamsSpan.end();
return { err: errors.AccessDenied.customizeDescription('Authentication requires a valid Date or x-amz-date header') };
}
activeSpan?.addEvent('Checked timestamp');
activeSpan?.addEvent('Checking request expiry');
const err = checkRequestExpiry(timestamp, log);
if (err) {
activeSpan.recordException(err);
extractParamsSpan.end();
return { err };
}
activeSpan?.addEvent('Checked request expiry');
activeSpan?.addEvent('Extracting authorization header');
// Authorization Header should be in the format of 'AWS AccessKey:Signature'
const authInfo = headers.authorization;
activeSpan?.addEvent('Extracted authorization header');
if (!authInfo) {
log.debug('missing authorization security header');
activeSpan.recordException(errors.MissingSecurityHeader);
extractParamsSpan.end();
return { err: errors.MissingSecurityHeader };
}
const semicolonIndex = authInfo.indexOf(':');
if (semicolonIndex < 0) {
log.debug('invalid authorization header', { authInfo });
activeSpan.recordException(errors.InvalidArgument);
extractParamsSpan.end();
return { err: errors.InvalidArgument };
}
const accessKey = semicolonIndex > 4 ? authInfo.substring(4, semicolonIndex).trim() : undefined;
if (typeof accessKey !== 'string' || accessKey.length === 0) {
log.trace('invalid authorization header', { authInfo });
activeSpan.recordException(errors.MissingSecurityHeader);
extractParamsSpan.end();
return { err: errors.MissingSecurityHeader };
}
// @ts-ignore
log.addDefaultFields({ accessKey });
const signatureFromRequest = authInfo.substring(semicolonIndex + 1).trim();
log.trace('signature from request', { signatureFromRequest });
activeSpan?.addEvent('Extracting signature from request');
activeSpan?.addEvent('Constructing string to sign');
const stringToSign = constructStringToSign(request, data, log);
log.trace('constructed string to sign', { stringToSign });
activeSpan?.addEvent('Constructed string to sign v2 headers');
const algo = algoCheck(signatureFromRequest.length);
log.trace('algo for calculating signature', { algo });
activeSpan?.addEvent('Checked algorithm for calculating signature');
if (algo === undefined) {
activeSpan.recordException(errors.InvalidArgument);
extractParamsSpan.end();
return { err: errors.InvalidArgument };
}
activeSpan?.addEvent('Exiting V2 header auth check');
extractParamsSpan.end();
return {
err: null,
params: {
version: 2,
data: {
accessKey,
signatureFromRequest,
stringToSign,
algo,
authType: 'REST-HEADER',
signatureVersion: 'AWS',
signatureAge: Date.now() - timestamp,
securityToken: token,
return tracer.startActiveSpan('V2 Header Auth Check', undefined, activeTracerContext, authCheckSpan => {
log.trace('running header auth check');
activeSpan?.addEvent('Running header auth check');
const headers = request.headers;
activeSpan?.addEvent('Extracting security token');
const token = headers['x-amz-security-token'];
if (token && !constants.iamSecurityToken.pattern.test(token)) {
log.debug('invalid security token', { token });
activeSpan.recordException(errors.InvalidToken);
authCheckSpan.end();
return { err: errors.InvalidToken };
}
activeSpan?.addEvent('Extracted security token');
activeSpan?.addEvent('Checking timestamp');
// Check to make sure timestamp is within 15 minutes of current time
let timestamp = headers['x-amz-date'] ? headers['x-amz-date'] : headers.date;
timestamp = Date.parse(timestamp);
if (!timestamp) {
log.debug('missing or invalid date header', { method: 'auth/v2/headerAuthCheck.check' });
activeSpan.recordException(errors.AccessDenied.customizeDescription('Authentication requires a valid Date or x-amz-date header'));
authCheckSpan.end();
return { err: errors.AccessDenied.customizeDescription('Authentication requires a valid Date or x-amz-date header') };
}
activeSpan?.addEvent('Checked timestamp');
activeSpan?.addEvent('Checking request expiry');
const err = checkRequestExpiry(timestamp, log);
if (err) {
activeSpan.recordException(err);
authCheckSpan.end();
return { err };
}
activeSpan?.addEvent('Checked request expiry');
activeSpan?.addEvent('Extracting authorization header');
// Authorization Header should be in the format of 'AWS AccessKey:Signature'
const authInfo = headers.authorization;
activeSpan?.addEvent('Extracted authorization header');
if (!authInfo) {
log.debug('missing authorization security header');
activeSpan.recordException(errors.MissingSecurityHeader);
authCheckSpan.end();
return { err: errors.MissingSecurityHeader };
}
const semicolonIndex = authInfo.indexOf(':');
if (semicolonIndex < 0) {
log.debug('invalid authorization header', { authInfo });
activeSpan.recordException(errors.InvalidArgument);
authCheckSpan.end();
return { err: errors.InvalidArgument };
}
const accessKey = semicolonIndex > 4 ? authInfo.substring(4, semicolonIndex).trim() : undefined;
if (typeof accessKey !== 'string' || accessKey.length === 0) {
log.trace('invalid authorization header', { authInfo });
activeSpan.recordException(errors.MissingSecurityHeader);
authCheckSpan.end();
return { err: errors.MissingSecurityHeader };
}
// @ts-ignore
log.addDefaultFields({ accessKey });
const signatureFromRequest = authInfo.substring(semicolonIndex + 1).trim();
log.trace('signature from request', { signatureFromRequest });
activeSpan?.addEvent('Extracting signature from request');
const stringToSign = constructStringToSign(request, data, log);
log.trace('constructed string to sign', { stringToSign });
activeSpan?.addEvent('Constructed string to sign');
const algo = algoCheck(signatureFromRequest.length);
log.trace('algo for calculating signature', { algo });
activeSpan?.addEvent('Checked algorithm for calculating signature');
if (algo === undefined) {
activeSpan.recordException(errors.InvalidArgument);
authCheckSpan.end();
return { err: errors.InvalidArgument };
}
activeSpan?.addEvent('Exiting V2 header auth check');
authCheckSpan.end();
return {
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';
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');
log.trace('running query auth check');
activeSpan?.addEvent('Running query auth check');
if (request.method === 'POST') {
log.debug('query string auth not supported for post requests');
activeSpan.recordException(errors.NotImplemented);
extractParamsSpan.end();
return { err: errors.NotImplemented };
}
const token = data.SecurityToken;
activeSpan?.addEvent('Extracting security token');
if (token && !constants.iamSecurityToken.pattern.test(token)) {
log.debug('invalid security token', { token });
activeSpan.recordException(errors.InvalidToken);
extractParamsSpan.end();
return { err: errors.InvalidToken };
}
activeSpan?.addEvent('Extracted security token');
/*
Check whether request has expired or if
expires parameter is more than 604800000 milliseconds
(7 days) in the future.
Expires time is provided in seconds so need to
multiply by 1000 to obtain
milliseconds to compare to Date.now()
*/
activeSpan?.addEvent('Checking expiration time');
const expirationTime = parseInt(data.Expires, 10) * 1000;
if (Number.isNaN(expirationTime)) {
log.debug('invalid expires parameter', { expires: data.Expires });
activeSpan.recordException(errors.MissingSecurityHeader);
extractParamsSpan.end();
return { err: errors.MissingSecurityHeader };
}
activeSpan?.addEvent('Checked expiration time');
const currentTime = Date.now();
const preSignedURLExpiry = process.env.PRE_SIGN_URL_EXPIRY
&& !Number.isNaN(process.env.PRE_SIGN_URL_EXPIRY)
? Number.parseInt(process.env.PRE_SIGN_URL_EXPIRY, 10)
: constants.defaultPreSignedURLExpiry * 1000;
if (expirationTime > currentTime + preSignedURLExpiry) {
log.debug('expires parameter too far in future', { expires: request.query.Expires });
activeSpan.recordException(errors.AccessDenied);
extractParamsSpan.end();
return { err: errors.AccessDenied };
}
if (currentTime > expirationTime) {
log.debug('current time exceeds expires time', { expires: request.query.Expires });
activeSpan.recordException(errors.RequestTimeTooSkewed);
extractParamsSpan.end();
return { err: errors.RequestTimeTooSkewed };
}
const accessKey = data.AWSAccessKeyId;
// @ts-ignore
log.addDefaultFields({ accessKey });
const signatureFromRequest = decodeURIComponent(data.Signature);
log.trace('signature from request', { signatureFromRequest });
activeSpan?.addEvent('Extracting signature from request');
if (!accessKey || !signatureFromRequest) {
log.debug('invalid access key/signature parameters');
activeSpan.recordException(errors.MissingSecurityHeader);
extractParamsSpan.end();
return { err: errors.MissingSecurityHeader };
}
const stringToSign = constructStringToSign(request, data, log);
log.trace('constructed string to sign', { stringToSign });
activeSpan?.addEvent('Constructed string to sign v2 query');
const algo = algoCheck(signatureFromRequest.length);
log.trace('algo for calculating signature', { algo });
activeSpan?.addEvent('Checked algorithm for calculating signature');
if (algo === undefined) {
activeSpan.recordException(errors.InvalidArgument);
extractParamsSpan.end();
return { err: errors.InvalidArgument };
}
activeSpan?.addEvent('Exiting query auth check');
extractParamsSpan.end();
return {
err: null,
params: {
version: 2,
data: {
accessKey,
signatureFromRequest,
stringToSign,
algo,
authType: 'REST-QUERY-STRING',
signatureVersion: 'AWS',
securityToken: token,
return tracer.startActiveSpan('Query Auth Check', undefined, activeTracerContext, authCheckSpan => {
log.trace('running query auth check');
activeSpan?.addEvent('Running query auth check');
if (request.method === 'POST') {
log.debug('query string auth not supported for post requests');
activeSpan.recordException(errors.NotImplemented);
authCheckSpan.end();
return { err: errors.NotImplemented };
}
const token = data.SecurityToken;
activeSpan?.addEvent('Extracting security token');
if (token && !constants.iamSecurityToken.pattern.test(token)) {
log.debug('invalid security token', { token });
activeSpan.recordException(errors.InvalidToken);
authCheckSpan.end();
return { err: errors.InvalidToken };
}
activeSpan?.addEvent('Extracted security token');
/*
Check whether request has expired or if
expires parameter is more than 604800000 milliseconds
(7 days) in the future.
Expires time is provided in seconds so need to
multiply by 1000 to obtain
milliseconds to compare to Date.now()
*/
activeSpan?.addEvent('Checking expiration time');
const expirationTime = parseInt(data.Expires, 10) * 1000;
if (Number.isNaN(expirationTime)) {
log.debug('invalid expires parameter', { expires: data.Expires });
activeSpan.recordException(errors.MissingSecurityHeader);
authCheckSpan.end();
return { err: errors.MissingSecurityHeader };
}
activeSpan?.addEvent('Checked expiration time');
const currentTime = Date.now();
const preSignedURLExpiry = process.env.PRE_SIGN_URL_EXPIRY
&& !Number.isNaN(process.env.PRE_SIGN_URL_EXPIRY)
? Number.parseInt(process.env.PRE_SIGN_URL_EXPIRY, 10)
: constants.defaultPreSignedURLExpiry * 1000;
if (expirationTime > currentTime + preSignedURLExpiry) {
log.debug('expires parameter too far in future', { expires: request.query.Expires });
activeSpan.recordException(errors.AccessDenied);
authCheckSpan.end();
return { err: errors.AccessDenied };
}
if (currentTime > expirationTime) {
log.debug('current time exceeds expires time', { expires: request.query.Expires });
activeSpan.recordException(errors.RequestTimeTooSkewed);
authCheckSpan.end();
return { err: errors.RequestTimeTooSkewed };
}
const accessKey = data.AWSAccessKeyId;
// @ts-ignore
log.addDefaultFields({ accessKey });
const signatureFromRequest = decodeURIComponent(data.Signature);
log.trace('signature from request', { signatureFromRequest });
activeSpan?.addEvent('Extracting signature from request');
if (!accessKey || !signatureFromRequest) {
log.debug('invalid access key/signature parameters');
activeSpan.recordException(errors.MissingSecurityHeader);
authCheckSpan.end();
return { err: errors.MissingSecurityHeader };
}
const stringToSign = constructStringToSign(request, data, log);
log.trace('constructed string to sign', { stringToSign });
activeSpan?.addEvent('Constructed string to sign');
const algo = algoCheck(signatureFromRequest.length);
log.trace('algo for calculating signature', { algo });
activeSpan?.addEvent('Checked algorithm for calculating signature');
if (algo === undefined) {
activeSpan.recordException(errors.InvalidArgument);
authCheckSpan.end();
return { err: errors.InvalidArgument };
}
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;
proxyPath?: string;
awsService: string;
}, oTel?: any,): string | Error {
}): string | Error {
const {
request,
signedHeaders,
@ -29,12 +29,7 @@ export default function constructStringToSign(params: {
proxyPath,
} = params;
const path = proxyPath || request.path;
const {
activeSpan,
activeTracerContext,
tracer,
} = oTel;
activeSpan?.addEvent('Constructing canonical request for Authv4');
const canonicalReqResult = createCanonicalRequest({
pHttpVerb: request.method,
pResource: path,
@ -43,7 +38,8 @@ export default function constructStringToSign(params: {
pSignedHeaders: signedHeaders,
payloadChecksum,
service: params.awsService,
}, oTel);
});
// TODO Why that line?
// @ts-ignore
if (canonicalReqResult instanceof Error) {
@ -55,13 +51,9 @@ export default function constructStringToSign(params: {
if (log) {
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 canonicalHex = sha256.update(canonicalReqResult, 'binary')
.digest('hex');
activeSpan?.addEvent('Created signature hash for AuthV4 using crypto sha256');
createSignatureSpan.end();
const stringToSign = `AWS4-HMAC-SHA256\n${timestamp}\n` +
`${credentialScope}\n${canonicalHex}`;
return stringToSign;

View File

@ -19,16 +19,8 @@ export default function createCanonicalRequest(
pSignedHeaders: any;
service: string;
payloadChecksum: string;
},
oTel?: any,
}
) {
const {
activeSpan,
activeTracerContext,
tracer,
} = oTel;
activeSpan?.addEvent('Entered createCanonicalRequest');
const pHttpVerb = params.pHttpVerb;
const pResource = params.pResource;
const pQuery = params.pQuery;
@ -36,34 +28,35 @@ export default function createCanonicalRequest(
const pSignedHeaders = params.pSignedHeaders;
const service = params.service;
let payloadChecksum = params.payloadChecksum;
const payloadChecksumSpan = tracer.startSpan('ComputePayloadChecksum');
if (!payloadChecksum) {
if (pHttpVerb === 'GET') {
payloadChecksum = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b' +
'934ca495991b7852b855';
} else if (pHttpVerb === 'POST') {
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'])) {
notEncodeStar = true;
}
let payload = queryString.stringify(pQuery, undefined, undefined, {
encodeURIComponent: input => awsURIencode(input, true, notEncodeStar),
encodeURIComponent: input => awsURIencode(input, true,
notEncodeStar),
});
payload = payload.replace(/%20/g, '+');
payloadChecksum = crypto.createHash('sha256')
.update(payload, 'binary').digest('hex').toLowerCase();
}
}
payloadChecksumSpan.end();
const canonicalURISpan = tracer.startSpan('ComputeCanonicalURI');
const canonicalURI = !!pResource ? awsURIencode(pResource, false) : '/';
canonicalURISpan.end();
const canonicalQueryStrSpan = tracer.startSpan('ComputeCanonicalQueryStr');
// canonical query string
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 encodedKey = awsURIencode(key);
const value = pQuery[key] ? awsURIencode(pQuery[key]) : '';
@ -71,54 +64,32 @@ export default function createCanonicalRequest(
});
canonicalQueryStr = sortedQueryParams.join('&');
}
canonicalQueryStrSpan.end();
const signedHeadersSpan = tracer.startSpan('SortSignedHeadersAlphabetically');
activeSpan?.addEvent('Splitting signed headers using deliminator: ;');
// signed headers
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));
activeSpan?.addEvent('Sorted signed headers alphabetically');
activeSpan?.addEvent('Joining signed headers using deliminator: ;');
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) => {
if (pHeaders[signedHeader] !== undefined) {
const trimmedHeader = pHeaders[signedHeader]
.trim().replace(/\s+/g, ' ');
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') {
return `${signedHeader}:100-continue\n`;
}
// handle case where signed 'header' is actually query param
return `${signedHeader}:${pQuery[signedHeader]}\n`;
});
canonicalHeadersListSpan.end();
const canonicalHeadersSpan = tracer.startSpan('JoinAllCanonicalHeaders using no deliminator');
const canonicalHeaders = canonicalHeadersList.join('');
canonicalHeadersSpan.end();
const canonicalRequestSpan = tracer.startSpan('ConstructCanonicalRequest');
const canonicalRequest = `${pHttpVerb}\n${canonicalURI}\n` +
`${canonicalQueryStr}\n${canonicalHeaders}\n` +
`${signedHeaders}\n${payloadChecksum}`;
canonicalRequestSpan.end();
return canonicalRequest;
}

View File

@ -22,165 +22,181 @@ import {
* @param awsService - Aws service ('iam' or 's3')
*/
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');
log.trace('running header auth check');
activeSpan?.addEvent('Extracting security token');
const token = request.headers['x-amz-security-token'];
if (token && !constants.iamSecurityToken.pattern.test(token)) {
log.debug('invalid security token', { token });
activeSpan.recordException(errors.InvalidToken);
extractParamsSpan.end();
return { err: errors.InvalidToken };
}
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);
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 };
return tracer.startActiveSpan('V4 Header Auth Check', undefined, activeTracerContext, authCheckSpan => {
log.trace('running header auth check');
activeSpan?.addEvent('Extracting security token');
const token = request.headers['x-amz-security-token'];
if (token && !constants.iamSecurityToken.pattern.test(token)) {
log.debug('invalid security token', { token });
activeSpan.recordException(errors.InvalidToken);
authCheckSpan.end();
return { err: errors.InvalidToken };
}
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);
extractParamsSpan.end();
authCheckSpan.end();
return { err: errors.MissingSecurityHeader };
}
}
log.trace('authorization header from request', { authHeader });
const signatureFromRequest = authHeaderItems.signatureFromRequest!;
const credentialsArr = authHeaderItems.credentialsArr!;
const signedHeaders = authHeaderItems.signedHeaders!;
activeSpan.addEvent('Checking if signed headers are complete');
if (!areSignedHeadersComplete(signedHeaders, request.headers)) {
log.debug('signedHeaders are incomplete', { signedHeaders });
activeSpan.recordException(errors.AccessDenied);
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'];
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);
authCheckSpan.end();
return { err: errors.InvalidArgument };
}
} else if (request.headers.date) {
timestamp = convertUTCtoISO8601(request.headers.date);
}
if (!timestamp) {
log.debug('missing or invalid date header', { method: 'auth/v4/headerAuthCheck.check' });
activeSpan.recordException(errors.AccessDenied.customizeDescription('Authentication requires a valid Date or x-amz-date header'));
extractParamsSpan.end();
return { err: errors.AccessDenied.customizeDescription('Authentication requires a valid Date or x-amz-date header') };
}
activeSpan.addEvent('Request timestamp is valid');
activeSpan.addEvent('Validating credentials');
const validationResult = validateCredentials(credentialsArr, timestamp, log);
if (validationResult instanceof Error) {
log.debug('credentials in improper format', { credentialsArr, timestamp, validationResult });
activeSpan.recordException(validationResult);
extractParamsSpan.end();
return { err: validationResult };
}
activeSpan.addEvent('Credentials are valid');
// credentialsArr is [accessKey, date, region, aws-service, aws4_request]
const scopeDate = credentialsArr[1];
const region = credentialsArr[2];
const service = credentialsArr[3];
const accessKey = credentialsArr.shift();
const credentialScope = credentialsArr.join('/');
// In AWS Signature Version 4, the signing key is valid for up to seven days
// (see Introduction to Signing Requests.
// Therefore, a signature is also valid for up to seven days or
// less if specified by a bucket policy.
// Since policies are not yet implemented, we will have a 15
// 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);
extractParamsSpan.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,
}, oTel);
log.trace('constructed stringToSign', { stringToSign });
if (stringToSign instanceof Error) {
activeSpan.recordException(stringToSign);
extractParamsSpan.end();
return { err: stringToSign };
}
activeSpan.addEvent('Constructed string to sign v4 headers');
activeSpan.addEvent('Exiting V4 header auth check');
extractParamsSpan.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,
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);
authCheckSpan.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);
authCheckSpan.end();
return { err: errors.InvalidArgument };
}
if (!request.headers['x-amz-decoded-content-length']) {
activeSpan.recordException(errors.MissingSecurityHeader);
authCheckSpan.end();
return { err: errors.MissingSecurityHeader };
}
}
log.trace('authorization header from request', { authHeader });
const signatureFromRequest = authHeaderItems.signatureFromRequest!;
const credentialsArr = authHeaderItems.credentialsArr!;
const signedHeaders = authHeaderItems.signedHeaders!;
activeSpan.addEvent('Checking if signed headers are complete');
if (!areSignedHeadersComplete(signedHeaders, request.headers)) {
log.debug('signedHeaders are incomplete', { signedHeaders });
activeSpan.recordException(errors.AccessDenied);
authCheckSpan.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) {
timestamp = convertUTCtoISO8601(request.headers.date);
}
if (!timestamp) {
log.debug('missing or invalid date header', { method: 'auth/v4/headerAuthCheck.check' });
activeSpan.recordException(errors.AccessDenied.customizeDescription('Authentication requires a valid Date or x-amz-date header'));
authCheckSpan.end();
return { err: errors.AccessDenied.customizeDescription('Authentication requires a valid Date or x-amz-date header') };
}
activeSpan.addEvent('Request timestamp is valid');
activeSpan.addEvent('Validating credentials');
const validationResult = validateCredentials(credentialsArr, timestamp, log);
if (validationResult instanceof Error) {
log.debug('credentials in improper format', { credentialsArr, timestamp, validationResult });
activeSpan.recordException(validationResult);
authCheckSpan.end();
return { err: validationResult };
}
activeSpan.addEvent('Credentials are valid');
// credentialsArr is [accessKey, date, region, aws-service, aws4_request]
const scopeDate = credentialsArr[1];
const region = credentialsArr[2];
const service = credentialsArr[3];
const accessKey = credentialsArr.shift();
const credentialScope = credentialsArr.join('/');
// In AWS Signature Version 4, the signing key is valid for up to seven days
// (see Introduction to Signing Requests.
// Therefore, a signature is also valid for up to seven days or
// less if specified by a bucket policy.
// Since policies are not yet implemented, we will have a 15
// 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) {
const {
activeSpan,
extractParamsSpan,
activeTracerContext,
tracer,
} = oTel;
activeSpan?.addEvent('Arsenal:: entered Arsenal.auth.v4.queryAuthCheck');
activeSpan?.addEvent('Arsenal:: extracting query parameters')
const authParams = extractQueryParams(data, log);
activeSpan?.addEvent('Arsenal:: extracting query params');
if (Object.keys(authParams).length !== 5) {
activeSpan.recordException(errors.InvalidArgument);
extractParamsSpan.end();
return { err: errors.InvalidArgument };
}
// Query params are not specified in AWS documentation as case-insensitive,
// so we use case-sensitive
const token = data['X-Amz-Security-Token'];
if (token && !constants.iamSecurityToken.pattern.test(token)) {
log.debug('invalid security token', { token });
activeSpan.recordException(errors.InvalidToken);
extractParamsSpan.end();
return { err: errors.InvalidToken };
}
const signedHeaders = authParams.signedHeaders!;
const signatureFromRequest = authParams.signatureFromRequest!;
const timestamp = authParams.timestamp!;
const expiry = authParams.expiry!;
const credential = authParams.credential!;
if (!areSignedHeadersComplete(signedHeaders, request.headers)) {
log.debug('signedHeaders are incomplete', { signedHeaders });
activeSpan.recordException(errors.AccessDenied);
extractParamsSpan.end();
return { err: errors.AccessDenied };
}
const validationResult = validateCredentials(credential, timestamp,
log);
if (validationResult instanceof Error) {
log.debug('credentials in improper format', { credential,
timestamp, validationResult });
activeSpan.recordException(validationResult);
extractParamsSpan.end();
return { err: validationResult };
}
const accessKey = credential[0];
const scopeDate = credential[1];
const region = credential[2];
const service = credential[3];
const requestType = credential[4];
const isTimeSkewed = checkTimeSkew(timestamp, expiry, log);
if (isTimeSkewed) {
activeSpan.recordException(errors.RequestTimeTooSkewed);
extractParamsSpan.end();
return { err: errors.RequestTimeTooSkewed };
}
// In query v4 auth, the canonical request needs
// to include the query params OTHER THAN
// the signature so create a
// copy of the query object and remove
// the X-Amz-Signature property.
const queryWithoutSignature = Object.assign({}, data);
delete queryWithoutSignature['X-Amz-Signature'];
// For query auth, instead of a
// checksum of the contents, the
// string 'UNSIGNED-PAYLOAD' should be
// added to the canonicalRequest in
// building string to sign
const payloadChecksum = 'UNSIGNED-PAYLOAD';
activeSpan?.addEvent('Constructing string to sign');
const stringToSign = constructStringToSign({
log,
request,
query: queryWithoutSignature,
signedHeaders,
payloadChecksum,
timestamp,
credentialScope:
`${scopeDate}/${region}/${service}/${requestType}`,
awsService: service,
}, oTel);
activeSpan?.addEvent('Constructed string to sign v4 query');
if (stringToSign instanceof Error) {
activeSpan.recordException(stringToSign);
extractParamsSpan.end();
return { err: stringToSign };
}
log.trace('constructed stringToSign', { stringToSign });
activeSpan.addEvent('Arsenal:: exiting Arsenal.auth.v4.queryAuthCheck');
extractParamsSpan.end();
return {
err: null,
params: {
version: 4,
data: {
accessKey,
signatureFromRequest,
region,
scopeDate,
stringToSign,
authType: 'REST-QUERY-STRING',
signatureVersion: 'AWS4-HMAC-SHA256',
signatureAge: Date.now() - convertAmzTimeToMs(timestamp),
securityToken: token,
return tracer.startActiveSpan('Arsenal::Arsenal.auth.v4.queryAuthCheck', undefined, activeTracerContext, authCheckSpan => {
activeSpan?.addEvent('Arsenal:: extracting query parameters')
const authParams = extractQueryParams(data, log);
activeSpan?.addEvent('Arsenal:: extracting query params');
if (Object.keys(authParams).length !== 5) {
activeSpan.recordException(errors.InvalidArgument);
authCheckSpan.end();
return { err: errors.InvalidArgument };
}
// Query params are not specified in AWS documentation as case-insensitive,
// so we use case-sensitive
const token = data['X-Amz-Security-Token'];
if (token && !constants.iamSecurityToken.pattern.test(token)) {
log.debug('invalid security token', { token });
activeSpan.recordException(errors.InvalidToken);
authCheckSpan.end();
return { err: errors.InvalidToken };
}
const signedHeaders = authParams.signedHeaders!;
const signatureFromRequest = authParams.signatureFromRequest!;
const timestamp = authParams.timestamp!;
const expiry = authParams.expiry!;
const credential = authParams.credential!;
if (!areSignedHeadersComplete(signedHeaders, request.headers)) {
log.debug('signedHeaders are incomplete', { signedHeaders });
activeSpan.recordException(errors.AccessDenied);
authCheckSpan.end();
return { err: errors.AccessDenied };
}
const validationResult = validateCredentials(credential, timestamp,
log);
if (validationResult instanceof Error) {
log.debug('credentials in improper format', { credential,
timestamp, validationResult });
activeSpan.recordException(validationResult);
authCheckSpan.end();
return { err: validationResult };
}
const accessKey = credential[0];
const scopeDate = credential[1];
const region = credential[2];
const service = credential[3];
const requestType = credential[4];
const isTimeSkewed = checkTimeSkew(timestamp, expiry, log);
if (isTimeSkewed) {
activeSpan.recordException(errors.RequestTimeTooSkewed);
authCheckSpan.end();
return { err: errors.RequestTimeTooSkewed };
}
// In query v4 auth, the canonical request needs
// to include the query params OTHER THAN
// the signature so create a
// copy of the query object and remove
// the X-Amz-Signature property.
const queryWithoutSignature = Object.assign({}, data);
delete queryWithoutSignature['X-Amz-Signature'];
// For query auth, instead of a
// checksum of the contents, the
// string 'UNSIGNED-PAYLOAD' should be
// added to the canonicalRequest in
// building string to sign
const payloadChecksum = 'UNSIGNED-PAYLOAD';
const stringToSign = constructStringToSign({
log,
request,
query: queryWithoutSignature,
signedHeaders,
payloadChecksum,
timestamp,
credentialScope:
`${scopeDate}/${region}/${service}/${requestType}`,
awsService: service,
});
if (stringToSign instanceof Error) {
activeSpan.recordException(stringToSign);
authCheckSpan.end();
return { err: stringToSign };
}
log.trace('constructed stringToSign', { stringToSign });
activeSpan.addEvent('Arsenal:: exiting Arsenal.auth.v4.queryAuthCheck');
authCheckSpan.end();
return {
err: null,
params: {
version: 4,
data: {
accessKey,
signatureFromRequest,
region,
scopeDate,
stringToSign,
authType: 'REST-QUERY-STRING',
signatureVersion: 'AWS4-HMAC-SHA256',
signatureAge: Date.now() - convertAmzTimeToMs(timestamp),
securityToken: token,
},
},
},
};
};
});
}

View File

@ -23,6 +23,11 @@ export default function routerGET(
} = dataRetrievalParams;
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');
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()')
log.debug('routing request', { method: 'routerGET' });

View File

@ -638,7 +638,7 @@ export function responseStreamData(
},
} = retrieveDataParams;
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({
'code.function': 'Arsenal:: responseStreamData()',
'code.filepath': 'lib/s3routes/routesUtils.js',