diff --git a/.github/docker/local.sh b/.github/docker/local.sh index 9a4aed898a..376bf26427 100755 --- a/.github/docker/local.sh +++ b/.github/docker/local.sh @@ -9,7 +9,7 @@ export S3METADATA=file export S3VAULT=scality export MPU_TESTING="yes" -export CLOUDSERVER_IMAGE_BEFORE_SSE_MIGRATION=ghcr.io/scality/cloudserver:7.70.66 +export CLOUDSERVER_IMAGE_BEFORE_SSE_MIGRATION=ghcr.io/scality/cloudserver:7.70.63 export CLOUDSERVER_IMAGE_ORIGINAL=ghcr.io/scality/cloudserver:7.70.70 export VAULT_IMAGE_BEFORE_SSE_MIGRATION=ghcr.io/scality/vault:7.70.31 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 2ff158e196..62de266b0e 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -407,10 +407,10 @@ jobs: S3BACKEND: file S3VAULT: scality # Versions before using kms scality arn prefix & sse migration used to seed buckets & objects - CLOUDSERVER_VERSION_BEFORE: 7.70.66 + CLOUDSERVER_VERSION_BEFORE: 7.70.63 VAULT_VERSION_BEFORE: 7.70.31 VAULT_VERSION_CURRENT: 7.70.32 - CLOUDSERVER_IMAGE_BEFORE_SSE_MIGRATION: ghcr.io/${{ github.repository }}:7.70.66 + CLOUDSERVER_IMAGE_BEFORE_SSE_MIGRATION: ghcr.io/${{ github.repository }}:7.70.63 VAULT_IMAGE_BEFORE_SSE_MIGRATION: ghcr.io/scality/vault:7.70.31 CLOUDSERVER_IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }} VAULT_IMAGE: ghcr.io/scality/vault:7.70.32 diff --git a/lib/api/initiateMultipartUpload.js b/lib/api/initiateMultipartUpload.js index 7bfd3cd66a..6ac4d104f8 100644 --- a/lib/api/initiateMultipartUpload.js +++ b/lib/api/initiateMultipartUpload.js @@ -22,6 +22,7 @@ const { setExpirationHeaders } = require('./apiUtils/object/expirationHeaders'); const { data } = require('../data/wrapper'); const { setSSEHeaders } = require('./apiUtils/object/sseHeaders'); const { updateEncryption } = require('./apiUtils/bucket/updateEncryption'); +const kms = require('../kms/wrapper'); /* Sample xml response: @@ -335,6 +336,14 @@ function initiateMultipartUpload(authInfo, request, log, callback) { return next(null, corsHeaders, destinationBucket, objectSSEConfig); } ), + // If SSE configured, test kms key encryption access, but ignore cipher bundle + (corsHeaders, destinationBucket, objectSSEConfig, next) => { + if (objectSSEConfig) { + return kms.createCipherBundle(objectSSEConfig, log, + err => next(err, corsHeaders, destinationBucket, objectSSEConfig)); + } + return next(null, corsHeaders, destinationBucket, objectSSEConfig); + }, ], (error, corsHeaders, destinationBucket, objectSSEConfig) => { if (error) { diff --git a/package.json b/package.json index 37ef7f851a..4d83bc8ad3 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "homepage": "https://github.com/scality/S3#readme", "dependencies": { "@hapi/joi": "^17.1.0", - "arsenal": "git+https://github.com/scality/Arsenal#7.70.47", + "arsenal": "git+https://github.com/scality/Arsenal#7.70.48", "async": "~2.5.0", "aws-sdk": "2.905.0", "azure-storage": "^2.1.0", diff --git a/tests/functional/sse-kms-migration/arnPrefix.js b/tests/functional/sse-kms-migration/arnPrefix.js index a18bd40e04..7cb19ee5ef 100644 --- a/tests/functional/sse-kms-migration/arnPrefix.js +++ b/tests/functional/sse-kms-migration/arnPrefix.js @@ -505,3 +505,163 @@ describe('ensure MPU use good SSE', () => { ); }); }); +describe('KMS error', () => { + const sseConfig = { algo: 'aws:kms', masterKeyId: true }; + const Bucket = 'bkt-kms-err'; + const Key = 'obj'; + const body = 'content'; + + let mpuEncrypted; + let mpuPlaintext; + + let masterKeyId; + let masterKeyArn; + + let expected; + + const expectedKMIP = { + code: 'KMS.NotFoundException', + msg: (action, keyId) => new RegExp(`^KMS \\(KMIP\\) error for ${action} on ${keyId}\\..*`), + }; + const expectedAWS = { + code: 'KMS.KMSInvalidStateException', + msg: (_, keyId) => new RegExp(`${keyId} is pending deletion\\.`), + }; + /** + * localkms container returns a different error message when the key is pending deletion + * as we decrypt without passing the keyId, so we need to handle it separately + */ + const expectedLocalKms = { + code: 'KMS.AccessDeniedException', + msg: () => new RegExp( + 'The ciphertext refers to a customer master key that does not exist, ' + + 'does not exist in this region, or you are not allowed to access\\.' + ), + }; + if (helpers.config.backends.kms === 'kmip') { + expected = expectedKMIP; + } else if (helpers.config.backends.kms === 'aws') { + expected = expectedAWS; + } else { + throw new Error(`Unsupported KMS backend: ${helpers.config.backends.kms}`); + } + + function assertKmsError(action, keyId) { + return err => { + if (helpers.config.backends.kms === 'aws' && action === 'Decrypt') { + assert.strictEqual(err.name, expectedLocalKms.code); + assert.match(err.message, expectedLocalKms.msg(action, keyId)); + return true; + } + assert.strictEqual(err.name, expected.code); + assert.match(err.message, expected.msg(action, keyId)); + return true; + }; + } + + before(async () => { + void await helpers.s3.createBucket({ Bucket }).promise(); + + await helpers.s3.putObject({ + ...helpers.putObjParams(Bucket, 'plaintext', {}, null), + Body: body, + }).promise(); + + mpuPlaintext = await helpers.s3.createMultipartUpload( + helpers.putObjParams(Bucket, 'mpuPlaintext', {}, null)).promise(); + + ({ masterKeyId, masterKeyArn } = await helpers.createKmsKey(log)); + + await helpers.putEncryptedObject(Bucket, Key, sseConfig, masterKeyArn, body); + // ensure we can decrypt and read the object + const obj = await helpers.s3.getObject({ Bucket, Key }).promise(); + assert.strictEqual(obj.Body.toString(), body); + + mpuEncrypted = await helpers.s3.createMultipartUpload( + helpers.putObjParams(Bucket, 'mpuEncrypted', sseConfig, masterKeyArn)).promise(); + + // make key unavailable + void await helpers.destroyKmsKey(masterKeyArn, log); + }); + + after(async () => { + void await helpers.cleanup(Bucket); + if (masterKeyArn) { + try { + void await helpers.destroyKmsKey(masterKeyArn, log); + } catch (e) { void e; } + [masterKeyArn, masterKeyId] = [null, null]; + } + }); + + const testCases = [ + { + action: 'putObject', kmsAction: 'Encrypt', + fct: async ({ masterKeyArn }) => + helpers.putEncryptedObject(Bucket, 'fail', sseConfig, masterKeyArn, body), + }, + { + action: 'getObject', kmsAction: 'Decrypt', + fct: async () => helpers.s3.getObject({ Bucket, Key }).promise(), + }, + { + action: 'copyObject', detail: ' when getting from source', kmsAction: 'Decrypt', + fct: async () => + helpers.s3.copyObject({ Bucket, Key: 'copy', CopySource: `${Bucket}/${Key}` }).promise(), + }, + { + action: 'copyObject', detail: ' when putting to destination', kmsAction: 'Encrypt', + fct: async ({ masterKeyArn }) => helpers.s3.copyObject({ + Bucket, + Key: 'copyencrypted', + CopySource: `${Bucket}/plaintext`, + ServerSideEncryption: 'aws:kms', + SSEKMSKeyId: masterKeyArn, + }).promise(), + }, + { + action: 'createMPU', kmsAction: 'Encrypt', + fct: async ({ masterKeyArn }) => helpers.s3.createMultipartUpload( + helpers.putObjParams(Bucket, 'mpuKeyEncryptedFail', sseConfig, masterKeyArn)).promise(), + }, + { + action: 'mpu uploadPartCopy', detail: ' when getting from source', kmsAction: 'Decrypt', + fct: async ({ mpuPlaintext }) => helpers.s3.uploadPartCopy({ + UploadId: mpuPlaintext.UploadId, + Bucket, + Key: 'mpuPlaintext', + PartNumber: 1, + CopySource: `${Bucket}/${Key}`, + }).promise(), + }, + { + action: 'mpu uploadPart', detail: ' when putting to destination', kmsAction: 'Encrypt', + fct: async ({ mpuEncrypted }) => helpers.s3.uploadPart({ + UploadId: mpuEncrypted.UploadId, + Bucket, + Key: 'mpuEncrypted', + PartNumber: 1, + Body: body, + }).promise(), + }, + { + action: 'mpu uploadPartCopy', detail: ' when putting to destination', kmsAction: 'Encrypt', + fct: async ({ mpuEncrypted }) => helpers.s3.uploadPartCopy({ + UploadId: mpuEncrypted.UploadId, + Bucket, + Key: 'mpuEncrypted', + PartNumber: 1, + CopySource: `${Bucket}/plaintext`, + }).promise(), + }, + ]; + + testCases.forEach(({ action, kmsAction, fct, detail }) => { + it(`${action} should fail with kms error${detail || ''}`, async () => { + await assert.rejects( + fct({ masterKeyArn, mpuEncrypted, mpuPlaintext }), + assertKmsError(kmsAction, masterKeyId), + ); + }); + }); +}); diff --git a/tests/functional/sse-kms-migration/helpers.js b/tests/functional/sse-kms-migration/helpers.js index 7dbd37534b..65061939dc 100644 --- a/tests/functional/sse-kms-migration/helpers.js +++ b/tests/functional/sse-kms-migration/helpers.js @@ -94,6 +94,8 @@ async function createKmsKey(log) { }); } +const destroyKmsKey = promisify(kms.destroyBucketKey); + async function cleanup(Bucket) { await bucketUtil.empty(Bucket); await s3.deleteBucket({ Bucket }).promise(); @@ -112,5 +114,6 @@ module.exports = { putEncryptedObject, getObjectMDSSE, createKmsKey, + destroyKmsKey, cleanup, }; diff --git a/yarn.lock b/yarn.lock index 1492ded08a..4ac777c4bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -501,9 +501,9 @@ arraybuffer.slice@~0.0.7: optionalDependencies: ioctl "^2.0.2" -"arsenal@git+https://github.com/scality/Arsenal#7.70.47": - version "7.70.47" - resolved "git+https://github.com/scality/Arsenal#6ff3267c33ceee0e4af3aa7757f0a17a5d53db54" +"arsenal@git+https://github.com/scality/Arsenal#7.70.48": + version "7.70.48" + resolved "git+https://github.com/scality/Arsenal#bf27689b29293a1f24ff3090720067525f282baa" dependencies: "@js-sdsl/ordered-set" "^4.4.2" "@types/async" "^3.2.12"