Skip to content

Commit 5d02093

Browse files
authored
Merge pull request #942 from DaniBunny/bdc-external-key-provider
External Provider Integration Template App
2 parents 2586b6f + c931a62 commit 5d02093

9 files changed

Lines changed: 345 additions & 0 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# SQL Server BDC Encryption at Rest
2+
3+
This folder contains the AppDeploy template application for integration with external key providers (such as HSMs, HashiCorp Vault, etc).
4+
5+
The folder kms_plugin_app should be downloaded as a zip file for usage in respect of the instructions at [External Key Providers](https://docs.microsoft.com/sql/big-data-cluster/encryption-at-rest-external-provider).
6+
7+
To learn more about how Key Versions are used on SQL Server Big Data Clusters see the following article: [Key Versions](https://docs.microsoft.com/sql/big-data-cluster/big-data-cluster-key-versions)
8+
9+
For information on configuring and using the Encryption at Rest feature see the following guides:
10+
* [Encryption at rest concepts and configuration guide](https://docs.microsoft.com/sql/big-data-cluster/encryption-at-rest-concepts-and-configuration)
11+
* [SQL Server Big Data Clusters HDFS Encryption Zones usage guide](https://docs.microsoft.com/sql/big-data-cluster/encryption-at-rest-hdfs-encryption-zones)
12+
* [SQL Server Big Data Clusters transparent data encryption (TDE) at rest usage guide](https://docs.microsoft.com/sql/big-data-cluster/encryption-at-rest-sql-server-tde)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# This is a script for running the HSM interaction service using PKCS11
2+
import json
3+
import sys
4+
# Append the current application path to sys path to be able to resolve local modules.
5+
#
6+
sys.path.append('.')
7+
sys.path.append('./model')
8+
from constants import ConfigurationConstants, Operations
9+
import utils
10+
from json_objects import EncryptDecryptRequest
11+
import custom2
12+
13+
def handler(operation, payload, pin, key_attributes, version):
14+
"""
15+
Entry point for the application.
16+
"""
17+
if (payload != None and len(payload) > 0):
18+
# The payload is base64 URL encoded and needs to be decoded first
19+
#
20+
json_request_payload = utils.urlsafe_base64decode_as_str(payload)
21+
22+
json_key_attributes_dict = json.loads(utils.urlsafe_base64decode_as_str(key_attributes))
23+
24+
if operation == Operations.OPERATION_ENCRYPT:
25+
encrypt_decrypt_dict = json.loads(json_request_payload)
26+
request = EncryptDecryptRequest(**encrypt_decrypt_dict)
27+
response = wrap_key(request, json_key_attributes_dict, pin, version)
28+
elif operation == Operations.OPERATION_DECRYPT:
29+
encrypt_decrypt_dict = json.loads(json_request_payload)
30+
request = EncryptDecryptRequest(**encrypt_decrypt_dict)
31+
response = unwrap_key(request, json_key_attributes_dict, pin, version)
32+
elif operation == Operations.OPERATION_GET_KEY:
33+
response = get_key(json_key_attributes_dict, pin, version)
34+
else:
35+
# Throw exception on unsupported operation.
36+
#
37+
raise Exception('Unsupported operation ' + operation)
38+
39+
# The response should be a base64 url encoded JSON expected by the control plane.
40+
#
41+
serialized_json_response = json.dumps(response.__dict__).encode("utf-8")
42+
return utils.urlsafe_b64encode_as_str(serialized_json_response)
43+
44+
def get_key(json_key_attributes_dict, pin, version):
45+
"""
46+
Call in to the custom key store module to get the key.
47+
"""
48+
return custom2.get_key(json_key_attributes_dict, pin, version)
49+
50+
def wrap_key(request, json_key_attributes_dict, pin, version):
51+
"""
52+
Call in to the custom key store module to encrypt.
53+
"""
54+
return custom2.encrypt(request, json_key_attributes_dict, pin, version)
55+
56+
def unwrap_key(request, json_key_attributes_dict, pin, version):
57+
"""
58+
Call in to the custom key store module to decrypt.
59+
"""
60+
return custom2.decrypt(request, json_key_attributes_dict, pin, version)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Definition of all the constants
2+
#
3+
class ConfigurationConstants(object):
4+
"""
5+
File where the application configurations are available
6+
"""
7+
CONFIG_HSM_SETTINGS_FILE = "configuration.ini"
8+
9+
"""
10+
Configuration section name where environment variables
11+
to be loaded before application execution are defined.
12+
"""
13+
CONFIG_SECTION_ENVIRONMENT_VARIABLE = "EnvironmentVariables"
14+
15+
CONFIG_SECTION_PKCS11_CONFIGURATION = "PKCS11Configuration"
16+
17+
CONFIG_KEY_PKCS11_MODULE_PATH = "PKCS11_MODULE_PATH"
18+
19+
class Operations(object):
20+
"""
21+
Operations supported by the application. These are the operations
22+
that the Big Data Cluster control plane will invoke.
23+
"""
24+
OPERATION_ENCRYPT='encrypt'
25+
OPERATION_DECRYPT='decrypt'
26+
OPERATION_GET_KEY='getKey'
27+
28+
class CryptoConstants(object):
29+
"""
30+
General constants for cryptography
31+
"""
32+
KTY_RSA="RSA"
33+
WRAP_RSA_OAEP="RSA-OAEP"
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Placeholder for adding logic specific to application
2+
# and backend key store.
3+
#
4+
import os
5+
import json
6+
from Crypto.Cipher import PKCS1_OAEP
7+
from Crypto.PublicKey import RSA
8+
from Crypto.Hash import SHA1
9+
import sys
10+
# Append the current application path to sys path to be able to resolve local modules.
11+
#
12+
sys.path.append('.')
13+
sys.path.append('./model')
14+
from constants import ConfigurationConstants, Operations, CryptoConstants
15+
import utils
16+
from json_objects import EncryptDecryptRequest, JsonWebKeyResponse, EncryptDecryptResponse
17+
18+
19+
def decrypt(request, json_key_attributes_dict, pin, version):
20+
"""
21+
This method will be called by the application entry point
22+
for decrypting the payload.
23+
request.value has the plaintext payload
24+
request.alg contains the padding algorithm for encryption.
25+
"""
26+
key_name = json_key_attributes_dict["keyname"]
27+
file_name = '{}.pem'.format(key_name)
28+
# Decode the base64 url to get the bytes.
29+
with open(file_name, 'r') as key_file:
30+
key = RSA.import_key(key_file.read())
31+
if request.alg == CryptoConstants.WRAP_RSA_OAEP:
32+
cipher_algo = PKCS1_OAEP.new(key, hashAlgo = SHA1)
33+
plain_text = cipher_algo.decrypt(request.value)
34+
response = EncryptDecryptResponse(plain_text)
35+
return response
36+
37+
38+
def encrypt(request, json_key_attributes_dict, pin, version):
39+
"""
40+
This method will be called by the application entry point
41+
for encrypting the payload.
42+
request.value has the plaintext payload
43+
request.alg contains the padding algorithm for encryption.
44+
"""
45+
key_name = json_key_attributes_dict["keyname"]
46+
file_name = '{}.pem'.format(key_name)
47+
with open(file_name, 'r') as key_file:
48+
key = RSA.import_key(key_file.read())
49+
if request.alg == CryptoConstants.WRAP_RSA_OAEP:
50+
cipher_algo = PKCS1_OAEP.new(key)
51+
cipher_text = cipher_algo.encrypt(request.value)
52+
response = EncryptDecryptResponse(cipher_text)
53+
return response
54+
55+
def get_key(json_key_attributes_dict, pin, version):
56+
key_name = json_key_attributes_dict["keyname"]
57+
file_name = '{}.pem'.format(key_name)
58+
with open(file_name, 'r') as key_file:
59+
key = RSA.import_key(key_file.read())
60+
jwk = JsonWebKeyResponse(key.n, key.e)
61+
return jwk
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Placeholder for adding logic specific to application
2+
# and backend key store.
3+
#
4+
import os
5+
import json
6+
from Crypto.Cipher import PKCS1_OAEP
7+
from Crypto.PublicKey import RSA
8+
from Crypto.Hash import SHA1
9+
import sys
10+
import hvac
11+
12+
# Append the current application path to sys path to be able to resolve local modules.
13+
#
14+
sys.path.append('.')
15+
sys.path.append('./model')
16+
from constants import ConfigurationConstants, Operations, CryptoConstants
17+
import utils
18+
from json_objects import EncryptDecryptRequest, JsonWebKeyResponse, EncryptDecryptResponse
19+
20+
def decrypt(request, json_key_attributes_dict, pin, version):
21+
"""
22+
This method will be called by the application entry point
23+
for decrypting the payload.
24+
request.value has the plaintext payload
25+
request.alg contains the padding algorithm for encryption.
26+
"""
27+
key_path = json_key_attributes_dict["keypath"]
28+
vault_url = json_key_attributes_dict["vaulturl"]
29+
key_name = json_key_attributes_dict["keyname"]
30+
hvac_client = hvac.Client(
31+
url=vault_url,
32+
token=pin
33+
)
34+
read_response = hvac_client.secrets.kv.read_secret_version(path=key_path)
35+
rsa_key_pem = read_response['data']['data'][key_name]
36+
37+
key = RSA.import_key(rsa_key_pem)
38+
if request.alg == CryptoConstants.WRAP_RSA_OAEP:
39+
cipher_algo = PKCS1_OAEP.new(key, hashAlgo = SHA1)
40+
plain_text = cipher_algo.decrypt(request.value)
41+
response = EncryptDecryptResponse(plain_text)
42+
return response
43+
44+
45+
def encrypt(request, json_key_attributes_dict, pin, version):
46+
"""
47+
This method will be called by the application entry point
48+
for encrypting the payload.
49+
request.value has the plaintext payload
50+
request.alg contains the padding algorithm for encryption.
51+
"""
52+
key_path = json_key_attributes_dict["keypath"]
53+
vault_url = json_key_attributes_dict["vaulturl"]
54+
key_name = json_key_attributes_dict["keyname"]
55+
hvac_client = hvac.Client(
56+
url=vault_url,
57+
token=pin
58+
)
59+
hvac_client = hvac.Client(
60+
url=vault_url,
61+
token=pin
62+
)
63+
read_response = hvac_client.secrets.kv.read_secret_version(path=key_path)
64+
rsa_key_pem = read_response['data']['data'][key_name]
65+
66+
key = RSA.import_key(rsa_key_pem)
67+
if request.alg == CryptoConstants.WRAP_RSA_OAEP:
68+
cipher_algo = PKCS1_OAEP.new(key)
69+
cipher_text = cipher_algo.encrypt(request.value)
70+
response = EncryptDecryptResponse(cipher_text)
71+
return response
72+
73+
def get_key(json_key_attributes_dict, pin, version):
74+
key_path = json_key_attributes_dict["keypath"]
75+
vault_url = json_key_attributes_dict["vaulturl"]
76+
key_name = json_key_attributes_dict["keyname"]
77+
hvac_client = hvac.Client(
78+
url=vault_url,
79+
token=pin
80+
)
81+
hvac_client = hvac.Client(
82+
url=vault_url,
83+
token=pin
84+
)
85+
read_response = hvac_client.secrets.kv.read_secret_version(path=key_path)
86+
rsa_key_pem = read_response['data']['data'][key_name]
87+
88+
key = RSA.import_key(rsa_key_pem)
89+
jwk = JsonWebKeyResponse(key.n, key.e)
90+
return jwk
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Contains the JSON objects for the application.
2+
#
3+
import sys
4+
import os
5+
sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir))
6+
import utils
7+
from constants import CryptoConstants
8+
9+
class EncryptDecryptRequest(object):
10+
"""
11+
Represents the encryption and decryption request
12+
"""
13+
def __init__(self, value, alg):
14+
self.value = utils.urlsafe_base64decode(value)
15+
self.alg = alg
16+
17+
class EncryptDecryptResponse(object):
18+
"""
19+
Represents the encryption and decryption response
20+
"""
21+
def __init__(self, value):
22+
self.value = utils.urlsafe_b64encode_as_str(value)
23+
24+
class JsonWebKeyResponse(object):
25+
"""
26+
Represents the getKey operation response
27+
"""
28+
def __init__(self, modulus, exponent):
29+
self.n = utils.urlsafe_b64encode_as_str(utils._int_to_bytes(modulus))
30+
self.e = utils.urlsafe_b64encode_as_str(utils._int_to_bytes(exponent))
31+
self.kty = CryptoConstants.KTY_RSA
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pycrypto==2.6.1
2+
pycryptodome==3.10.1
3+
cryptography==3.2.1
4+
hvac==0.10.11
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: encryption
2+
version: v1
3+
runtime: kms-plugin-python
4+
src: ./app.py
5+
entrypoint: handler
6+
replicas: 1
7+
poolsize: 1
8+
inputs:
9+
operation: str
10+
payload: str
11+
pin: str
12+
key_attributes: str
13+
version: str
14+
output:
15+
result: str
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Definition of all the constants
2+
import codecs
3+
import base64
4+
5+
def urlsafe_base64decode(value):
6+
"""
7+
urlsafe_b64decode without padding
8+
"""
9+
# Python requires padding even for base64 URL encoded
10+
# strings. Compute the required padding and append.
11+
pad = '=' * (4 - (len(value) % 3))
12+
return base64.urlsafe_b64decode(value + pad)
13+
14+
def urlsafe_base64decode_as_str(value):
15+
"""
16+
urlsafe_b64decode without padding. Returns result as UTF-8 encoded string.
17+
"""
18+
return urlsafe_base64decode(value).decode("utf-8")
19+
20+
def urlsafe_b64encode_as_str(value):
21+
"""
22+
base64 url safe encoding which returns result as UTF-8 encoded string
23+
"""
24+
return base64.urlsafe_b64encode(value).decode("utf-8")
25+
26+
def _int_to_bytes(i):
27+
"""
28+
Converts the given int to the big-endian bytes
29+
"""
30+
h = hex(i)
31+
if len(h) > 1 and h[0:2] == "0x":
32+
h = h[2:]
33+
34+
# need to strip L in python 2.x
35+
h = h.strip("L")
36+
37+
if len(h) % 2:
38+
h = "0" + h
39+
return codecs.decode(h, "hex")

0 commit comments

Comments
 (0)