From fa0e784e28e3d03f625bd40bbe7504d5bfb9a07e Mon Sep 17 00:00:00 2001 From: Leif Henriksen Date: Mon, 9 Feb 2026 17:54:24 +0100 Subject: [PATCH 1/3] CLDSRV-847: bump arsenal version --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 7f38a50b41..f8fae84828 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "@azure/storage-blob": "^12.28.0", "@hapi/joi": "^17.1.1", - "arsenal": "git+https://github.com/scality/arsenal#8.2.44", + "arsenal": "git+https://github.com/scality/arsenal#8.2.45", "async": "2.6.4", "aws-sdk": "^2.1692.0", "bucketclient": "scality/bucketclient#8.2.7", diff --git a/yarn.lock b/yarn.lock index 033250cbc6..7f7c2d1403 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1527,9 +1527,9 @@ arraybuffer.prototype.slice@^1.0.4: optionalDependencies: ioctl "^2.0.2" -"arsenal@git+https://github.com/scality/arsenal#8.2.44": - version "8.2.44" - resolved "git+https://github.com/scality/arsenal#960e77028b6eb614dde297e50a1530dcd9015f16" +"arsenal@git+https://github.com/scality/arsenal#8.2.45": + version "8.2.45" + resolved "git+https://github.com/scality/arsenal#af610f2510084a6e12a7c4c38b85eeb24a0e468c" dependencies: "@azure/identity" "^4.13.0" "@azure/storage-blob" "^12.28.0" From e819b487f474057a590a67bcf452921ca2f9b949 Mon Sep 17 00:00:00 2001 From: Leif Henriksen Date: Tue, 10 Feb 2026 13:22:42 +0100 Subject: [PATCH 2/3] CLDSRV-847: test x-amz-content-256 is not required in signed headers --- .../raw-node/test/unsignedChecksumHeaders.js | 126 ++++++++++++++++++ .../raw-node/utils/HttpRequestAuthV4.js | 18 ++- 2 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 tests/functional/raw-node/test/unsignedChecksumHeaders.js diff --git a/tests/functional/raw-node/test/unsignedChecksumHeaders.js b/tests/functional/raw-node/test/unsignedChecksumHeaders.js new file mode 100644 index 0000000000..26340ff496 --- /dev/null +++ b/tests/functional/raw-node/test/unsignedChecksumHeaders.js @@ -0,0 +1,126 @@ +const assert = require('assert'); +const async = require('async'); +const crypto = require('crypto'); +const { makeS3Request } = require('../utils/makeRequest'); +const HttpRequestAuthV4 = require('../utils/HttpRequestAuthV4'); +const url = require('url'); + +const bucket = 'testunsignedcontentshabucket'; +const objectKey = 'key'; +const objData = Buffer.alloc(1024, 'a'); + +const config = require('../../config.json'); +const authCredentials = { + accessKey: config.accessKey, + secretKey: config.secretKey, +}; + +class HttpRequestAuthV4NoSHA256SignedHeader extends HttpRequestAuthV4 { + constructor(url, params, callback) { + super(url, params, callback); + } + + _constructRequest() { + const dateObj = new Date(); + const isoDate = dateObj.toISOString(); + this._timestamp = [ + isoDate.slice(0, 4), + isoDate.slice(5, 7), + isoDate.slice(8, 13), + isoDate.slice(14, 16), + isoDate.slice(17, 19), + 'Z', + ].join(''); + + const urlObj = new url.URL(this._url); + const signedHeaders = { + 'host': urlObj.host, + 'x-amz-date': this._timestamp, + }; + const httpHeaders = Object.assign({}, this._httpParams.headers); + Object.keys(httpHeaders).forEach(header => { + const lowerHeader = header.toLowerCase(); + if (!['connection', 'transfer-encoding', 'x-amz-content-sha256'].includes(lowerHeader)) { + signedHeaders[lowerHeader] = httpHeaders[header]; + } + }); + httpHeaders.Authorization = + this.getAuthorizationHeader(urlObj, signedHeaders, httpHeaders['x-amz-content-sha256']); + return Object.assign(httpHeaders, signedHeaders); + } +} + +describe('unsigned x-amz-content-sha256 header in AuthV4 requests:', () => { + before(done => { + makeS3Request({ + method: 'PUT', + authCredentials, + bucket, + }, err => { + assert.ifError(err); + done(); + }); + }); + + after(done => { + async.series([ + next => makeS3Request({ + method: 'DELETE', + authCredentials, + bucket, + objectKey, + }, next), + next => makeS3Request({ + method: 'DELETE', + authCredentials, + bucket, + }, next), + ], err => { + assert.ifError(err); + done(); + }); + }); + + it('should accept x-amz-content-sha256 header not in SignedHeaders list', done => { + // Calculate the SHA256 hash of the data + const contentSha256 = crypto.createHash('sha256') + .update(objData) + .digest('hex'); + + const req = new HttpRequestAuthV4NoSHA256SignedHeader( + `http://localhost:8000/${bucket}/${objectKey}`, + Object.assign( + { + method: 'PUT', + headers: { + 'content-length': objData.length, + 'x-amz-content-sha256': contentSha256, + }, + }, + authCredentials + ), + res => { + let body = ''; + res.on('data', chunk => { + body += chunk; + }); + res.on('end', () => { + assert.strictEqual(body, '', 'expected empty body'); + assert.strictEqual(res.statusCode, 200, + 'Request should succeed even when x-amz-content-sha256 is not signed'); + done(); + }); + } + ); + + req.on('error', err => { + assert.ifError(err); + }); + + req.write(objData); + + req.once('drain', () => { + req.end(); + }); + }); +}); diff --git a/tests/functional/raw-node/utils/HttpRequestAuthV4.js b/tests/functional/raw-node/utils/HttpRequestAuthV4.js index f766421eac..b979fd8ce2 100644 --- a/tests/functional/raw-node/utils/HttpRequestAuthV4.js +++ b/tests/functional/raw-node/utils/HttpRequestAuthV4.js @@ -89,7 +89,7 @@ class HttpRequestAuthV4 extends stream.Writable { .update(stringToSign).digest('hex'); } - getCanonicalRequest(urlObj, signedHeaders) { + getCanonicalRequest(urlObj, signedHeaders, contentSha256) { const method = this._httpParams.method || 'GET'; const signedHeadersList = Object.keys(signedHeaders).sort(); const qsParams = []; @@ -115,7 +115,7 @@ class HttpRequestAuthV4 extends stream.Writable { canonicalQueryString, canonicalSignedHeaders, signedHeadersList.join(';'), - signedHeaders['x-amz-content-sha256'], + contentSha256, ].join('\n'); // console.log(`CANONICAL REQUEST: "${canonicalRequest}"`); @@ -131,17 +131,17 @@ class HttpRequestAuthV4 extends stream.Writable { return stringToSign; } - getAuthorizationSignature(urlObj, signedHeaders) { + getAuthorizationSignature(urlObj, signedHeaders, contentSha256) { const canonicalRequest = - this.getCanonicalRequest(urlObj, signedHeaders); + this.getCanonicalRequest(urlObj, signedHeaders, contentSha256); this._lastSignature = this.createSignature( this.constructRequestStringToSign(canonicalRequest)); return this._lastSignature; } - getAuthorizationHeader(urlObj, signedHeaders) { + getAuthorizationHeader(urlObj, signedHeaders, contentSha256) { const authorizationSignature = - this.getAuthorizationSignature(urlObj, signedHeaders); + this.getAuthorizationSignature(urlObj, signedHeaders, contentSha256); const signedHeadersList = Object.keys(signedHeaders).sort(); return ['AWS4-HMAC-SHA256', @@ -206,8 +206,7 @@ class HttpRequestAuthV4 extends stream.Writable { if (lowerHeader === 'content-length') { contentLengthHeader = header; } - if (!['connection', - 'transfer-encoding'].includes(lowerHeader)) { + if (!['connection', 'transfer-encoding'].includes(lowerHeader)) { signedHeaders[lowerHeader] = httpHeaders[header]; } }); @@ -229,8 +228,7 @@ class HttpRequestAuthV4 extends stream.Writable { } } httpHeaders.Authorization = - this.getAuthorizationHeader(urlObj, signedHeaders); - + this.getAuthorizationHeader(urlObj, signedHeaders, signedHeaders['x-amz-content-sha256']); return Object.assign(httpHeaders, signedHeaders); } From 795f0899816ee53a7a58f68bb49f0b2647e6db89 Mon Sep 17 00:00:00 2001 From: Leif Henriksen Date: Wed, 11 Feb 2026 12:22:34 +0100 Subject: [PATCH 3/3] CLDSRV-847: bump package.json to 9.2.26 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f8fae84828..0da0ffc422 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zenko/cloudserver", - "version": "9.2.25", + "version": "9.2.26", "description": "Zenko CloudServer, an open-source Node.js implementation of a server handling the Amazon S3 protocol", "main": "index.js", "engines": {