Commit 1cbd2e34 authored by nicoo's avatar nicoo

New upstream version 2.1.0

parent f3a130d6
......@@ -5,4 +5,6 @@ include resources/*
include doc/*.adoc
recursive-include test *
include README.adoc
include man/*
include ykman/VERSION
recursive-exclude *.pyc
* Version 2.0.0 (unreleased)
* Version 2.1.0 (released 2019-03-11)
** Add --reader flag to ykman list, to list available smart card readers
** FIPS: Checking if a YubiKey FIPS is in FIPS mode is now opt-in, with the --check-fips flag
** PIV: Add commands for writing and reading arbitrary PIV objects
** PIV: Verify that the PIN must be between 6 - 8 characters long
** PIV: In import-certificate, make the verification that the certificate and private key matches opt-in, with the --verify flag
** PIV: The piv info command now shows the serial number of the certificates
** PIV: The piv info command now shows the full Distinguished Name (DN) of the certificate subject and issuer, if possible
** PIV: Malformed certificates are now handled better
** OpenPGP: The openpgp touch command now shows current touch policies
** The ykman usb/nfc config command now accepts openpgp as well as opgp as an argument
** Bugfix: Fix support for german (DE) keyboard layout for static passwords
* Version 2.0.0 (released 2019-01-09)
** Add support for Security Key NFC
** Add experimental support for external smart card reader. See --reader flag
** Add a minimal manpage
......
Metadata-Version: 1.2
Metadata-Version: 1.1
Name: yubikey-manager
Version: 2.0.0
Version: 2.1.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
......
......@@ -6,17 +6,27 @@ Python library and command line tool for configuring a YubiKey. If you're lookin
=== Usage
For more usage information and examples, see the https://support.yubico.com/support/solutions/articles/15000012643-yubikey-manager-cli-ykman-user-guide[YubiKey Manager CLI User Manual].
....
Usage: ykman [OPTIONS] COMMAND [ARGS]...
Configure your YubiKey via the command line.
Examples:
List connected YubiKeys, only output serial number:
$ ykman list --serials
Show information about YubiKey with serial number 0123456:
$ ykman --device 0123456 info
Options:
-v, --version
-d, --device SERIAL
-l, --log-level [DEBUG|INFO|WARNING|ERROR|CRITICAL]
Enable logging at given verbosity level.
--log-file FILE Write logs to the given FILE instead of standard error; ignored unless --log-level is also set.
-r, --reader NAME Use an external smart card reader. Conflicts with --device and list.
-h, --help Show this message and exit.
Commands:
......
......@@ -64,3 +64,7 @@ WARNING: ONLY run these on a dedicated developer key, as it will permanently del
To run integration tests, indicate the serial number (given by `ykman list`) of the YubiKey to test with:
$ DESTRUCTIVE_TEST_YUBIKEY_SERIAL=123456 python setup.py test
=== Packaging
For third-party packaging, use the source releases and signatures available https://developers.yubico.com/yubikey-manager/Releases/[here].
.TH YKMAN "1" "March 2019" "ykman 2.1.0" "User Commands"
.SH NAME
ykman \- YubiKey Manager (ykman)
.SH SYNOPSIS
.B ykman
[\fI\,OPTIONS\/\fR] \fI\,COMMAND \/\fR[\fI\,ARGS\/\fR]...
.SH DESCRIPTION
.PP
Configure your YubiKey via the command line.
.SH OPTIONS
.HP
\fB\-v\fR, \fB\-\-version\fR
.HP
\fB\-d\fR, \fB\-\-device\fR SERIAL
.TP
\fB\-l\fR, \fB\-\-log\-level\fR [DEBUG|INFO|WARNING|ERROR|CRITICAL]
Enable logging at given verbosity level.
.TP
\fB\-\-log\-file\fR FILE
Write logs to the given FILE instead of
standard error; ignored unless \fB\-\-log\-level\fR
is also set.
.TP
\fB\-r\fR, \fB\-\-reader\fR NAME
Use an external smart card reader. Conflicts
with \fB\-\-device\fR and list.
.TP
\fB\-h\fR, \fB\-\-help\fR
Show this message and exit.
.SS "Commands:"
.TP
config
Enable/Disable applications.
.TP
fido
Manage FIDO applications.
.TP
info
Show general information.
.TP
list
List connected YubiKeys.
.TP
mode
Manage connection modes (USB Interfaces).
.TP
oath
Manage OATH Application.
.TP
openpgp
Manage OpenPGP Application.
.TP
otp
Manage OTP Application.
.TP
piv
Manage PIV Application.
.SH EXAMPLES
.PP
List connected YubiKeys, only output serial number:
.PP
$ ykman list --serials
.PP
Show information about YubiKey with serial number 0123456:
.PP
$ ykman --device 0123456 info
......@@ -182,7 +182,7 @@ class KeyManagement(PivTestCase):
ykman_cli(
'piv', 'import-certificate', '9a', '-',
'-m', DEFAULT_MANAGEMENT_KEY, '-P', DEFAULT_PIN, '--no-verify',
'-m', DEFAULT_MANAGEMENT_KEY, '-P', DEFAULT_PIN,
input=cert_pem)
@unittest.skipIf(*no_attestation)
......
from ..util import ykman_cli
from .util import PivTestCase
from .util import PivTestCase, DEFAULT_MANAGEMENT_KEY
class Misc(PivTestCase):
......@@ -11,3 +10,11 @@ class Misc(PivTestCase):
def test_reset(self):
output = ykman_cli('piv', 'reset', '-f')
self.assertIn('Success!', output)
def test_write_read_object(self):
ykman_cli(
'piv', 'write-object',
'-m', DEFAULT_MANAGEMENT_KEY,'0x5f0001',
'-', input='test data')
output = ykman_cli('piv', 'read-object', '0x5f0001')
self.assertEquals('test data\n', output)
......@@ -30,6 +30,11 @@ class TestConfigUSB(DestructiveYubikeyTestCase):
output = ykman_cli('config', 'usb', '--list')
self.assertNotIn('OpenPGP', output)
def test_disable_openpgp_alternative_syntax(self):
ykman_cli('config', 'usb', '--disable', 'openpgp', '-f')
output = ykman_cli('config', 'usb', '--list')
self.assertNotIn('OpenPGP', output)
def test_disable_piv(self):
ykman_cli('config', 'usb', '--disable', 'PIV', '-f')
output = ykman_cli('config', 'usb', '--list')
......
......@@ -13,12 +13,12 @@ class TestYkmanInfo(DestructiveYubikeyTestCase):
@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')
info = ykman_cli('info --check-fips')
self.assertNotIn('FIPS', info)
@unittest.skipIf(not is_fips(), 'YubiKey FIPS required.')
def test_ykman_info_reports_fips_status(self):
info = ykman_cli('info')
info = ykman_cli('info', '--check-fips')
self.assertIn('FIPS Approved Mode:', info)
self.assertIn(' FIDO U2F:', info)
self.assertIn(' OATH:', info)
......
......@@ -31,10 +31,9 @@ class TestUtilityFunctions(unittest.TestCase):
def test_generate_static_pw(self):
for l in range(0, 38):
self.assertRegex(generate_static_pw(l),
b'^[cbdefghijklnrtuvCBDEFGHIJKLNRTUV]{' +
'{:d}'.format(l).encode('ascii') +
b'}$')
self.assertRegex(
generate_static_pw(l),
'^[cbdefghijklnrtuvCBDEFGHIJKLNRTUV]{' + '{:d}'.format(l) + '}$')
def test_hmac_shorten_key(self):
self.assertEqual(b'short', hmac_shorten_key(b'short', 'sha1'))
......
......@@ -32,7 +32,7 @@ from ykman import __version__
from ..util import TRANSPORT, Cve201715361VulnerableError, YUBIKEY
from ..native.pyusb import get_usb_backend_version
from ..driver_otp import libversion as ykpers_version
from ..driver_ccid import open_devices as open_ccid
from ..driver_ccid import open_devices as open_ccid, list_readers
from ..device import YubiKey
from ..descriptor import (get_descriptors, list_devices, open_device,
FailedOpeningDeviceException, Descriptor)
......@@ -194,11 +194,18 @@ def cli(ctx, device, log_level, log_file, reader):
@cli.command('list')
@click.option('-s', '--serials', is_flag=True, help='Output only serial '
'numbers, one per line (devices without serial will be omitted).')
@click.option(
'-r', '--readers', is_flag=True, help='List available smart card readers.')
@click.pass_context
def list_keys(ctx, serials):
def list_keys(ctx, serials, readers):
"""
List connected YubiKeys.
"""
if readers:
for reader in list_readers():
click.echo(reader.name)
ctx.exit()
all_descriptors = get_descriptors()
descriptors = [d for d in all_descriptors if d.key_type != YUBIKEY.SKY]
skys = len(all_descriptors) - len(descriptors)
......
......@@ -42,6 +42,15 @@ logger = logging.getLogger(__name__)
CLEAR_LOCK_CODE = '0' * 32
class ApplicationsChoice(UpperCaseChoice):
"""
Special version of UpperCaseChoice that accepts openpgp as OPGP
"""
def convert(self, value, param, ctx):
return 'OPGP' if value.lower() == 'openpgp' \
else super().convert(value, param, ctx)
def prompt_lock_code(prompt='Enter your lock code'):
return click.prompt(
prompt, default='', hide_input=True, show_default=False, err=True)
......@@ -174,10 +183,10 @@ def set_lock_code(ctx, lock_code, new_lock_code, clear, generate, force):
@click.pass_context
@click_force_option
@click.option(
'-e', '--enable', multiple=True, type=UpperCaseChoice(
'-e', '--enable', multiple=True, type=ApplicationsChoice(
APPLICATION.__members__.keys()), help='Enable applications.')
@click.option(
'-d', '--disable', multiple=True, type=UpperCaseChoice(
'-d', '--disable', multiple=True, type=ApplicationsChoice(
APPLICATION.__members__.keys()), help='Disable applications.')
@click.option('-l', '--list', is_flag=True, help='List enabled applications.')
@click.option(
......@@ -307,10 +316,10 @@ def usb(
@click.pass_context
@click_force_option
@click.option(
'-e', '--enable', multiple=True, type=UpperCaseChoice(
'-e', '--enable', multiple=True, type=ApplicationsChoice(
APPLICATION.__members__.keys()), help='Enable applications.')
@click.option(
'-d', '--disable', multiple=True, type=UpperCaseChoice(
'-d', '--disable', multiple=True, type=ApplicationsChoice(
APPLICATION.__members__.keys()), help='Disable applications.')
@click.option(
'-a', '--enable-all', is_flag=True, help='Enable all applications.')
......
......@@ -121,9 +121,13 @@ def get_overall_fips_status(serial, config):
return statuses
@click.option(
'-c', '--check-fips',
help='Check if YubiKey is in FIPS Approved mode.',
is_flag=True)
@click.command()
@click.pass_context
def info(ctx):
def info(ctx, check_fips):
"""
Show general information.
......@@ -132,7 +136,7 @@ def info(ctx):
"""
dev = ctx.obj['dev']
if dev.is_fips:
if dev.is_fips and check_fips:
fips_status = get_overall_fips_status(dev.serial, dev.config)
click.echo('Device type: {}'.format(dev.device_name))
......@@ -158,7 +162,7 @@ def info(ctx):
print_app_status_table(config)
if dev.is_fips:
if dev.is_fips and check_fips:
click.echo()
click.echo('FIPS Approved Mode: {}'.format(
......
......@@ -39,15 +39,15 @@ logger = logging.getLogger(__name__)
KEY_NAMES = dict(
sig=KEY_SLOT.SIGN,
enc=KEY_SLOT.ENCRYPT,
aut=KEY_SLOT.AUTHENTICATE
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.ON_FIXED
fixed=TOUCH_MODE.FIXED
)
......@@ -116,6 +116,17 @@ def info(ctx):
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)))
@openpgp.command()
......@@ -143,10 +154,10 @@ def echo_default_pins():
@openpgp.command()
@click.argument('key', type=click.Choice(sorted(KEY_NAMES)),
@click.argument('key', metavar='KEY', type=click.Choice(sorted(KEY_NAMES)),
callback=lambda c, p, k: KEY_NAMES.get(k))
@click.argument('policy', type=click.Choice(sorted(MODE_NAMES)),
callback=lambda c, p, k: MODE_NAMES.get(k), required=False)
@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.')
@click_force_option
......@@ -156,17 +167,13 @@ def touch(ctx, key, policy, admin_pin, force):
Manage touch policy for OpenPGP keys.
\b
KEY Key slot to get/set (sig, enc or aut).
KEY Key slot to set (sig, enc or aut).
POLICY Touch policy to set (on, off or fixed).
"""
controller = ctx.obj['controller']
old_policy = controller.get_touch(key)
click.echo('Current touch policy of {.name} key is {.name}.'.format(
key, old_policy))
if policy is None:
return
if old_policy == TOUCH_MODE.ON_FIXED:
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(
......@@ -174,7 +181,6 @@ def touch(ctx, key, policy, admin_pin, force):
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'))
click.echo('Touch policy successfully set.')
@openpgp.command('set-pin-retries')
......
......@@ -362,7 +362,7 @@ def static(
if not password and not generate:
password = click.prompt('Enter a static password', err=True)
elif not password and generate:
password = generate_static_pw(length, keyboard_layout).decode()
password = generate_static_pw(length, keyboard_layout)
if not force:
_confirm_slot_overwrite(controller, slot)
......
This diff is collapsed.
......@@ -32,9 +32,7 @@ import click
import sys
from ..util import parse_b32_key
from collections import OrderedDict, MutableMapping
click_force_option = click.option('-f', '--force', is_flag=True,
help='Confirm the action without prompting.')
from cryptography.hazmat.primitives import serialization
class UpperCaseChoice(click.Choice):
......@@ -68,6 +66,26 @@ def click_callback(invoke_on_missing=False):
return wrap
@click_callback()
def click_parse_format(ctx, param, val):
if val == 'PEM':
return serialization.Encoding.PEM
elif val == 'DER':
return serialization.Encoding.DER
else:
raise ValueError(val)
click_force_option = click.option(
'-f', '--force', is_flag=True, help='Confirm the action without prompting.')
click_format_option = click.option(
'-F', '--format',
type=UpperCaseChoice(['PEM', 'DER']), default='PEM', show_default=True,
help='Encoding format.', callback=click_parse_format)
class YkmanContextObject(MutableMapping):
def __init__(self):
self._objects = OrderedDict()
......@@ -81,7 +99,7 @@ class YkmanContextObject(MutableMapping):
def resolve(self):
if not self._resolved:
self._resolved = True
for k, f in self._objects.items():
for k, f in self._objects.copy().items():
self._objects[k] = f()
def __getitem__(self, key):
......
......@@ -307,7 +307,7 @@ def kill_scdaemon():
return killed
def _list_readers():
def list_readers():
try:
return System.readers()
except ListReadersException:
......@@ -319,11 +319,11 @@ def _list_readers():
def open_devices(name_filter=YK_READER_NAME):
readers = _list_readers()
readers = list_readers()
while readers:
try_again = []
for reader in readers:
if reader.name.lower().startswith(name_filter.lower()):
if name_filter.lower() in reader.name.lower():
try:
conn = reader.createConnection()
conn.connect()
......
......@@ -41,6 +41,7 @@ CONFIG2_VALID = 0x02
CMD_VERIFY_FIPS_MODE = 0x14
MISSING_LIBYKPERS_MSG = 'libykpers not found, OTP functionality not available'
try:
ykpers = Ykpers('ykpers-1', '1')
......@@ -50,8 +51,7 @@ try:
.decode('ascii').split('.'))
except Exception as e:
logger.error('libykpers not found', exc_info=e)
ykpers = MissingLibrary(
'libykpers not found, slot functionality not available!')
ykpers = MissingLibrary(MISSING_LIBYKPERS_MSG)
libversion = None
......@@ -210,6 +210,9 @@ class OTPDriver(AbstractDriver):
def open_devices():
if not libversion:
logger.error(MISSING_LIBYKPERS_MSG)
return
if libversion < (1, 18):
yield OTPDriver(ykpers.yk_open_first_key())
else:
......
......@@ -37,16 +37,16 @@ from collections import namedtuple
@unique
class KEY_SLOT(IntEnum): # noqa: N801
SIGN = 0xd6
ENCRYPT = 0xd7
AUTHENTICATE = 0xd8
SIGNATURE = 0xd6
ENCRYPTION = 0xd7
AUTHENTICATION = 0xd8
@unique
class TOUCH_MODE(IntEnum): # noqa: N801
OFF = 0x00
ON = 0x01
ON_FIXED = 0x02
FIXED = 0x02
@unique
......
......@@ -810,7 +810,7 @@ class PivController(object):
id_bytes = struct.pack(b'>I', object_id).lstrip(b'\0')
tlv = Tlv(self.send_cmd(INS.GET_DATA, 0x3f, 0xff,
Tlv(TAG.OBJ_ID, id_bytes)))
if tlv.tag != TAG.OBJ_DATA:
if tlv.tag not in [TAG.OBJ_DATA, OBJ.DISCOVERY]:
raise ValueError('Wrong tag in response data!')
return tlv.value
......@@ -912,7 +912,8 @@ class PivController(object):
self.send_cmd(INS.IMPORT_KEY, algorithm, slot, data)
return algorithm
def import_certificate(self, slot, certificate, verify=False):
def import_certificate(
self, slot, certificate, verify=False, touch_callback=None):
cert_data = certificate.public_bytes(Encoding.DER)
if verify:
......@@ -922,9 +923,17 @@ class PivController(object):
public_key = certificate.public_key()
test_data = b'test'
if touch_callback is not None:
touch_timer = Timer(0.500, touch_callback)
touch_timer.start()
test_sig = self.sign(
slot, ALGO.from_public_key(public_key), test_data)
if touch_callback is not None:
touch_timer.cancel()
if isinstance(public_key, rsa.RSAPublicKey):
public_key.verify(
test_sig, test_data, padding.PKCS1v15(),
......
......@@ -27,11 +27,11 @@
from __future__ import absolute_import
import os
import six
import struct
import re
import logging
import random
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.backends import default_backend
from cryptography import x509
......@@ -94,11 +94,11 @@ class APPLICATION(BitflagEnum):
return APPLICATION.OPGP | APPLICATION.OATH | APPLICATION.PIV
def __str__(self):
if self == self.U2F:
if self == APPLICATION.U2F:
return 'FIDO U2F'
elif self == self.FIDO2:
elif self == APPLICATION.FIDO2:
return 'FIDO2'
elif self == self.OPGP:
elif self == APPLICATION.OPGP:
return 'OpenPGP'
else:
return self.name
......@@ -113,15 +113,15 @@ class FORM_FACTOR(IntEnum):
USB_C_NANO = 0x04
def __str__(self):
if self == self.USB_A_KEYCHAIN:
if self == FORM_FACTOR.USB_A_KEYCHAIN:
return 'Keychain (USB-A)'
elif self == self.USB_A_NANO:
elif self == FORM_FACTOR.USB_A_NANO:
return 'Nano (USB-A)'
elif self == self.USB_C_KEYCHAIN:
elif self == FORM_FACTOR.USB_C_KEYCHAIN:
return 'Keychain (USB-C)'
elif self == self.USB_C_NANO:
elif self == FORM_FACTOR.USB_C_NANO:
return 'Nano (USB-C)'
elif self == self.UNKNOWN:
elif self == FORM_FACTOR.UNKNOWN:
return 'Unknown.'
@classmethod
......@@ -351,14 +351,12 @@ def modhex_encode(value):
def generate_static_pw(
length, keyboard_layout=KEYBOARD_LAYOUT.MODHEX,
length,
keyboard_layout=KEYBOARD_LAYOUT.MODHEX,
blacklist=DEFAULT_PW_CHAR_BLACKLIST):
data = os.urandom(length)
keys = ''.join([
k for k in keyboard_layout.value.keys() if k not in blacklist]).encode()
return bytes(
bytearray(six.indexbytes(
keys, d % len(keys)) for d in six.iterbytes(data)))
chars = [k for k in keyboard_layout.value.keys() if k not in blacklist]
sr = random.SystemRandom()
return ''.join([sr.choice(chars) for _ in range(length)])
def format_code(code, digits=6, steam=False):
......@@ -473,7 +471,9 @@ def parse_certificates(data, password):
PEM_IDENTIFIER + cert, default_backend()))
except Exception:
pass
return certs
# Could be valid PEM but not certificates.
if len(certs) > 0:
return certs
# PKCS12
if is_pkcs12(data):
......
Metadata-Version: 1.2
Metadata-Version: 1.1
Name: yubikey-manager
Version: 2.0.0
Version: 2.1.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
......
......@@ -5,6 +5,7 @@ README.adoc
setup.cfg
setup.py
doc/development.adoc
man/ykman.1
test/__init__.py
test/test_device.py
test/test_external_libs.py
......@@ -13,6 +14,14 @@ test/test_piv.py
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
......@@ -31,6 +40,16 @@ test/on_yubikey/test_fips_u2f_commands.py