Skip to content

Commit f0a6f55

Browse files
Enable AES-GCM and AES-CTR
Fixes #196.
1 parent 2c03610 commit f0a6f55

File tree

4 files changed

+217
-1
lines changed

4 files changed

+217
-1
lines changed

pkcs11/_pkcs11.pxd

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,18 @@ cdef extern from '../extern/cryptoki.h':
255255
CK_ULONG ulPublicDataLen
256256
CK_BYTE *pPublicData
257257

258+
ctypedef struct CK_AES_CTR_PARAMS:
259+
CK_ULONG ulCounterBits
260+
CK_BYTE[16] cb
261+
262+
ctypedef struct CK_GCM_PARAMS:
263+
CK_BYTE *pIv
264+
CK_ULONG ulIvLen
265+
CK_ULONG ulIvBits
266+
CK_BYTE *pAAD
267+
CK_ULONG ulAADLen
268+
CK_ULONG ulTagBits
269+
258270
ctypedef struct CK_KEY_DERIVATION_STRING_DATA:
259271
CK_BYTE *pData
260272
CK_ULONG ulLen

pkcs11/_pkcs11.pyx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,19 @@ cdef class MechanismWithParam:
142142
"""The mechanism."""
143143
cdef void *param
144144
"""Reference to a pointer we might need to free."""
145+
cdef object _python_param
146+
"""
147+
Hold a reference to the original parameter object so it doesn't get
148+
GC'd before this one.
149+
"""
145150

146151
def __cinit__(self, *args):
147152
self.data = <CK_MECHANISM *> PyMem_Malloc(sizeof(CK_MECHANISM))
148153
self.param = NULL
154+
self._python_param = None
149155

150156
def __init__(self, key_type, mapping, mechanism=None, param=None):
157+
self._python_param = param
151158
if mechanism is None:
152159
try:
153160
mechanism = mapping[key_type]
@@ -166,6 +173,8 @@ cdef class MechanismWithParam:
166173
cdef CK_ECDH1_DERIVE_PARAMS *ecdh1_params
167174
cdef CK_KEY_DERIVATION_STRING_DATA *aes_ecb_params
168175
cdef CK_AES_CBC_ENCRYPT_DATA_PARAMS *aes_cbc_params
176+
cdef CK_GCM_PARAMS *gcm_params
177+
cdef CK_AES_CTR_PARAMS *aes_ctr_params
169178

170179
# Unpack mechanism parameters
171180

@@ -264,6 +273,31 @@ cdef class MechanismWithParam:
264273
aes_cbc_params.pData = <CK_BYTE *> data
265274
aes_cbc_params.length = <CK_ULONG> len(data)
266275

276+
elif mechanism == Mechanism.AES_GCM:
277+
paramlen = sizeof(CK_GCM_PARAMS)
278+
if not isinstance(param, GCMParams):
279+
raise TypeError
280+
self.param = gcm_params = <CK_GCM_PARAMS *> PyMem_Malloc(paramlen)
281+
gcm_params.pIv = <CK_BYTE *> param.nonce
282+
gcm_params.ulIvLen = <CK_ULONG> len(param.nonce)
283+
gcm_params.ulIvBits = <CK_ULONG> len(param.nonce) * 8
284+
if param.aad is not None:
285+
gcm_params.pAAD = <CK_BYTE *> param.aad
286+
gcm_params.ulAADLen = <CK_ULONG> len(param.aad)
287+
else:
288+
gcm_params.pAAD = NULL
289+
gcm_params.ulAADLen = 0
290+
gcm_params.ulTagBits = <CK_ULONG> param.tag_bits
291+
292+
elif mechanism == Mechanism.AES_CTR:
293+
paramlen = sizeof(CK_AES_CTR_PARAMS)
294+
self.param = aes_ctr_params = <CK_AES_CTR_PARAMS *> PyMem_Malloc(paramlen)
295+
# use a wrapper type to not break the forwards compat rule for params specified as `bytes`
296+
if not isinstance(param, CTRParams):
297+
raise TypeError
298+
aes_ctr_params.ulCounterBits = (16 - len(param.nonce)) * 8
299+
aes_ctr_params.cb = param.nonce + b"\x00" * (15 - len(param.nonce)) + b"\x01"
300+
267301
elif param is None:
268302
self.data.pParameter = NULL
269303
paramlen = 0

pkcs11/mechanisms.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from enum import IntEnum
22

3+
from pkcs11.exceptions import ArgumentsBad
4+
35

46
class KeyType(IntEnum):
57
"""
@@ -794,3 +796,22 @@ class MGF(IntEnum):
794796

795797
def __repr__(self):
796798
return "<MGF.%s>" % self.name
799+
800+
801+
class GCMParams:
802+
def __init__(self, nonce, aad=None, tag_bits=128):
803+
if len(nonce) > 12:
804+
raise ArgumentsBad("IV must be less than 12 bytes")
805+
self.nonce = nonce
806+
self.aad = aad
807+
self.tag_bits = tag_bits
808+
809+
810+
class CTRParams:
811+
def __init__(self, nonce):
812+
if len(nonce) >= 16:
813+
raise ArgumentsBad(
814+
f"{nonce.hex()} is too long to serve as a CTR nonce, must be 15 bytes or less "
815+
f"to leave room for the block counter."
816+
)
817+
self.nonce = nonce

tests/test_aes.py

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from parameterized import parameterized
66

77
import pkcs11
8-
from pkcs11 import Mechanism
8+
from pkcs11 import ArgumentsBad, CTRParams, GCMParams, Mechanism, PKCS11Error
99

1010
from . import FIXME, TestCase, requires
1111

@@ -462,3 +462,152 @@ def test_encrypt_with_key_derived_using_cbc_encrypt(
462462
text = self.key.decrypt(crypttext, mechanism_param=iv)
463463

464464
self.assertEqual(text, data)
465+
466+
@requires(Mechanism.AES_GCM)
467+
def test_encrypt_gcm(self):
468+
data = b"INPUT DATA"
469+
nonce = b"0" * 12
470+
471+
crypttext = self.key.encrypt(
472+
data, mechanism=Mechanism.AES_GCM, mechanism_param=GCMParams(nonce)
473+
)
474+
self.assertIsInstance(crypttext, bytes)
475+
self.assertNotEqual(data, crypttext)
476+
text = self.key.decrypt(
477+
crypttext, mechanism=Mechanism.AES_GCM, mechanism_param=GCMParams(nonce)
478+
)
479+
self.assertEqual(data, text)
480+
481+
def test_gcm_nonce_size_limit(self):
482+
def _inst():
483+
return GCMParams(nonce=b"0" * 13)
484+
485+
self.assertRaises(ArgumentsBad, _inst)
486+
487+
@requires(Mechanism.AES_GCM)
488+
def test_encrypt_gcm_with_aad(self):
489+
data = b"INPUT DATA"
490+
nonce = b"0" * 12
491+
492+
crypttext = self.key.encrypt(
493+
data, mechanism=Mechanism.AES_GCM, mechanism_param=GCMParams(nonce, b"foo")
494+
)
495+
self.assertIsInstance(crypttext, bytes)
496+
self.assertNotEqual(data, crypttext)
497+
text = self.key.decrypt(
498+
crypttext, mechanism=Mechanism.AES_GCM, mechanism_param=GCMParams(nonce, b"foo")
499+
)
500+
self.assertEqual(data, text)
501+
502+
@requires(Mechanism.AES_GCM)
503+
def test_encrypt_gcm_with_mismatching_nonces(self):
504+
data = b"INPUT DATA"
505+
nonce1 = b"0" * 12
506+
nonce2 = b"1" * 12
507+
508+
crypttext = self.key.encrypt(
509+
data, mechanism=Mechanism.AES_GCM, mechanism_param=GCMParams(nonce1, b"foo")
510+
)
511+
self.assertIsInstance(crypttext, bytes)
512+
self.assertNotEqual(data, crypttext)
513+
# This should be EncryptedDataInvalid, but in practice not all tokens support this
514+
with self.assertRaises(PKCS11Error):
515+
self.key.decrypt(
516+
crypttext, mechanism=Mechanism.AES_GCM, mechanism_param=GCMParams(nonce2, b"foo")
517+
)
518+
519+
@requires(Mechanism.AES_GCM)
520+
def test_encrypt_gcm_with_mismatching_aad(self):
521+
data = b"INPUT DATA"
522+
nonce = b"0" * 12
523+
524+
crypttext = self.key.encrypt(
525+
data, mechanism=Mechanism.AES_GCM, mechanism_param=GCMParams(nonce, b"foo")
526+
)
527+
self.assertIsInstance(crypttext, bytes)
528+
self.assertNotEqual(data, crypttext)
529+
with self.assertRaises(PKCS11Error):
530+
self.key.decrypt(
531+
crypttext, mechanism=Mechanism.AES_GCM, mechanism_param=GCMParams(nonce, b"bar")
532+
)
533+
534+
@requires(Mechanism.AES_GCM)
535+
def test_encrypt_gcm_with_custom_tag_length(self):
536+
data = b"INPUT DATA"
537+
nonce = b"0" * 12
538+
539+
crypttext = self.key.encrypt(
540+
data, mechanism=Mechanism.AES_GCM, mechanism_param=GCMParams(nonce, b"foo", 120)
541+
)
542+
self.assertIsInstance(crypttext, bytes)
543+
self.assertNotEqual(data, crypttext)
544+
text = self.key.decrypt(
545+
crypttext, mechanism=Mechanism.AES_GCM, mechanism_param=GCMParams(nonce, b"foo", 120)
546+
)
547+
self.assertEqual(data, text)
548+
549+
# This should be EncryptedDataInvalid, but in practice not all tokens support this
550+
with self.assertRaises(PKCS11Error):
551+
text = self.key.decrypt(
552+
crypttext,
553+
mechanism=Mechanism.AES_GCM,
554+
mechanism_param=GCMParams(nonce, b"foo", 128),
555+
)
556+
557+
@parameterized.expand(
558+
[
559+
(b""),
560+
(b"0" * 12),
561+
(b"0" * 15),
562+
]
563+
)
564+
@requires(Mechanism.AES_CTR)
565+
@FIXME.opencryptoki # opencryptoki incorrectly forces AES-CTR input to be padded
566+
def test_encrypt_ctr(self, nonce):
567+
data = b"INPUT DATA SEVERAL BLOCKS LONG SO THE COUNTER GOES UP A FEW TIMES" * 20
568+
569+
crypttext = self.key.encrypt(
570+
data, mechanism=Mechanism.AES_CTR, mechanism_param=CTRParams(nonce)
571+
)
572+
self.assertIsInstance(crypttext, bytes)
573+
self.assertNotEqual(data, crypttext)
574+
text = self.key.decrypt(
575+
crypttext, mechanism=Mechanism.AES_CTR, mechanism_param=CTRParams(nonce)
576+
)
577+
self.assertEqual(data, text)
578+
579+
@requires(Mechanism.AES_CTR)
580+
def test_encrypt_ctr_exactly_padded(self):
581+
# let's still verify the "restricted" AES-CTR supported by opencryptoki
582+
data = b"PADDED INPUT DATA TO MAKE OPENCRYPTOKI HAPPY" * 16
583+
nonce = b"0" * 15
584+
585+
crypttext = self.key.encrypt(
586+
data, mechanism=Mechanism.AES_CTR, mechanism_param=CTRParams(nonce)
587+
)
588+
self.assertIsInstance(crypttext, bytes)
589+
self.assertNotEqual(data, crypttext)
590+
text = self.key.decrypt(
591+
crypttext, mechanism=Mechanism.AES_CTR, mechanism_param=CTRParams(nonce)
592+
)
593+
self.assertEqual(data, text)
594+
595+
def test_ctr_nonce_size_limit(self):
596+
def _inst():
597+
return CTRParams(nonce=b"0" * 16)
598+
599+
self.assertRaises(ArgumentsBad, _inst)
600+
601+
@requires(Mechanism.AES_CTR)
602+
@FIXME.opencryptoki # opencryptoki incorrectly forces AES-CTR input to be padded
603+
def test_encrypt_ctr_nonce_mismatch(self):
604+
data = b"INPUT DATA SEVERAL BLOCKS LONG SO THE COUNTER GOES UP A FEW TIMES" * 20
605+
crypttext = self.key.encrypt(
606+
data, mechanism=Mechanism.AES_CTR, mechanism_param=CTRParams(b"0" * 12)
607+
)
608+
self.assertIsInstance(crypttext, bytes)
609+
self.assertNotEqual(data, crypttext)
610+
text = self.key.decrypt(
611+
crypttext, mechanism=Mechanism.AES_CTR, mechanism_param=CTRParams(b"1" * 12)
612+
)
613+
self.assertNotEqual(data, text)

0 commit comments

Comments
 (0)