commit yubikey-manager for openSUSE:Factory
Hello community, here is the log from the commit of package yubikey-manager for openSUSE:Factory checked in at 2019-06-30 10:22:09 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/yubikey-manager (Old) and /work/SRC/openSUSE:Factory/.yubikey-manager.new.4615 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Package is "yubikey-manager" Sun Jun 30 10:22:09 2019 rev:11 rq:712543 version:3.0.0 Changes: -------- --- /work/SRC/openSUSE:Factory/yubikey-manager/yubikey-manager.changes 2019-06-18 14:56:18.253404922 +0200 +++ /work/SRC/openSUSE:Factory/.yubikey-manager.new.4615/yubikey-manager.changes 2019-06-30 10:22:26.987708605 +0200 @@ -1,0 +2,10 @@ +Sat Jun 29 20:32:49 UTC 2019 - Karol Babioch <kbabioch@suse.com> + +- Version 3.0.0 (released 2019-06-24) + * Add support for new YubiKey Preview and lightning form factor + * FIDO: Support for credential management + * OpenPGP: Support for OpenPGP attestation, cardholder certificates and + cached touch policies + * OTP: Add flag for using numeric keypad when sending digits + +------------------------------------------------------------------- Old: ---- yubikey-manager-2.1.1.tar.gz yubikey-manager-2.1.1.tar.gz.sig New: ---- yubikey-manager-3.0.0.tar.gz yubikey-manager-3.0.0.tar.gz.sig ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ yubikey-manager.spec ++++++ --- /var/tmp/diff_new_pack.t9CyQJ/_old 2019-06-30 10:22:27.447709320 +0200 +++ /var/tmp/diff_new_pack.t9CyQJ/_new 2019-06-30 10:22:27.451709326 +0200 @@ -17,7 +17,7 @@ Name: yubikey-manager -Version: 2.1.1 +Version: 3.0.0 Release: 0 Summary: Python 3 library and command line tool for configuring a YubiKey License: BSD-2-Clause ++++++ yubikey-manager-2.1.1.tar.gz -> yubikey-manager-3.0.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/NEWS new/yubikey-manager-3.0.0/NEWS --- old/yubikey-manager-2.1.1/NEWS 2019-05-28 09:04:43.000000000 +0200 +++ new/yubikey-manager-3.0.0/NEWS 2019-06-24 09:07:27.000000000 +0200 @@ -1,3 +1,9 @@ +* Version 3.0.0 (released 2019-06-24) + ** Add support for new YubiKey Preview and lightning form factor + ** FIDO: Support for credential management + ** OpenPGP: Support for OpenPGP attestation, cardholder certificates and cached touch policies + ** OTP: Add flag for using numeric keypad when sending digits + * Version 2.1.1 (released 2019-05-28) ** OTP: Add initial support for uploading Yubico OTP credentials to YubiCloud ** Don't automatically select the U2F applet on YubiKey NEO, it might be blocked by the OS diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/PKG-INFO new/yubikey-manager-3.0.0/PKG-INFO --- old/yubikey-manager-2.1.1/PKG-INFO 2019-05-28 10:05:25.000000000 +0200 +++ new/yubikey-manager-3.0.0/PKG-INFO 2019-06-24 09:12:28.000000000 +0200 @@ -1,13 +1,12 @@ -Metadata-Version: 1.2 +Metadata-Version: 1.1 Name: yubikey-manager -Version: 2.1.1 +Version: 3.0.0 Summary: Tool for managing your YubiKey configuration. Home-page: https://github.com/Yubico/yubikey-manager -Author: Dain Nilsson -Author-email: dain@yubico.com -Maintainer: Yubico Open Source Maintainers -Maintainer-email: ossmaint@yubico.com +Author: Yubico Open Source Maintainers +Author-email: ossmaint@yubico.com License: BSD 2 clause +Description-Content-Type: UNKNOWN Description: UNKNOWN Platform: UNKNOWN Classifier: License :: OSI Approved :: BSD License diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/man/ykman.1 new/yubikey-manager-3.0.0/man/ykman.1 --- old/yubikey-manager-2.1.1/man/ykman.1 2019-05-28 08:53:55.000000000 +0200 +++ new/yubikey-manager-3.0.0/man/ykman.1 2019-06-24 09:07:27.000000000 +0200 @@ -1,4 +1,4 @@ -.TH YKMAN "1" "May 2019" "ykman 2.1.1" "User Commands" +.TH YKMAN "1" "June 2019" "ykman 3.0.0" "User Commands" .SH NAME ykman \- YubiKey Manager (ykman) .SH SYNOPSIS Binary files old/yubikey-manager-2.1.1/test/__pycache__/__init__.cpython-36.pyc and new/yubikey-manager-3.0.0/test/__pycache__/__init__.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/__pycache__/test_device.cpython-36.pyc and new/yubikey-manager-3.0.0/test/__pycache__/test_device.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/__pycache__/test_external_libs.cpython-36.pyc and new/yubikey-manager-3.0.0/test/__pycache__/test_external_libs.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/__pycache__/test_oath.cpython-36.pyc and new/yubikey-manager-3.0.0/test/__pycache__/test_oath.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/__pycache__/test_piv.cpython-36.pyc and new/yubikey-manager-3.0.0/test/__pycache__/test_piv.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/__pycache__/test_scancodes.cpython-36.pyc and new/yubikey-manager-3.0.0/test/__pycache__/test_scancodes.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/__pycache__/test_util.cpython-36.pyc and new/yubikey-manager-3.0.0/test/__pycache__/test_util.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/__pycache__/util.cpython-36.pyc and new/yubikey-manager-3.0.0/test/__pycache__/util.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/__pycache__/__init__.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/__pycache__/__init__.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/__pycache__/test_cli_config.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/__pycache__/test_cli_config.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/__pycache__/test_cli_misc.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/__pycache__/test_cli_misc.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/__pycache__/test_cli_oath.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/__pycache__/test_cli_oath.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/__pycache__/test_cli_openpgp.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/__pycache__/test_cli_openpgp.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/__pycache__/test_cli_otp.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/__pycache__/test_cli_otp.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/__pycache__/test_fips_u2f_commands.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/__pycache__/test_fips_u2f_commands.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/__pycache__/test_interfaces.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/__pycache__/test_interfaces.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/__pycache__/test_piv.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/__pycache__/test_piv.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/__pycache__/util.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/__pycache__/util.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/cli_piv/__pycache__/__init__.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/cli_piv/__pycache__/__init__.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/cli_piv/__pycache__/test_fips.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/cli_piv/__pycache__/test_fips.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/cli_piv/__pycache__/test_generate_cert_and_csr.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/cli_piv/__pycache__/test_generate_cert_and_csr.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/cli_piv/__pycache__/test_key_management.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/cli_piv/__pycache__/test_key_management.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/cli_piv/__pycache__/test_management_key.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/cli_piv/__pycache__/test_management_key.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/cli_piv/__pycache__/test_misc.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/cli_piv/__pycache__/test_misc.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/cli_piv/__pycache__/test_pin_puk.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/cli_piv/__pycache__/test_pin_puk.cpython-36.pyc differ Binary files old/yubikey-manager-2.1.1/test/on_yubikey/cli_piv/__pycache__/util.cpython-36.pyc and new/yubikey-manager-3.0.0/test/on_yubikey/cli_piv/__pycache__/util.cpython-36.pyc differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/test/on_yubikey/cli_piv/test_key_management.py new/yubikey-manager-3.0.0/test/on_yubikey/cli_piv/test_key_management.py --- old/yubikey-manager-2.1.1/test/on_yubikey/cli_piv/test_key_management.py 2019-04-09 09:39:55.000000000 +0200 +++ new/yubikey-manager-3.0.0/test/on_yubikey/cli_piv/test_key_management.py 2019-06-24 09:07:27.000000000 +0200 @@ -107,7 +107,7 @@ csr = x509.load_pem_x509_csr(output.encode(), default_backend()) self.assertTrue(csr.is_signature_valid) - def test_import_correct_cert_succeeds_with_pin(self): + def test_import_verify_correct_cert_succeeds_with_pin(self): # Set up a key in the slot and create a certificate for it public_key_pem = ykman_cli( 'piv', 'generate-key', '9a', '-a', 'ECCP256', '-m', @@ -122,17 +122,20 @@ with self.assertRaises(SystemExit): ykman_cli( - 'piv', 'import-certificate', '9a', '/tmp/test-pub-key.pem', + 'piv', 'import-certificate', '--verify', + '9a', '/tmp/test-pub-key.pem', '-m', DEFAULT_MANAGEMENT_KEY) ykman_cli( - 'piv', 'import-certificate', '9a', '/tmp/test-pub-key.pem', + 'piv', 'import-certificate', '--verify', + '9a', '/tmp/test-pub-key.pem', '-m', DEFAULT_MANAGEMENT_KEY, '-P', DEFAULT_PIN) ykman_cli( - 'piv', 'import-certificate', '9a', '/tmp/test-pub-key.pem', + 'piv', 'import-certificate', '--verify', + '9a', '/tmp/test-pub-key.pem', '-m', DEFAULT_MANAGEMENT_KEY, input=DEFAULT_PIN) - def test_import_wrong_cert_fails(self): + def test_import_verify_wrong_cert_fails(self): # Set up a key in the slot and create a certificate for it public_key_pem = ykman_cli( 'piv', 'generate-key', '9a', '-a', 'ECCP256', '-m', @@ -153,10 +156,10 @@ with self.assertRaises(SystemExit): ykman_cli( - 'piv', 'import-certificate', '9a', '-', + 'piv', 'import-certificate', '--verify', '9a', '-', '-m', DEFAULT_MANAGEMENT_KEY, '-P', DEFAULT_PIN, input=cert_pem) - def test_import_wrong_cert_can_be_forced(self): + def test_import_no_verify_wrong_cert_succeeds(self): # Set up a key in the slot and create a certificate for it public_key_pem = ykman_cli( 'piv', 'generate-key', '9a', '-a', 'ECCP256', '-m', @@ -177,7 +180,7 @@ with self.assertRaises(SystemExit): ykman_cli( - 'piv', 'import-certificate', '9a', '-', + 'piv', 'import-certificate', '--verify', '9a', '-', '-m', DEFAULT_MANAGEMENT_KEY, '-P', DEFAULT_PIN, input=cert_pem) ykman_cli( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/test/on_yubikey/cli_piv/test_misc.py new/yubikey-manager-3.0.0/test/on_yubikey/cli_piv/test_misc.py --- old/yubikey-manager-2.1.1/test/on_yubikey/cli_piv/test_misc.py 2019-04-09 09:39:55.000000000 +0200 +++ new/yubikey-manager-3.0.0/test/on_yubikey/cli_piv/test_misc.py 2019-06-24 09:07:27.000000000 +0200 @@ -1,6 +1,7 @@ from ..util import ykman_cli from .util import PivTestCase, DEFAULT_MANAGEMENT_KEY + class Misc(PivTestCase): def test_info(self): @@ -14,7 +15,7 @@ def test_write_read_object(self): ykman_cli( 'piv', 'write-object', - '-m', DEFAULT_MANAGEMENT_KEY,'0x5f0001', + '-m', DEFAULT_MANAGEMENT_KEY, '0x5f0001', '-', input='test data') output = ykman_cli('piv', 'read-object', '0x5f0001') self.assertEquals('test data\n', output) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/test/on_yubikey/test_cli_misc.py new/yubikey-manager-3.0.0/test/on_yubikey/test_cli_misc.py --- old/yubikey-manager-2.1.1/test/on_yubikey/test_cli_misc.py 2019-04-09 09:39:55.000000000 +0200 +++ new/yubikey-manager-3.0.0/test/on_yubikey/test_cli_misc.py 2019-06-24 09:07:27.000000000 +0200 @@ -13,7 +13,7 @@ @unittest.skipIf(is_fips(), 'Not applicable to YubiKey FIPS.') def test_ykman_info_does_not_report_fips_for_non_fips_device(self): - info = ykman_cli('info --check-fips') + info = ykman_cli('info', '--check-fips') self.assertNotIn('FIPS', info) @unittest.skipIf(not is_fips(), 'YubiKey FIPS required.') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/test/test_util.py new/yubikey-manager-3.0.0/test/test_util.py --- old/yubikey-manager-2.1.1/test/test_util.py 2019-04-09 09:39:55.000000000 +0200 +++ new/yubikey-manager-3.0.0/test/test_util.py 2019-06-24 09:07:27.000000000 +0200 @@ -33,7 +33,7 @@ for l in range(0, 38): self.assertRegex( generate_static_pw(l), - '^[cbdefghijklnrtuvCBDEFGHIJKLNRTUV]{' + '{:d}'.format(l) + '}$') + '^[cbdefghijklnrtuvCBDEFGHIJKLNRTUV]{%d}$' % l) def test_hmac_shorten_key(self): self.assertEqual(b'short', hmac_shorten_key(b'short', 'sha1')) @@ -146,15 +146,14 @@ with open_file('rsa_2048_key_cert.pfx') as rsa_2048_key_cert_pfx: self.assertFalse(is_pem(rsa_2048_key_cert_pfx.read())) - with open_file('rsa_2048_cert_metadata.pem') as rsa_2048_cert_metadata_pem: - self.assertTrue(is_pem(rsa_2048_cert_metadata_pem.read())) + with open_file('rsa_2048_cert_metadata.pem') as f: + self.assertTrue(is_pem(f.read())) with open_file( 'rsa_2048_key_cert_encrypted.pfx') as \ rsa_2048_key_cert_encrypted_pfx: self.assertFalse(is_pem(rsa_2048_key_cert_encrypted_pfx.read())) - def test_form_factor_from_code(self): self.assertEqual(FORM_FACTOR.UNKNOWN, FORM_FACTOR.from_code(None)) with self.assertRaises(ValueError): @@ -166,4 +165,6 @@ self.assertEqual( FORM_FACTOR.USB_C_KEYCHAIN, FORM_FACTOR.from_code(0x03)) self.assertEqual(FORM_FACTOR.USB_C_NANO, FORM_FACTOR.from_code(0x04)) - self.assertEqual(FORM_FACTOR.UNKNOWN, FORM_FACTOR.from_code(0x05)) + self.assertEqual(FORM_FACTOR.USB_C_LIGHTNING, + FORM_FACTOR.from_code(0x05)) + self.assertEqual(FORM_FACTOR.UNKNOWN, FORM_FACTOR.from_code(0x06)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/ykman/VERSION new/yubikey-manager-3.0.0/ykman/VERSION --- old/yubikey-manager-2.1.1/ykman/VERSION 2019-05-27 12:35:48.000000000 +0200 +++ new/yubikey-manager-3.0.0/ykman/VERSION 2019-06-24 09:07:27.000000000 +0200 @@ -1 +1 @@ -2.1.1 +3.0.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/ykman/cli/fido.py new/yubikey-manager-3.0.0/ykman/cli/fido.py --- old/yubikey-manager-2.1.1/ykman/cli/fido.py 2019-05-08 09:19:37.000000000 +0200 +++ new/yubikey-manager-3.0.0/ykman/cli/fido.py 2019-06-24 09:07:27.000000000 +0200 @@ -102,6 +102,74 @@ click.echo('PIN is not set.') +@fido.command('list') +@click.pass_context +@click.option('-P', '--pin', help='PIN code.') +def list_creds(ctx, pin): + """ + List resident credentials. + """ + controller = ctx.obj['controller'] + + if not controller.has_pin: + ctx.fail('No PIN set.') + + if controller.has_pin and pin is None: + pin = _prompt_current_pin(prompt='Enter your PIN') + + try: + for cred in controller.get_resident_credentials(pin): + click.echo('{} ({})'.format(cred.user_name, cred.rp_id)) + except CtapError as e: + if e.code == CtapError.ERR.PIN_INVALID: + ctx.fail('Wrong PIN.') + except Exception as e: + logger.debug('Failed to list resident credentials', exc_info=e) + ctx.fail('Failed to list resident credentials.') + + +@fido.command() +@click.pass_context +@click.argument('query') +@click.option('-P', '--pin', help='PIN code.') +@click.option('-f', '--force', is_flag=True, + help='Confirm deletion without prompting') +def delete(ctx, query, pin, force): + """ + Delete a resident credential. + """ + controller = ctx.obj['controller'] + + if not controller.has_pin: + ctx.fail('No PIN set.') + + if controller.has_pin and pin is None: + pin = _prompt_current_pin(prompt='Enter your PIN') + + try: + hits = [ + cred for cred in controller.get_resident_credentials(pin) + if query.lower() in cred.user_name or query.lower() in cred.rp_id + ] + if len(hits) == 0: + ctx.fail('No matches, nothing to be done.') + elif len(hits) == 1: + cred = hits[0] + if force or click.confirm( + 'Delete credential {} ({})?'.format( + cred.user_name, cred.rp_id)): + controller.delete_resident_credential( + cred.credential_id, pin) + else: + ctx.fail('Multiple matches, make the query more specific.') + except CtapError as e: + if e.code == CtapError.ERR.PIN_INVALID: + ctx.fail('Wrong PIN.') + except Exception as e: + logger.debug('Failed to delete resident credential', exc_info=e) + ctx.fail('Failed to delete resident credential.') + + @fido.command('set-pin') @click.pass_context @click.option('-P', '--pin', help='Current PIN code.') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/ykman/cli/opgp.py new/yubikey-manager-3.0.0/ykman/cli/opgp.py --- old/yubikey-manager-2.1.1/ykman/cli/opgp.py 2019-04-09 09:39:55.000000000 +0200 +++ new/yubikey-manager-3.0.0/ykman/cli/opgp.py 2019-06-24 09:07:27.000000000 +0200 @@ -29,28 +29,17 @@ import logging import click -from ..util import TRANSPORT +from ..util import TRANSPORT, parse_certificates, parse_private_key from ..opgp import OpgpController, KEY_SLOT, TOUCH_MODE from ..driver_ccid import APDUError, SW -from .util import click_force_option, click_postpone_execution +from .util import ( + click_force_option, click_format_option, click_postpone_execution, + UpperCaseChoice) logger = logging.getLogger(__name__) -KEY_NAMES = dict( - sig=KEY_SLOT.SIGNATURE, - enc=KEY_SLOT.ENCRYPTION, - aut=KEY_SLOT.AUTHENTICATION -) - -MODE_NAMES = dict( - off=TOUCH_MODE.OFF, - on=TOUCH_MODE.ON, - fixed=TOUCH_MODE.FIXED -) - - def one_of(data): def inner(ctx, param, key): if key is not None: @@ -92,7 +81,7 @@ \b Require touch to use the authentication key: - $ ykman openpgp touch aut on + $ ykman openpgp set-touch aut on """ try: ctx.obj['controller'] = OpgpController(ctx.obj['dev'].driver) @@ -116,17 +105,23 @@ click.echo('PIN tries remaining: {}'.format(retries.pin)) click.echo('Reset code tries remaining: {}'.format(retries.reset)) click.echo('Admin PIN tries remaining: {}'.format(retries.admin)) - click.echo() - click.echo('Touch policies') - click.echo( - 'Signature key {.name}'.format( - controller.get_touch(KEY_SLOT.SIGNATURE))) - click.echo( - 'Encryption key {.name}'.format( - controller.get_touch(KEY_SLOT.ENCRYPTION))) - click.echo( - 'Authentication key {.name}'.format( - controller.get_touch(KEY_SLOT.AUTHENTICATION))) + # Touch only available on YK4 and later + if controller.version >= (4, 2, 6): + click.echo() + click.echo('Touch policies') + click.echo( + 'Signature key {!s}'.format( + controller.get_touch(KEY_SLOT.SIGNATURE))) + click.echo( + 'Encryption key {!s}'.format( + controller.get_touch(KEY_SLOT.ENCRYPTION))) + click.echo( + 'Authentication key {!s}'.format( + controller.get_touch(KEY_SLOT.AUTHENTICATION))) + if controller.supports_attestation: + click.echo( + 'Attestation key {!s}'.format( + controller.get_touch(KEY_SLOT.ATTESTATION))) @openpgp.command() @@ -153,63 +148,242 @@ click.echo('Admin PIN: 12345678') -@openpgp.command() -@click.argument('key', metavar='KEY', type=click.Choice(sorted(KEY_NAMES)), - callback=lambda c, p, k: KEY_NAMES.get(k)) -@click.argument('policy', metavar='POLICY', type=click.Choice(sorted(MODE_NAMES)), - callback=lambda c, p, k: MODE_NAMES.get(k)) -@click.option('--admin-pin', required=False, metavar='PIN', - help='Admin PIN for OpenPGP.') +@openpgp.command('set-touch') +@click.argument( + 'key', metavar='KEY', type=UpperCaseChoice(['AUT', 'ENC', 'SIG', 'ATT']), + callback=lambda c, p, v: KEY_SLOT(v)) +@click.argument( + 'policy', metavar='POLICY', + type=UpperCaseChoice(['ON', 'OFF', 'FIXED', 'CACHED', 'CACHED-FIXED']), + callback=lambda c, p, v: TOUCH_MODE[v.replace('-', '_')]) +@click.option('-a', '--admin-pin', help='Admin PIN for OpenPGP.') @click_force_option @click.pass_context -def touch(ctx, key, policy, admin_pin, force): +def set_touch(ctx, key, policy, admin_pin, force): """ - Manage touch policy for OpenPGP keys. + Set touch policy for OpenPGP keys. \b - KEY Key slot to set (sig, enc or aut). - POLICY Touch policy to set (on, off or fixed). + KEY Key slot to set (sig, enc, aut or att). + POLICY Touch policy to set (on, off, fixed, cached or cached-fixed). """ controller = ctx.obj['controller'] - old_policy = controller.get_touch(key) - if old_policy == TOUCH_MODE.FIXED: - ctx.fail('A FIXED policy cannot be changed!') - - force or click.confirm('Set touch policy of {.name} key to {.name}?'.format( - key, policy), abort=True, err=True) if admin_pin is None: admin_pin = click.prompt('Enter admin PIN', hide_input=True, err=True) - controller.set_touch(key, policy, admin_pin.encode('utf8')) + + policy_name = policy.name.lower().replace('_', '-') + + if policy not in controller.supported_touch_policies: + ctx.fail('Touch policy {} not supported.'.format(policy_name)) + + if force or click.confirm( + 'Set touch policy of {} key to {}?'.format( + key.name.lower(), + policy_name), + abort=True, err=True): + try: + controller.set_touch(key, policy, admin_pin) + except APDUError as e: + if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: + ctx.fail('Touch policy not allowed.') + logger.debug('Failed to set touch policy', exc_info=e) + ctx.fail('Failed to set touch policy.') @openpgp.command('set-pin-retries') -@click.argument('pw-attempts', nargs=3, type=click.IntRange(1, 99)) -@click.password_option('--admin-pin', metavar='PIN', prompt='Enter admin PIN', - confirmation_prompt=False) +@click.argument( + 'pin-retries', type=click.IntRange(1, 99), metavar='PIN-RETRIES') +@click.argument( + 'reset-code-retries', + type=click.IntRange(1, 99), metavar='RESET-CODE-RETRIES') +@click.argument( + 'admin-pin-retries', + type=click.IntRange(1, 99), metavar='ADMIN-PIN-RETRIES') +@click.option('-a', '--admin-pin', help='Admin PIN for OpenPGP.') @click_force_option @click.pass_context -def set_pin_retries(ctx, pw_attempts, admin_pin, force): +def set_pin_retries( + ctx, admin_pin, pin_retries, + reset_code_retries, admin_pin_retries, force): """ - Manage pin-retries. - - Sets the number of attempts available before locking for each PIN. - - PW_ATTEMPTS should be three integer values corresponding to the number of - attempts for the PIN, Reset Code, and Admin PIN, respectively. + Set PIN, Reset Code and Admin PIN retries. """ controller = ctx.obj['controller'] + + if admin_pin is None: + admin_pin = click.prompt('Enter admin PIN', hide_input=True, err=True) + resets_pins = controller.version < (4, 0, 0) if resets_pins: click.echo('WARNING: Setting PIN retries will reset the values for all ' '3 PINs!') - force or click.confirm('Set PIN retry counters to: {} {} {}?'.format( - *pw_attempts), abort=True, err=True) - controller.set_pin_retries(*(pw_attempts + (admin_pin.encode('utf8'),))) - click.echo('PIN retries successfully set.') - if resets_pins: - click.echo('Default PINs are set.') - echo_default_pins() + if force or click.confirm( + 'Set PIN retry counters to: {} {} {}?'.format( + pin_retries, reset_code_retries, + admin_pin_retries), abort=True, err=True): + + controller.set_pin_retries( + pin_retries, reset_code_retries, admin_pin_retries, admin_pin) + + if resets_pins: + click.echo('Default PINs are set.') + echo_default_pins() + + +@openpgp.command() +@click.pass_context +@click.option('-P', '--pin', help='PIN code.') +@click_format_option +@click.argument( + 'key', metavar='KEY', type=UpperCaseChoice(['AUT', 'ENC', 'SIG']), + callback=lambda c, p, v: KEY_SLOT(v)) +@click.argument('certificate', type=click.File('wb'), metavar='CERTIFICATE') +def attest(ctx, key, certificate, pin, format): + """ + Generate a attestation certificate for a key. + + Attestation is used to show that an asymmetric key was generated on the + YubiKey and therefore doesn't exist outside the device. + + \b + KEY Key slot to attest (sig, enc, aut). + CERTIFICATE File to write attestation certificate to. Use '-' to use stdout. + """ + + controller = ctx.obj['controller'] + + if not pin: + pin = click.prompt( + 'Enter PIN', default='', hide_input=True, + show_default=False, err=True) + + try: + cert = controller.read_certificate(key) + except ValueError: + cert = None + + if not cert or click.confirm( + 'There is already data stored in the certificate slot for {}, ' + 'do you want to overwrite it?'.format(key.name)): + touch_policy = controller.get_touch(KEY_SLOT.ATTESTATION) + if touch_policy in [TOUCH_MODE.ON, TOUCH_MODE.FIXED]: + click.echo('Touch your YubiKey...') + try: + cert = controller.attest(key, pin) + certificate.write(cert.public_bytes(encoding=format)) + except Exception as e: + logger.debug('Failed to attest', exc_info=e) + ctx.fail('Attestation failed') + + +@openpgp.command('export-certificate') +@click.pass_context +@click.argument( + 'key', metavar='KEY', type=UpperCaseChoice(['AUT', 'ENC', 'SIG', 'ATT']), + callback=lambda c, p, v: KEY_SLOT(v)) +@click_format_option +@click.argument('certificate', type=click.File('wb'), metavar='CERTIFICATE') +def export_certificate(ctx, key, format, certificate): + """ + Export an OpenPGP Cardholder certificate. + + \b + KEY Key slot to read from (sig, enc, aut, or att). + CERTIFICATE File to write certificate to. Use '-' to use stdout. + """ + controller = ctx.obj['controller'] + try: + cert = controller.read_certificate(key) + except ValueError: + ctx.fail('Failed to read certificate from {}'.format(key.name)) + certificate.write(cert.public_bytes(encoding=format)) + + +@openpgp.command('delete-certificate') +@click.option('-a', '--admin-pin', help='Admin PIN for OpenPGP.') +@click.pass_context +@click.argument( + 'key', metavar='KEY', type=UpperCaseChoice(['AUT', 'ENC', 'SIG', 'ATT']), + callback=lambda c, p, v: KEY_SLOT(v)) +def delete_certificate(ctx, key, admin_pin): + """ + Delete an OpenPGP Cardholder certificate. + + \b + KEY Key slot to delete certificate from (sig, enc, aut, or att). + """ + controller = ctx.obj['controller'] + if admin_pin is None: + admin_pin = click.prompt('Enter admin PIN', hide_input=True, err=True) + try: + controller.delete_certificate(key, admin_pin) + except Exception as e: + logger.debug('Failed to delete ', exc_info=e) + ctx.fail('Failed to delete certificate.') + + +@openpgp.command('import-certificate') +@click.option('-a', '--admin-pin', help='Admin PIN for OpenPGP.') +@click.pass_context +@click.argument( + 'key', metavar='KEY', type=UpperCaseChoice(['AUT', 'ENC', 'SIG', 'ATT']), + callback=lambda c, p, v: KEY_SLOT(v)) +@click.argument('cert', type=click.File('rb'), metavar='CERTIFICATE') +def import_certificate(ctx, key, cert, admin_pin): + """ + Import an OpenPGP Cardholder certificate. + + \b + KEY Key slot to import certificate to (sig, enc, aut, or att). + CERTIFICATE File containing the certificate. Use '-' to use stdin. + """ + controller = ctx.obj['controller'] + + if admin_pin is None: + admin_pin = click.prompt('Enter admin PIN', hide_input=True, err=True) + + try: + certs = parse_certificates(cert.read(), password=None) + except Exception as e: + logger.debug('Failed to parse', exc_info=e) + ctx.fail('Failed to parse certificate.') + if len(certs) != 1: + ctx.fail('Can only import one certificate.') + try: + controller.import_certificate(key, certs[0], admin_pin) + except Exception as e: + logger.debug('Failed to import', exc_info=e) + ctx.fail('Failed to import certificate') + + +@openpgp.command('import-attestation-key') +@click.option('-a', '--admin-pin', help='Admin PIN for OpenPGP.') +@click.pass_context +@click.argument('private-key', type=click.File('rb'), metavar='PRIVATE-KEY') +def import_attestation_key(ctx, private_key, admin_pin): + """ + Import a private attestation key. + + Import a private key for OpenPGP attestation. + + \b + PRIVATE-KEY File containing the private key. Use '-' to use stdin. + """ + controller = ctx.obj['controller'] + + if admin_pin is None: + admin_pin = click.prompt('Enter admin PIN', hide_input=True, err=True) + try: + private_key = parse_private_key(private_key.read(), password=None) + except Exception as e: + logger.debug('Failed to parse', exc_info=e) + ctx.fail('Failed to parse private key.') + try: + controller.import_attestation_key(private_key, admin_pin) + except Exception as e: + logger.debug('Failed to import', exc_info=e) + ctx.fail('Failed to import attestation key.') openpgp.transports = TRANSPORT.CCID diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/ykman/cli/otp.py new/yubikey-manager-3.0.0/ykman/cli/otp.py --- old/yubikey-manager-2.1.1/ykman/cli/otp.py 2019-05-27 12:04:25.000000000 +0200 +++ new/yubikey-manager-3.0.0/ykman/cli/otp.py 2019-06-24 09:07:27.000000000 +0200 @@ -36,7 +36,7 @@ from binascii import a2b_hex, b2a_hex from .. import __version__ from ..driver_otp import YkpersError -from ..otp import OtpController, PrepareUploadFailed +from ..otp import OtpController, PrepareUploadFailed, SlotConfig from ..scancodes import KEYBOARD_LAYOUT import logging import os @@ -339,7 +339,9 @@ abort=True, err=True) try: - controller.program_otp(slot, key, public_id, private_id, not no_enter) + controller.program_otp(slot, key, public_id, private_id, SlotConfig( + append_cr=not no_enter + )) except YkpersError as e: _failed_to_write_msg(ctx, e) @@ -394,8 +396,9 @@ if not force: _confirm_slot_overwrite(controller, slot) try: - controller.program_static( - slot, password, not no_enter, keyboard_layout=keyboard_layout) + controller.program_static(slot, password, keyboard_layout, SlotConfig( + append_cr=not no_enter + )) except YkpersError as e: _failed_to_write_msg(ctx, e) @@ -546,7 +549,9 @@ err=True) try: controller.program_hotp( - slot, key, counter, int(digits) == 8, not no_enter) + slot, key, counter, int(digits) == 8, SlotConfig( + append_cr=not no_enter + )) except YkpersError as e: _failed_to_write_msg(ctx, e) @@ -569,8 +574,11 @@ '-p', '--pacing', type=click.Choice(['0', '20', '40', '60']), default='0', show_default=True, help='Throttle output speed by ' 'adding a delay (in ms) between characters emitted.') +@click.option('--use-numeric-keypad', is_flag=True, show_default=True, + help='Use scancodes for numeric keypad when sending digits.' + ' Helps with some keyboard layouts. ') def settings(ctx, slot, new_access_code, delete_access_code, enter, pacing, - force): + use_numeric_keypad, force): """ Update the settings for a slot. @@ -605,7 +613,11 @@ pacing = int(pacing) try: - controller.update_settings(slot, enter=enter, pacing=pacing) + controller.update_settings(slot, SlotConfig( + append_cr=enter, + pacing=pacing, + numeric_keypad=use_numeric_keypad + )) except YkpersError as e: _failed_to_write_msg(ctx, e) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/ykman/cli/piv.py new/yubikey-manager-3.0.0/ykman/cli/piv.py --- old/yubikey-manager-2.1.1/ykman/cli/piv.py 2019-04-30 12:51:29.000000000 +0200 +++ new/yubikey-manager-3.0.0/ykman/cli/piv.py 2019-06-24 09:07:27.000000000 +0200 @@ -165,9 +165,11 @@ except AttributeError: print_dn = False logger.debug('Failed to read DN, falling back to only CNs') - subject_cn = cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME) + subject_cn = cert.subject.get_attributes_for_oid( + x509.NameOID.COMMON_NAME) subject_cn = subject_cn[0].value if subject_cn else 'None' - issuer_cn = cert.issuer.get_attributes_for_oid(x509.NameOID.COMMON_NAME) + issuer_cn = cert.issuer.get_attributes_for_oid( + x509.NameOID.COMMON_NAME) issuer_cn = issuer_cn[0].value if issuer_cn else 'None' except ValueError as e: # Malformed certificates may throw ValueError @@ -823,6 +825,7 @@ show_default=False, hide_input=True, err=True) controller.unblock_pin(puk, new_pin) + @piv.command('read-object') @click_pin_option @click.pass_context @@ -875,8 +878,8 @@ the range 5f0000 - 5fffff. \b - OBJECT-ID Id of PIV object in HEX. - DATA File containing the data to be written. Use '-' to use stdin. + OBJECT-ID Id of PIV object in HEX. + DATA File containing the data to be written. Use '-' to use stdin. """ controller = ctx.obj['controller'] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/ykman/device.py new/yubikey-manager-3.0.0/ykman/device.py --- old/yubikey-manager-2.1.1/ykman/device.py 2019-05-27 12:04:25.000000000 +0200 +++ new/yubikey-manager-3.0.0/ykman/device.py 2019-06-24 09:07:27.000000000 +0200 @@ -270,6 +270,7 @@ FORM_FACTOR.USB_A_NANO, FORM_FACTOR.USB_C_KEYCHAIN, FORM_FACTOR.USB_C_NANO, + FORM_FACTOR.USB_C_LIGHTNING ): config._set(TAG.NFC_SUPPORTED, 0) config._set(TAG.NFC_ENABLED, 0) @@ -284,7 +285,8 @@ if config.nfc_supported: self.device_name = 'Security Key NFC' elif self._key_type == YUBIKEY.YK4: - if (5, 1, 0) > self.version >= (5, 0, 0): + if (5, 0, 0) <= self.version < (5, 1, 0) or \ + self.version in [(5, 2, 0), (5, 2, 1), (5, 2, 2)]: self.device_name = 'YubiKey Preview' elif self.version >= (5, 1, 0): logger.debug('Identified YubiKey 5') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/ykman/fido.py new/yubikey-manager-3.0.0/ykman/fido.py --- old/yubikey-manager-2.1.1/ykman/fido.py 2019-04-29 14:32:48.000000000 +0200 +++ new/yubikey-manager-3.0.0/ykman/fido.py 2019-06-24 09:07:27.000000000 +0200 @@ -29,13 +29,35 @@ import six import time +import logging from fido2.ctap1 import CTAP1, ApduError -from fido2.ctap2 import CTAP2, PinProtocolV1 +from fido2.ctap2 import CTAP2, PinProtocolV1, CredentialManagement from threading import Timer from .driver_ccid import SW from .driver_fido import FIPS_U2F_CMD +logger = logging.getLogger(__name__) + + +class ResidentCredential(object): + def __init__(self, raw_credential, raw_rp): + self._raw_credential = raw_credential + self._raw_rp = raw_rp + + @property + def credential_id(self): + return self._raw_credential[CredentialManagement.RESULT.CREDENTIAL_ID] + + @property + def rp_id(self): + return self._raw_rp[CredentialManagement.RESULT.RP]['id'] + + @property + def user_name(self): + return self._raw_credential[CredentialManagement.RESULT.USER]['name'] + + class Fido2Controller(object): def __init__(self, driver): @@ -48,6 +70,27 @@ def has_pin(self): return self._pin + def get_resident_credentials(self, pin): + _credman = CredentialManagement( + self.ctap, + self.pin.VERSION, + self.pin.get_pin_token(pin)) + + for rp in _credman.enumerate_rps(): + for cred in _credman.enumerate_creds( + rp[CredentialManagement.RESULT.RP_ID_HASH]): + yield ResidentCredential(cred, rp) + + def delete_resident_credential(self, credential_id, pin): + _credman = CredentialManagement( + self.ctap, + self.pin.VERSION, + self.pin.get_pin_token(pin)) + + for cred in self.get_resident_credentials(pin): + if credential_id == cred.credential_id: + _credman.delete_cred(credential_id) + def get_pin_retries(self): return self.pin.get_pin_retries() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/ykman/opgp.py new/yubikey-manager-3.0.0/ykman/opgp.py --- old/yubikey-manager-2.1.1/ykman/opgp.py 2019-04-09 09:39:55.000000000 +0200 +++ new/yubikey-manager-3.0.0/ykman/opgp.py 2019-06-24 09:07:27.000000000 +0200 @@ -28,18 +28,62 @@ from __future__ import absolute_import import six +import struct +import logging from .util import AID from .driver_ccid import (APDUError, SW, GP_INS_SELECT) -from enum import IntEnum, unique -from binascii import b2a_hex +from enum import Enum, IntEnum, unique +from binascii import b2a_hex, a2b_hex from collections import namedtuple +from cryptography import x509 +from cryptography.utils import int_to_bytes +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.serialization import Encoding +from cryptography.hazmat.primitives.asymmetric import rsa, ec + + +logger = logging.getLogger(__name__) @unique -class KEY_SLOT(IntEnum): # noqa: N801 - SIGNATURE = 0xd6 - ENCRYPTION = 0xd7 - AUTHENTICATION = 0xd8 +class KEY_SLOT(Enum): # noqa: N801 + SIGNATURE = 'SIG' + ENCRYPTION = 'ENC' + AUTHENTICATION = 'AUT' + ATTESTATION = 'ATT' + + @property + def key_position(self): + if self == KEY_SLOT.SIGNATURE: + return 0x01 + if self == KEY_SLOT.ENCRYPTION: + return 0x02 + if self == KEY_SLOT.AUTHENTICATION: + return 0x03 + if self == KEY_SLOT.ATTESTATION: + return 0x04 + + @property + def touch_position(self): + if self == KEY_SLOT.SIGNATURE: + return 0xd6 + if self == KEY_SLOT.ENCRYPTION: + return 0xd7 + if self == KEY_SLOT.AUTHENTICATION: + return 0xd8 + if self == KEY_SLOT.ATTESTATION: + return 0xd9 + + @property + def cert_position(self): + if self == KEY_SLOT.SIGNATURE: + return 0x02 + if self == KEY_SLOT.ENCRYPTION: + return 0x01 + if self == KEY_SLOT.AUTHENTICATION: + return 0x00 + if self == KEY_SLOT.ATTESTATION: + return 0x03 @unique @@ -47,6 +91,25 @@ OFF = 0x00 ON = 0x01 FIXED = 0x02 + CACHED = 0x03 + CACHED_FIXED = 0x04 + + def __str__(self): + if self == TOUCH_MODE.OFF: + return 'Off' + elif self == TOUCH_MODE.ON: + return 'On' + elif self == TOUCH_MODE.FIXED: + return 'On (fixed)' + elif self == TOUCH_MODE.CACHED: + return 'Cached' + elif self == TOUCH_MODE.CACHED_FIXED: + return 'Cached (fixed)' + + +@unique +class TAG(IntEnum): # noqa: N801 + CARDHOLDER_CERTIFICATE = 0x7f @unique @@ -58,6 +121,9 @@ TERMINATE = 0xe6 ACTIVATE = 0x44 PUT_DATA = 0xda + GET_ATTESTATION = 0xfb + SEND_REMAINING = 0xc0 + SELECT_DATA = 0xa5 PinRetries = namedtuple('PinRetries', ['pin', 'reset', 'admin']) @@ -92,6 +158,23 @@ return self._driver.send_apdu(cl, ins, p1, p2, data, check) raise + def send_cmd(self, cl, ins, p1=0, p2=0, data=b'', check=SW.OK): + while len(data) > 0xff: + self._driver.send_apdu(0x10, ins, p1, p2, data[:0xff]) + data = data[0xff:] + resp, sw = self._driver.send_apdu(0, ins, p1, p2, data, check=None) + + while (sw >> 8) == SW.MORE_DATA: + more, sw = self._driver.send_apdu( + 0, INS.SEND_REMAINING, 0, 0, b'', check=None) + resp += more + + if check is None: + return resp, sw + elif sw != check: + raise APDUError(resp, sw) + return resp + def _read_version(self): bcd_hex = b2a_hex(self.send_apdu(0, INS.GET_VERSION, 0, 0)) return tuple(int(bcd_hex[i:i+2]) for i in range(0, 6, 2)) @@ -118,32 +201,163 @@ def _verify(self, pw, pin): try: + pin = pin.encode('utf-8') self.send_apdu(0, INS.VERIFY, 0, pw, pin) except APDUError: pw_remaining = self.get_remaining_pin_tries()[pw-PW1] raise ValueError('Invalid PIN, {} tries remaining.'.format( pw_remaining)) - def get_touch(self, key_slot): + @property + def supported_touch_policies(self): if self.version < (4, 2, 0): + return [] + if self.version < (5, 2, 1): + return [TOUCH_MODE.ON, TOUCH_MODE.OFF, TOUCH_MODE.FIXED] + if self.version >= (5, 2, 1): + return [ + TOUCH_MODE.ON, TOUCH_MODE.OFF, TOUCH_MODE.FIXED, + TOUCH_MODE.CACHED, TOUCH_MODE.CACHED_FIXED] + + @property + def supports_attestation(self): + return self.version >= (5, 2, 1) + + def get_touch(self, key_slot): + if not self.supported_touch_policies: raise ValueError('Touch policy is available on YubiKey 4 or later.') - data = self.send_apdu(0, INS.GET_DATA, 0, key_slot) + if key_slot == KEY_SLOT.ATTESTATION and not self.supports_attestation: + raise ValueError('Attestation key not available on this device.') + data = self.send_apdu(0, INS.GET_DATA, 0, key_slot.touch_position) return TOUCH_MODE(six.indexbytes(data, 0)) - def set_touch(self, key_slot, mode, pin): - if self.version < (4, 2, 0): + def set_touch(self, key_slot, mode, admin_pin): + if not self.supported_touch_policies: raise ValueError('Touch policy is available on YubiKey 4 or later.') - self._verify(PW3, pin) - self.send_apdu(0, INS.PUT_DATA, 0, key_slot, + if mode not in self.supported_touch_policies: + raise ValueError('Touch policy not available on this device.') + self._verify(PW3, admin_pin) + self.send_apdu(0, INS.PUT_DATA, 0, key_slot.touch_position, bytes(bytearray([mode, TOUCH_METHOD_BUTTON]))) - def set_pin_retries(self, pw1_tries, pw2_tries, pw3_tries, pin): + def set_pin_retries(self, pw1_tries, pw2_tries, pw3_tries, admin_pin): if self.version < (1, 0, 7): # For YubiKey NEO raise ValueError('Setting PIN retry counters requires version ' '1.0.7 or later.') if (4, 0, 0) <= self.version < (4, 3, 1): # For YubiKey 4 raise ValueError('Setting PIN retry counters requires version ' '4.3.1 or later.') - self._verify(PW3, pin) + self._verify(PW3, admin_pin) self.send_apdu(0, INS.SET_PIN_RETRIES, 0, 0, bytes(bytearray([pw1_tries, pw2_tries, pw3_tries]))) + + def read_certificate(self, key_slot): + self.send_cmd( + 0, INS.SELECT_DATA, key_slot.cert_position, + 0x04, data=a2b_hex('0660045C027F21')) + data = self.send_cmd( + 0, INS.GET_DATA, TAG.CARDHOLDER_CERTIFICATE, 0x21) + if not data: + raise ValueError('No certificate found!') + return x509.load_der_x509_certificate(data, default_backend()) + + def import_certificate(self, key_slot, certificate, admin_pin): + self._verify(PW3, admin_pin) + cert_data = certificate.public_bytes(Encoding.DER) + self.send_cmd( + 0, INS.SELECT_DATA, key_slot.cert_position, + 0x04, data=a2b_hex('0660045C027F21')) + self.send_cmd( + 0, INS.PUT_DATA, TAG.CARDHOLDER_CERTIFICATE, 0x21, data=cert_data) + + def _get_key_attributes(self, key): + if isinstance(key, rsa.RSAPrivateKey): + return struct.pack('>BHHB', 0x01, key.key_size, 32, 0) + if isinstance(key, ec.EllipticCurvePrivateKey): + return int_to_bytes( + self._get_opgp_algo_id_from_ec( + key)) + a2b_hex(self._get_oid_from_ec(key)) + raise ValueError('Not a valid private key!') + + def _get_oid_from_ec(self, key): + curve = key.curve.name + if curve == 'secp384r1': + return '2B81040022' + if curve == 'secp256r1': + return '2A8648CE3D030107' + if curve == 'secp521r1': + return '2B81040023' + if curve == 'x25519': + return '2B060104019755010501' + raise ValueError('No OID for curve: ' + curve) + + def _get_opgp_algo_id_from_ec(self, key): + curve = key.curve.name + if curve in ['secp384r1', 'secp256r1', 'secp521r1']: + return 0x13 + if curve == 'x25519': + return 0x16 + raise ValueError('No Algo ID for curve: ' + curve) + + def _get_key_data(self, key): + + def _der_len(data): + ln = len(data) + if ln <= 128: + res = [ln] + elif ln <= 255: + res = [0x81, ln] + else: + res = [0x82, (ln >> 8) & 0xff, ln & 0xff] + return bytearray(res) + + private_numbers = key.private_numbers() + data = a2b_hex('B603840181') + + if isinstance(key, rsa.RSAPrivateKey): + ln = key.key_size // 8 // 2 + data += b'\x7f\x48\x08\x91\x03\x92\x81\x80\x93\x81\x80\x5f\x48\x82\x01\x03\x01\x00\x01' # noqa: E501 + data += int_to_bytes(private_numbers.p, ln) + data += int_to_bytes(private_numbers.q, ln) + return b'\x4d' + _der_len(data) + data + elif isinstance(key, ec.EllipticCurvePrivateKey): + ln = key.key_size // 8 + privkey = int_to_bytes(private_numbers.private_value, ln) + data += b'\x7f\x48\x02\x92' + _der_len(privkey) + data += b'\x5f\x48' + _der_len(privkey) + privkey + return b'\x4d' + _der_len(data) + data + + def import_attestation_key(self, key, admin_pin): + self._verify(PW3, admin_pin) + data = self._get_key_attributes(key) + self.send_cmd(0, INS.PUT_DATA, 0, 0xda, data=data) + data = self._get_key_data(key) + self.send_cmd(0, 0xdb, 0x3f, 0xff, data=data) + + def delete_attestation_key(self, admin_pin): + self._verify(PW3, admin_pin) + # Delete attestation key by changing the key attributes twice. + self.send_cmd( + 0, INS.PUT_DATA, 0, 0xda, + data=struct.pack('>BHHB', 0x01, 2048, 32, 0)) + self.send_cmd( + 0, INS.PUT_DATA, 0, 0xda, + data=struct.pack('>BHHB', 0x01, 4096, 32, 0)) + + def delete_certificate(self, key_slot, admin_pin): + self._verify(PW3, admin_pin) + self.send_cmd( + 0, INS.SELECT_DATA, key_slot.cert_position(), + 0x04, data=a2b_hex('0660045C027F21')) + self.send_apdu( + 0, INS.PUT_DATA, TAG.CARDHOLDER_CERTIFICATE, 0x21, data=b'') + + def attest(self, key_slot, pin): + self._verify(PW1, pin) + self.send_apdu(0x80, INS.GET_ATTESTATION, key_slot.key_position, 0) + self.send_cmd( + 0, INS.SELECT_DATA, key_slot.cert_position(), + 0x04, data=a2b_hex('0660045C027F21')) + data = self.send_cmd( + 0, INS.GET_DATA, TAG.CARDHOLDER_CERTIFICATE, 0x21) + return x509.load_der_x509_certificate(data, default_backend()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/ykman/otp.py new/yubikey-manager-3.0.0/ykman/otp.py --- old/yubikey-manager-2.1.1/ykman/otp.py 2019-05-16 09:16:05.000000000 +0200 +++ new/yubikey-manager-3.0.0/ykman/otp.py 2019-06-24 09:07:27.000000000 +0200 @@ -111,6 +111,68 @@ return [e.message() for e in self.errors] +class SlotConfig(object): + def __init__( + self, + serial_api_visible=True, + allow_update=True, + append_cr=True, + pacing=None, + numeric_keypad=False, + ): + self.serial_api_visible = serial_api_visible + self.allow_update = allow_update + self.append_cr = append_cr + self.pacing = pacing + self.numeric_keypad = numeric_keypad + + +class _SlotConfigContext(object): + + def __init__(self, dev, cmd, conf): + st = ykpers.ykds_alloc() + self.cfg = ykpers.ykp_alloc() + try: + check(ykpers.yk_get_status(dev, st)) + ykpers.ykp_configure_version(self.cfg, st) + ykpers.ykp_configure_command(self.cfg, cmd) + except YkpersError: + ykpers.ykp_free_config(self.cfg) + raise + finally: + ykpers.ykds_free(st) + + if conf is None: + conf = SlotConfig() + self._apply(conf) + + def _apply(self, config): + if config.serial_api_visible: + check(ykpers.ykp_set_extflag(self.cfg, 'SERIAL_API_VISIBLE')) + if config.allow_update: + check(ykpers.ykp_set_extflag(self.cfg, 'ALLOW_UPDATE')) + if config.append_cr: + check(ykpers.ykp_set_tktflag(self.cfg, 'APPEND_CR')) + + # Output speed throttling + if config.pacing == 20: + check(ykpers.ykp_set_cfgflag(self.cfg, 'PACING_10MS')) + elif config.pacing == 40: + check(ykpers.ykp_set_cfgflag(self.cfg, 'PACING_20MS')) + elif config.pacing == 60: + check(ykpers.ykp_set_cfgflag(self.cfg, 'PACING_10MS')) + check(ykpers.ykp_set_cfgflag(self.cfg, 'PACING_20MS')) + + if config.numeric_keypad: + check(ykpers.ykp_set_extflag(self.cfg, 'USE_NUMERIC_KEYPAD')) + + def __enter__(self): + return self.cfg + + def __exit__(self, type, value, traceback): + ykpers.ykp_free_config(self.cfg) + + class OtpController(object): def __init__(self, driver): @@ -126,30 +188,19 @@ def access_code(self, value): self._access_code = value - def _create_cfg(self, cmd): - st = ykpers.ykds_alloc() - cfg = ykpers.ykp_alloc() - try: - check(ykpers.yk_get_status(self._dev, st)) - ykpers.ykp_configure_version(cfg, st) - ykpers.ykp_configure_command(cfg, cmd) - check(ykpers.ykp_set_extflag(cfg, 'SERIAL_API_VISIBLE')) - check(ykpers.ykp_set_extflag(cfg, 'ALLOW_UPDATE')) - if self.access_code is not None: - check(ykpers.ykp_set_access_code( - cfg, self.access_code, _ACCESS_CODE_LENGTH)) - return cfg - except YkpersError: - ykpers.ykp_free_config(cfg) - raise - finally: - ykpers.ykds_free(st) + def _create_cfg(self, cmd, conf=None): + context = _SlotConfigContext(self._dev, cmd, conf) + if self.access_code is not None: + check(ykpers.ykp_set_access_code( + context.cfg, self.access_code, _ACCESS_CODE_LENGTH)) + + return context @property def slot_status(self): return self._driver.slot_status - def program_otp(self, slot, key, fixed, uid, append_cr=True): + def program_otp(self, slot, key, fixed, uid, config=None): if len(key) != 16: raise ValueError('key must be 16 bytes') if len(uid) != 6: @@ -158,19 +209,16 @@ raise ValueError('public ID must be <= 16 bytes') cmd = slot_to_cmd(slot) - cfg = self._create_cfg(cmd) - try: + with self._create_cfg(cmd, config) as cfg: check(ykpers.ykp_set_fixed(cfg, fixed, len(fixed))) check(ykpers.ykp_set_uid(cfg, uid, 6)) ykpers.ykp_AES_key_from_raw(cfg, key) - if append_cr: - check(ykpers.ykp_set_tktflag(cfg, 'APPEND_CR')) - check(ykpers.yk_write_command(self._dev, - ykpers.ykp_core_config(cfg), - cmd, self.access_code)) - finally: - ykpers.ykp_free_config(cfg) + check(ykpers.yk_write_command( + self._dev, + ykpers.ykp_core_config(cfg), + cmd, self.access_code + )) def prepare_upload_key(self, key, public_id, private_id, serial=None, user_agent='python-yubikey-manager/' + __version__): @@ -218,8 +266,13 @@ errors = [] raise PrepareUploadFailed(resp.status, resp_body, errors) - def program_static(self, slot, password, append_cr=True, - keyboard_layout=KEYBOARD_LAYOUT.MODHEX): + def program_static( + self, + slot, + password, + keyboard_layout=KEYBOARD_LAYOUT.MODHEX, + config=None + ): pw_len = len(password) if self._driver.version < (2, 0, 0): raise ValueError('static password requires YubiKey 2.0.0 or later') @@ -231,14 +284,9 @@ 'maximum of %d characters' % 38) cmd = slot_to_cmd(slot) - cfg = self._create_cfg(cmd) - try: + with self._create_cfg(cmd, config) as cfg: check(ykpers.ykp_set_cfgflag(cfg, 'SHORT_TICKET')) - - if append_cr: - check(ykpers.ykp_set_tktflag(cfg, 'APPEND_CR')) - pw_bytes = encode(password, keyboard_layout=keyboard_layout) if pw_len <= 16: # All in fixed check(ykpers.ykp_set_fixed(cfg, pw_bytes, pw_len)) @@ -252,10 +300,8 @@ check(ykpers.yk_write_command( self._dev, ykpers.ykp_core_config(cfg), cmd, self.access_code)) - finally: - ykpers.ykp_free_config(cfg) - def program_chalresp(self, slot, key, touch=False): + def program_chalresp(self, slot, key, touch=False, config=None): if self._driver.version < (2, 2, 0): raise ValueError('challenge-response requires YubiKey 2.2.0 or ' 'later') @@ -263,9 +309,9 @@ if len(key) > 20: raise ValueError('key lengths >20 bytes not supported') cmd = slot_to_cmd(slot) - cfg = self._create_cfg(cmd) key = key.ljust(20, b'\0') # Pad key to 20 bytes - try: + + with self._create_cfg(cmd, config) as cfg: check(ykpers.ykp_set_tktflag(cfg, 'CHAL_RESP')) check(ykpers.ykp_set_cfgflag(cfg, 'CHAL_HMAC')) check(ykpers.ykp_set_cfgflag(cfg, 'HMAC_LT64')) @@ -274,8 +320,6 @@ ykpers.ykp_HMAC_key_from_raw(cfg, key) check(ykpers.yk_write_command( self._dev, ykpers.ykp_core_config(cfg), cmd, self.access_code)) - finally: - ykpers.ykp_free_config(cfg) def calculate( self, slot, challenge=None, totp=False, @@ -323,7 +367,7 @@ else: return b2a_hex(resp.raw[:20]) - def program_hotp(self, slot, key, imf=0, hotp8=False, append_cr=True): + def program_hotp(self, slot, key, imf=0, hotp8=False, config=None): if self._driver.version < (2, 1, 0): raise ValueError('HOTP requires YubiKey 2.1.0 or later') key = hmac_shorten_key(key, 'SHA1') @@ -333,20 +377,15 @@ if imf % 16 != 0: raise ValueError('imf must be a multiple of 16') cmd = slot_to_cmd(slot) - cfg = self._create_cfg(cmd) - try: + with self._create_cfg(cmd, config) as cfg: check(ykpers.ykp_set_tktflag(cfg, 'OATH_HOTP')) check(ykpers.ykp_set_oath_imf(cfg, imf)) if hotp8: check(ykpers.ykp_set_cfgflag(cfg, 'OATH_HOTP8')) - if append_cr: - check(ykpers.ykp_set_tktflag(cfg, 'APPEND_CR')) ykpers.ykp_HMAC_key_from_raw(cfg, key) check(ykpers.yk_write_command( self._dev, ykpers.ykp_core_config(cfg), cmd, self.access_code)) - finally: - ykpers.ykp_free_config(cfg) def zap_slot(self, slot): check(ykpers.yk_write_command(self._dev, None, slot_to_cmd(slot), @@ -355,12 +394,10 @@ def swap_slots(self): if self._driver.version < (2, 3, 0): raise ValueError('swapping slots requires YubiKey 2.3.0 or later') - cfg = self._create_cfg(SLOT.SWAP) - try: + + with self._create_cfg(SLOT.SWAP) as cfg: ycfg = ykpers.ykp_core_config(cfg) check(ykpers.yk_write_command(self._dev, ycfg, SLOT.SWAP, None)) - finally: - ykpers.ykp_free_config(cfg) def configure_ndef_slot(self, slot, prefix='https://my.yubico.com/yk/#'): ndef = ykpers.ykp_alloc_ndef() @@ -389,8 +426,7 @@ 'code when initially programming the slot instead.') cmd = slot_to_cmd(slot, update) - cfg = self._create_cfg(cmd) - try: + with self._create_cfg(cmd) as cfg: check(ykpers.ykp_set_access_code( cfg, new_code or _RESET_ACCESS_CODE, _ACCESS_CODE_LENGTH)) ycfg = ykpers.ykp_core_config(cfg) @@ -399,9 +435,6 @@ self.access_code = new_code - finally: - ykpers.ykp_free_config(cfg) - def delete_access_code(self, slot): if self._has_update_access_code_bug: raise ValueError( @@ -411,26 +444,11 @@ self.set_access_code(slot, None) - def update_settings(self, slot, enter=True, pacing=None): + def update_settings(self, slot, config=None): cmd = slot_to_cmd(slot, update=True) - cfg = self._create_cfg(cmd) - if enter: - check(ykpers.ykp_set_tktflag(cfg, 'APPEND_CR')) - - # Output speed throttling - if pacing == 20: - check(ykpers.ykp_set_cfgflag(cfg, 'PACING_10MS')) - elif pacing == 40: - check(ykpers.ykp_set_cfgflag(cfg, 'PACING_20MS')) - elif pacing == 60: - check(ykpers.ykp_set_cfgflag(cfg, 'PACING_10MS')) - check(ykpers.ykp_set_cfgflag(cfg, 'PACING_20MS')) - - try: + with self._create_cfg(cmd, config) as cfg: check(ykpers.yk_write_command( self._dev, ykpers.ykp_core_config(cfg), cmd, self.access_code)) - finally: - ykpers.ykp_free_config(cfg) @property def is_in_fips_mode(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/ykman/piv.py new/yubikey-manager-3.0.0/ykman/piv.py --- old/yubikey-manager-2.1.1/ykman/piv.py 2019-04-09 09:39:55.000000000 +0200 +++ new/yubikey-manager-3.0.0/ykman/piv.py 2019-06-24 09:07:27.000000000 +0200 @@ -846,10 +846,18 @@ ).public_key(default_backend()) elif algorithm in [ALGO.ECCP256, ALGO.ECCP384]: curve = ec.SECP256R1 if algorithm == ALGO.ECCP256 else ec.SECP384R1 - return ec.EllipticCurvePublicNumbers.from_encoded_point( - curve(), - resp[5:] - ).public_key(default_backend()) + + try: + # Added in cryptography 2.5 + return ec.EllipticCurvePublicKey.from_encoded_point( + curve(), + resp[5:] + ) + except AttributeError: + return ec.EllipticCurvePublicNumbers.from_encoded_point( + curve(), + resp[5:] + ).public_key(default_backend()) raise UnsupportedAlgorithm( 'Invalid algorithm: {}'.format(algorithm), @@ -949,7 +957,7 @@ raise KeypairMismatch(slot, certificate) raise - except InvalidSignature as e: + except InvalidSignature: raise KeypairMismatch(slot, certificate) self.put_data(OBJ.from_slot(slot), Tlv(TAG.CERTIFICATE, cert_data) + @@ -1010,10 +1018,10 @@ def update_chuid(self): # Non-Federal Issuer FASC-N # [9999-9999-999999-0-1-0000000000300001] - FASC_N=b'\xd4\xe7\x39\xda\x73\x9c\xed\x39\xce\x73\x9d\x83\x68' + \ - b'\x58\x21\x08\x42\x10\x84\x21\xc8\x42\x10\xc3\xeb' + FASC_N = b'\xd4\xe7\x39\xda\x73\x9c\xed\x39\xce\x73\x9d\x83\x68' + \ + b'\x58\x21\x08\x42\x10\x84\x21\xc8\x42\x10\xc3\xeb' # Expires on: 2030-01-01 - EXPIRY=b'\x32\x30\x33\x30\x30\x31\x30\x31' + EXPIRY = b'\x32\x30\x33\x30\x30\x31\x30\x31' self.put_data( OBJ.CHUID, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/ykman/util.py new/yubikey-manager-3.0.0/ykman/util.py --- old/yubikey-manager-2.1.1/ykman/util.py 2019-05-27 10:21:11.000000000 +0200 +++ new/yubikey-manager-3.0.0/ykman/util.py 2019-06-24 09:07:27.000000000 +0200 @@ -111,6 +111,7 @@ USB_A_NANO = 0x02 USB_C_KEYCHAIN = 0x03 USB_C_NANO = 0x04 + USB_C_LIGHTNING = 0x05 def __str__(self): if self == FORM_FACTOR.USB_A_KEYCHAIN: @@ -121,6 +122,8 @@ return 'Keychain (USB-C)' elif self == FORM_FACTOR.USB_C_NANO: return 'Nano (USB-C)' + elif self == FORM_FACTOR.USB_C_LIGHTNING: + return 'Keychain (USB-C, Lightning)' elif self == FORM_FACTOR.UNKNOWN: return 'Unknown.' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/yubikey_manager.egg-info/PKG-INFO new/yubikey-manager-3.0.0/yubikey_manager.egg-info/PKG-INFO --- old/yubikey-manager-2.1.1/yubikey_manager.egg-info/PKG-INFO 2019-05-28 10:05:25.000000000 +0200 +++ new/yubikey-manager-3.0.0/yubikey_manager.egg-info/PKG-INFO 2019-06-24 09:12:28.000000000 +0200 @@ -1,13 +1,12 @@ -Metadata-Version: 1.2 +Metadata-Version: 1.1 Name: yubikey-manager -Version: 2.1.1 +Version: 3.0.0 Summary: Tool for managing your YubiKey configuration. Home-page: https://github.com/Yubico/yubikey-manager -Author: Dain Nilsson -Author-email: dain@yubico.com -Maintainer: Yubico Open Source Maintainers -Maintainer-email: ossmaint@yubico.com +Author: Yubico Open Source Maintainers +Author-email: ossmaint@yubico.com License: BSD 2 clause +Description-Content-Type: UNKNOWN Description: UNKNOWN Platform: UNKNOWN Classifier: License :: OSI Approved :: BSD License diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/yubikey-manager-2.1.1/yubikey_manager.egg-info/SOURCES.txt new/yubikey-manager-3.0.0/yubikey_manager.egg-info/SOURCES.txt --- old/yubikey-manager-2.1.1/yubikey_manager.egg-info/SOURCES.txt 2019-05-28 10:05:25.000000000 +0200 +++ new/yubikey-manager-3.0.0/yubikey_manager.egg-info/SOURCES.txt 2019-06-24 09:12:28.000000000 +0200 @@ -14,6 +14,14 @@ test/test_scancodes.py test/test_util.py test/util.py +test/__pycache__/__init__.cpython-36.pyc +test/__pycache__/test_device.cpython-36.pyc +test/__pycache__/test_external_libs.cpython-36.pyc +test/__pycache__/test_oath.cpython-36.pyc +test/__pycache__/test_piv.cpython-36.pyc +test/__pycache__/test_scancodes.cpython-36.pyc +test/__pycache__/test_util.cpython-36.pyc +test/__pycache__/util.cpython-36.pyc test/files/rsa_1024_key.pem test/files/rsa_2048_cert.der test/files/rsa_2048_cert.pem @@ -32,6 +40,16 @@ test/on_yubikey/test_interfaces.py test/on_yubikey/test_piv.py test/on_yubikey/util.py +test/on_yubikey/__pycache__/__init__.cpython-36.pyc +test/on_yubikey/__pycache__/test_cli_config.cpython-36.pyc +test/on_yubikey/__pycache__/test_cli_misc.cpython-36.pyc +test/on_yubikey/__pycache__/test_cli_oath.cpython-36.pyc +test/on_yubikey/__pycache__/test_cli_openpgp.cpython-36.pyc +test/on_yubikey/__pycache__/test_cli_otp.cpython-36.pyc +test/on_yubikey/__pycache__/test_fips_u2f_commands.cpython-36.pyc +test/on_yubikey/__pycache__/test_interfaces.cpython-36.pyc +test/on_yubikey/__pycache__/test_piv.cpython-36.pyc +test/on_yubikey/__pycache__/util.cpython-36.pyc test/on_yubikey/cli_piv/__init__.py test/on_yubikey/cli_piv/test_fips.py test/on_yubikey/cli_piv/test_generate_cert_and_csr.py @@ -40,6 +58,14 @@ test/on_yubikey/cli_piv/test_misc.py test/on_yubikey/cli_piv/test_pin_puk.py test/on_yubikey/cli_piv/util.py +test/on_yubikey/cli_piv/__pycache__/__init__.cpython-36.pyc +test/on_yubikey/cli_piv/__pycache__/test_fips.cpython-36.pyc +test/on_yubikey/cli_piv/__pycache__/test_generate_cert_and_csr.cpython-36.pyc +test/on_yubikey/cli_piv/__pycache__/test_key_management.cpython-36.pyc +test/on_yubikey/cli_piv/__pycache__/test_management_key.cpython-36.pyc +test/on_yubikey/cli_piv/__pycache__/test_misc.cpython-36.pyc +test/on_yubikey/cli_piv/__pycache__/test_pin_puk.cpython-36.pyc +test/on_yubikey/cli_piv/__pycache__/util.cpython-36.pyc ykman/VERSION ykman/__init__.py ykman/descriptor.py
participants (1)
-
root