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

opgp: add support for KDF PINs #325

Merged
merged 1 commit into from
Mar 31, 2020
Merged
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
opgp: add support for KDF PINs
Resolves #279.

Signed-off-by: Dag Heyman <dag@yubico.com>
  • Loading branch information
emilazy authored and dagheyman committed Mar 31, 2020
commit 4bea80dc4e258d1044effa53a2fc224a3ed90672
76 changes: 76 additions & 0 deletions ykman/opgp.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from cryptography import x509
from cryptography.utils import int_to_bytes, int_from_bytes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.serialization import (
Encoding, PrivateFormat, NoEncryption
)
Expand Down Expand Up @@ -120,6 +121,7 @@ class DO(IntEnum):
PW_STATUS = 0xc4
CARDHOLDER_CERTIFICATE = 0x7f21
ATT_CERTIFICATE = 0xfc
KDF = 0xf9


@unique
Expand Down Expand Up @@ -218,6 +220,70 @@ def _pack_tlvs(tlvs):
return Tlv(0x4d, key_slot.crt + _pack_tlvs(values))


@unique
class KdfAlgorithm(bytes, Enum):
NONE = b'\x00'
KDF_ITERSALTED_S2K = b'\x03'


@unique
class HashAlgorithm(bytes, Enum):
SHA256 = b'\x08'
SHA512 = b'\x0a'

def create_digest(self):
algorithm = {
self.SHA256: hashes.SHA256,
self.SHA512: hashes.SHA512,
}[self]
return hashes.Hash(algorithm(), default_backend())


class Kdf(object):
_fields = {
b'\x81': ('kdf_algorithm', KdfAlgorithm),
b'\x82': ('hash_algorithm', HashAlgorithm),
b'\x83': ('iteration_count', lambda data: struct.unpack('>I', data)[0]),
b'\x84': ('pw1_salt_bytes', bytes),
b'\x85': ('pw2_salt_bytes', bytes),
b'\x86': ('pw3_salt_bytes', bytes),
b'\x87': ('pw1_initial_hash', bytes),
b'\x88': ('pw3_initial_hash', bytes),
}

__slots__ = (name for name, _ in _fields.values())

def __init__(self, data):
for field_tag, (field_name, field_type) in self._fields.items():
tag, size = struct.unpack('cB', data[:2])
assert tag == field_tag
setattr(self, field_name, field_type(data[2:2+size]))
data = data[2+size:]

def process(self, pw, pin):
if self.kdf_algorithm != KdfAlgorithm.KDF_ITERSALTED_S2K:
raise ValueError('Unsupported KDF algorithm')
if pw == PW1:
salt = self.pw1_salt_bytes
elif pw == PW3:
salt = self.pw3_salt_bytes
else:
raise ValueError('Unsupported PIN type')
return self._itersalted_s2k(salt, pin)

def _itersalted_s2k(self, salt, pin):
data = salt + pin
digest = self.hash_algorithm.create_digest()
# Although the field is called "iteration count", it's actually
# the number of bytes to be passed to the hash function, which
# is called only once. Go figure!
data_count, trailing_bytes = divmod(self.iteration_count, len(data))
for _ in range(data_count):
digest.update(data)
digest.update(data[:trailing_bytes])
return digest.finalize()


class OpgpController(object):

def __init__(self, driver):
Expand Down Expand Up @@ -298,9 +364,19 @@ def reset(self):
self.send_apdu(0, INS.TERMINATE, 0, 0)
self.send_apdu(0, INS.ACTIVATE, 0, 0)

def _get_kdf(self):
data = self._get_data(DO.KDF)
if data == b'\x81\x01\x00':
return None
else:
return Kdf(data)

def _verify(self, pw, pin):
try:
pin = pin.encode('utf-8')
kdf = self._get_kdf()
if kdf:
pin = kdf.process(pw, pin)
self.send_apdu(0, INS.VERIFY, 0, pw, pin)
except APDUError:
pw_remaining = self.get_remaining_pin_tries()[pw-PW1]
Expand Down