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
122 changes: 122 additions & 0 deletions lib/api/apiUtils/integrity/validateChecksums.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { CrtCrc64Nvme } = require('@aws-sdk/crc64-nvme-crt');
const { errors: ArsenalErrors, errorInstances } = require('arsenal');
const { config } = require('../../../Config');
const { combinePartCrcs } = require('./crcCombine');
const { supportedSignatureChecksums, unsupportedSignatureChecksums } = require('../../../../constants');

const defaultChecksumData = Object.freeze({ algorithm: 'crc64nvme', isTrailer: false, expected: undefined });

Expand Down Expand Up @@ -45,6 +46,21 @@ const errCopyChecksumAlgoNotSupported = errorInstances.InvalidRequest.customizeD
'[CRC32, CRC32C, CRC64NVME, SHA1, SHA256]',
);

const errContentSHA256Mismatch = errorInstances.XAmzContentSHA256Mismatch;

const errMissingContentSHA256 = errorInstances.InvalidRequest.customizeDescription(
'Missing required header for this request: x-amz-content-sha256',
);

const errInvalidContentSHA256 = errorInstances.InvalidArgument.customizeDescription(
'x-amz-content-sha256 must be UNSIGNED-PAYLOAD, STREAMING-UNSIGNED-PAYLOAD-TRAILER, ' +
'STREAMING-AWS4-HMAC-SHA256-PAYLOAD, STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER, ' +
'STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD, STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER ' +
'or a valid sha256 value.',
);

const sha256HexRegex = /^[0-9a-f]{64}$/i;

// Methods that validate BOTH Content-MD5 and x-amz-checksum-* against the
// buffered request body. For these, x-amz-checksum-* is the body digest.
const checksumedMethods = Object.freeze({
Expand Down Expand Up @@ -98,6 +114,9 @@ const ChecksumError = Object.freeze({
MPUTypeWithoutAlgo: 'MPUTypeWithoutAlgo',
MPUInvalidCombination: 'MPUInvalidCombination',
CopyChecksumAlgoNotSupported: 'CopyChecksumAlgoNotSupported',
ContentSHA256Missing: 'ContentSHA256Missing',
ContentSHA256Invalid: 'ContentSHA256Invalid',
ContentSHA256Mismatch: 'ContentSHA256Mismatch',
});

const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
Expand Down Expand Up @@ -368,6 +387,90 @@ function validateContentMd5(headers, body) {
return null;
}

// Classification of an x-amz-content-sha256 header value, returned by
// parseContentSHA256.
const ContentSHA256Type = Object.freeze({
Skip: 0, // not SigV4 header auth; do not enforce
Absent: 1, // SigV4 header auth but header missing
Unsigned: 2, // UNSIGNED-PAYLOAD
Streaming: 3, // a STREAMING-* token
HexSHA256: 4, // a hex sha256 of the payload
Invalid: 5, // anything else (malformed)
});

/**
* parseContentSHA256 - Classify x-amz-content-sha256 (the SigV4 payload hash)
* for a request. The header is only meaningful for SigV4 header-authenticated
* requests, so anything else (SigV2, presigned/query, anonymous) returns
* { type: ContentSHA256Type.Skip } and is not enforced. Otherwise the header
* value is classified.
*
* @param {object} headers - http request headers
* @return {object} { type, value } where type is a ContentSHA256Type and value
* is the raw header value (null when not sent). Streaming results also carry
* a { supported } boolean.
*/
function parseContentSHA256(headers) {
const value = headers['x-amz-content-sha256'] ?? null;
const authHeader = headers.authorization;
if (typeof authHeader !== 'string' || !authHeader.startsWith('AWS4')) {
return { type: ContentSHA256Type.Skip, value };
}
if (value === null) {
return { type: ContentSHA256Type.Absent, value };
}
if (value === 'UNSIGNED-PAYLOAD') {
return { type: ContentSHA256Type.Unsigned, value };
}
if (supportedSignatureChecksums.has(value)) {
return { type: ContentSHA256Type.Streaming, value, supported: true };
}
if (unsupportedSignatureChecksums.has(value)) {
return { type: ContentSHA256Type.Streaming, value, supported: false };
}
if (sha256HexRegex.test(value)) {
return { type: ContentSHA256Type.HexSHA256, value };
}
return { type: ContentSHA256Type.Invalid, value };
}

/**
* validateXAmzContentSHA256 - Validate the SHA256 of a SigV4 request.
*
* @param {object} headers - http request headers
* @param {Buffer} body - buffered http request body
* @return {object|null} - { error: ChecksumError } on missing/invalid/mismatch,
* else null
*/
function validateXAmzContentSHA256(headers, body) {
const parsed = parseContentSHA256(headers);
switch (parsed.type) {
case ContentSHA256Type.Absent: // required for SigV4 header auth
return { error: ChecksumError.ContentSHA256Missing };
case ContentSHA256Type.Invalid:
return { error: ChecksumError.ContentSHA256Invalid, details: { value: parsed.value } };
case ContentSHA256Type.HexSHA256: {
const computed = crypto.createHash('sha256').update(body).digest('hex');
if (computed !== parsed.value.toLowerCase()) {
return {
error: ChecksumError.ContentSHA256Mismatch,
details: { calculated: computed, expected: parsed.value },
};
}
return null;
}
// Skip (non-SigV4-header auth), Unsigned and Streaming are not a literal
// payload hash, so there is nothing to validate here.
case ContentSHA256Type.Skip:
return null;
case ContentSHA256Type.Unsigned:
return null;
case ContentSHA256Type.Streaming:
return null;
}
return null;
}

/**
* validateChecksumsNoChunking - Validate the checksums of a request.
* @param {object} headers - http headers
Expand Down Expand Up @@ -448,6 +551,12 @@ function arsenalErrorFromChecksumError(err) {
);
case ChecksumError.CopyChecksumAlgoNotSupported:
return errCopyChecksumAlgoNotSupported;
case ChecksumError.ContentSHA256Missing:
return errMissingContentSHA256;
case ChecksumError.ContentSHA256Invalid:
return errInvalidContentSHA256;
case ChecksumError.ContentSHA256Mismatch:
return errContentSHA256Mismatch;
default:
return ArsenalErrors.BadDigest;
}
Expand Down Expand Up @@ -547,6 +656,15 @@ function md5OnlyValidationFunc(request, body, log) {
* @return {object} - error
*/
async function validateMethodChecksumNoChunking(request, body, log) {
const contentSHA256Err = validateXAmzContentSHA256(request.headers, body);
if (contentSHA256Err) {
log.debug('failed x-amz-content-sha256 validation', {
method: request.apiMethod,
...contentSHA256Err.details,
});
return arsenalErrorFromChecksumError(contentSHA256Err);
}

if (config.integrityChecks[request.apiMethod] === false) {
return null;
}
Expand Down Expand Up @@ -738,6 +856,10 @@ module.exports = {
ChecksumError,
defaultChecksumData,
validateChecksumsNoChunking,
ContentSHA256Type,
parseContentSHA256,
errInvalidContentSHA256,
validateXAmzContentSHA256,
validateMethodChecksumNoChunking,
getChecksumDataFromHeaders,
arsenalErrorFromChecksumError,
Expand Down
Loading
Loading