Repository: scality/Arsenal
Pull request #2385: ARSN-485: KMS SSE external & internal (Cherry-pick from hotfix)
Author: login: BourgoisMickael
Created: 2025-05-14T08:47:27Z | Merged: 2025-05-15T09:53:51Z

Cherry-picked from implementation for hotfix 9.2.0:
- https://github.com/scality/Arsenal/pull/2349
- https://github.com/scality/Arsenal/pull/2378
- https://github.com/scality/Arsenal/pull/2384

### + One last commit to adapt the cherry-picks to latest implementation: 097dd43b4cfd70dca5b542c53baceebd002ed6db


Diff:
diff --git a/lib/auth/in_memory/Backend.ts b/lib/auth/in_memory/Backend.ts
index 0d1c8062b..9c0e17cce 100644
--- a/lib/auth/in_memory/Backend.ts
+++ b/lib/auth/in_memory/Backend.ts
@@ -3,6 +3,7 @@ import errors from '../../errors';
 import { calculateSigningKey, hashSignature } from './vaultUtilities';
 import Indexer from './Indexer';
 import { Accounts } from './types';
+import { KmsType, KmsProtocol, makeScalityArnPrefix } from '../../network/KMSInterface';
 
 function _formatResponse(userInfoToSend: any) {
     return {
@@ -201,11 +202,13 @@ class Backend {
             _options: any, 
             cb: (err: null, data: { message: { body: { canonicalId: string, encryptionKeyId: string, action: string } } }) => void
         ): void {
+            const kmsProtocol = process.env.S3KMS === 'file' ? KmsProtocol.file : KmsProtocol.mem;
+            const arnPrefix = makeScalityArnPrefix(KmsType.internal, kmsProtocol, 'scality');
             return cb(null, {
                 message: {
                     body: {
                         canonicalId,
-                        encryptionKeyId: 'account-level-master-encryption-key',
+                        encryptionKeyId: `${arnPrefix}account-level-master-encryption-key`,
                         action: 'retrieved',
                     }
                 }
diff --git a/lib/network/KMSInterface.ts b/lib/network/KMSInterface.ts
index 87d678a84..1c04f444e 100644
--- a/lib/network/KMSInterface.ts
+++ b/lib/network/KMSInterface.ts
@@ -1,30 +1,199 @@
 
 import type * as werelogs from 'werelogs';
 // Logger should probably by RequestLogger instead
+import errors from "../errors";
+
+export const SCAL_KMS_ARN = 'arn:scality:kms:';
+
+export enum KmsType {
+    /** Internal scality provider */
+    internal = 'internal',
+    /** External provider */
+    external = 'external',
+};
+
+export enum KmsProtocol {
+    /** For tests & dev */
+    mem = 'mem',
+    /** Default if no provider */
+    file = 'file',
+    /** (SafeNet from Gemalto/Thales using scality-kms) */
+    scality = 'scality',
+    aws_kms = 'aws_kms',
+    kmip = 'kmip',
+};
+
+const kmsTypeProtocolMapping = {
+    [KmsType.internal]: [KmsProtocol.mem, KmsProtocol.file] as const,
+    [KmsType.external]: [KmsProtocol.scality, KmsProtocol.aws_kms, KmsProtocol.kmip] as const,
+} as const;
+export type KmsProtocolByType<T extends KmsType> = typeof kmsTypeProtocolMapping[T][number];
+
+/**
+ * Possible arn are:
+ *
+ * - `arn:scality:kms:internal:mem:scality:key/$keyId` (tests & dev)
+ * - `arn:scality:kms:internal:file:scality:key/$keyId` (default if no provider)
+ * - `arn:scality:kms:external:scality:safenet:key/$keyId` (deprecated provider)
+ * - `arn:scality:kms:external:aws_kms:aws:key/$keyIdOrArn`
+ * - `arn:scality:kms:external:aws_kms:custom:alias/$keyIdOrArn`
+ * - `arn:scality:kms:external:kmip:thales:key/$keyId`
+ * - `arn:scality:kms:external:kmip:hashicorp:key/$keyId`
+ * - `arn:scality:kms:external:kmip:hytrust:key/$keyId`
+ */
+export type AwsLikeKmsArnPrefix<T extends KmsType = KmsType> =
+    `${typeof SCAL_KMS_ARN}${T}:${KmsProtocolByType<T>}:${string}:${'key' | 'alias'}/`;
+/** Scality arn prefixing KMS KeyId to keep track of KMS backend */
+export type AwsLikeKmsArn<T extends KmsType = KmsType> = `${AwsLikeKmsArnPrefix<T>}${string}`;
+
+export interface KmsBackend<T extends KmsType> {
+    readonly type: T;
+    readonly protocol: KmsProtocolByType<T>;
+    readonly provider: string;
+    readonly arnPrefix: AwsLikeKmsArnPrefix<T>;
+}
+
+export function makeScalityArnPrefix<T extends KmsType>(
+    type: T, protocol: KmsProtocolByType<T>, provider: string
+): AwsLikeKmsArnPrefix<T> {
+    return `${SCAL_KMS_ARN}${type}:${protocol}:${provider}:key/`;
+}
+
+export function makeBackend<T extends KmsType>(
+    type: T, protocol: KmsProtocolByType<T>, provider: string
+): KmsBackend<T> {
+    return {
+        type,
+        protocol,
+        provider,
+        arnPrefix: makeScalityArnPrefix(type, protocol, provider),
+    }
+}
+
+export function isScalityKmsArn(kmsKey: string) {
+    return kmsKey.startsWith(SCAL_KMS_ARN);
+}
+
+/**
+ * Drop scality arn prefix from a KMS Key so it can be sent to the backend KMS.
+ * This is used in KMS clients as a safety but the key should be passed
+ * without arnPrefix as it should be extract by a KMS wrapper using `extractDetailFromArn`.
+ * For old key without arnPrefix this does nothing.
+ * Can be used to return KeyId in HTTP headers as well.
+ * @param kmsKeyIdOrArn KMS key coming from object metadata
+ * @returns KeyId extracted from scality arn (for AWS can still be an aws arn)
+ */
+export function getKeyIdFromArn(kmsKeyIdOrArn: AwsLikeKmsArn | string) {
+    if (isScalityKmsArn(kmsKeyIdOrArn)) {
+        // Do not use full arnPrefix to allow for different providerName
+        return kmsKeyIdOrArn.substring(kmsKeyIdOrArn.indexOf('/') + 1)
+    }
+    return kmsKeyIdOrArn;
+}
+
+// Functions used to validate configuration
+const kmsTypes = Object.values(KmsType);
+export function isValidType(type: KmsType | string): type is KmsType {
+    return kmsTypes.includes(type as KmsType);
+}
+export function isValidProtocol<T extends KmsType = KmsType>(
+    type: T, protocol: KmsProtocol | string
+): protocol is KmsProtocolByType<T> {
+    return (kmsTypeProtocolMapping[type] as unknown as KmsProtocol[] | undefined)
+        ?.includes(protocol as KmsProtocol) || false;
+}
+/** must be lowercase alphanumeric */
+export function isValidProvider(provider: string) {
+    return /^[a-z0-9]+$/.test(provider);
+}
+// end of functions to validate configuration
+
+export interface KmsArnDetail {
+    type: KmsType,
+    protocol: KmsProtocol,
+    provider: string,
+    arnType: 'key' | 'alias', // we don't support alias yet
+    id: string,
+}
+type NotValidatedKmsArnDetail = { [Key in keyof KmsArnDetail]?: KmsArnDetail[Key] | string };
+
+/**
+ * Split the scality arn to extract KMS backend identifiers.
+ * @param keyArn KMS Key prefixed with scality arn
+ * @returns KMS Key id with KMS backend identifiers
+ */
+export function extractDetailFromArn(keyArn: AwsLikeKmsArn): NotValidatedKmsArnDetail {
+    if (!isScalityKmsArn(keyArn)) {
+        return { id: keyArn };
+    }
+
+    const [arnPrefix, ...keyIdOrArn] = keyArn.split('/');
+    const [,,,type, protocol, provider, arnType] = arnPrefix.split(':');
+
+    return {
+        type,
+        protocol,
+        provider,
+        arnType,
+        id: keyIdOrArn?.join('/')
+    }
+}
+
+/**
+ * Validate arn detail provided as input from PutObject or retrieved from GetObject
+ * List of backends should include the currently configured KMS.
+ * Optionally the sseMigration can be included in backends for Get requests pulling key from DB
+ * @returns error or null
+ */
+export function validateKeyDetail(keyDetail: NotValidatedKmsArnDetail, backends: KmsBackend<KmsType>[]) {
+    const isValidBackend = backends.some(backend =>
+        keyDetail.type === backend.type
+        && keyDetail.protocol === backend.protocol
+        && keyDetail.provider === backend.provider
+    )
+    // Produce error description to end user with detail of backend configuration for troubleshooting
+    if (!isValidBackend) {
+        return errors.InvalidArgument.customizeDescription(
+            `KMS Scality KeyArn doesn't match any configured providers. Possible arn are: "${
+                backends.map(b => b.arnPrefix).join('", "')}"`
+        )
+    }
+    if (keyDetail.arnType !== 'key') {
+        return errors.InvalidArgument.customizeDescription(
+            `Invalid KMS Scality KeyArn, expected "key" instead of "${keyDetail.arnType}"`);
+    }
+    if (!keyDetail.id) {
+        return errors.InvalidArgument.customizeDescription(`Invalid KMS Scality KeyArn, missing KeyId`);
+    }
+
+    return null;
+}
 
 export interface KMSInterface {
+    readonly backend: KmsBackend<KmsType>;
+
     createBucketKey(
         bucketName: string,
         logger: werelogs.Logger,
-        cb: (err?: Error | null, keyId?: string) => void,
+        cb: (err?: Error | null, keyId?: string, keyArn?: string) => void,
     ): void
 
     destroyBucketKey(
-        bucketKeyId: string,
+        bucketKeyIdOrArn: string,
         logger: werelogs.Logger,
         cb: (err?: Error | null) => void,
     ): void
 
     generateDataKey?(
         cryptoScheme: number,
-        masterKeyId: string,
+        masterKeyIdOrArn: string,
         logger: werelogs.Logger,
         cb: (err: Error | null, plainTextDataKey?: Buffer, cipheredDataKey?: Buffer) => void,
     ): void
 
     cipherDataKey(
         cryptoScheme: number,
-        masterKeyId: string,
+        masterKeyIdOrArn: string,
         plainTextDataKey: Buffer,
         logger: werelogs.Logger,
         cb: (err: Error | null, cipheredDataKey?: Buffer) => void,
@@ -32,7 +201,7 @@ export interface KMSInterface {
 
     decipherDataKey(
         cryptoScheme: number,
-        masterKeyId: string,
+        masterKeyIdOrArn: string,
         cipheredDataKey: Buffer,
         logger: werelogs.Logger,
         cb: (err: Error | null, plainTextDataKey?: Buffer) => void,
diff --git a/lib/network/kmip/Client.ts b/lib/network/kmip/Client.ts
index f52ffafd5..bebfd0555 100644
--- a/lib/network/kmip/Client.ts
+++ b/lib/network/kmip/Client.ts
@@ -7,9 +7,9 @@ import TlsTransport from './transport/tls';
 import KMIP from '.';
 import * as werelogs from 'werelogs';
 import { arsenalErrorKMIP } from '../utils'
-import { KMSInterface } from '../KMSInterface';
 import { errorInstances } from '../../errors';
 import { kmipMsg } from './errorMapping';
+import { KMSInterface, getKeyIdFromArn, KmsBackend, KmsProtocol, KmsType, makeBackend } from '../KMSInterface';
 
 const CRYPTOGRAPHIC_OBJECT_TYPE = 'Symmetric Key';
 const CRYPTOGRAPHIC_ALGORITHM = 'AES';
@@ -235,17 +235,31 @@ function _queryOperationsAndObjects(client: any, logger: werelogs.Logger, cb: an
     });
 }
 
+export interface KmipClientOptions {
+    kmip: {
+        providerName: string;
+        codec: any;
+        transport: any;
+        client: {
+            compoundCreateActivate: any;
+            bucketNameAttributeName: any;
+        };
+    }
+}
+
 export default class Client implements KMSInterface {
     options: any;
     vendorIdentification: string;
     serverInformation: any[];
     kmip: KMIP;
     host: string;
+    public readonly backend: KmsBackend<KmsType.external>;
 
     /**
      * Construct a high level KMIP driver suitable for cloudserver
      * @param options - Instance options
      * @param options.kmip - Low level driver options
+     * @param options.kmip.providerName - Name of kmip provider
      * @param options.kmip.client - This high level driver options
      * @param options.kmip.client.compoundCreateActivate -
      *                 Depends on the server's ability. False offers the best
@@ -264,16 +278,7 @@ export default class Client implements KMSInterface {
      *                                 defaults to TlsTransport
      */
     constructor(
-        options: {
-            kmip: {
-                codec: any;
-                transport: any;
-                client: {
-                    compoundCreateActivate: any;
-                    bucketNameAttributeName: any;
-                };
-            }
-        },
+        options: KmipClientOptions,
         CodecClass: any,
         TransportClass: any,
     ) {
@@ -287,6 +292,8 @@ export default class Client implements KMSInterface {
         this.kmip.registerHandshakeFunction((logger, cb) => {
             this._kmipHandshake(logger, cb);
         });
+
+        this.backend = makeBackend(KmsType.external, KmsProtocol.kmip, options.kmip.providerName);
     }
 
     /**
@@ -340,11 +347,12 @@ export default class Client implements KMSInterface {
      * Activate a cryptographic key managed by the server,
      * for a specific bucket. This is a required action to perform after
      * the key creation.
-     * @param keyIdentifier - The bucket key Id
+     * @param keyIdentifierOrArn - The bucket key Id
      * @param logger - Werelog logger object
-     * @param cb - The callback(err: Error)
+     * @param cb - The callback(err: Error, bucketKeyId: String, bucketKeyArn: String)
      */
-    _activateBucketKey(keyIdentifier: string, logger: werelogs.Logger, cb: any) {
+    _activateBucketKey(keyIdentifierOrArn: string, logger: werelogs.Logger, cb: any) {
+        const keyIdentifier = getKeyIdFromArn(keyIdentifierOrArn);
         return this.kmip.request(logger, 'Activate', [
             KMIP.TextString('Unique Identifier', keyIdentifier),
         ], (error, response) => {
@@ -355,7 +363,7 @@ export default class Client implements KMSInterface {
                 return cb(error);
             }
             const keyErr = this._checkUniqueIdentifier(keyIdentifier, response, 'Activate', logger);
-            return cb(keyErr, keyIdentifier);
+            return cb(keyErr, keyIdentifier, `${this.backend.arnPrefix}${keyIdentifier}`);
         }, keyIdentifier);
     }
 
@@ -364,7 +372,7 @@ export default class Client implements KMSInterface {
      * for a specific bucket
      * @param bucketName - The bucket name
      * @param logger - Werelog logger object
-     * @param cb - The callback(err: Error, bucketKeyId: String)
+     * @param cb - The callback(err: Error, bucketKeyId: String, bucketKeyArn: String)
      */
     createBucketKey(bucketName: string, logger: werelogs.Logger, cb: any) {
         const attributes: any = [];
@@ -415,7 +423,7 @@ export default class Client implements KMSInterface {
             if (!this.options.compoundCreateActivate) {
                 return this._activateBucketKey(uniqueIdentifier, logger, cb);
             }
-            return cb(null, uniqueIdentifier);
+            return cb(null, uniqueIdentifier, `${this.backend.arnPrefix}${uniqueIdentifier}`);
         }, bucketName);
     }
 
@@ -423,11 +431,12 @@ export default class Client implements KMSInterface {
      * Revoke a cryptographic key managed by the server, for a specific bucket.
      * This is a required action to perform before being able to destroy the
      * managed key.
-     * @param bucketKeyId - The bucket key Id
+     * @param bucketKeyIdOrArn - The bucket key Id
      * @param logger - Werelog logger object
      * @param cb - The callback(err: Error)
      */
-    _revokeBucketKey(bucketKeyId: string, logger: werelogs.Logger, cb: any) {
+    _revokeBucketKey(bucketKeyIdOrArn: string, logger: werelogs.Logger, cb: any) {
+        const bucketKeyId = getKeyIdFromArn(bucketKeyIdOrArn);
         // maybe revoke first
         return this.kmip.request(logger, 'Revoke', [
             KMIP.TextString('Unique Identifier', bucketKeyId),
@@ -451,11 +460,12 @@ export default class Client implements KMSInterface {
 
     /**
      * Destroy a cryptographic key managed by the server, for a specific bucket.
-     * @param bucketKeyId - The bucket key Id
+     * @param bucketKeyIdOrArn - The bucket key Id
      * @param logger - Werelog logger object
      * @param cb - The callback(err: Error)
      */
-    destroyBucketKey(bucketKeyId: string, logger: werelogs.Logger, cb: any) {
+    destroyBucketKey(bucketKeyIdOrArn: string, logger: werelogs.Logger, cb: any) {
+        const bucketKeyId = getKeyIdFromArn(bucketKeyIdOrArn);
         return this._revokeBucketKey(bucketKeyId, logger, err => {
             if (err) {
                 const error = arsenalErrorKMIP(err);
@@ -483,7 +493,7 @@ export default class Client implements KMSInterface {
     /**
      *
      * @param cryptoScheme - crypto scheme version number
-     * @param masterKeyId - key to retrieve master key
+     * @param masterKeyIdOrArn - key to retrieve master key
      * @param plainTextDataKey - data key
      * @param logger - werelog logger object
      * @param cb - callback
@@ -491,11 +501,12 @@ export default class Client implements KMSInterface {
      */
     cipherDataKey(
         cryptoScheme: number,
-        masterKeyId: string,
+        masterKeyIdOrArn: string,
         plainTextDataKey: Buffer,
         logger: werelogs.Logger,
         cb: any,
     ) {
+        const masterKeyId = getKeyIdFromArn(masterKeyIdOrArn);
         return this.kmip.request(logger, 'Encrypt', [
             KMIP.TextString('Unique Identifier', masterKeyId),
             KMIP.Structure('Cryptographic Parameters', [
@@ -524,7 +535,7 @@ export default class Client implements KMSInterface {
     /**
      *
      * @param cryptoScheme - crypto scheme version number
-     * @param masterKeyId - key to retrieve master key
+     * @param masterKeyIdOrArn - key to retrieve master key
      * @param cipheredDataKey - data key
      * @param logger - werelog logger object
      * @param cb - callback
@@ -532,11 +543,12 @@ export default class Client implements KMSInterface {
      */
     decipherDataKey(
         cryptoScheme: number,
-        masterKeyId: string,
+        masterKeyIdOrArn: string,
         cipheredDataKey: Buffer,
         logger: werelogs.Logger,
         cb: any,
     ) {
+        const masterKeyId = getKeyIdFromArn(masterKeyIdOrArn);
         return this.kmip.request(logger, 'Decrypt', [
             KMIP.TextString('Unique Identifier', masterKeyId),
             KMIP.Structure('Cryptographic Parameters', [
diff --git a/lib/network/kmip/ClusterClient.ts b/lib/network/kmip/ClusterClient.ts
index 8b3286bd2..64d81e42b 100644
--- a/lib/network/kmip/ClusterClient.ts
+++ b/lib/network/kmip/ClusterClient.ts
@@ -3,19 +3,21 @@
 
 import TTLVCodec from './codec/ttlv';
 import TlsTransport from './transport/tls';
-import KMIPClient from './Client';
-import { KMSInterface } from '../KMSInterface';
+import KMIPClient, { KmipClientOptions } from './Client';
+import { KmsBackend, KMSInterface, KmsType } from '../KMSInterface';
 import type { Logger } from 'werelogs';
 import async from 'async';
 
 export default class ClusterClient implements KMSInterface {
     private readonly clients: KMIPClient[];
     private roundRobinIndex = 0;
+    public readonly backend: KmsBackend<KmsType.external>;
 
     /**
      * Construct a high level cluster of KMIP drivers suitable for cloudserver
      * @param options - Instance options
      * @param options.kmip - Low level driver options
+     * @param options.kmip.providerName - Name of kmip provider
      * @param options.kmip.client - This high level driver options
      * @param options.kmip.client.compoundCreateActivate -
      *                 Depends on the server's ability. False offers the best
@@ -34,25 +36,17 @@ export default class ClusterClient implements KMSInterface {
      *                                 defaults to TlsTransport
      */
     constructor(
-        options: {
-            kmip: {
-                codec: any;
-                transport: any[];
-                client: {
-                    compoundCreateActivate: any;
-                    bucketNameAttributeName: any;
-                };
-            }
-        },
+        options: KmipClientOptions,
         CodecClass: any,
         TransportClass: any,
     ) {
-        const { codec, client } = options.kmip;
+        const { codec, client, providerName } = options.kmip;
         this.clients = options.kmip.transport.map(transport => new KMIPClient(
-            { kmip: { codec, transport, client } },
+            { kmip: { codec, transport, client, providerName } },
             CodecClass || TTLVCodec,
             TransportClass || TlsTransport,
         ));
+        this.backend = this.clients[0].backend;
     }
 
     next() {
diff --git a/lib/network/kmsAWS/Client.ts b/lib/network/kmsAWS/Client.ts
index 3b954a3da..cd185d615 100644
--- a/lib/network/kmsAWS/Client.ts
+++ b/lib/network/kmsAWS/Client.ts
@@ -8,11 +8,13 @@ import { Agent as HttpsAgent } from 'https';
 import { KMS, AWSError } from 'aws-sdk';
 import * as werelogs from 'werelogs';
 import assert from 'assert';
-import { KMSInterface } from '../KMSInterface';
+import { KMSInterface, KmsBackend, getKeyIdFromArn, KmsProtocol, KmsType, makeBackend } from '../KMSInterface';
 
 type TLSVersion = 'TLSv1.3' | 'TLSv1.2' | 'TLSv1.1' | 'TLSv1';
 
 interface KMSOptions {
+    /** To be included in KMS key arn */
+    providerName: string;
     region?: string;
     endpoint?: string;
     ak?: string;
@@ -25,6 +27,18 @@ interface KMSOptions {
         maxVersion?: TLSVersion;
         key?: Buffer | Buffer[];
     };
+    /**
+     * Do not use AWS KMS Key arn but id (to keep backward compatibility).
+     *
+     * Some providers (okms) might not have accountId in their arn and don't need cross-account.
+     *
+     * Not set or false by default to use arn to be future-proof (multi account, region).
+     *
+     * Difference in kms key returned:
+     *  - `arn:scality:kms:external:aws_kms:custom:key/arn:aws:kms:region:accountId:key/cbd69d33-ba8e-4b56-8cfe-ae0844f69966`
+     *  - `arn:scality:kms:external:aws_kms:custom:key/cbd69d33-ba8e-4b56-8cfe-ae0844f69966`
+     */
+    noAwsArn?: boolean;
 }
 
 interface ClientOptions {
@@ -34,10 +48,12 @@ interface ClientOptions {
 export default class Client implements KMSInterface {
     private _supportsDefaultKeyPerAccount: boolean;
     private client: KMS;
+    public readonly backend: KmsBackend<KmsType.external>;
+    public readonly noAwsArn?: boolean;
 
     constructor(options: ClientOptions) {
         this._supportsDefaultKeyPerAccount = true;
-        const { tls, ak, sk, region, endpoint } = options.kmsAWS;
+        const { providerName, tls, ak, sk, region, endpoint, noAwsArn } = options.kmsAWS;
 
         const httpOptions = tls ? {
             agent: new HttpsAgent({
@@ -68,6 +84,8 @@ export default class Client implements KMSInterface {
             httpOptions,
             ...credentials,
         });
+        this.backend = makeBackend(KmsType.external, KmsProtocol.aws_kms, providerName);
+        this.noAwsArn = noAwsArn;
     }
 
     get supportsDefaultKeyPerAccount(): boolean {
@@ -93,12 +111,12 @@ export default class Client implements KMSInterface {
     // createBucketKey is a method used by CloudServer to create a default master encryption key per bucket.
     // New KMS backends like AWS KMS now allow the customer to use the default master encryption key per account.
     // To achieve this, Vault will call createMasterKey and store the master encryption ID in the account metadata.
-    createBucketKey(bucketName: string, logger: werelogs.Logger, cb: (err: Error | null, keyId?: string) => void): void {
+    createBucketKey(bucketName: string, logger: werelogs.Logger, cb: (err: Error | null, keyId?: string, keyArn?: string) => void): void {
         logger.debug("AWS KMS: creating encryption key managed at the bucket level", { bucketName });
         this.createMasterKey(logger, cb);
     }
 
-    createMasterKey(logger: werelogs.Logger, cb: (err: Error | null, keyId?: string) => void): void {
+    createMasterKey(logger: werelogs.Logger, cb: (err: Error | null, keyId?: string, keyArn?: string) => void): void {
         logger.debug("AWS KMS: creating master encryption key");
         this.client.createKey({}, (err: AWSError, data) => {
             if (err) {
@@ -107,21 +125,37 @@ export default class Client implements KMSInterface {
                 cb(error);
                 return;
             }
-            logger.debug("AWS KMS: master encryption key created", { KeyMetadata: data?.KeyMetadata });
-            cb(null, data?.KeyMetadata?.KeyId);
+            const keyMetadata = data?.KeyMetadata;
+            logger.debug("AWS KMS: master encryption key created", { KeyMetadata: keyMetadata });
+            let keyId: string;
+            if (this.noAwsArn) {
+                // Use KeyId when ARN is not wanted
+                keyId = keyMetadata?.KeyId!;
+            } else {
+                // Prefer ARN, but fall back to KeyId if ARN is missing
+                keyId = keyMetadata?.Arn ?? keyMetadata?.KeyId!;
+            }
+            // May produce double arn prefix: scality arn + aws arn
+            // arn:scality:kms:external:aws_kms:custom:key/arn:aws:kms:region:accountId:key/cbd69d33-ba8e-4b56-8cfe-ae0844f69966
+            // If this is a problem, a config flag should be used to hide the scality arn when returning the KMS KeyId
+            // or aws arn when creating the KMS Key
+            const arn = `${this.backend.arnPrefix}${keyId}`;
+            cb(null, keyId, arn);
         });
     }
 
     // destroyBucketKey is a method used by CloudServer to remove the default master encryption key for a bucket.
     // New KMS backends like AWS KMS allow customers to delete the default master encryption key at the account level.
     // To achieve this, Vault will call deleteMasterKey before deleting the account.
-    destroyBucketKey(bucketKeyId: string, logger: werelogs.Logger, cb: (err: Error | null) => void): void {
-        logger.debug("AWS KMS: deleting encryption key managed at the bucket level", { bucketKeyId });
+    destroyBucketKey(bucketKeyIdOrArn: string, logger: werelogs.Logger, cb: (err: Error | null) => void): void {
+        const bucketKeyId = getKeyIdFromArn(bucketKeyIdOrArn);
+        logger.debug("AWS KMS: deleting encryption key managed at the bucket level", { bucketKeyId, bucketKeyIdOrArn });
         this.deleteMasterKey(bucketKeyId, logger, cb);
     }
 
-    deleteMasterKey(masterKeyId: string, logger: werelogs.Logger, cb: (err: Error | null) => void): void {
-        logger.debug("AWS KMS: deleting master encryption key", { masterKeyId });
+    deleteMasterKey(masterKeyIdOrArn: string, logger: werelogs.Logger, cb: (err: Error | null) => void): void {
+        const masterKeyId = getKeyIdFromArn(masterKeyIdOrArn);
+        logger.debug("AWS KMS: deleting master encryption key", { masterKeyId, masterKeyIdOrArn });
         const params = {
             KeyId: masterKeyId,
             PendingWindowInDays: 7,
@@ -154,11 +188,12 @@ export default class Client implements KMSInterface {
 
     generateDataKey(
         cryptoScheme: number,
-        masterKeyId: string,
+        masterKeyIdOrArn: string,
         logger: werelogs.Logger,
         cb: (err: Error | null, plainTextDataKey?: Buffer, cipheredDataKey?: Buffer) => void
     ): void {
-        logger.debug("AWS KMS: generating data key", { cryptoScheme, masterKeyId });
+        const masterKeyId = getKeyIdFromArn(masterKeyIdOrArn);
+        logger.debug("AWS KMS: generating data key", { cryptoScheme, masterKeyId, masterKeyIdOrArn });
         assert.strictEqual(cryptoScheme, 1);
 
         const params = {
@@ -190,12 +225,14 @@ export default class Client implements KMSInterface {
 
     cipherDataKey(
         cryptoScheme: number,
-        masterKeyId: string,
+        masterKeyIdOrArn: string,
         plainTextDataKey: Buffer,
         logger: werelogs.Logger,
         cb: (err: Error | null, cipheredDataKey?: Buffer) => void
     ): void {
-        logger.debug("AWS KMS: ciphering data key", { cryptoScheme, masterKeyId });
+        const masterKeyId = getKeyIdFromArn(masterKeyIdOrArn);
+
+        logger.debug("AWS KMS: ciphering data key", { cryptoScheme, masterKeyId, masterKeyIdOrArn });
         assert.strictEqual(cryptoScheme, 1);
 
         const params = {
@@ -226,12 +263,14 @@ export default class Client implements KMSInterface {
 
     decipherDataKey(
         cryptoScheme: number,
-        masterKeyId: string,
+        masterKeyIdOrArn: string,
         cipheredDataKey: Buffer,
         logger: werelogs.Logger,
         cb: (err: Error | null, plainTextDataKey?: Buffer) => void
     ): void {
-        logger.debug("AWS KMS: deciphering data key", { cryptoScheme, masterKeyId });
+        const masterKeyId = getKeyIdFromArn(masterKeyIdOrArn);
+
+        logger.debug("AWS KMS: deciphering data key", { cryptoScheme, masterKeyId, masterKeyIdOrArn });
         assert.strictEqual(cryptoScheme, 1);
 
         const params = {
diff --git a/lib/network/kmsAWS/README.md b/lib/network/kmsAWS/README.md
index 5002ea325..dda48f47c 100644
--- a/lib/network/kmsAWS/README.md
+++ b/lib/network/kmsAWS/README.md
@@ -11,10 +11,12 @@ Supported parameters:
 
 | Config File        | Description                                             |
 |--------------------|---------------------------------------------------------|
+| kmsAWS.providerName| Friendly name to identify the KMS key provider in MD    |
 | kmsAWS.region      | AWS region to use                                       |
 | kmsAWS.endpoint    | Endpoint URL                                            |
 | kmsAWS.ak          | Credentials, Access Key                                 |
 | kmsAWS.sk          | Credentials, Secret Key                                 |
+| kmsAWS.noAwsArn    | Optional (default false): use KeyId instead of KeyArn   |
 | kmsAWS.tls         | TLS configuration (Object, see below)                   |
 
 TLS configuration attributes:
@@ -41,10 +43,12 @@ and
 ```json
 {
     "kmsAWS": {
+        "providerName": "aws",
         "region": "us-east-1",
         "endpoint": "https://kms.us-east-1.amazonaws.com",
         "ak": "xxxxxxx",
-        "sk": "xxxxxxx"
+        "sk": "xxxxxxx",
+        "noAwsArn": true
     }
 }
 ```
@@ -53,6 +57,7 @@ With TLS configuration:
 
 ```json
     "kmsAWS": {
+        "providerName": "aws",
         "region": "us-east-1",
         "endpoint": "https://kms.us-east-1.amazonaws.com",
         "ak": "xxxxxxx",
diff --git a/lib/storage/data/DataWrapper.js b/lib/storage/data/DataWrapper.js
index 02d6de67d..008554363 100644
--- a/lib/storage/data/DataWrapper.js
+++ b/lib/storage/data/DataWrapper.js
@@ -402,13 +402,13 @@ class DataWrapper {
      * dataStoreContext.uploadId: uploadId
      * dataStoreContext.partNumber: request.query.partNumber
      * @param {function} lcCheckFn: locationConstraintCheck function
+     * @param {object} serverSideEncryption - server side encryption configuration
      * @param {function} callback - callback
      * @returns {function} cb - callback
      */
     uploadPartCopy(request, log, destBucketMD, sourceLocationConstraintName,
         destLocationConstraintName, dataLocator, dataStoreContext, lcCheckFn,
-        callback) {
-        const serverSideEncryption = destBucketMD.getServerSideEncryption();
+        serverSideEncryption, callback) {
         const lastModified = new Date().toJSON();
 
         // skip if 0 byte object
@@ -787,7 +787,7 @@ class DataWrapper {
 
     /**
      * _dataCopyPut - put used for copying object with and without encryption
-     * @param {string} serverSideEncryption - Server side encryption
+     * @param {object} serverSideEncryption - Server side encryption configuration
      * @param {object} stream - stream containing the data
      * @param {object} part - element of dataLocator array
      * @param {object} dataStoreContext - information of the destination object
@@ -906,7 +906,9 @@ class DataWrapper {
                             locations.push(partResult);
                             return cb();
                         });
-                });
+                // Allow KMS to use a key from previous provider (if sseMigration configured)
+                // Because ongoing MPU started before sseMigration is not migrated
+                }, { previousOk: true });
         }
         // Copied object is not encrypted so just put it
         // without a cipherBundle
diff --git a/package.json b/package.json
index 41e4817fc..1c3109e81 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
   "engines": {
     "node": ">=16"
   },
-  "version": "7.70.43",
+  "version": "7.70.44",
   "description": "Common utilities for the S3 project components",
   "main": "build/index.js",
   "repository": {
diff --git a/tests/functional/kmip/highlevel.spec.js b/tests/functional/kmip/highlevel.spec.js
index fea7d6639..24b6cdbfe 100644
--- a/tests/functional/kmip/highlevel.spec.js
+++ b/tests/functional/kmip/highlevel.spec.js
@@ -27,6 +27,7 @@ describe('KMIP High Level Driver', () => {
         [false, true].forEach(compoundCreateActivate => {
             const options = {
                 kmip: {
+                    providerName: 'tests',
                     client: {
                         bucketNameAttributeName,
                         compoundCreateActivate,
@@ -49,11 +50,13 @@ describe('KMIP High Level Driver', () => {
                 const plaintext = Buffer.from(crypto.randomBytes(32));
                 async.waterfall([
                     next => kmipClient.createBucketKey('plop', logger, next),
-                    (id, next) =>
+                    (id, arn, next) => {
+                        assert.match(arn, /arn:scality:kms:external:kmip:tests:key\//);
                         kmipClient.cipherDataKey(1, id, plaintext,
                             logger, (err, ciphered) => {
                                 next(err, id, ciphered);
-                            }),
+                            });
+                    },
                     (id, ciphered, next) =>
                         kmipClient.decipherDataKey(
                             1, id, ciphered, logger, (err, deciphered) => {
@@ -70,6 +73,7 @@ describe('KMIP High Level Driver', () => {
     it('should succeed healthcheck with working KMIP client and server', done => {
         const options = {
             kmip: {
+                providerName: 'tests',
                 client: {
                     bucketNameAttributeName: null,
                     compoundCreateActivate: false,
@@ -94,6 +98,7 @@ describe('KMIP High Level Driver', () => {
     it('should fail healthcheck with KMIP server not running', done => {
         const options = {
             kmip: {
+                providerName: 'tests',
                 client: {
                     bucketNameAttributeName: null,
                     compoundCreateActivate: false,
diff --git a/tests/functional/kmsAWS/highlevel.spec.js b/tests/functional/kmsAWS/highlevel.spec.js
index ab6255634..a9f50b645 100644
--- a/tests/functional/kmsAWS/highlevel.spec.js
+++ b/tests/functional/kmsAWS/highlevel.spec.js
@@ -20,6 +20,7 @@ describe('KmsAWSClient', () => {
     beforeEach(() => {
         client = new Client({
             kmsAWS: {
+                providerName: 'tests',
                 region: 'us-east-1',
                 ak: 'ak',
                 sk: 'sk',
@@ -48,6 +49,10 @@ describe('KmsAWSClient', () => {
         assert.strictEqual(client.supportsDefaultKeyPerAccount, true);
     });
 
+    it('should be configured with noAwsArn falsy (undefined) by default', () => {
+        assert.strictEqual(client.noAwsArn, undefined);
+    });
+
     it('should create a new master encryption key', done => {
         const mockResponse = {
             KeyMetadata: {
@@ -56,9 +61,85 @@ describe('KmsAWSClient', () => {
         };
         createKeyStub.yields(null, mockResponse);
 
-        client.createMasterKey(logger, (err, keyId) => {
+        client.createMasterKey(logger, (err, keyId, keyArn) => {
+            assert.ifError(err);
+            assert.strictEqual(keyId, 'mock-key-id');
+            assert.strictEqual(keyArn, 'arn:scality:kms:external:aws_kms:tests:key/mock-key-id');
+            assert(createKeyStub.calledOnce);
+            done();
+        });
+    });
+
+    it('should create a new master encryption key when configured to use Arn but fallback to KeyId', done => {
+        client = new Client({
+            kmsAWS: {
+                providerName: 'tests',
+                region: 'us-east-1',
+                ak: 'ak',
+                sk: 'sk',
+                noAwsArn: false, // ignore default enforce using aws arn
+            },
+        });
+        createKeyStub = sinon.stub(client.client, 'createKey');
+        const mockResponse = {
+            KeyMetadata: {
+                KeyId: 'mock-key-id',
+            },
+        };
+        createKeyStub.yields(null, mockResponse);
+
+        client.createMasterKey(logger, (err, keyId, keyArn) => {
+            assert.ifError(err);
+            assert.strictEqual(keyId, 'mock-key-id');
+            assert.strictEqual(keyArn, 'arn:scality:kms:external:aws_kms:tests:key/mock-key-id');
+            assert(createKeyStub.calledOnce);
+            done();
+        });
+    });
+
+    it('should create a new master encryption key with aws arn', done => {
+        const mockResponse = {
+            KeyMetadata: {
+                KeyId: 'mock-key-id',
+                Arn: 'arn:aws:kms:region:accountId:key/mock-key-id',
+            },
+        };
+        createKeyStub.yields(null, mockResponse);
+
+        client.createMasterKey(logger, (err, keyId, keyArn) => {
+            assert.ifError(err);
+            assert.strictEqual(keyId, 'arn:aws:kms:region:accountId:key/mock-key-id');
+            assert.strictEqual(keyArn,
+                'arn:scality:kms:external:aws_kms:tests:key/arn:aws:kms:region:accountId:key/mock-key-id');
+            assert(createKeyStub.calledOnce);
+            done();
+        });
+    });
+
+    it('should create a new master encryption key without aws arn', done => {
+        client = new Client({
+            kmsAWS: {
+                providerName: 'tests',
+                region: 'us-east-1',
+                ak: 'ak',
+                sk: 'sk',
+                noAwsArn: true,
+            },
+        });
+        createKeyStub = sinon.stub(client.client, 'createKey');
+        const mockResponse = {
+            KeyMetadata: {
+                KeyId: 'mock-key-id',
+                Arn: 'arn:aws:kms:region:accountId:key/mock-key-id',
+            },
+        };
+        createKeyStub.yields(null, mockResponse);
+
+        client.createMasterKey(logger, (err, keyId, keyArn) => {
             assert.ifError(err);
             assert.strictEqual(keyId, 'mock-key-id');
+            assert.strictEqual(keyArn,
+                'arn:scality:kms:external:aws_kms:tests:key/mock-key-id');
             assert(createKeyStub.calledOnce);
             done();
         });
@@ -83,9 +164,10 @@ describe('KmsAWSClient', () => {
         };
         createKeyStub.yields(null, mockResponse);
 
-        client.createBucketKey('bucketName', logger, (err, keyId) => {
+        client.createBucketKey('bucketName', logger, (err, keyId, keyArn) => {
             assert.ifError(err);
             assert.strictEqual(keyId, 'mock-bucket-key-id');
+            assert.strictEqual(keyArn, 'arn:scality:kms:external:aws_kms:tests:key/mock-bucket-key-id');
             assert(createKeyStub.calledOnce);
             done();
         });
@@ -209,6 +291,25 @@ describe('KmsAWSClient', () => {
         });
     });
 
+    it('should extract key from arn input to call generate a data key', done => {
+        const arnPrefix = 'arn:scality:kms:external:aws_kms:tests:key/';
+        const awsArn = 'arn:aws:kms:region:accountId:key/mock-key-id';
+        const key = `${arnPrefix}${awsArn}`;
+        const mockResponse = {
+            Plaintext: Buffer.from('plaintext'),
+            CiphertextBlob: Buffer.from('ciphertext'),
+            KeyId: 'mocked-kms-key-id',
+        };
+        generateDataKeyStub.yields(null, mockResponse);
+
+        client.generateDataKey(1, key, logger, (err) => {
+            assert.ifError(err);
+            assert(generateDataKeyStub.calledOnce);
+            assert.strictEqual(generateDataKeyStub.getCall(0).firstArg.KeyId, awsArn);
+            done();
+        });
+    });
+
     it('should handle errors generating a data key', done => {
         const mockError = new Error('mock error');
         generateDataKeyStub.yields(mockError, null);
diff --git a/tests/unit/network/KMSInterface.spec.js b/tests/unit/network/KMSInterface.spec.js
new file mode 100644
index 000000000..02922d239
--- /dev/null
+++ b/tests/unit/network/KMSInterface.spec.js
@@ -0,0 +1,225 @@
+const assert = require('assert');
+const {
+    SCAL_KMS_ARN,
+    KmsType,
+    KmsProtocol,
+    makeScalityArnPrefix,
+    makeBackend,
+    isScalityKmsArn,
+    getKeyIdFromArn,
+    isValidType,
+    isValidProtocol,
+    isValidProvider,
+    validateKeyDetail,
+    extractDetailFromArn,
+} = require('../../../lib/network/KMSInterface');
+
+const awsArn = 'arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab';
+
+describe('KMSInterface', () => {
+    const backends = [
+        { type: KmsType.internal, protocol: KmsProtocol.mem, provider: 'mtests' },
+        { type: KmsType.internal, protocol: KmsProtocol.file, provider: 'ftests' },
+        { type: KmsType.external, protocol: KmsProtocol.scality, provider: 'safenet' },
+        { type: KmsType.external, protocol: KmsProtocol.kmip, provider: 'ktests' },
+        { type: KmsType.external, protocol: KmsProtocol.aws_kms, provider: 'atests' },
+        { type: KmsType.external, protocol: KmsProtocol.aws_kms, provider: 'aws', key: awsArn },
+    ];
+
+    describe('makeScalityArnPrefix', () => {
+        backends.forEach(({ type, protocol, provider }) => it(
+            `should succeed for ${type}:${protocol}:${provider}`, () => {
+                const res = makeScalityArnPrefix(type, protocol, provider);
+                assert.strictEqual(res, `${SCAL_KMS_ARN}${type}:${protocol}:${provider}:key/`);
+            }));
+    });
+
+    describe('makeBackend', () => {
+        backends.forEach(({ type, protocol, provider }) => it(
+            `should succeed for ${type}:${protocol}:${provider}`, () => {
+                const res = makeBackend(type, protocol, provider);
+                assert.strictEqual(res.type, type);
+                assert.strictEqual(res.protocol, protocol);
+                assert.strictEqual(res.provider, provider);
+                assert.strictEqual(res.arnPrefix, `${SCAL_KMS_ARN}${type}:${protocol}:${provider}:key/`);
+            }));
+    });
+
+    describe('isScalityKmsArn', () => {
+        backends.forEach(({ type, protocol, provider, key }) => it(
+            `should return true for ${type}:${protocol}:${provider}`, () => {
+                const arn = `${makeScalityArnPrefix(type, protocol, provider)}${key || '12345'}`;
+                const res = isScalityKmsArn(arn);
+                assert.strictEqual(res, true);
+            }));
+
+        it('should return false for non arn', () => {
+            assert.strictEqual(isScalityKmsArn('12345'), false);
+        });
+        it('should return false for aws arn', () => {
+            assert.strictEqual(isScalityKmsArn(awsArn), false);
+        });
+    });
+
+    describe('getKeyIdFromArn', () => {
+        backends.forEach(({ type, protocol, provider, key }) => it(
+            `should return KeyId for ${type}:${protocol}:${provider}`, () => {
+                const arn = `${makeScalityArnPrefix(type, protocol, provider)}${key || '12345'}`;
+                const res = getKeyIdFromArn(arn);
+                assert.strictEqual(res, key || '12345');
+            }));
+
+        it('should return KeyId for non arn', () => {
+            assert.strictEqual(getKeyIdFromArn('12345'), '12345');
+        });
+        it('should return aws arn', () => {
+            assert.strictEqual(getKeyIdFromArn(awsArn), awsArn);
+        });
+    });
+
+    describe('isValidType', () => {
+        it('should return true for internal', () => assert.strictEqual(isValidType(KmsType.internal), true));
+        it('should return true for external', () => assert.strictEqual(isValidType(KmsType.external), true));
+        it('should return false for anything', () => assert.strictEqual(isValidType('nope'), false));
+    });
+
+    describe('isValidProtocol', () => {
+        backends.forEach(({ type, protocol, provider }) => it(
+            `should return true for ${type}:${protocol}:${provider}`, () => {
+                assert.strictEqual(isValidProtocol(type, protocol), true);
+            }));
+
+        it('should return false for mismatching type and protocol', () => {
+            assert.strictEqual(isValidProtocol(KmsType.internal, KmsProtocol.kmip), false);
+        });
+        it('should return false for bad protocol', () => {
+            assert.strictEqual(isValidProtocol(KmsType.internal, 'bad protocol'), false);
+        });
+        it('should return false for bad type', () => {
+            assert.strictEqual(isValidProtocol('bad type', KmsProtocol.file), false);
+        });
+    });
+
+    describe('isValidProvider', () => {
+        it('should return true for lowercase alphanumeric', () => {
+            assert.strictEqual(isValidProvider('scality01'), true);
+        });
+        it('should return false not lowercase alphanumeric', () => {
+            assert.strictEqual(isValidProvider('A BAD PROVIDER !!!'), false);
+        });
+    });
+
+    describe('extractDetailFromArn', () => {
+        backends.forEach(({ type, protocol, provider, key }) => it(
+            `should return detail for ${type}:${protocol}:${provider}`, () => {
+                const arn = `${makeScalityArnPrefix(type, protocol, provider)}${key || '12345'}`;
+                const detail = extractDetailFromArn(arn);
+
+                assert.strictEqual(detail.type, type);
+                assert.strictEqual(detail.protocol, protocol);
+                assert.strictEqual(detail.provider, provider);
+                assert.strictEqual(detail.arnType, 'key');
+                assert.strictEqual(detail.id, key || '12345');
+            }));
+
+        it('should return only id for non arn', () => {
+            const detail = extractDetailFromArn('12345');
+
+            assert.strictEqual(detail.type, undefined);
+            assert.strictEqual(detail.protocol, undefined);
+            assert.strictEqual(detail.provider, undefined);
+            assert.strictEqual(detail.arnType, undefined);
+            assert.strictEqual(detail.id, '12345');
+        });
+
+        it('should return only id (aws arn) for aws arn', () => {
+            const detail = extractDetailFromArn(awsArn);
+
+            assert.strictEqual(detail.type, undefined);
+            assert.strictEqual(detail.protocol, undefined);
+            assert.strictEqual(detail.provider, undefined);
+            assert.strictEqual(detail.arnType, undefined);
+            assert.strictEqual(detail.id, awsArn);
+        });
+    });
+
+    describe('validateKeyDetail', () => {
+        backends.forEach(({ type, protocol, provider, key }) => it(
+            `should return no error ${type}:${protocol}:${provider}`, () => {
+                const arn = `${makeScalityArnPrefix(type, protocol, provider)}${key || '12345'}`;
+                const detail = extractDetailFromArn(arn);
+                const backend = makeBackend(type, protocol, provider);
+
+                assert.strictEqual(validateKeyDetail(detail, [backend]), null);
+            }));
+
+        it('should return an error for non arn key', () => {
+            const b1 = backends[1];
+            const detail = extractDetailFromArn('12345');
+            const backend = makeBackend(b1.type, b1.protocol, b1.provider);
+
+            const err = validateKeyDetail(detail, [backend]);
+            assert.notStrictEqual(err, null);
+            assert.strictEqual(err.InvalidArgument, true);
+            assert.strictEqual(err.description,
+                `KMS Scality KeyArn doesn\'t match any configured providers. Possible arn are: \"${
+                    backend.arnPrefix}\"`,
+            );
+        });
+
+        it('should return an error for aws arn', () => {
+            const b1 = backends[1];
+            const detail = extractDetailFromArn(awsArn);
+            const backend = makeBackend(b1.type, b1.protocol, b1.provider);
+
+            const err = validateKeyDetail(detail, [backend]);
+            assert.notStrictEqual(err, null);
+            assert.strictEqual(err.InvalidArgument, true);
+            assert.strictEqual(err.description,
+                `KMS Scality KeyArn doesn\'t match any configured providers. Possible arn are: \"${
+                    backend.arnPrefix}\"`,
+            );
+        });
+
+        it('should return an error for mismatching key and provider', () => {
+            const b0 = backends[0];
+            const b1 = backends[1];
+            const arn = `${makeScalityArnPrefix(b0.type, b0.protocol, b0.provider)}12345`;
+            const detail = extractDetailFromArn(arn);
+            const backend = makeBackend(b1.type, b1.protocol, b1.provider);
+
+            const err = validateKeyDetail(detail, [backend]);
+            assert.notStrictEqual(err, null);
+            assert.strictEqual(err.InvalidArgument, true);
+            assert.strictEqual(err.description,
+                `KMS Scality KeyArn doesn\'t match any configured providers. Possible arn are: \"${
+                    backend.arnPrefix}\"`,
+            );
+        });
+
+        it('should return an error for invalid arnType', () => {
+            const b0 = backends[0];
+            const arn = `${SCAL_KMS_ARN}${b0.type}:${b0.protocol}:${b0.provider}:wrongResourceType/1234`;
+            const detail = extractDetailFromArn(arn);
+            const backend = makeBackend(b0.type, b0.protocol, b0.provider);
+
+            const err = validateKeyDetail(detail, [backend]);
+            assert.notStrictEqual(err, null);
+            assert.strictEqual(err.InvalidArgument, true);
+            assert.strictEqual(err.description,
+                'Invalid KMS Scality KeyArn, expected "key" instead of "wrongResourceType"');
+        });
+
+        it('should return an error for missing KeyId after arnPrefix', () => {
+            const b0 = backends[0];
+            const arn = `${SCAL_KMS_ARN}${b0.type}:${b0.protocol}:${b0.provider}:key/`;
+            const detail = extractDetailFromArn(arn);
+            const backend = makeBackend(b0.type, b0.protocol, b0.provider);
+
+            const err = validateKeyDetail(detail, [backend]);
+            assert.notStrictEqual(err, null);
+            assert.strictEqual(err.InvalidArgument, true);
+            assert.strictEqual(err.description, 'Invalid KMS Scality KeyArn, missing KeyId');
+        });
+    });
+});


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR cherry-picks and adapts several hotfix commits implementing updates for KMS server-side encryption functionality across internal and external providers. Key changes include the introduction of new configuration parameters (e.g., providerName and noAwsArn), updates to unit and functional tests to verify key ARN generation and extraction, and documentation updates to reflect the new features.

### Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                                        | Description                                                    |
|---------------------------------------------|----------------------------------------------------------------|
| tests/unit/network/KMSInterface.spec.js     | Adds comprehensive unit tests for KMSInterface functions.      |
| tests/functional/kmsAWS/highlevel.spec.js   | Updates tests to check new key ARN behavior and configuration.   |
| tests/functional/kmip/highlevel.spec.js     | Adjusts KMIP functional tests with provider configuration changes. |
| package.json                                | Bumps version as part of the cherry-pick.                        |
| lib/storage/data/DataWrapper.js             | Updates method signatures and comments for server-side encryption.   |
| lib/network/kmsAWS/README.md                  | Updates documentation to include new parameters.                 |
| lib/network/kmsAWS/Client.ts                  | Refactors key creation and deletion flows to support ARN and keyId.  |
| lib/network/kmip/ClusterClient.ts             | Configures KMIP cluster client to use providerName.                |
| lib/network/kmip/Client.ts                    | Updates key processing methods to handle both ARN and key identifiers.|
| lib/network/KMSInterface.ts                   | Enhances type definitions and functions for ARN parsing and validation. |
| lib/auth/in_memory/Backend.ts                 | Adjusts encryption key ID formatting to include ARN prefix.         |
</details>





state: COMMENTED
author_association: NONE
submitted_at: 2025-05-14T08:49:34Z
user: login: BourgoisMickael
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T08:53:58Z
user: login: anurag4DSB
body: approving without much review as cherry picked so code must be already approved
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T11:55:35Z
user: login: tcarmet
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T22:31:20Z

Review comments:
diff_hunk: @@ -107,21 +125,37 @@ export default class Client implements KMSInterface {
                 cb(error);
                 return;
             }
-            logger.debug("AWS KMS: master encryption key created", { KeyMetadata: data?.KeyMetadata });
-            cb(null, data?.KeyMetadata?.KeyId);
+            const keyMetadata = data?.KeyMetadata;
+            logger.debug("AWS KMS: master encryption key created", { KeyMetadata: keyMetadata });
+            let keyId: string;
+            if (this.noAwsArn) {
+                // Use KeyId when ARN is not wanted
+                keyId = keyMetadata?.KeyId!;
+            } else {
+                // Prefer ARN, but fall back to KeyId if ARN is missing
+                keyId = keyMetadata?.Arn ?? keyMetadata?.KeyId!;
+            }
+            // May produce double arn prefix: scality arn + aws arn
+            // arn:scality:kms:external:aws_kms:custom:key/arn:aws:kms:region:accountId:key/cbd69d33-ba8e-4b56-8cfe-ae0844f69966
+            // If this is a problem, a config flag should be used to hide the scality arn when returning the KMS KeyId
+            // or aws arn when creating the KMS Key
+            const arn = `${this.backend.arnPrefix}${keyId}`;
path: lib/network/kmsAWS/Client.ts
user: login: Copilot
body: The logic concatenates an ARN prefix to keyId even when keyId is derived from keyMetadata.Arn, which may result in a double ARN prefix. Consider adding a conditional check to remove the redundant prefix when keyId already includes an AWS ARN.
created_at: 2025-05-14T08:49:34Z
author_association: NONE
diff_hunk: @@ -107,21 +125,37 @@ export default class Client implements KMSInterface {
                 cb(error);
                 return;
             }
-            logger.debug("AWS KMS: master encryption key created", { KeyMetadata: data?.KeyMetadata });
-            cb(null, data?.KeyMetadata?.KeyId);
+            const keyMetadata = data?.KeyMetadata;
+            logger.debug("AWS KMS: master encryption key created", { KeyMetadata: keyMetadata });
+            let keyId: string;
+            if (this.noAwsArn) {
+                // Use KeyId when ARN is not wanted
+                keyId = keyMetadata?.KeyId!;
+            } else {
+                // Prefer ARN, but fall back to KeyId if ARN is missing
+                keyId = keyMetadata?.Arn ?? keyMetadata?.KeyId!;
+            }
+            // May produce double arn prefix: scality arn + aws arn
+            // arn:scality:kms:external:aws_kms:custom:key/arn:aws:kms:region:accountId:key/cbd69d33-ba8e-4b56-8cfe-ae0844f69966
+            // If this is a problem, a config flag should be used to hide the scality arn when returning the KMS KeyId
+            // or aws arn when creating the KMS Key
+            const arn = `${this.backend.arnPrefix}${keyId}`;
path: lib/network/kmsAWS/Client.ts
user: login: BourgoisMickael
body: This is expected
created_at: 2025-05-14T08:53:57Z
author_association: CONTRIBUTOR

PR comments:
user: login: bert-e
created_at: 2025-05-14T08:47:30Z
body: # Hello bourgoismickael,



My role is to assist you with the merge of this
pull request. Please type `@bert-e help` to get information
on this process, or consult the [user documentation](
https://bert-e.scality.net/scality/arsenal/doc/user).


<details>
  <summary><b>Available options</b></summary>

  name   | description  | privileged   | authored
  ------ | ------------ | ------------ |----------
  `/after_pull_request` | Wait for the given pull request id to be merged before continuing with the current one. |  | 
  `/bypass_author_approval` | Bypass the pull request author's approval |  :star:  | 
  `/bypass_build_status` | Bypass the build and test status |  :star:  | 
  `/bypass_commit_size` | Bypass the check on the size of the changeset ```TBA``` |  :star:  | 
  `/bypass_incompatible_branch` | Bypass the check on the source branch prefix |  :star:  | 
  `/bypass_jira_check` | Bypass the Jira issue check |  :star:  | 
  `/bypass_peer_approval` | Bypass the pull request peers' approval |  :star:  | 
  `/bypass_leader_approval` | Bypass the pull request leaders' approval |  :star:  | 
  `/approve` | Instruct Bert-E that the author has approved the pull request. |  |  :writing_hand: 
  `/create_pull_requests` | Allow the creation of integration pull requests. |  | 
  `/create_integration_branches` | Allow the creation of integration branches. |  | 
  `/no_octopus` | Prevent Wall-E from doing any octopus merge and use multiple consecutive merge instead |  | 
  `/unanimity` | Change review acceptance criteria from `one reviewer at least` to `all reviewers` |  | 
  `/wait` | Instruct Bert-E not to run until further notice. |  | 
  

</details>



<details>
  <summary><b>Available commands</b></summary>

  name   | description  | privileged
  ------ | ------------ | ------------
  `/help` | Print Bert-E's manual in the pull request. | 
  `/status` | Print Bert-E's current status in the pull request ```TBA``` | 
  `/clear` | Remove all comments from Bert-E from the history ```TBA``` | 
  `/retry` | Re-start a fresh build ```TBA``` | 
  `/build` | Re-start a fresh build ```TBA``` | 
  `/force_reset` | Delete integration branches & pull requests, and restart merge process from the beginning. | 
  `/reset` | Try to remove integration branches unless there are commits on them which do not appear on the source branch. | 
  
</details>




*Status report is not available.*







author_association: CONTRIBUTOR
user: login: bert-e
created_at: 2025-05-14T08:47:41Z
body: # Request integration branches



Waiting for integration branch creation to be requested by the user.

To request integration branches, please comment on this pull request with the following command:

```
/create_integration_branches
```

Alternatively, the `/approve` and `/create_pull_requests` commands will automatically
create the integration branches.






author_association: CONTRIBUTOR
user: login: BourgoisMickael
created_at: 2025-05-15T07:50:00Z
body: /approve
author_association: CONTRIBUTOR
user: login: bert-e
created_at: 2025-05-15T07:50:10Z
body: # Conflict




A conflict has been raised during the creation of
integration branch `w/8.1/improvement/ARSN-485-kms-cherry-pick` with contents from `improvement/ARSN-485-kms-cherry-pick`
and `development/8.1`.

**I have not created the integration branch.**


Here are the steps to resolve this conflict:

```bash
 $ git fetch
 $ git checkout -B w/8.1/improvement/ARSN-485-kms-cherry-pick origin/development/8.1
 $ git merge origin/improvement/ARSN-485-kms-cherry-pick
 $ # <intense conflict resolution>
 $ git commit
 $ git push -u origin w/8.1/improvement/ARSN-485-kms-cherry-pick
```





*The following options are set:* **approve**


author_association: CONTRIBUTOR
user: login: bert-e
created_at: 2025-05-15T08:15:56Z
body: # Conflict




A conflict has been raised during the creation of
integration branch `w/8.2/improvement/ARSN-485-kms-cherry-pick` with contents from `w/8.1/improvement/ARSN-485-kms-cherry-pick`
and `development/8.2`.

**I have not created the integration branch.**


Here are the steps to resolve this conflict:

```bash
 $ git fetch
 $ git checkout -B w/8.2/improvement/ARSN-485-kms-cherry-pick origin/development/8.2
 $ git merge origin/w/8.1/improvement/ARSN-485-kms-cherry-pick
 $ # <intense conflict resolution>
 $ git commit
 $ git push -u origin w/8.2/improvement/ARSN-485-kms-cherry-pick
```





*The following options are set:* **approve**


author_association: CONTRIBUTOR
user: login: BourgoisMickael
created_at: 2025-05-15T08:35:55Z
body: Added this commit https://github.com/scality/Arsenal/commit/6296f6bbd0a7ecec528b17f216d97bba0d166b99 on integration branches to fix unit test of DataWrapper.uploadPartCopy with sse
author_association: CONTRIBUTOR
user: login: bert-e
created_at: 2025-05-15T08:46:54Z
body: # Build failed



The [build](https://github.com/scality/Arsenal/actions?query=branch%3Aw/8.2/improvement/ARSN-485-kms-cherry-pick) for [commit](https://github.com/scality/Arsenal/commit/ffa9401d50da02abac20d5a618163853690bb73f) did not succeed in branch [w/8.2/improvement/ARSN-485-kms-cherry-pick](https://github.com/scality/arsenal/compare/development/8.2...w/8.2/improvement/ARSN-485-kms-cherry-pick)



*The following options are set:* **approve**


author_association: CONTRIBUTOR
user: login: bert-e
created_at: 2025-05-15T09:53:51Z
body: # 


I have successfully merged the changeset of this pull request
into targetted development branches:


* :heavy_check_mark: `development/7.70`

* :heavy_check_mark: `development/8.1`

* :heavy_check_mark: `development/8.2`



The following branches have **NOT** changed:

* `development/6.4`
* `development/7.10`
* `development/7.4`



Please check the status of the associated issue ARSN-485.

Goodbye bourgoismickael.




*The following options are set:* **approve**


author_association: CONTRIBUTOR