Skip to content

Commit 11c78f8

Browse files
committed
security: Add Windows CNG support
1 parent be15a06 commit 11c78f8

File tree

4 files changed

+291
-3
lines changed

4 files changed

+291
-3
lines changed

src/ndn/client_conf.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
from .transport.stream_socket import Face, UnixFace, TcpFace
2424
if sys.platform == 'darwin':
2525
from .security.tpm.tpm_osx_keychain import TpmOsxKeychain
26+
if sys.platform == 'win32':
27+
from .security.tpm.tpm_cng import TpmCng
2628

2729

2830
def read_client_conf():
@@ -91,6 +93,8 @@ def default_keychain(pib: str, tpm: str) -> Keychain:
9193
tpm = TpmFile(tpm_loc)
9294
elif tpm_schema == 'tpm-osxkeychain':
9395
tpm = TpmOsxKeychain()
96+
elif tpm_schema == 'tpm-cng':
97+
tpm = TpmCng()
9498
else:
9599
raise ValueError(f'Unrecognized tpm schema: {tpm}')
96100
if pib_schema == 'pib-sqlite3':

src/ndn/platform/windows.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,30 @@ class SockaddrUn(c.Structure):
3535
class Cng:
3636
__instance = None
3737

38+
STATUS_UNSUCCESSFUL = c.c_long(0xC0000001).value # -0x3FFFFFFF
39+
NTE_BAD_KEYSET = c.c_long(0x80090016).value
40+
NCRYPT_OVERWRITE_KEY_FLAG = c.c_long(0x00000080)
41+
BCRYPT_SHA256_ALGORITHM = c.c_wchar_p('SHA256')
42+
BCRYPT_ECDSA_P256_ALGORITHM = c.c_wchar_p('ECDSA_P256')
43+
BCRYPT_ECDSA_P384_ALGORITHM = c.c_wchar_p('ECDSA_P384')
44+
BCRYPT_ECDSA_P521_ALGORITHM = c.c_wchar_p('ECDSA_P521')
45+
NCRYPT_ECDSA_P256_ALGORITHM = BCRYPT_ECDSA_P256_ALGORITHM
46+
NCRYPT_ECDSA_P384_ALGORITHM = BCRYPT_ECDSA_P384_ALGORITHM
47+
NCRYPT_ECDSA_P521_ALGORITHM = BCRYPT_ECDSA_P521_ALGORITHM
48+
BCRYPT_OBJECT_LENGTH = c.c_wchar_p('ObjectLength')
49+
BCRYPT_HASH_LENGTH = c.c_wchar_p('HashDigestLength')
50+
MS_PLATFORM_KEY_STORAGE_PROVIDER = c.c_wchar_p('Microsoft Platform Crypto Provider')
51+
MS_KEY_STORAGE_PROVIDER = c.c_wchar_p('Microsoft Software Key Storage Provider')
52+
BCRYPT_ECCPUBLIC_BLOB = c.c_wchar_p('ECCPUBLICBLOB')
53+
BCRYPT_ECCPRIVATE_BLOB = c.c_wchar_p('ECCPRIVATEBLOB')
54+
55+
class BcryptEcckeyBlob(c.Structure):
56+
_fields_ = [("dw_magic", c.c_ulong), ("cb_key", c.c_ulong)]
57+
58+
@staticmethod
59+
def nt_success(status):
60+
return status >= 0
61+
3862
def __new__(cls):
3963
if Cng.__instance is None:
4064
Cng.__instance = object.__new__(cls)
@@ -46,8 +70,6 @@ def __init__(self):
4670
self.bcrypt = c.windll.bcrypt
4771
self.ncrypt = c.windll.ncrypt
4872

49-
# TODO: Finish Windows 10 CNG
50-
5173

5274
class Win32(Platform):
5375
def client_conf_paths(self):
@@ -140,3 +162,30 @@ async def open_unix_connection(self, path=None):
140162
transport, _ = await Win32._create_unix_connection(loop, lambda: protocol, path)
141163
writer = aio.StreamWriter(transport, protocol, reader, loop)
142164
return reader, writer
165+
166+
167+
class ReleaseGuard:
168+
def __init__(self):
169+
self.__dict__['_list'] = []
170+
171+
def __getattr__(self, idx):
172+
return self._list[idx]
173+
174+
def __setattr__(self, idx, value):
175+
self._list[idx] = value
176+
177+
def __iadd__(self, defer):
178+
self._list.append(defer)
179+
return self
180+
181+
def __enter__(self):
182+
if len(self._list) > 0:
183+
raise RuntimeError('Re-enter a ReleaseGuard')
184+
return self
185+
186+
def __exit__(self, exc_type, exc_val, exc_tb):
187+
self._list.reverse()
188+
for f in self._list:
189+
if f:
190+
f()
191+
return False

src/ndn/security/tpm/tpm_cng.py

Lines changed: 235 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,239 @@
1616
# limitations under the License.
1717
# -----------------------------------------------------------------------------
1818
import sys
19+
import ctypes as c
20+
import logging
21+
from typing import Tuple
22+
from Cryptodome.PublicKey import ECC, RSA
23+
from Cryptodome.Util.asn1 import DerSequence
24+
from ndn.encoding import FormalName, BinaryStr
25+
26+
from ...encoding import Signer, NonStrictName, SignatureType, KeyLocator, Name
27+
from .tpm import Tpm
1928
if sys.platform == 'win32':
20-
from ...platform.windows import Cng
29+
from ...platform.windows import Cng, ReleaseGuard
30+
31+
32+
class CngSigner(Signer):
33+
def __init__(self, key_name: NonStrictName, sig_len, key_type, h_key):
34+
self.h_key = h_key
35+
self.key_name = key_name
36+
if key_type == "RSA":
37+
self.key_type = SignatureType.SHA256_WITH_RSA
38+
self.sig_len = sig_len
39+
elif key_type == "ECDSA":
40+
self.key_type = SignatureType.SHA256_WITH_ECDSA
41+
self.sig_len = sig_len + 8
42+
else:
43+
raise ValueError(f'Unrecognized key type {key_type}')
44+
45+
def write_signature_info(self, signature_info):
46+
signature_info.signature_type = self.key_type
47+
signature_info.key_locator = KeyLocator()
48+
signature_info.key_locator.name = self.key_name
49+
50+
def get_signature_value_size(self):
51+
return self.sig_len
52+
53+
def __del__(self):
54+
Cng().ncrypt.NCryptFreeObject(self.h_key)
55+
56+
@staticmethod
57+
def _hash_contents(contents):
58+
cng = Cng()
59+
with ReleaseGuard() as defer:
60+
h_hash_alg = c.c_void_p()
61+
status = cng.bcrypt.BCryptOpenAlgorithmProvider(c.pointer(h_hash_alg), cng.BCRYPT_SHA256_ALGORITHM, 0, 0)
62+
if not cng.nt_success(status):
63+
raise OSError(f'Error {status} returned by BCryptOpenAlgorithmProvider')
64+
defer += lambda: cng.bcrypt.BCryptCloseAlgorithmProvider(h_hash_alg, 0)
65+
66+
cb_hash_obj = c.c_ulong(0)
67+
cb_data = c.c_ulong(0)
68+
status = cng.bcrypt.BCryptGetProperty(h_hash_alg, cng.BCRYPT_OBJECT_LENGTH, c.pointer(cb_hash_obj), 4,
69+
c.pointer(cb_data), 0)
70+
if not cng.nt_success(status):
71+
raise OSError(f'Error {status} returned by BCryptGetProperty')
72+
hash_obj = (c.c_byte * cb_hash_obj.value)()
73+
74+
cb_hash = c.c_ulong(0)
75+
status = cng.bcrypt.BCryptGetProperty(h_hash_alg, cng.BCRYPT_HASH_LENGTH, c.pointer(cb_hash), 4,
76+
c.pointer(cb_data), 0)
77+
if not cng.nt_success(status):
78+
raise OSError(f'Error {status} returned by BCryptGetProperty')
79+
hash_val = (c.c_byte * cb_hash.value)()
80+
81+
h_hash = c.c_void_p()
82+
status = cng.bcrypt.BCryptCreateHash(h_hash_alg, c.pointer(h_hash), hash_obj, cb_hash_obj,
83+
0, 0, 0)
84+
if not cng.nt_success(status):
85+
raise OSError(f'Error {status} returned by BCryptCreateHash')
86+
defer += lambda: cng.bcrypt.BCryptDestroyHash(h_hash)
87+
88+
for blk in contents:
89+
status = cng.bcrypt.BCryptHashData(h_hash, c.c_char_p(bytes(blk)), len(blk), 0)
90+
if not cng.nt_success(status):
91+
raise OSError(f'Error {status} returned by BCryptHashData')
92+
93+
status = cng.bcrypt.BCryptFinishHash(h_hash, hash_val, cb_hash, 0)
94+
if not cng.nt_success(status):
95+
raise OSError(f'Error {status} returned by BCryptFinishHash')
96+
97+
return hash_val, cb_hash
98+
99+
def write_signature_value(self, wire, contents) -> int:
100+
cng = Cng()
101+
hash_val, cb_hash = self._hash_contents(contents)
102+
103+
cb_signature = c.c_ulong()
104+
status = cng.ncrypt.NCryptSignHash(self.h_key, 0, hash_val, cb_hash, 0, 0,
105+
c.pointer(cb_signature), 0)
106+
if not cng.nt_success(status):
107+
raise OSError(f'Error {status} returned by NCryptSignHash')
108+
109+
signature = (c.c_ubyte * cb_signature.value)()
110+
status = cng.ncrypt.NCryptSignHash(self.h_key, 0, hash_val, cb_hash, signature, cb_signature,
111+
c.pointer(cb_signature), 0)
112+
if not cng.nt_success(status):
113+
raise OSError(f'Error {status} returned by NCryptSignHash')
114+
115+
if self.key_type == SignatureType.SHA256_WITH_ECDSA:
116+
der = DerSequence((int.from_bytes(signature[:cb_signature.value//2], 'big'),
117+
int.from_bytes(signature[cb_signature.value//2:], 'big'))).encode()
118+
else:
119+
der = bytes(signature)
120+
121+
real_len = len(der)
122+
wire[:real_len] = der
123+
return real_len
124+
125+
126+
class TpmCng(Tpm):
127+
def __init__(self):
128+
cng = Cng()
129+
self.h_prov = c.c_void_p()
130+
# Try TPM2.0 platform key storage
131+
status = cng.ncrypt.NCryptOpenStorageProvider(c.pointer(self.h_prov), cng.MS_PLATFORM_KEY_STORAGE_PROVIDER, 0)
132+
if not cng.nt_success(status):
133+
# If failed, try software key storage
134+
status = cng.ncrypt.NCryptOpenStorageProvider(c.pointer(self.h_prov), cng.MS_KEY_STORAGE_PROVIDER, 0)
135+
if not cng.nt_success(status):
136+
raise OSError(f'Error {status} returned by NCryptOpenStorageProvider', status)
137+
138+
def __del__(self):
139+
Cng().ncrypt.NCryptFreeObject(self.h_prov)
140+
141+
@staticmethod
142+
def _convert_key_format(key_bits, key_type: str):
143+
if key_type == 'rsa':
144+
raise NotImplementedError('RSA on CNG is not implemented yet')
145+
elif key_type == 'ec':
146+
cng = Cng()
147+
pubkey_stru = c.cast(key_bits, c.POINTER(cng.BcryptEcckeyBlob))[0]
148+
base_idx = c.sizeof(cng.BcryptEcckeyBlob)
149+
key_x = int.from_bytes(key_bits[base_idx:base_idx + pubkey_stru.cb_key], 'big')
150+
key_y = int.from_bytes(key_bits[base_idx + pubkey_stru.cb_key:], 'big')
151+
return ECC.construct(curve='P-256', point_x=key_x, point_y=key_y).export_key(format='DER')
152+
else:
153+
raise ValueError(f'Unsupported key type {key_type}')
154+
155+
def _get_key(self, key_label: str):
156+
cng = Cng()
157+
h_key = c.c_void_p()
158+
status = cng.ncrypt.NCryptOpenKey(self.h_prov, c.pointer(h_key), c.c_wchar_p(key_label), 0, 0)
159+
if not cng.nt_success(status):
160+
if status == cng.NTE_BAD_KEYSET:
161+
raise KeyError(f"Unable to find key with label {key_label}")
162+
else:
163+
raise OSError(f'Error {status} returned by NCryptOpenKey', status)
164+
165+
cb_property = c.c_ulong(4)
166+
sig_len = c.c_ulong()
167+
status = cng.ncrypt.NCryptGetProperty(h_key, c.c_wchar_p('SignatureLength'), c.pointer(sig_len), cb_property,
168+
c.pointer(cb_property), 0)
169+
if not cng.nt_success(status):
170+
raise OSError(f'Error {status} returned by NCryptGetProperty', status)
171+
172+
cb_property.value = 40
173+
key_type = (c.c_wchar * 20)()
174+
status = cng.ncrypt.NCryptGetProperty(h_key, c.c_wchar_p('AlgorithmName'), c.pointer(key_type), cb_property,
175+
c.pointer(cb_property), 0)
176+
if not cng.nt_success(status):
177+
raise OSError(f'Error {status} returned by NCryptGetProperty', status)
178+
179+
return key_type.value, sig_len.value, h_key
180+
181+
def get_signer(self, key_name: NonStrictName) -> Signer:
182+
name_hash = Name.to_bytes(key_name).hex()
183+
key_type, sig_len, h_key = self._get_key(name_hash)
184+
return CngSigner(key_name, sig_len, key_type, h_key)
185+
186+
def key_exist(self, key_name: FormalName) -> bool:
187+
try:
188+
name_hash = Name.to_bytes(key_name).hex()
189+
self._get_key(name_hash)
190+
return True
191+
except KeyError:
192+
return False
193+
194+
def delete_key(self, key_name: FormalName):
195+
name_hash = Name.to_bytes(key_name).hex()
196+
_, _, h_key = self._get_key(name_hash)
197+
Cng().ncrypt.NCryptDeleteKey(h_key, 0)
198+
199+
@staticmethod
200+
def _convert_pub_key_format(key_bits: BinaryStr, key_type: str):
201+
if key_type == 'rsa':
202+
return RSA.import_key(key_bits).export_key(format='DER')
203+
elif key_type == 'ec':
204+
xp = int.from_bytes(key_bits[1:33], 'big')
205+
yp = int.from_bytes(key_bits[33:], 'big')
206+
return ECC.construct(curve='P-256', point_x=xp, point_y=yp).export_key(format='DER')
207+
else:
208+
raise ValueError(f'Unsupported key type {key_type}')
209+
210+
def generate_key(self, id_name: FormalName, key_type: str = 'rsa', **kwargs) -> Tuple[FormalName, BinaryStr]:
211+
cng = Cng()
212+
with ReleaseGuard() as defer:
213+
logging.debug('Generating CNG Key %s' % key_type)
214+
key_name = self.construct_key_name(id_name, b'', key_id_type='random')
215+
name_hash = Name.to_bytes(key_name).hex()
216+
217+
if key_type == 'ec':
218+
algo = cng.NCRYPT_ECDSA_P256_ALGORITHM
219+
elif key_type == 'rsa':
220+
raise NotImplementedError('RSA on CNG is not implemented yet')
221+
else:
222+
raise ValueError(f'Unsupported key type {key_type}')
223+
224+
h_key = c.c_void_p()
225+
status = cng.ncrypt.NCryptCreatePersistedKey(self.h_prov, c.pointer(h_key), algo,
226+
c.c_wchar_p(name_hash), 0,
227+
cng.NCRYPT_OVERWRITE_KEY_FLAG)
228+
if not cng.nt_success(status):
229+
raise OSError(f'Error {status} returned by NCryptCreatePersistedKey', status)
230+
defer += lambda: cng.ncrypt.NCryptFreeObject(h_key)
231+
232+
status = cng.ncrypt.NCryptFinalizeKey(h_key, 0)
233+
if not cng.nt_success(status):
234+
raise OSError(f'Error {status} returned by NCryptFinalizeKey', status)
235+
236+
if key_type == 'ec':
237+
prop = cng.BCRYPT_ECCPUBLIC_BLOB
238+
elif key_type == 'rsa':
239+
raise NotImplementedError('RSA on CNG is not implemented yet')
240+
else:
241+
raise ValueError(f'Unsupported key type {key_type}')
242+
243+
cb_pubkey = c.c_ulong()
244+
status = cng.ncrypt.NCryptExportKey(h_key, 0, prop, 0, 0, 0, c.pointer(cb_pubkey), 0)
245+
if not cng.nt_success(status):
246+
raise OSError(f'Error {status} returned by NCryptExportKey', status)
247+
248+
pubkey_blob = (c.c_ubyte * cb_pubkey.value)()
249+
status = cng.ncrypt.NCryptExportKey(h_key, 0, prop, 0, pubkey_blob, cb_pubkey, c.pointer(cb_pubkey), 0)
250+
if not cng.nt_success(status):
251+
raise OSError(f'Error {status} returned by NCryptExportKey', status)
252+
253+
pub_key = self._convert_key_format(bytes(pubkey_blob), key_type)
254+
return key_name, bytes(pub_key)

src/ndn/security/tpm/tpm_osx_keychain.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class TpmOsxKeychain(Tpm):
8282
def _get_key(key_name: NonStrictName):
8383
sec = OsxSec()
8484
with ReleaseGuard() as g:
85+
# TODO: what about name convension?
8586
logging.debug('Get OSX Key %s' % Name.to_str(key_name))
8687
g.key_label = CFSTR(Name.to_str(key_name))
8788
g.query = ObjCInstance(cf.CFDictionaryCreateMutable(None, 6, cf.kCFTypeDictionaryKeyCallBacks, None))

0 commit comments

Comments
 (0)