Skip to content

Commit 0a01938

Browse files
committed
CLDSRV-848: check x-amz-checksum-[crc64nvme, crc32, crc32C, sha1, sha256] headers
1 parent 5889490 commit 0a01938

File tree

5 files changed

+444
-66
lines changed

5 files changed

+444
-66
lines changed

lib/Config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,11 +556,15 @@ function parseIntegrityChecks(config) {
556556
'bucketPutReplication': true,
557557
'bucketPutVersioning': true,
558558
'bucketPutWebsite': true,
559+
'bucketPutLogging': true,
560+
'bucketPutTagging': true,
559561
'multiObjectDelete': true,
560562
'objectPutACL': true,
561563
'objectPutLegalHold': true,
562564
'objectPutTagging': true,
563565
'objectPutRetention': true,
566+
'objectRestore': true,
567+
'completeMultipartUpload': true,
564568
};
565569

566570
if (config && config.integrityChecks) {

lib/api/api.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -300,15 +300,19 @@ const api = {
300300
}
301301

302302
const buff = Buffer.concat(post, bodyLength);
303+
return validateMethodChecksumNoChunking(request, buff, log)
304+
.then(error => {
305+
if (error) {
306+
return next(error);
307+
}
303308

304-
const err = validateMethodChecksumNoChunking(request, buff, log);
305-
if (err) {
306-
return next(err);
307-
}
308-
309-
// Convert array of post buffers into one string
310-
request.post = buff.toString();
311-
return next(null, userInfo, authorizationResults, streamingV4Params, infos);
309+
// Convert array of post buffers into one string
310+
request.post = buff.toString();
311+
return next(null, userInfo, authorizationResults, streamingV4Params, infos);
312+
})
313+
.catch(error => {
314+
next(error);
315+
});
312316
});
313317
return undefined;
314318
},

lib/api/apiUtils/integrity/validateChecksums.js

Lines changed: 199 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,226 @@
11
const crypto = require('crypto');
2+
const { Crc32 } = require('@aws-crypto/crc32');
3+
const { Crc32c } = require('@aws-crypto/crc32c');
4+
const { CrtCrc64Nvme } = require('@aws-sdk/crc64-nvme-crt');
25
const { errors: ArsenalErrors } = require('arsenal');
36
const { config } = require('../../../Config');
47

58
const ChecksumError = Object.freeze({
69
MD5Mismatch: 'MD5Mismatch',
10+
XAmzMismatch: 'XAmzMismatch',
711
MissingChecksum: 'MissingChecksum',
12+
AlgoNotSupported: 'AlgoNotSupported',
13+
AlgoNotSupportedSDK: 'AlgoNotSupportedSDK',
14+
MultipleChecksumTypes: 'MultipleChecksumTypes',
15+
MissingCorresponding: 'MissingCorresponding',
16+
InvalidAlgoValue: 'InvalidAlgoValue',
817
});
918

19+
const algorithms = {
20+
'crc64nvme': {
21+
'digest': async data => {
22+
const input = Buffer.isBuffer(data) ? data : Buffer.from(data);
23+
const crc = new CrtCrc64Nvme();
24+
crc.update(input);
25+
const result = await crc.digest();
26+
return Buffer.from(result).toString('base64');
27+
},
28+
'checkExpected': expected => {
29+
if (typeof expected !== 'string') {
30+
return false;
31+
}
32+
return expected.length === 12;
33+
},
34+
},
35+
'crc32': {
36+
'digest': data => {
37+
const input = Buffer.isBuffer(data) ? data : Buffer.from(data);
38+
return uint32ToBase64(new Crc32().update(input).digest() >>> 0);
39+
},
40+
'checkExpected': expected => {
41+
if (typeof expected !== 'string') {
42+
return false;
43+
}
44+
return expected.length === 8;
45+
},
46+
},
47+
'crc32c': {
48+
'digest': data => {
49+
const input = Buffer.isBuffer(data) ? data : Buffer.from(data);
50+
return uint32ToBase64(new Crc32c().update(input).digest() >>> 0);
51+
},
52+
'checkExpected': expected => {
53+
if (typeof expected !== 'string') {
54+
return false;
55+
}
56+
return expected.length === 8;
57+
},
58+
},
59+
'sha1': {
60+
'digest': data => {
61+
const input = Buffer.isBuffer(data) ? data : Buffer.from(data);
62+
return crypto.createHash('sha1').update(input).digest('base64');
63+
},
64+
'checkExpected': expected => {
65+
if (typeof expected !== 'string') {
66+
return false;
67+
}
68+
return expected.length === 28;
69+
},
70+
},
71+
'sha256': {
72+
'digest': data => {
73+
const input = Buffer.isBuffer(data) ? data : Buffer.from(data);
74+
return crypto.createHash('sha256').update(input).digest('base64');
75+
},
76+
'checkExpected': expected => {
77+
if (typeof expected !== 'string') {
78+
return false;
79+
}
80+
return expected.length === 44;
81+
},
82+
}
83+
};
84+
85+
function uint32ToBase64(num) {
86+
const buf = Buffer.alloc(4);
87+
buf.writeUInt32BE(num, 0);
88+
return buf.toString('base64');
89+
}
90+
91+
async function validateXAmzChecksums(headers, body) {
92+
const checksumHeaders = Object.keys(headers).filter(header => header.startsWith('x-amz-checksum-'));
93+
const xAmzCheckumCnt = checksumHeaders.length;
94+
if (xAmzCheckumCnt > 1) {
95+
return { error: ChecksumError.MultipleChecksumTypes, details: { algorithms: checksumHeaders } };
96+
}
97+
98+
if (xAmzCheckumCnt === 0 && 'x-amz-sdk-checksum-algorithm' in headers) {
99+
return {
100+
error: ChecksumError.MissingCorresponding,
101+
details: { expected: headers['x-amz-sdk-checksum-algorithm'] }
102+
};
103+
} else if (xAmzCheckumCnt === 0) {
104+
return { error: ChecksumError.MissingChecksum, details: null };
105+
}
106+
107+
// No x-amz-sdk-checksum-algorithm we expect one x-amz-checksum-[crc64nvme, crc32, crc32C, sha1, sha256].
108+
const algo = checksumHeaders[0].split('-')[3];
109+
if (typeof algo !== 'string') {
110+
return { error: ChecksumError.AlgoNotSupported, details: { algorithm: algo } };
111+
}
112+
113+
if (algo in algorithms === false) {
114+
return { error: ChecksumError.AlgoNotSupported, details: { algorithm: algo } };;
115+
}
116+
117+
const expected = headers[`x-amz-checksum-${algo}`];
118+
if (!algorithms[algo].checkExpected(expected)) {
119+
return { error: ChecksumError.InvalidAlgoValue, details: { algorithm: algo, expected } };
120+
}
121+
122+
const calculated = await algorithms[algo].digest(body);
123+
if (expected !== calculated) {
124+
return { error: ChecksumError.XAmzMismatch, details: { algorithm: algo, calculated, expected } };
125+
}
126+
127+
// AWS checks x-amz-checksum- first and then x-amz-sdk-checksum-algorithm
128+
if ('x-amz-sdk-checksum-algorithm' in headers) {
129+
const sdkAlgo = headers['x-amz-sdk-checksum-algorithm'];
130+
if (typeof sdkAlgo !== 'string') {
131+
return { error: ChecksumError.AlgoNotSupportedSDK, details: { algorithm: sdkAlgo } };
132+
}
133+
134+
const sdkLowerAlgo = sdkAlgo.toLowerCase();
135+
if (sdkLowerAlgo in algorithms === false) {
136+
return { error: ChecksumError.AlgoNotSupportedSDK, details: { algorithm: sdkAlgo } };
137+
}
138+
139+
// If AWS there is a mismatch, AWS returns the same error as if the algo was invalid.
140+
if (sdkLowerAlgo !== algo) {
141+
return { error: ChecksumError.AlgoNotSupportedSDK, details: { algorithm: sdkAlgo } };
142+
}
143+
}
144+
145+
return null;
146+
}
147+
10148
/**
11149
* validateChecksumsNoChunking - Validate the checksums of a request.
12150
* @param {object} headers - http headers
13151
* @param {Buffer} body - http request body
14152
* @return {object} - error
15153
*/
16-
function validateChecksumsNoChunking(headers, body) {
17-
if (headers && 'content-md5' in headers) {
154+
async function validateChecksumsNoChunking(headers, body) {
155+
if (!headers) {
156+
return { error: ChecksumError.MissingChecksum, details: null };
157+
}
158+
159+
let md5Present = false;
160+
if ('content-md5' in headers) {
161+
// TODO: check if the content-md5 is valid base64
18162
const md5 = crypto.createHash('md5').update(body).digest('base64');
19163
if (md5 !== headers['content-md5']) {
20164
return { error: ChecksumError.MD5Mismatch, details: { calculated: md5, expected: headers['content-md5'] } };
21165
}
22166

167+
md5Present = true;
168+
}
169+
170+
const err = await validateXAmzChecksums(headers, body);
171+
if (err && err.error === ChecksumError.MissingChecksum && md5Present) {
172+
// Don't return MissingChecksum if MD5 is present.
23173
return null;
24174
}
25175

26-
return { error: ChecksumError.MissingChecksum, details: null };
176+
return err;
27177
}
28178

29-
function defaultValidationFunc(request, body, log) {
30-
const err = validateChecksumsNoChunking(request.headers, body);
31-
if (err && err.error !== ChecksumError.MissingChecksum) {
32-
log.debug('failed checksum validation', { method: request.apiMethod }, err);
33-
return ArsenalErrors.BadDigest;
179+
async function defaultValidationFunc(request, body, log) {
180+
const err = await validateChecksumsNoChunking(request.headers, body);
181+
if (!err) {
182+
return null;
34183
}
35184

36-
return null;
185+
log.debug('failed checksum validation', { method: request.apiMethod }, err);
186+
187+
switch (err.error) {
188+
case ChecksumError.MissingChecksum:
189+
return null;
190+
case ChecksumError.XAmzMismatch: {
191+
const algoUpper = err.details.algorithm.toUpperCase();
192+
return ArsenalErrors.BadDigest.customizeDescription(
193+
`The ${algoUpper} you specified did not match the calculated checksum.`
194+
);
195+
}
196+
case ChecksumError.AlgoNotSupported:
197+
return ArsenalErrors.InvalidRequest.customizeDescription(
198+
'The algorithm type you specified in x-amz-checksum- header is invalid.'
199+
);
200+
case ChecksumError.AlgoNotSupportedSDK:
201+
return ArsenalErrors.InvalidRequest.customizeDescription(
202+
'Value for x-amz-sdk-checksum-algorithm header is invalid.'
203+
);
204+
case ChecksumError.MissingCorresponding:
205+
return ArsenalErrors.InvalidRequest.customizeDescription(
206+
'x-amz-sdk-checksum-algorithm specified, but no corresponding x-amz-checksum-* ' +
207+
'or x-amz-trailer headers were found.'
208+
);
209+
case ChecksumError.MultipleChecksumTypes:
210+
return ArsenalErrors.InvalidRequest.customizeDescription(
211+
'Expecting a single x-amz-checksum- header. Multiple checksum Types are not allowed.'
212+
);
213+
case ChecksumError.InvalidAlgoValue:
214+
return ArsenalErrors.InvalidRequest.customizeDescription(
215+
`Value for x-amz-checksum-${err.details.algorithm} header is invalid.`
216+
);
217+
default:
218+
return ArsenalErrors.BadDigest;
219+
}
37220
}
38221

39222
const methodValidationFunc = Object.freeze({
223+
'completeMultipartUpload': defaultValidationFunc,
40224
'bucketPutACL': defaultValidationFunc,
41225
'bucketPutCors': defaultValidationFunc,
42226
'bucketPutEncryption': defaultValidationFunc,
@@ -47,12 +231,15 @@ const methodValidationFunc = Object.freeze({
47231
'bucketPutReplication': defaultValidationFunc,
48232
'bucketPutVersioning': defaultValidationFunc,
49233
'bucketPutWebsite': defaultValidationFunc,
234+
'bucketPutLogging': defaultValidationFunc,
235+
'bucketPutTagging': defaultValidationFunc,
50236
// TODO: DeleteObjects requires a checksum. Should return an error if ChecksumError.MissingChecksum.
51237
'multiObjectDelete': defaultValidationFunc,
52238
'objectPutACL': defaultValidationFunc,
53239
'objectPutLegalHold': defaultValidationFunc,
54240
'objectPutTagging': defaultValidationFunc,
55241
'objectPutRetention': defaultValidationFunc,
242+
'objectRestore': defaultValidationFunc,
56243
});
57244

58245
/**
@@ -62,14 +249,13 @@ const methodValidationFunc = Object.freeze({
62249
* @param {object} log - logger
63250
* @return {object} - error
64251
*/
65-
function validateMethodChecksumNoChunking(request, body, log) {
252+
async function validateMethodChecksumNoChunking(request, body, log) {
66253
if (config.integrityChecks[request.apiMethod]) {
67254
const validationFunc = methodValidationFunc[request.apiMethod];
68255
if (!validationFunc) {
69-
return null;
256+
return null; //await defaultValidationFunc2(request, body, log);
70257
}
71-
72-
return validationFunc(request, body, log);
258+
return await validationFunc(request, body, log);
73259
}
74260

75261
return null;

tests/unit/Config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,11 +933,15 @@ describe('Config', () => {
933933
'bucketPutReplication': false,
934934
'bucketPutVersioning': false,
935935
'bucketPutWebsite': false,
936+
'bucketPutLogging': false,
937+
'bucketPutTagging': false,
936938
'multiObjectDelete': false,
937939
'objectPutACL': false,
938940
'objectPutLegalHold': false,
939941
'objectPutTagging': false,
940942
'objectPutRetention': false,
943+
'objectRestore': false,
944+
'completeMultipartUpload': false,
941945
},
942946
};
943947

0 commit comments

Comments
 (0)