Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev/ft/kms aws tls #2255

Open
wants to merge 6 commits into
base: development/7.70
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/network/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export const probe = { ProbeServer };
export { default as RoundRobin } from './RoundRobin';
export { default as kmip } from './kmip';
export { default as kmipClient } from './kmip/Client';
export { default as awsClient } from './kmsAWS/Client';
export * as rpc from './rpc/rpc';
export * as level from './rpc/level-net';
276 changes: 276 additions & 0 deletions lib/network/kmsAWS/Client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
'use strict'; // eslint-disable-line
/* eslint new-cap: "off" */

import errors from '../../errors';
import { Agent } from "https";
import { SecureVersion } from "tls";
import * as werelogs from 'werelogs';
import { KMSClient, CreateKeyCommand, ScheduleKeyDeletionCommand, EncryptCommand, DecryptCommand, GenerateDataKeyCommand, DataKeySpec } from "@aws-sdk/client-kms";
import { NodeHttpHandler } from "@smithy/node-http-handler";
import { AwsCredentialIdentity } from "@smithy/types";
import assert from 'assert';

/**
* Normalize errors according to arsenal definitions
* @param err - an Error instance or a message string
* @returns - arsenal error
*
* @note Copied from the KMIP implementation
*/
function _arsenalError(err: string | Error) {
const messagePrefix = 'AWS_KMS:';
if (typeof err === 'string') {
return errors.InternalError
.customizeDescription(`${messagePrefix} ${err}`);
} else if (
err instanceof Error ||
// INFO: The second part is here only for Jest, to remove when we'll be
// fully migrated to TS
// @ts-expect-error
(err && typeof err.message === 'string')
) {
return errors.InternalError
.customizeDescription(`${messagePrefix} ${err.message}`);
}
return errors.InternalError
.customizeDescription(`${messagePrefix} Unspecified error`);
}

export default class Client {
client: KMSClient;
options: any;

/**
* Construct a high level KMIP driver suitable for cloudserver
* @param options - Instance options
* @param options.kmsAWS - AWS client options
* @param options.kmsAWS.region - KMS region
* @param options.kmsAWS.endpoint - Endpoint URL of the KMS service
* @param options.kmsAWS.ak - Application Key
* @param options.kmsAWS.sk - Secret Key
* @param options.kmsAWS.tls.rejectUnauthorized - default to true, reject unauthenticated TLS connections (set to false to accept auto-signed certificates, useful in development ONLY)
* @param options.kmsAWS.tls.ca - override CA definition(s)
* @param options.kmsAWS.tls.cert - certificate or list of certificates
* @param options.kmsAWS.tls.minVersion - min TLS version accepted, One of 'TLSv1.3', 'TLSv1.2', 'TLSv1.1', or 'TLSv1' (see https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions)
* @param options.kmsAWS.tls.maxVersion - max TLS version accepted, One of 'TLSv1.3', 'TLSv1.2', 'TLSv1.1', or 'TLSv1' (see https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions)
* @param options.kmsAWS.tls.key - private key or list of private keys
*
* This client also looks in the standard AWS configuration files (https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html).
* If no option is passed to this constructor, the client will try to get it from the configuration file.
*
* TLS configuration options are those of nodejs, you can refere to https://nodejs.org/api/tls.html#tlsconnectoptions and https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions
*/
constructor(
options: {
kmsAWS: {
region?: string,
endpoint?: string,
ak?: string,
sk?: string,
tls?: {
rejectUnauthorized?: boolean,
ca?: [Buffer] | Buffer,
cert?: [Buffer] | Buffer,
minVersion?: string,
maxVersion?: string,
key?: [Buffer] | Buffer,
}
}
},
) {
let requestHandler: {requestHandler: NodeHttpHandler} | null = null;
const tlsOpts = options.kmsAWS.tls;
if (tlsOpts) {
const agent = new Agent({
rejectUnauthorized: tlsOpts?.rejectUnauthorized,
ca: tlsOpts?.ca,
cert: tlsOpts?.cert,
minVersion: <SecureVersion>tlsOpts?.minVersion,
maxVersion: <SecureVersion>tlsOpts?.maxVersion,
key: tlsOpts?.key,
});

requestHandler = {requestHandler: new NodeHttpHandler({
httpAgent: agent,
httpsAgent: agent,
})}
}

let credentials: {credentials: AwsCredentialIdentity} | null = null;
if (options.kmsAWS.ak && options.kmsAWS.sk) {
credentials = {credentials: {
accessKeyId: options.kmsAWS.ak,
secretAccessKey: options.kmsAWS.sk,
}};
}

this.client = new KMSClient({
region: options.kmsAWS.region,
endpoint: options.kmsAWS.endpoint,
...credentials,
...requestHandler
});
}

/**
* Create a new cryptographic key managed by the server,
* for a specific bucket
* @param bucketName - The bucket name
* @param logger - Werelog logger object
* @param cb - The callback(err: Error, bucketKeyId: String)
*/
createBucketKey(bucketName: string, logger: werelogs.Logger, cb: any) {
logger.debug("AWS KMS: createBucketKey", {bucketName});

const command = new CreateKeyCommand({});
this.client.send(command, (err, data) => {
if (err) {
const error = _arsenalError(err);
logger.error("AWS_KMS::createBucketKey", {err, bucketName});
cb (error);
} else {
logger.debug("AWS KMS: createBucketKey", {bucketName, KeyMetadata: data?.KeyMetadata});
cb(null, data?.KeyMetadata?.KeyId);
}
});
}

/**
* Destroy a cryptographic key managed by the server, for a specific bucket.
* @param bucketKeyId - The bucket key Id
* @param logger - Werelog logger object
* @param cb - The callback(err: Error)
*/
destroyBucketKey(bucketKeyId: string, logger: werelogs.Logger, cb: any) {
logger.debug("AWS KMS: destroyBucketKey", {bucketKeyId: bucketKeyId});

// Schedule a deletion in 7 days (the minimum value on this API)
const command = new ScheduleKeyDeletionCommand({KeyId: bucketKeyId, PendingWindowInDays: 7});
this.client.send(command, (err, data) => {
if (err) {
const error = _arsenalError(err);
logger.error("AWS_KMS::destroyBucketKey", {err});
cb (error);
} else {
// Sanity check
if (data?.KeyState != "PendingDeletion") {
const error = _arsenalError("Key is not in PendingDeletion state")
logger.error("AWS_KMS::destroyBucketKey", {err, data});
cb(error);
} else {
cb();
}
}
});
}

/**
* @param cryptoScheme - crypto scheme version number
* @param masterKeyId - key to retrieve master key
* @param logger - werelog logger object
* @param cb - callback
* @callback called with (err, plainTextDataKey: Buffer, cipheredDataKey: Buffer)
*/
generateDataKey(
cryptoScheme: number,
masterKeyId: string,
logger: werelogs.Logger,
cb: any,
) {
logger.debug("AWS KMS: generateDataKey", {cryptoScheme, masterKeyId});

// Only support cryptoScheme v1
assert.strictEqual (cryptoScheme, 1);

const command = new GenerateDataKeyCommand({KeyId: masterKeyId, KeySpec: DataKeySpec.AES_256});
this.client.send(command, (err, data) => {
if (err) {
const error = _arsenalError(err);
logger.error("AWS_KMS::generateDataKey", {err});
cb (error);
} else if (!data) {
const error = _arsenalError("generateDataKey: empty response");
logger.error("AWS_KMS::generateDataKey empty reponse");
cb (error);
} else {
// Convert to a buffer. This allows the wrapper to use .toString("base64")
cb(null, Buffer.from(data.Plaintext!), Buffer.from(data.CiphertextBlob!));
}
});
}

/**
*
* @param cryptoScheme - crypto scheme version number
* @param masterKeyId - key to retrieve master key
* @param plainTextDataKey - data key
* @param logger - werelog logger object
* @param cb - callback
* @callback called with (err, cipheredDataKey: Buffer)
*/
cipherDataKey(
cryptoScheme: number,
masterKeyId: string,
plainTextDataKey: Buffer,
logger: werelogs.Logger,
cb: any,
) {
logger.debug("AWS KMS: cipherDataKey", {cryptoScheme, masterKeyId});

// Only support cryptoScheme v1
assert.strictEqual (cryptoScheme, 1);

const command = new EncryptCommand({KeyId: masterKeyId, Plaintext: plainTextDataKey});
this.client.send(command, (err, data) => {
if (err) {
const error = _arsenalError(err);
logger.error("AWS_KMS::cipherDataKey", {err});
cb (error);
} else if (!data) {
const error = _arsenalError("cipherDataKey: empty response");
logger.error("AWS_KMS::cipherDataKey empty reponse");
cb (error);
} else {
// Convert to a buffer. This allows the wrapper to use .toString("base64")
cb(null, Buffer.from(data.CiphertextBlob!));
}
});
}

/**
*
* @param cryptoScheme - crypto scheme version number
* @param masterKeyId - key to retrieve master key
* @param cipheredDataKey - data key
* @param logger - werelog logger object
* @param cb - callback
* @callback called with (err, plainTextDataKey: Buffer)
*/
decipherDataKey(
cryptoScheme: number,
masterKeyId: string,
cipheredDataKey: Buffer,
logger: werelogs.Logger,
cb: any,
) {
logger.debug("AWS KMS: decipherDataKey", {cryptoScheme, masterKeyId});

// Only support cryptoScheme v1
assert.strictEqual (cryptoScheme, 1);

const command = new DecryptCommand({CiphertextBlob: cipheredDataKey});
this.client.send(command, (err, data) => {
if (err) {
const error = _arsenalError(err);
logger.error("AWS_KMS::decipherDataKey", {err});
cb (error);
} else if (!data) {
const error = _arsenalError("decipherDataKey: empty response");
logger.error("AWS_KMS::decipherDataKey empty reponse");
cb (error);
} else {
cb(null, Buffer.from(data?.Plaintext!));
}
});
}
}
58 changes: 58 additions & 0 deletions lib/network/kmsAWS/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# AWS KMS connector

Allow to use AWS KMS backend for encryption of objects. It currently only support AK+SK for authentication.
mTLS can also be used to add extra security.

## Configuration

Configuration is done using the configuration file or environment variables. A Mix of both can be used, the configuration file takes precedence over environment variables.
Environment variables are the same as the ones used by the AWS CLI prefixed with "KMS_" (in order to scope them to the KMS module).

The following parameters are supported:

| config file | env variable | Description
|---------------------|--------------------------------------------------|------------
| kmsAWS.region | KMS_AWS_REGION or KMS_AWS_DEFAULT_REGION | AWS region tu use
| kmsAWS.endpoint | KMS_AWS_ENDPOINT_URL_KMS or KMS_AWS_ENDPOINT_URL | Endpoint URL
| kmsAWS.ak | KMS_AWS_ACCESS_KEY_ID | Credentials, Access Key
| kmsAWS.sk | KMS_AWS_SECRET_ACCESS_KEY | Credentials, Secret Key
| kmsAWS.tls | | TLS configuration (Object, see below)

The TLS configuration is can contain the following attributes:

| config file | Description
|---------------------|--------------------------------------------------
| rejectUnauthorized | false to disable TLS certificates checks (useful in development, DON'T disable in production)
| minVersion | min TLS version, One of 'TLSv1.3', 'TLSv1.2', 'TLSv1.1', or 'TLSv1' (see https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions)
| maxVersion | max TLS version, One of 'TLSv1.3', 'TLSv1.2', 'TLSv1.1', or 'TLSv1' (see https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions)
| ca | filename or array of filenames containing CA(s)
| cert | filename or array of filenames containing certificate(s)
| key | filename or array of filenames containing private key(s)

All TLS attributes conform to their nodejs definition. See https://nodejs.org/api/tls.html#tlsconnectoptions and https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions.

Configuration example:
```json
"kmsAWS": {
"region": "us-east-1",
"endpoint": "https://kms.us-east-1.amazonaws.com",
"ak": "xxxxxxx",
"sk": "xxxxxxx"
},
```

With TLS configuration:
```json
"kmsAWS": {
"region": "us-east-1",
"endpoint": "https://kms.us-east-1.amazonaws.com",
"ak": "xxxxxxx",
"sk": "xxxxxxx",
"tls": {
"rejectUnauthorized": false,
"cert": "mtls.crt.pem",
"key": "mtls.key.pem",
"minVersion": "TLSv1.3"
}
},
```
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
},
"homepage": "https://github.com/scality/Arsenal#readme",
"dependencies": {
"@aws-sdk/client-kms": "^3.485.0",
"@js-sdsl/ordered-set": "^4.4.2",
"@types/async": "^3.2.12",
"@types/utf8": "^3.0.1",
Expand Down Expand Up @@ -63,6 +64,7 @@
"@types/jest": "^27.4.1",
"@types/node": "^17.0.21",
"@types/xml2js": "^0.4.11",
"aws-sdk-client-mock": "^3.0.1",
"eslint": "^8.12.0",
"eslint-config-airbnb": "6.2.0",
"eslint-config-scality": "scality/Guidelines#7.10.2",
Expand Down
Loading
Loading