Skip to content

Commit a8a6cec

Browse files
committed
CLDSRV-863: handle trailer headers and add new trailer errors
1 parent 7fb197c commit a8a6cec

File tree

2 files changed

+422
-12
lines changed

2 files changed

+422
-12
lines changed

lib/api/apiUtils/integrity/validateChecksums.js

Lines changed: 143 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ const ChecksumError = Object.freeze({
3737
MultipleChecksumTypes: 'MultipleChecksumTypes',
3838
MissingCorresponding: 'MissingCorresponding',
3939
MalformedChecksum: 'MalformedChecksum',
40+
TrailerAlgoMismatch: 'TrailerAlgoMismatch',
41+
TrailerChecksumMalformed: 'TrailerChecksumMalformed',
42+
TrailerMissing: 'TrailerMissing',
43+
TrailerUnexpected: 'TrailerUnexpected',
44+
TrailerAndChecksum: 'TrailerAndChecksum',
45+
TrailerNotSupported: 'TrailerNotSupported',
4046
});
4147

4248
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
@@ -56,35 +62,51 @@ const algorithms = Object.freeze({
5662
const result = await crc.digest();
5763
return Buffer.from(result).toString('base64');
5864
},
65+
digestFromHash: async hash => {
66+
const result = await hash.digest();
67+
return Buffer.from(result).toString('base64');
68+
},
5969
isValidDigest: expected => typeof expected === 'string' && expected.length === 12 && base64Regex.test(expected),
70+
createHash: () => new CrtCrc64Nvme()
6071
},
6172
crc32: {
6273
digest: data => {
6374
const input = Buffer.isBuffer(data) ? data : Buffer.from(data);
6475
return uint32ToBase64(new Crc32().update(input).digest() >>> 0); // >>> 0 coerce number to uint32
6576
},
77+
digestFromHash: hash => {
78+
const result = hash.digest();
79+
return uint32ToBase64(result >>> 0);
80+
},
6681
isValidDigest: expected => typeof expected === 'string' && expected.length === 8 && base64Regex.test(expected),
82+
createHash: () => new Crc32()
6783
},
6884
crc32c: {
6985
digest: data => {
7086
const input = Buffer.isBuffer(data) ? data : Buffer.from(data);
7187
return uint32ToBase64(new Crc32c().update(input).digest() >>> 0); // >>> 0 coerce number to uint32
7288
},
89+
digestFromHash: hash => uint32ToBase64(hash.digest() >>> 0),
7390
isValidDigest: expected => typeof expected === 'string' && expected.length === 8 && base64Regex.test(expected),
91+
createHash: () => new Crc32c()
7492
},
7593
sha1: {
7694
digest: data => {
7795
const input = Buffer.isBuffer(data) ? data : Buffer.from(data);
7896
return crypto.createHash('sha1').update(input).digest('base64');
7997
},
98+
digestFromHash: hash => hash.digest('base64'),
8099
isValidDigest: expected => typeof expected === 'string' && expected.length === 28 && base64Regex.test(expected),
100+
createHash: () => crypto.createHash('sha1')
81101
},
82102
sha256: {
83103
digest: data => {
84104
const input = Buffer.isBuffer(data) ? data : Buffer.from(data);
85105
return crypto.createHash('sha256').update(input).digest('base64');
86106
},
107+
digestFromHash: hash => hash.digest('base64'),
87108
isValidDigest: expected => typeof expected === 'string' && expected.length === 44 && base64Regex.test(expected),
109+
createHash: () => crypto.createHash('sha256')
88110
}
89111
});
90112

@@ -141,6 +163,94 @@ async function validateXAmzChecksums(headers, body) {
141163
return null;
142164
}
143165

166+
function getChecksumDataFromHeaders(headers) {
167+
const checkSdk = algo => {
168+
if (!('x-amz-sdk-checksum-algorithm' in headers)) {
169+
return null;
170+
}
171+
172+
const sdkAlgo = headers['x-amz-sdk-checksum-algorithm'];
173+
if (typeof sdkAlgo !== 'string') {
174+
return { error: ChecksumError.AlgoNotSupportedSDK, details: { algorithm: sdkAlgo } };
175+
}
176+
177+
const sdkLowerAlgo = sdkAlgo.toLowerCase();
178+
if (!(sdkLowerAlgo in algorithms)) {
179+
return { error: ChecksumError.AlgoNotSupportedSDK, details: { algorithm: sdkAlgo } };
180+
}
181+
182+
// If AWS there is a mismatch, AWS returns the same error as if the algo was invalid.
183+
if (sdkLowerAlgo !== algo) {
184+
return { error: ChecksumError.AlgoNotSupportedSDK, details: { algorithm: sdkAlgo } };
185+
}
186+
187+
return null;
188+
};
189+
190+
const checksumHeaders = Object.keys(headers).filter(header => header.startsWith('x-amz-checksum-'));
191+
const xAmzChecksumCnt = checksumHeaders.length;
192+
if (xAmzChecksumCnt > 1) {
193+
return { error: ChecksumError.MultipleChecksumTypes, details: { algorithms: checksumHeaders } };
194+
}
195+
196+
if (xAmzChecksumCnt === 0 && !('x-amz-trailer' in headers) && 'x-amz-sdk-checksum-algorithm' in headers) {
197+
return {
198+
error: ChecksumError.MissingCorresponding,
199+
details: { expected: headers['x-amz-sdk-checksum-algorithm'] }
200+
};
201+
}
202+
203+
if ('x-amz-trailer' in headers) {
204+
if (xAmzChecksumCnt !== 0) {
205+
return {
206+
error: ChecksumError.TrailerAndChecksum,
207+
details: { trailer: headers['x-amz-trailer'], checksum: checksumHeaders },
208+
};
209+
}
210+
211+
const trailer = headers['x-amz-trailer'];
212+
if (!trailer.startsWith('x-amz-checksum-')) {
213+
return { error: ChecksumError.TrailerNotSupported, details: { value: trailer } };
214+
}
215+
216+
const trailerAlgo = trailer.slice('x-amz-checksum-'.length);
217+
if (!(trailerAlgo in algorithms)) {
218+
return { error: ChecksumError.TrailerNotSupported, details: { value: trailer } };
219+
}
220+
221+
const err = checkSdk(trailerAlgo);
222+
if (err) {
223+
return err;
224+
}
225+
226+
return { algorithm: trailerAlgo, isTrailer: true, expected: undefined };
227+
}
228+
229+
if (xAmzChecksumCnt === 0) {
230+
// There was no x-amz-checksum- or x-amz-trailer return crc64nvme.
231+
// The calculated crc64nvme will be stored in the object metadata.
232+
return { algorithm: 'crc64nvme', isTrailer: false, expected: undefined };
233+
}
234+
235+
// No x-amz-sdk-checksum-algorithm we expect one x-amz-checksum-[crc64nvme, crc32, crc32C, sha1, sha256].
236+
const algo = checksumHeaders[0].slice('x-amz-checksum-'.length);
237+
if (!(algo in algorithms)) {
238+
return { error: ChecksumError.AlgoNotSupported, details: { algorithm: algo } };
239+
}
240+
241+
const expected = headers[`x-amz-checksum-${algo}`];
242+
if (!algorithms[algo].isValidDigest(expected)) {
243+
return { error: ChecksumError.MalformedChecksum, details: { algorithm: algo, expected } };
244+
}
245+
246+
const err = checkSdk(algo);
247+
if (err) {
248+
return err;
249+
}
250+
251+
return { algorithm: algo, isTrailer: false, expected };
252+
}
253+
144254
/**
145255
* validateChecksumsNoChunking - Validate the checksums of a request.
146256
* @param {object} headers - http headers
@@ -183,16 +293,7 @@ async function validateChecksumsNoChunking(headers, body) {
183293
return err;
184294
}
185295

186-
async function defaultValidationFunc(request, body, log) {
187-
const err = await validateChecksumsNoChunking(request.headers, body);
188-
if (!err) {
189-
return null;
190-
}
191-
192-
if (err.error !== ChecksumError.MissingChecksum) {
193-
log.debug('failed checksum validation', { method: request.apiMethod }, err);
194-
}
195-
296+
function arsenalErrorFromChecksumError(err) {
196297
switch (err.error) {
197298
case ChecksumError.MissingChecksum:
198299
return null;
@@ -225,11 +326,40 @@ async function defaultValidationFunc(request, body, log) {
225326
);
226327
case ChecksumError.MD5Invalid:
227328
return ArsenalErrors.InvalidDigest;
329+
case ChecksumError.TrailerAlgoMismatch:
330+
return ArsenalErrors.MalformedTrailerError;
331+
case ChecksumError.TrailerMissing:
332+
return ArsenalErrors.MalformedTrailerError;
333+
case ChecksumError.TrailerUnexpected:
334+
return ArsenalErrors.MalformedTrailerError;
335+
case ChecksumError.TrailerChecksumMalformed:
336+
return ArsenalErrors.InvalidRequest.customizeDescription(
337+
`Value for x-amz-checksum-${err.details.algorithm} trailing header is invalid.`
338+
);
339+
case ChecksumError.TrailerAndChecksum:
340+
return ArsenalErrors.InvalidRequest.customizeDescription('Expecting a single x-amz-checksum- header');
341+
case ChecksumError.TrailerNotSupported:
342+
return ArsenalErrors.InvalidRequest.customizeDescription(
343+
'The value specified in the x-amz-trailer header is not supported'
344+
);
228345
default:
229346
return ArsenalErrors.BadDigest;
230347
}
231348
}
232349

350+
async function defaultValidationFunc(request, body, log) {
351+
const err = await validateChecksumsNoChunking(request.headers, body);
352+
if (!err) {
353+
return null;
354+
}
355+
356+
if (err.error !== ChecksumError.MissingChecksum) {
357+
log.debug('failed checksum validation', { method: request.apiMethod }, err);
358+
}
359+
360+
return arsenalErrorFromChecksumError(err);
361+
}
362+
233363
/**
234364
* validateMethodChecksumsNoChunking - Validate the checksums of a request.
235365
* @param {object} request - http request
@@ -253,5 +383,8 @@ module.exports = {
253383
ChecksumError,
254384
validateChecksumsNoChunking,
255385
validateMethodChecksumNoChunking,
386+
getChecksumDataFromHeaders,
387+
arsenalErrorFromChecksumError,
388+
algorithms,
256389
checksumedMethods,
257390
};

0 commit comments

Comments
 (0)