Skip to content

Commit ebda236

Browse files
authored
Document HSM usage for members identity and encryption keys (#1884)
1 parent 33c51fc commit ebda236

File tree

6 files changed

+210
-5
lines changed

6 files changed

+210
-5
lines changed

doc/members/adding_member.rst

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ Adding New Members
33

44
It is possible for existing members to add new members to the consortium after a CCF network has been started.
55

6-
.. note:: The maximum number of active consortium members at any given time is 255.
6+
.. note:: The maximum number of allowed active recovery members (i.e. those with a recovery share) at any given time is 255.
77

88
Generating Member Keys and Certificates
99
---------------------------------------
1010

11+
.. note:: See :doc:`/members/hsm_keys` for a guide on how to used member keys and certificate store in Azure Key Vault.
12+
1113
First, the identity and encryption public and private key pairs of the new member should be created.
1214

1315
The ``keygenerator.sh`` script can be used to generate the member’s certificate and associated private key as well as their encryption public and private keys.
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"issuerParameters": {
3+
"certificateTransparency": null,
4+
"name": "Self"
5+
},
6+
"keyProperties": {
7+
"curve": "P-384",
8+
"exportable": true,
9+
"keyType": "EC",
10+
"reuseKey": true
11+
},
12+
"lifetimeActions": [
13+
{
14+
"action": {
15+
"actionType": "AutoRenew"
16+
},
17+
"trigger": {
18+
"daysBeforeExpiry": 90
19+
}
20+
}
21+
],
22+
"secretProperties": {
23+
"contentType": "application/x-pkcs12"
24+
},
25+
"x509CertificateProperties": {
26+
"keyUsage": ["digitalSignature"],
27+
"subject": "CN=Member",
28+
"validityInMonths": 12
29+
}
30+
}

doc/members/hsm_keys.rst

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
Using Member Keys Stored in HSM
2+
===============================
3+
4+
This page explains how members' identity certificates and encryption keys stored in an `HSM <https://en.wikipedia.org/wiki/Hardware_security_module>`_ can be used with CCF. The following guide describes the usage of `Azure Key Vault <https://azure.microsoft.com/en-gb/services/key-vault>`_
5+
6+
.. note::
7+
8+
It is assumed that CCF members already have access to an existing Azure Key Vault. See `here <https://docs.microsoft.com/en-us/azure/key-vault/secrets/quick-create-portal#create-a-vault>`_ for more details on how to create one. Using the `Azure CLI <https://docs.microsoft.com/en-us/cli/azure/install-azure-cli>`_, it is possible to check the list of available Key Vault instances:
9+
10+
.. code-block:: bash
11+
12+
$ az keyvault list
13+
# Outputs list of available vaults, including name
14+
$ export VAULT_NAME="<vault_name>"
15+
16+
Certificate and Key Generation
17+
------------------------------
18+
19+
Members' identity certificates should be generated on the `secp384r1` elliptic curve, using the `az keyvault certificate create <https://docs.microsoft.com/en-us/cli/azure/keyvault/certificate?view=azure-cli-latest#az_keyvault_certificate_create>`_ command, with the following ``identity_cert_policy_example.json`` policy:
20+
21+
.. include:: akv_identity_cert_policy.json
22+
:literal:
23+
24+
.. code-block:: bash
25+
26+
$ export IDENTITY_CERT_NAME="<identity-cert-name>"
27+
$ az keyvault certificate create --vault-name $VAULT_NAME -n $IDENTITY_CERT_NAME -p @identity_cert_policy_example.json
28+
# Outputs certificate details
29+
30+
# Corresponding private key is accessible at the same URL (substituting /certificate/ with /key/)
31+
$ az keyvault key show --vault-name $VAULT_NAME --name $IDENTITY_CERT_NAME
32+
# Outputs key information, including kid url
33+
34+
Members' encryption keys should be RSA 2048 keys, generated with the `az keyvault key create <https://docs.microsoft.com/en-us/cli/azure/keyvault/key?view=azure-cli-latest#az_keyvault_key_create>`_ command:
35+
36+
.. code-block:: bash
37+
38+
$ export ENCRYPTION_KEY_NAME="<encryption-key-name>"
39+
$ az keyvault key create --vault-name $VAULT_NAME --name $ENCRYPTION_KEY_NAME --kty RSA --ops decrypt
40+
# Outputs key details, including kid url
41+
42+
The identity certificate and public encryption key can be downloaded to a PEM file and be passed on to members to be registered in a CCF service as a trusted member identity (see :ref:`members/adding_member:Registering a New Member`). Alternatively, if the service has not yet been started, the public member identity can be passed on to operators and registered via the ``cchost --member-info`` option (see :ref:`operators/start_network:Starting the First Node`):
43+
44+
.. code-block:: bash
45+
46+
$ az keyvault certificate download --file $IDENTITY_CERT_NAME.pem --vault-name $VAULT_NAME --name $IDENTITY_CERT_NAME
47+
# Downloads PEM identity certificate
48+
49+
$ az keyvault key download --file $ENCRYPTION_KEY_NAME.pem --vault-name $VAULT_NAME --name $ENCRYPTION_KEY_NAME
50+
# Downloads PEM encryption public key
51+
52+
HTTP Request Signature
53+
----------------------
54+
55+
As the Azure CLI (``az keyvault ...``) does not currently support signing/verifying, it is required to use the `corresponding REST API <https://docs.microsoft.com/en-us/rest/api/keyvault/sign/sign>`_ instead. To do so, it is necessary to create a service principal that will be used for authentication:
56+
57+
.. code-block:: bash
58+
59+
$ export SP_NAME="<sp-name>"
60+
$ az ad sp create-for-rbac --name $SP_NAME
61+
# Returns client id (appId), client secret (password)
62+
63+
.. note:: To retrieve the service principal credentials after its creation, the credentials should be refreshed:
64+
65+
.. code-block:: bash
66+
67+
$ az ad sp credential reset --name <app_id>
68+
# Returns client id (appId), updated client secret (password)
69+
70+
Once created, the service principal should be given access to Key Vault in Azure. This can be done through the Azure Portal, under the "Access policies" setting of the vault. The service principal should be given access to the vault with "Sign" key permission. See `here <https://docs.microsoft.com/en-us/azure/key-vault/general/assign-access-policy-portal>`_ for more details.
71+
72+
Then, the following command should be run to retrieve an access token, replacing the values for ``<appid>``, ``<password>`` and ``<tenant>`` with the service principal credentials:
73+
74+
.. code-block:: bash
75+
76+
export AZ_TOKEN=$(curl -X POST -d "grant_type=client_credentials&client_id=<appid>&client_secret=<password>&resource=https://vault.azure.net" https://login.microsoftonline.com/<tenant>/oauth2/token | jq -r .access_token)
77+
78+
The member's identity key is now ready to be used for signing HTTP requests. The ``scurl.sh`` script can be used with the ``--print-digest-to-sign`` option to print the SHA384 to be signed as well as the required headers for HTTP signatures (following the scheme described `here <https://tools.ietf.org/html/draft-cavage-http-signatures-12>`_):
79+
80+
.. code-block:: bash
81+
82+
# First, retrieve the hash to be signed
83+
$ scurl.sh https://<ccf-node-address>/gov/<endpoint> -X [GET|POST] --cert $IDENTITY_CERT_NAME.pem --print-digest-to-sign
84+
Hash to sign: <hash_to_sign> # To be signed by AKV
85+
Request headers:
86+
-H 'Digest: SHA-256=...'
87+
-H 'Authorization: Signature keyId="...",signature_algorithm="hs2019",headers="(request-target) digest content-length",signature="<insert_base64_signature_here>"' # Replace signature with AKV signature here
88+
-H 'content-length: 0'
89+
90+
# Then, retrieve the kid url for the identity key
91+
$ export IDENTITY_AKV_KID=$(az keyvault key show --vault-name $VAULT_NAME --name $IDENTITY_CERT_NAME --query key.kid --output tsv)
92+
93+
# Then, sign the request hash to be signed (as output by scurl.sh --print-digest-to-sign)
94+
$ export base64url_signature=$(curl -s -X POST $IDENTITY_AKV_KID/sign?api-version=7.1 --data '{alg: "ES384", "value": "<hash_to_sign>"}' -H "Authorization: Bearer ${AZ_TOKEN}" -H "Content-Type: application/json" | jq -r .value)
95+
96+
.. note:: The signatures returned by AKV are returned as a `JWS signature <https://tools.ietf.org/html/rfc7518#section-3.4>`_ and encoded in `base64url <https://tools.ietf.org/html/rfc4648#section-5>`_ format and are not directly compatible with the signatures supported by CCF.
97+
98+
The `jws_to_der.py <https://github.com/microsoft/CCF/blob/master/doc/members/jws_to_der.py>`_ Python script can be used to convert a JWS signature generated by AKV to a DER signature compatible with CCF:
99+
100+
.. code-block:: bash
101+
102+
$ pip install pyasn1
103+
$ export ccf_signature=$(python3.8 jws_to_der.py $base64url_signature)
104+
105+
Finally, the signed HTTP request can be issued, using the request headers printed by ``scurl.sh --print-digest-to-sign``:
106+
107+
.. code-block:: bash
108+
109+
$ curl https://<ccf-node-address>/gov/<endpoint> -X [GET|POST] --cert $IDENTITY_CERT_NAME.pem \
110+
-H 'Digest: SHA-256=...' \
111+
-H 'Authorization: Signature keyId="...",signature_algorithm="hs2019",headers="(request-target) digest content-length",signature="$ccf_signature"' \
112+
-H 'content-length: <content-length>'
113+
114+
Recovery Share Decryption
115+
-------------------------
116+
117+
To retrieve their encrypted recovery share, a member should issue a signed HTTP request against the ``/gov/recovery_share`` endpoint (see :ref:`members/accept_recovery:Submitting Recovery Shares`). Signing the request will allow the member to authenticate themself to CCF (see :ref:`members/hsm_keys:HTTP Request Signature`).
118+
119+
The retrieved encrypted recovery share can be decrypted with the encryption key stored in Key Vault:
120+
121+
.. code-block::
122+
123+
$ az keyvault key decrypt --vault-name $VAULT_NAME --name $ENCRYPTION_KEY_NAME --algorithm RSA-OAEP-256 --value <base64_encrypted_share>
124+
# Outputs base64 decrypted share
125+
126+
The decrypted recovery share can then be submitted to the CCF recovered service (see :ref:`members/accept_recovery:Submitting Recovery Shares`).

doc/members/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Before creating a new CCF network, the identity of the initial member(s) of the
1515
accept_recovery
1616
common_member_operations
1717
adding_member
18+
hsm_keys
1819
member_rpc_api
1920

2021

doc/members/jws_to_der.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the Apache 2.0 License.
3+
import sys
4+
5+
from pyasn1.type.namedtype import NamedTypes, NamedType
6+
from pyasn1.type.univ import Integer, Sequence
7+
from pyasn1.codec.der.encoder import encode
8+
import base64
9+
10+
11+
class DERSignature(Sequence):
12+
componentType = NamedTypes(
13+
NamedType("r", Integer()),
14+
NamedType("s", Integer()),
15+
)
16+
17+
18+
if __name__ == "__main__":
19+
if len(sys.argv) != 2:
20+
print("Error: base64url signature should be specified as first argument")
21+
sys.exit(1)
22+
23+
jws_raw = base64.urlsafe_b64decode(sys.argv[1])
24+
jws_raw_len = len(jws_raw)
25+
26+
sig = DERSignature()
27+
sig["r"] = int.from_bytes(jws_raw[: int(jws_raw_len / 2)], byteorder="big")
28+
sig["s"] = int.from_bytes(jws_raw[-int(jws_raw_len / 2) :], byteorder="big")
29+
output_buf = encode(sig)
30+
31+
print(base64.b64encode(output_buf).decode())

python/utils/scurl.sh

+19-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ next_is_cert=false
1414

1515
url=$1
1616
command="post"
17+
is_print_digest_to_sign=false
1718

1819
fwd_args=()
1920
for item in "$@" ; do
@@ -64,14 +65,18 @@ for item in "$@" ; do
6465
continue
6566
fi
6667
fi
68+
if [ "$item" == "--print-digest-to-sign" ]; then
69+
is_print_digest_to_sign=true
70+
continue
71+
fi
6772
fwd_args+=("$item")
6873
done
6974

7075
if [ -z "$cert" ]; then
7176
echo "Error: No certificate found in arguments (--cert)"
7277
exit 1
7378
fi
74-
if [ -z "$privk" ]; then
79+
if [ -z "$privk" ] && [ "$is_print_digest_to_sign" == false ]; then
7580
echo "Error: No private key found in arguments (--key)"
7681
exit 1
7782
fi
@@ -108,12 +113,22 @@ content-length: $content_length"
108113
# https://tools.ietf.org/html/draft-cavage-http-signatures-12#appendix-E.2
109114
signature_algorithm="hs2019"
110115

111-
# Compute signature
112-
signed_raw=$(echo -n "$string_to_sign" | openssl dgst -sha384 -sign "$privk" | openssl base64 -A)
113-
114116
# Compute key ID
115117
key_id=$(openssl dgst -sha256 "$cert" | cut -d ' ' -f 2)
116118

119+
if [ "$is_print_digest_to_sign" == true ]; then
120+
hash_to_sign=$(echo -n "$string_to_sign" | openssl dgst -binary -sha384 | openssl base64 -A)
121+
echo "Hash to sign: $hash_to_sign"
122+
echo "Request headers:"
123+
echo "-H 'Digest: SHA-256=$req_digest'"
124+
echo "-H 'Authorization: Signature keyId=\"$key_id\",signature_algorithm=\"$signature_algorithm\",headers=\"(request-target) digest content-length\",signature=\"<insert_base64_signature_here>\"'"
125+
echo "${additional_curl_args[@]}"
126+
exit 0
127+
fi
128+
129+
# Compute signature
130+
signed_raw=$(echo -n "$string_to_sign" | openssl dgst -sha384 -sign "$privk" | openssl base64 -A)
131+
117132
curl \
118133
-H "Digest: SHA-256=$req_digest" \
119134
-H "Authorization: Signature keyId=\"$key_id\",signature_algorithm=\"$signature_algorithm\",headers=\"(request-target) digest content-length\",signature=\"$signed_raw\"" \

0 commit comments

Comments
 (0)