test_write.doctest - tests for writing PSKC files Copyright (C) 2014-2022 Arthur de Jong This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA >>> from pskc import PSKC >>> import datetime >>> import os >>> import sys >>> import tempfile >>> from binascii import a2b_hex >>> from dateutil.tz import tzutc Build a PSKC structure. >>> pskc = PSKC() Add a key with all attributes set. >>> key = pskc.add_key(id='456', manufacturer='Manufacturer') >>> key.id = '123' >>> key.serial = '987654321' >>> key.model = 'Model' >>> key.issue_no = 2 >>> key.start_date = datetime.datetime(2006, 5, 1, 0, 0, tzinfo=tzutc()) >>> key.expiry_date = datetime.datetime(2014, 5, 31, 0, 0, tzinfo=tzutc()) >>> key.device_userid = 'uid=arthur, dc=arthurdejong, dc=org' >>> key.crypto_module = 'CyrptoId' >>> key.algorithm = 'urn:ietf:params:xml:ns:keyprov:pskc:hotp' >>> key.issuer = 'Issuer' >>> key.key_profile = 'key profile id' >>> key.key_reference = 'reference to some key' >>> key.friendly_name = 'a friendly key' >>> key.key_userid = 'cn=Arthur de Jong, dc=arthurdejong, dc=org' >>> key.algorithm_suite = 'Clubs' >>> key.challenge_encoding = 'DECIMAL' >>> key.challenge_min_length = 6 >>> key.challenge_max_length = 8 >>> key.challenge_check = True >>> key.response_encoding = 'DECIMAL' >>> key.response_length = 8 >>> key.response_check = False >>> key.counter = 0 >>> key.secret = a2b_hex('4e1790ba272406ba309c5a31') Add policy information and a PIN. >>> key.policy.key_usage.append('OTP') >>> key.policy.key_usage.append(key.policy.KEY_USE_VERIFY) >>> key.policy.start_date = datetime.datetime(2008, 5, 1, 0, 0, tzinfo=tzutc()) >>> key.policy.expiry_date = datetime.datetime(2012, 6, 13, 0, 0, tzinfo=tzutc()) >>> key.policy.number_of_transactions = 42 >>> key.policy.pin_key_id = 'pinID' >>> key.policy.pin_usage = 'Local' >>> key.policy.pin_max_failed_attempts = 3 >>> key.policy.pin_min_length = 4 >>> key.policy.pin_max_length = 4 >>> key.policy.pin_encoding = 'DECIMAL' >>> pin_key = pskc.add_key(id='pinID', secret='1234', ... algorithm='urn:ietf:params:xml:ns:keyprov:pskc:pin', ... response_encoding='DECIMAL', response_length=4) Add a second (TOTP) key: >>> key = pskc.add_key() >>> key.serial = key.id = '267469811' >>> key.start_date = datetime.datetime(2006, 5, 1, 0, 0) >>> key.expiry_date = datetime.datetime(2014, 5, 31, 0, 0) >>> key.algorithm = 'urn:ietf:params:xml:ns:keyprov:pskc:totp' >>> key.response_encoding = 'DECIMAL' >>> key.response_length = 6 >>> key.time_offset = 0 >>> key.time_interval = 30 >>> key.time_drift = 6 >>> key.secret = a2b_hex('4e1790ba272406ba309c5a31') Write the PSKC file (use temporary file to test passing file name as argument). >>> f = tempfile.NamedTemporaryFile() >>> pskc.write(f.name) >>> with open(f.name, 'r') as r: ... x = sys.stdout.write(r.read()) #doctest: +REPORT_UDIFF Manufacturer 987654321 Model 2 2006-05-01T00:00:00Z 2014-05-31T00:00:00Z uid=arthur, dc=arthurdejong, dc=org CyrptoId Issuer Clubs key profile id reference to some key a friendly key TheQuickBrownFox 0 cn=Arthur de Jong, dc=arthurdejong, dc=org 2008-05-01T00:00:00Z 2012-06-13T00:00:00Z OTP Verify 42 MTIzNA== 267469811 2006-05-01T00:00:00 2014-05-31T00:00:00 TheQuickBrownFox 0 30 6 Read an encrypted PSKC file and write it out as an unencrypted file. >>> pskc = PSKC('tests/encryption/kw-aes128.pskcxml') >>> pskc.encryption.key = a2b_hex('000102030405060708090a0b0c0d0e0f') >>> pskc.encryption.remove_encryption() >>> pskc.write(sys.stdout) #doctest: +REPORT_UDIFF ABEiM0RVZneImaq7zN3u/w== Read an encrypted PSKC file and write it out as-is. This does not require providing the encryption key. >>> pskc = PSKC('tests/rfc6030/figure6.pskcxml') >>> pskc.write(sys.stdout) #doctest: +ELLIPSIS +REPORT_UDIFF Pre-shared-key ESIzRFVmd4iZABEiM0RVZgKn6WjLaTC1sbeBMSvIhRejN9vJa2BOlSaMrR7I5wSX Manufacturer 987654321 CM_ID_001 Issuer AAECAwQFBgcICQoLDA0OD+cIHItlB3Wra1DUpxVvOx2lef1VmNPCMl8jwZqIUqGv Su+NvtQfmvfJzF6bmQiJqoLRExc= 0 Read a legacy encrypted PSKC file and write it out as-is. This should convert the format to RFC 6030 format as best it can. Note that this does not include a MAC key (but does include a MAC algorithm because the MAC key is not specified and we assume to use the encryption key as MAC key). >>> pskc = PSKC('tests/draft-hoyer-keyprov-portable-symmetric-key-container-01/password-encrypted.pskcxml') >>> pskc.write(sys.stdout) #doctest: +ELLIPSIS +REPORT_UDIFF y6TzckeLRQw= 999 16 Token Manufacturer 98765432187 2008-01-01T00:00:00 Credential Issuer MySecondToken F/CY93NYc/SvmxT3oB6PzG7p6zpG92/t hN793ZE7GM6yCM6gz9OKNRzibhg= VVBYqRF1QSpetvIB2vBAzw== 6clqJvT9l0xIZtWSch2t6zr0IwU= If we decrypt the file the MAC key will be included in encrypted form. >>> pskc.encryption.derive_key('qwerty') >>> pskc.write(sys.stdout) #doctest: +ELLIPSIS +REPORT_UDIFF y6TzckeLRQw= 999 16 ... ... Set up an encrypted PSKC file and generate a pre-shared key for it. >>> pskc = PSKC() >>> key = pskc.add_key( ... id='1', serial='123456', secret=b'1234', counter=42) >>> pskc.encryption.setup_preshared_key( ... algorithm='aes128-cbc', ... key=a2b_hex('12345678901234567890123456789012'), ... key_name='Pre-shared KEY', fields = ['secret', 'counter']) >>> f = tempfile.NamedTemporaryFile() >>> pskc.write(f.name) >>> with open(f.name, 'r') as r: ... x = sys.stdout.write(r.read()) #doctest: +ELLIPSIS +REPORT_UDIFF Pre-shared KEY ... 123456 ... ... ... ... Read the generated file back in and verify that it matches the original data. >>> newpskc = PSKC(f.name) >>> newpskc.encryption.algorithm == pskc.encryption.algorithm True >>> newpskc.encryption.key = pskc.encryption.key >>> all(newkey.check() for newkey in newpskc.keys) True >>> key = pskc.keys[0] >>> newkey = newpskc.keys[0] >>> newkey.secret == key.secret True >>> newkey.counter == key.counter True Use PBKDF2 to derive a key instead of using a pre-shared key. >>> pskc = PSKC() >>> key = pskc.add_key( ... id='1', serial='123456', secret=b'1234', counter=42) >>> pskc.encryption.setup_pbkdf2( ... 'passphrase', key_name='Passphrase') >>> pskc.write(f.name) >>> with open(f.name, 'r') as r: ... x = sys.stdout.write(r.read()) #doctest: +ELLIPSIS +REPORT_UDIFF ... ... 16 Passphrase ... 123456 ... ... 42 Read the generated file back in and verify that it matches the original data. >>> newpskc = PSKC(f.name) >>> newpskc.encryption.algorithm == pskc.encryption.algorithm True >>> newpskc.encryption.derive_key('passphrase') >>> all(newkey.check() for newkey in newpskc.keys) True >>> key = pskc.keys[0] >>> newkey = newpskc.keys[0] >>> newkey.secret == key.secret True >>> newkey.counter == key.counter True Test encryption and decryption of the generated file to test encryption/ decryption combinations. >>> def test_algorithm(algorithm): ... f = tempfile.NamedTemporaryFile() ... pskc1 = PSKC() ... pskc1.add_key(secret=os.urandom(16)) ... pskc1.encryption.setup_preshared_key(algorithm=algorithm) ... pskc1.write(f.name) ... pskc2 = PSKC(f.name) ... pskc2.encryption.key = pskc1.encryption.key ... assert pskc1.keys[0].secret == pskc2.keys[0].secret ... return (pskc1, pskc2) >>> pskc1, pskc2 = test_algorithm('aes192-cbc') >>> len(pskc1.encryption.key) 24 >>> pskc1, pskc2 = test_algorithm('aes256-cbc') >>> len(pskc1.encryption.key) 32 >>> pskc1, pskc2 = test_algorithm('tripledes-cbc') >>> len(pskc1.encryption.key) 24 >>> pskc1, pskc2 = test_algorithm('kw-aes128') >>> len(pskc1.encryption.key) 16 >>> pskc1, pskc2 = test_algorithm('kw-aes192') >>> len(pskc1.encryption.key) 24 >>> pskc1, pskc2 = test_algorithm('kw-aes256') >>> len(pskc1.encryption.key) 32 >>> pskc1, pskc2 = test_algorithm('kw-tripledes') >>> len(pskc1.encryption.key) 24 Not having a key and trying encryption will fail. >>> f = tempfile.NamedTemporaryFile() >>> pskc = PSKC() >>> key = pskc.add_key(secret='1234') >>> pskc.encryption.setup_preshared_key() >>> pskc.encryption.key = None >>> pskc.write(f.name) Traceback (most recent call last): ... EncryptionError: No key available >>> pskc = PSKC() >>> key = pskc.add_key(secret='1234') >>> pskc.encryption.setup_preshared_key() >>> pskc.encryption.algorithm = None >>> pskc.write(f.name) Traceback (most recent call last): ... EncryptionError: No algorithm specified >>> pskc = PSKC() >>> key = pskc.add_key(secret='1234') >>> pskc.encryption.setup_preshared_key() >>> pskc.encryption.algorithm = 'FOOBAR' >>> pskc.write(f.name) Traceback (most recent call last): ... DecryptionError: Unsupported algorithm: 'FOOBAR' >>> pskc = PSKC() >>> key = pskc.add_key(secret='1234') >>> pskc.encryption.setup_preshared_key() >>> pskc.encryption.algorithm = 'aes256-cbc' >>> pskc.write(f.name) Traceback (most recent call last): ... EncryptionError: Invalid key length Setting up something else than PBKDF2 as derivation algorithm will just result in an empty KeyDerivation element. >>> pskc = PSKC() >>> key = pskc.add_key(secret='1234') >>> pskc.encryption.setup_pbkdf2('qwerty') >>> pskc.encryption.derivation.algorithm = 'unknown' >>> pskc.write(sys.stdout) #doctest: +ELLIPSIS +REPORT_UDIFF ... ... We can make the PKKDF2 salt have to be transmitted out-of-bounds: >>> pskc = PSKC() >>> key = pskc.add_key(secret='1234') >>> pskc.encryption.setup_pbkdf2('qwerty', salt=a2b_hex('1234567890')) >>> pskc.encryption.derivation.pbkdf2_salt = None >>> pskc.write(sys.stdout) #doctest: +ELLIPSIS +REPORT_UDIFF ... 16 ... ... Write a PSKC file with two keys in onde KeyPackage section. Note that this is not allowed in the RFC 6030 schema. Note that device properties that are set on one key end up being applied to both keys. >>> pskc = PSKC() >>> device = pskc.add_device(manufacturer='TokenVendorAcme') >>> key = device.add_key(id='1', serial='123456', secret='1234', counter=42) >>> key = device.add_key(id='pin0', secret='5678') >>> pskc.write(sys.stdout) #doctest: +ELLIPSIS +REPORT_UDIFF TokenVendorAcme 123456 MTIzNA== 42 NTY3OA== If we specify a global IV it will be used for all encrypted values but will be not be written as a global IV in the PSKC file because RFC 6030 does not specify this (and re-using an IV is a bad idea). >>> pskc = PSKC() >>> key = pskc.add_key(secret='1234') >>> pskc.encryption.setup_preshared_key(key=a2b_hex('12345678901234567890123456789012')) >>> pskc.encryption.iv = a2b_hex('000102030405060708090a0b0c0d0e0f') >>> pskc.write(sys.stdout) #doctest: +ELLIPSIS +REPORT_UDIFF AAECAwQFBgcICQoLDA0OD... AAECAwQFBgcICQoLDA0OD... ... Check that we can add secrets as bytearray values >>> pskc = PSKC() >>> key = pskc.add_key( ... id='1', serial='123456', secret=bytearray(b'1234'), counter=42) >>> pskc.encryption.setup_preshared_key( ... algorithm='aes128-cbc', ... key=bytearray(a2b_hex('12345678901234567890123456789012')), ... key_name='Pre-shared KEY', fields = ['secret', 'counter']) >>> f = tempfile.NamedTemporaryFile() >>> pskc.write(f.name) >>> with open(f.name, 'r') as r: ... x = sys.stdout.write(r.read()) #doctest: +ELLIPSIS +REPORT_UDIFF Pre-shared KEY ... 123456 ... ... ... ...