test_write.doctest - tests for writing PSKC files
Copyright (C) 2014-2024 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(b'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
...
...
...
...