├── .gitattributes ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── .travis.yml ├── COPYING ├── MANIFEST.in ├── NEWS ├── README ├── README.adoc ├── build-windows.bat ├── doc ├── Certificates.adoc ├── Device_Setup.adoc ├── PIN_and_Management_Key.adoc ├── Settings_and_Group_Policy.adoc └── development.adoc ├── man └── pivman.1 ├── pivman ├── __init__.py ├── __main__.py ├── controller.py ├── libykpiv.py ├── messages.py ├── piv.py ├── piv_cmd.py ├── storage.py ├── utils.py ├── view │ ├── __init__.py │ ├── cert.py │ ├── generate_dialog.py │ ├── init_dialog.py │ ├── main.py │ ├── manage.py │ ├── set_key_dialog.py │ ├── set_pin_dialog.py │ ├── settings_dialog.py │ ├── usage_policy_dialog.py │ └── utils.py ├── watcher.py └── yubicommon ├── qt_resources └── pivman.png ├── release-windows.bat ├── resources ├── installer_bg.png ├── osx-installer.pkgproj ├── pivman.desktop ├── pivman.xpm ├── win-installer.nsi ├── yubikey-piv-manager.icns ├── yubikey-piv-manager.ico └── yubikey-piv-manager.png ├── screenshot.png ├── setup.cfg ├── setup.py ├── test └── __init__.py └── vagrant ├── build-windows ├── Vagrantfile └── provision.bat ├── development ├── Vagrantfile └── provision.sh └── windows-user ├── README.md └── Vagrantfile /.gitattributes: -------------------------------------------------------------------------------- 1 | # By default, normalise line endings if it looks like a text file 2 | * text=auto 3 | 4 | # Force a specific line ending style for these file names 5 | *.sh text eol=lf 6 | *.bat text eol=crlf 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | *.egg/ 4 | .eggs/ 5 | build/ 6 | dist/ 7 | lib/ 8 | .ropeproject/ 9 | ChangeLog 10 | MANIFEST 11 | pivman/qt_resources.py 12 | .DS_Store 13 | /vagrant/*/.vagrant/ 14 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/yubicommon"] 2 | path = vendor/yubicommon 3 | url = https://github.com/Yubico/python-yubicommon.git 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: git://github.com/pre-commit/pre-commit-hooks 2 | sha: v0.8.0 3 | hooks: 4 | - id: flake8 5 | - id: double-quote-string-fixer 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | 4 | python: 5 | - "2.7" 6 | - "3.3" 7 | - "3.4" 8 | # - "3.5" PySide does not support Python 3.5. 9 | 10 | cache: 11 | directories: 12 | - $HOME/.cache/pip 13 | 14 | install: 15 | - pip install --disable-pip-version-check --upgrade pip 16 | - pip install flake8 17 | - pip install -e . 18 | 19 | before_script: 20 | - flake8 21 | 22 | script: 23 | - python setup.py test 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include COPYING 2 | include NEWS 3 | include ChangeLog 4 | include screenshot.png 5 | include resources/* 6 | include qt_resources/* 7 | include doc/*.adoc 8 | include man/* 9 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | * Version 1.4.2h (released 2018-09-28) 2 | ** Re-release for Mac because of hard coded path in installation package. 3 | 4 | * Version 1.4.2g (released 2018-09-26) 5 | ** Re-release for Mac and Windows with bundled yubico-piv-tool upgraded to version 1.6.2 to support the YubiKey 5 Series. 6 | 7 | * Version 1.4.2f (released 2018-08-08) 8 | ** Re-release for Mac and Windows with bundled yubico-piv-tool upgraded to version 1.6.0 to mitigate https://www.yubico.com/support/security-advisories/ysa-2018-03/[YSA-2018-03] 9 | 10 | * Version 1.4.2e (released 2017-10-25) 11 | ** Re-release for Mac with bundled yubico-piv-tool upgraded to version 1.4.4 to mitigate http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15361[CVE-2017-15361] 12 | 13 | * Version 1.4.2d (released 2017-10-25) 14 | ** Re-release for Windows with bundled yubico-piv-tool upgraded to version 1.4.4 to mitigate http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15361[CVE-2017-15361] 15 | 16 | * Version 1.4.2c (released 2017-07-04) 17 | ** A new installer for Windows, fixing an issue with start menu shortcuts on Windows 10. 18 | 19 | * Version 1.4.2b (released 2017-04-24) 20 | ** A new installer for macOS, due to some issues with how a dependency was being built, causing the application to crash randomly. 21 | 22 | * Version 1.4.2 (released 2017-04-19) 23 | ** PIN, PUK, management key and certificate passwords are now passed to yubico-piv-tool using 24 | standard input instead of as CLI arguments. This prevents them from being logged by some 25 | operating system auditing tools. 26 | 27 | * Version 1.4.1 (released 2016-09-30) 28 | ** Now displays a confirmation dialog box when overwriting certificate data in a slot (including in the macOS Sierra wizard). 29 | ** On initialization, now shows a warning when the PIN or PUK has been changed in another tool. 30 | ** It is now possible to close the initialization and the expired PIN dialog boxes without closing the application. 31 | ** Bugfix: Connecting to a Certificate Authority should now work as expected (even over a network). 32 | ** Bugfix: Layout in the initialization dialog box was not acting as expected when the radio buttons were clicked. 33 | 34 | * Version 1.4.0 (released 2016-09-20) 35 | ** New wizard for setting up a YubiKey for pairing with macOS Sierra, providing an easy way to generate certificates in the authentication and key management slots. After the YubiKey is paired, macOS Sierra will recognize the YubiKey and allow you to pair it with an account on macOS. 36 | ** Improved appearance on Retina displays. 37 | ** More specific error messages when trying to import a certificate that is too large. 38 | ** Bugfix: Connecting to a Certificate Authority should now work again. 39 | ** Removed touch policy that was erroneously added in the previous version. 40 | 41 | * Version 1.3.0 (released 2016-08-18) 42 | ** Updated to be inline with yubico-piv-tool 1.4.2. 43 | ** Allow generation of a authentication certificate when initialising a new YubiKey. 44 | ** Set the CCC when initialising a new YubiKey. 45 | ** Show an option for expiration date when generating new certificates. 46 | ** Add support for importing PEM files that contains both a private key and a certificate. 47 | ** Bugfix: Connecting to a Certificate Authority on a localised Windows environment should now work. 48 | ** Add compability with Python 3. 49 | 50 | * Version 1.2.1 (released 2015-12-08) 51 | ** Bugfix: The device initialization dialog was not being shown. 52 | 53 | * Version 1.2.0 (released 2015-12-07) 54 | ** Packaging improvements. 55 | ** Don't expose some advanced functions (certificate PIN/touch policy) by 56 | default, as they are likely to cause confusion. 57 | 58 | * Version 1.1.1 (released 2015-11-16) 59 | ** Better handling of intermittent device disconnects. 60 | 61 | * Version 1.1.0 (released 2015-11-12) 62 | ** Added support for ECC P-384 when supported by the device. 63 | ** Added usage policy when supported by the device. 64 | ** Update to libykpiv1. 65 | 66 | * Version 1.0.2 (released 2015-05-18) 67 | ** Fix bug preventing device from being found with multiple card readers attached. 68 | ** Don't save window position as it causes problems with multi-monitor setups. 69 | 70 | * Version 1.0.1 (released 2015-04-16) 71 | ** Packaging improvements release, no new features. 72 | 73 | * Version 1.0.0 (released 2015-04-14) 74 | ** Initial version. 75 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | == YubiKey PIV Manager 2 | image:https://travis-ci.org/Yubico/yubikey-piv-manager.svg?branch=master["Build Status", link="https://travis-ci.org/Yubico/yubikey-piv-manager"] 3 | 4 | Graphical application for configuring a PIV-enabled YubiKey. 5 | 6 | NOTE: This project is deprecated and is no longer being maintained. Use YubiKey Manager (https://developers.yubico.com/yubikey-manager-qt/[GUI], https://developers.yubico.com/yubikey-manager/[CLI]) to configure a YubiKey device. 7 | 8 | image::screenshot.png[] 9 | 10 | === Installation 11 | The recommended way to install this software including dependencies is by using 12 | the provided precompiled binaries for your platform. For Windows and OS X (10.7 and above), 13 | there are installers available for download 14 | https://developers.yubico.com/yubikey-piv-manager/Releases/[here]. 15 | For Ubuntu we have a custom PPA with a package for it 16 | https://launchpad.net/~yubico/+archive/ubuntu/stable[here]. 17 | 18 | 19 | ==== Building from source (Linux) 20 | 21 | 1. Install build dependencies: 22 | 23 | - Python 2 `setuptools` library 24 | - http://www.pyside.org/[PySide], Pyside tools and PySide development tools 25 | - https://cmake.org/[CMake] 26 | - https://doc.qt.io/archives/qt-4.8/[Qt 4] development tools 27 | - https://gcc.gnu.org/[`gcc` and `g++`] 28 | 29 | 2. Build and install: 30 | 31 | - `$ python setup.py qt_resources` 32 | - `$ sudo python setup.py install` 33 | 34 | 3. Install runtime dependencies: 35 | 36 | - https://developers.yubico.com/yubico-piv-tool/[Yubico PIV Tool] 37 | 38 | 39 | === Usage guides 40 | For information and examples on what you can do with a PIV enabled YubiKey, 41 | see https://developers.yubico.com/PIV/ 42 | 43 | 44 | === Known issues 45 | 46 | ==== Deleting certificates 47 | Note that the dialog showed in PIV Manager when deleting a certificate currently states 48 | that the private key is being deleted as well. That is not the case, only the certificate is deleted. 49 | To make sure the private key is destroyed, it is recommended to generate a new private key in the same slot or 50 | to https://support.yubico.com/support/solutions/articles/15000008587-resetting-the-smart-card-piv-applet-on-your-yubikey[reset the PIV application]. 51 | 52 | Thanks to Max from Max Tech Labs for pointing this out. 53 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | README -------------------------------------------------------------------------------- /build-windows.bat: -------------------------------------------------------------------------------- 1 | SET "PATH=%PATH%;C:\Program Files (x86)\Common Files\Microsoft\Visual C++ for Python\9.0\WinSDK\Bin" 2 | SET "PATH=%PATH%;C:\Program Files (x86)\NSIS" 3 | SET "PATH=%PATH%;C:\Python27\Lib\site-packages\PySide-1.2.4-py2.7-win-amd64.egg\PySide" 4 | SET "PIVTOOL_VERSION=1.4.4" 5 | 6 | REM Download yubico-piv-tool DLLs 7 | REM powershell -Command "(New-Object Net.WebClient).DownloadFile('https://developers.yubico.com/yubico-piv-tool/Releases/yubico-piv-tool-%PIVTOOL_VERSION%-win32.zip', 'yubico-piv-tool-%PIVTOOL_VERSION%-win32.zip')" 8 | REM 7z e "-olib yubico-piv-tool-%PIVTOOL_VERSION%-win32.zip" bin/ 9 | 10 | python setup.py qt_resources 11 | python setup.py executable 12 | -------------------------------------------------------------------------------- /doc/Certificates.adoc: -------------------------------------------------------------------------------- 1 | == Certificates 2 | The PIV functionality of the YubiKey provides 4 standard slots for storing 3 | private keys with accompanying X509 certificates. You can view and manage these 4 | slots from the *Certificates* dialog. For more information on the different 5 | slots and their use, see the 6 | link:http://csrc.nist.gov/groups/SNS/piv/standards.html[PIV standards documents]. 7 | 8 | === Generating a new key pair 9 | The first step is to generate a new cryptographic key pair. The *Certificates* 10 | dialog provides a *Generate new key...* button to start this process. Each slot 11 | is represented as a tab in the dialog, and each tab has its own button to 12 | generate a key. You will need to specify the algorithm for the key, and the 13 | output format. The private key will be generated on the YubiKey, and will never 14 | leave the device. What happens to the public key is determined by the output 15 | type. 16 | 17 | ==== Output: Public key 18 | The most basic output type is to just write the public key to the disk, in 19 | either PEM or DER format. 20 | 21 | NOTE: This output form is hidden by default, as it is of no use for most users. 22 | 23 | ==== Output: Self-signed certificate 24 | The public key is wrapped in an X509 certificate, which is then self-signed by 25 | the private key, and stored in the same slot as the private key of the YubiKey. 26 | You will need to provide a Subject DN for the certificate to use, in the 27 | following format: 28 | 29 | .... 30 | /CN=host.example.com/OU=test/O=example.com 31 | .... 32 | 33 | ==== Output: Certificate signing request (CSR) 34 | The public key is wrapped in a CSR, which can be signed by a Certificate 35 | Authority (CA), resulting in a CA-signed certificate. As with the previous 36 | output option, you will need to provide a Subject DN. The CSR is sent to the CA 37 | out-of-band, and the signed certificate should be imported into the YubiKey 38 | using the *Import from file...* button of the *Certificate* window, for the 39 | same slot that already holds the key. 40 | 41 | NOTE: If a private key but no certificate is loaded in a slot in the YubiKey, 42 | it will be indistinguishable from an empty slot. 43 | 44 | ==== Output: Request a certificate If the client is connected to a CA, the 45 | process of having the CA create and sign a certificate can be automated by 46 | YubiKey PIV Manager. This option will create a CSR, have the CA sign it, and 47 | import it back into the YubiKey. As with the previous output option, you will 48 | need to provide a Subject DN. 49 | 50 | NOTE: This feature is only available on Windows, and uses the *certreq* 51 | executable. 52 | 53 | === Importing a private key/certificate 54 | Instead of generating a key pair on the YubiKey itself, you can import an 55 | existing private key and/or certificate. To do so simply use the *Import from 56 | file...* button in the *Certificates* dialog. The YubiKey PIV Manager supports 57 | importing private keys in PEM and PFX format and certificates in DER, PEM and 58 | PFX format. 59 | 60 | NOTE: There is no way to see that a private key has been imported into a slot. 61 | -------------------------------------------------------------------------------- /doc/Device_Setup.adoc: -------------------------------------------------------------------------------- 1 | == Device Setup 2 | The YubiKey PIV Manager provides a wizard for initializing an un-initialized 3 | YubiKey. This wizard assumes the default values for the PIN, PUK and Management 4 | Key are set, and may not work correctly if they have been modified. Some 5 | features of the YubiKey PIV Manager require that all management of the device 6 | be done using the YubiKey PIV Manager, so it is recommended that you not use 7 | other tools in combination with the YubiKey PIV Manager to manage your YubiKeys 8 | PIV configuration. 9 | 10 | === Prerequisites 11 | You will need to have the YubiKey PIV Manager and its dependencies installed, 12 | as well as a PIV-enabled YubiKey, with the default PIN, PUK and Management Key 13 | values set. 14 | 15 | === Device Initialization 16 | A PIV-enabled YubiKey comes pre-programmed with the default values for the PIN, 17 | the PUK, and the Management Key. As these default values are known it is 18 | crucial that these be changed before any real use. The YubiKey PIV Manager will 19 | automatically show a _Device Initialization_ dialog if it detects a YubiKey 20 | that uses the default Management Key. It will ask you to provide a PIN, 21 | Management Key, and optionally a PUK. For more details on the various options 22 | provided by this wizard, see link:PIN_and_Management_Key.adoc[PIN and 23 | Management Key]. 24 | -------------------------------------------------------------------------------- /doc/PIN_and_Management_Key.adoc: -------------------------------------------------------------------------------- 1 | == PIN and Management Key 2 | A PIV-enabled YubiKey has a PIN, a PUK and a Management Key. These can be 3 | configured by using the *Manage Device PINs* window of the YubiKey PIV Manager. 4 | 5 | === PIN 6 | The PIN is used during normal operation to authorize an action such as creating 7 | a digital signature for any of the loaded certificates. Entering an incorrect 8 | PIN three times consecutively will cause the PIN to become blocked, rendering 9 | the PIV features unusable. The PIN must be at least 6 characters, and can 10 | contain any characters, though for cross-platform portability it is recommended 11 | to only use decimal digits. There is a limit of 8 bytes for a PIN, which allows 12 | for up to 8 ASCII characters. There is a setting in the YubiKey PIV Manager, 13 | which enforces password complexity rules on your PIN (as well as your PUK). If 14 | this setting is active, your PIN will need to: 15 | 16 | * Not contain all or part of the user's account name. 17 | * Contain characters from three of the following four categories: 18 | ** English uppercase characters (A through Z) 19 | ** English lowercase characters (a through z) 20 | ** Base 10 digits (0 through 9) 21 | ** Nonalphanumeric characters (e.g., !, $, #, %) 22 | 23 | ==== PIN expiration 24 | There is an option in the YubiKey PIV Manager to enforce PINs to expire after a 25 | certain number of days, requiring you to change your PIN periodically. This is 26 | not enabled by default. When enabled, the YubiKey PIV Manager will prompt you 27 | to change the PIN after the set number of days has passed. Changing the PIN 28 | using an external tool will not affect the PIN expiration date. When this 29 | option is enabled you will need to provide your Management Key each time the 30 | PIN is changed, as that is required to store the updated expiration time. 31 | Because of this it is often desirable to use your PIN as the Management Key in 32 | combination with this setting to not be prompted for the Key. 33 | 34 | === Management Key 35 | All PIV management operation of the YubiKey require a 24 byte 3DES key, known 36 | as the Management Key. You can either explicitly set a 24 byte key (the YubiKey 37 | PIV Manager can generate one for you), or you can choose to not set a 38 | Management Key, instead using the PIN for these operations. If so, you will be 39 | asked to provide your PIN instead of your Management Key whenever you perform a 40 | management operation, such as importing a new certificate, or generating a new 41 | key pair. 42 | 43 | NOTE: See the section Considerations when using a PIN for PIV management! 44 | 45 | === PUK 46 | The PUK can be used to reset the PIN if it is ever lost or becomes blocked 47 | after the maximum number of incorrect attempts. Setting a PUK is optional. If 48 | you use your PIN as the Management Key, the PUK is disabled for technical 49 | reasons, explained in a later section. The requirements and restrictions of the 50 | PUK are the same as for the PIN (see above). If PIN complexity is enforced, the 51 | same rules are applied to the PUK. If the PUK ever becomes blocked, either by 52 | deliberately choosing to block it or by giving the wrong PUK value 3 times, it 53 | can only be unblocked by performing a complete reset (explained below). 54 | 55 | === Resetting a device 56 | If an incorrect PIN is given 3 times consecutively, the PIN will become 57 | disabled. If you've set a PUK, then you can use that PUK to reset the PIN to a 58 | new value, and it will become enabled and usable again. If an incorrect PUK is 59 | given 3 times consecutively, it will become blocked as well. When both the PIN 60 | and the PUK are blocked, the device can be reset. This returns the PIV 61 | functionality of the YubiKey to a factory setting, setting the default PIN, PUK 62 | and Management Key values, as well as removing any stored keys and 63 | certificates. Once reset, the device is ready to be re-initialized. 64 | 65 | === Considerations when using a PIN for PIV management 66 | There are certain security and usability considerations which should be taken 67 | into account when using the PIN for PIV management, instead of a Management 68 | Key. The way this feature works, is that a Management Key is still used, but it 69 | is cryptographically derived from your PIN by the YubiKey PIV Manager, behind 70 | the scenes. One implication of this is that the Management Key changes whenever 71 | you change your PIN, and it is therefore crucial that you ONLY change your PIN 72 | using the YubiKey PIN Manager. Changing it using an external tool will render 73 | the YubiKey PIV Manager unable to derive the Management Key. There is also a 74 | security aspect to be aware of, and that is that even though the PIN is blocked 75 | if entered incorrectly 3 times, the Management Key is not. An attacker could 76 | use the knowledge of this to effectively brute-force the PIN. This is mitigated 77 | by a computationally expensive (slow) key derivation, but it should only be 78 | used with a long, complex PIN. 79 | 80 | ==== Technical description of Key derivation from PIN 81 | When choosing to use a Management Key derived from the PIN, the following takes 82 | place: 83 | 84 | 1. A random 8-byte SALT value is generated and stored on the YubiKey. 85 | 2. The derived Management Key is calculated as PBKDF2(PIN, SALT, 24, 10000). 86 | 87 | The PBKDF2 function (described in RFC 2898) is run using the PIN (encoded using 88 | UTF-8) as the password, for 10000 rounds, to produce a 24 byte key, which is 89 | used as the management key. Whenever the user changes the PIN this process is 90 | repeated, using a new SALT and the new PIN. 91 | -------------------------------------------------------------------------------- /doc/Settings_and_Group_Policy.adoc: -------------------------------------------------------------------------------- 1 | == Settings and Group Policy 2 | The YubiKey PIV Manager provides various settings which can be used to 3 | customize its behavior. Some of these can be changed in the *Settings* dialog 4 | from within the tool. Each of these can be enforced using a Group Policy on 5 | Windows, which prevents the user from changing them. To do so you will need to 6 | set certain registry keys. This can be used to simplify the tool by limiting 7 | it to a subset of its functionality, and to enforce company policy on usage. 8 | 9 | === User settings 10 | User settings are stored in a file named pivman.ini, located in the .pivman 11 | subdirectory of the users come directory. For example: 12 | 13 | .... 14 | /home/username/.pivman/pivman.ini 15 | .... 16 | 17 | on Linux, or: 18 | 19 | .... 20 | c:\Users\username\.pivman\pivman.ini 21 | .... 22 | 23 | on Windows. 24 | 25 | Some of these values are changed by normal use of the YubiKey PIV Manager by 26 | performing actions in the tool. Others are only available by manually editing 27 | the configuration file. 28 | 29 | === Group Policy settings 30 | All user settings are available on Group Policy level, and take precedence over 31 | those on user level. On Windows these settings are stored as registry keys, 32 | under either: 33 | 34 | .... 35 | Computer\HKEY_CURRENT_USER\Software\Yubico\YubiKey PIV Manager 36 | .... 37 | 38 | or: 39 | 40 | .... 41 | Computer\HKEY_LOCAL_MACHINE\Software\Yubico\YubiKey PIV Manager 42 | .... 43 | 44 | === Available settings 45 | 46 | ==== Algorithm 47 | Which algorithm to use for key pair generation. 48 | 49 | key:: algorithm 50 | type:: string 51 | registry key type:: REG_SZ 52 | valid options:: "RSA1024", "RSA2048", "ECC256", "ECC384" 53 | default value:: "RSA2048" 54 | 55 | ==== Card Reader 56 | String to match against when looking for compatible YubiKey devices. 57 | 58 | key:: card_reader 59 | type:: string 60 | registry key type:: REG_SZ 61 | default value:: None 62 | 63 | ==== Certreq Template 64 | Value to use in CertificateTemplate parameter when calling certreq.exe. 65 | 66 | key:: certreq_template 67 | type:: string 68 | registry key type:: REG_SZ 69 | default value:: None 70 | 71 | ==== Complex PIN/PUKs 72 | True to require complex PINs and PUKs. 73 | 74 | key:: complex_pins 75 | type:: string 76 | registry key type:: REG_SZ 77 | default value:: "false" 78 | 79 | ==== Enable Import 80 | When False, hide the "import from file..." button for certificates. 81 | 82 | key:: enable_import 83 | type:: string 84 | registry key type:: REG_SZ 85 | default value:: "true" 86 | 87 | ==== PIN as Management Key 88 | When true, the Management Key is based off of the PIN. 89 | 90 | key:: pin_as_key 91 | type:: bool 92 | registry key type:: REG_SZ 93 | default value:: "false" 94 | 95 | ==== PIN Expiration 96 | When non-zero causes a timestamp to be written when the PIN is changed, and to 97 | force a PIN change after the specified number of days. 98 | 99 | key:: pin_expiration 100 | type:: int 101 | registry key type:: REG_DWORD 102 | default value:: 0 103 | 104 | ==== PIN Requirement Policy 105 | When set to a value other than "default", override the PIV standard for when 106 | the PIN is required for using a particular slot. 107 | 108 | key:: pin_policy 109 | type:: string 110 | registry key type:: REG_SZ 111 | valid options:: "default" "never", "once", "always" 112 | default value:: "default" 113 | 114 | ==== PIN Policy Slots 115 | Which certificate slots to show the PIN Requirement Policy setting for. 116 | 117 | key:: pin_policy_slots 118 | type:: list of strings 119 | registry key type:: REG_MULTI_SZ 120 | valid options:: "9a", "9c", "9d", "9e" 121 | default value:: [] 122 | 123 | ==== Displayed Output Formats 124 | Output formats available when generating a key. 125 | 126 | key:: shown_outs 127 | type:: list of strings 128 | registry key type:: REG_MULTI_SZ 129 | valid options:: "pk", "ssc", "csr", "ca" 130 | default value:: ["ssc", "csr", "ca"] 131 | 132 | ==== Displayed Certificate Slots 133 | A list of which certificate slots to show in the UI. 134 | 135 | key:: shown_slots 136 | type:: list of strings 137 | registry key type:: REG_MULTI_SZ 138 | valid options:: "9a", "9c", "9d", "9e" 139 | default value:: ["9a", "9c", "9d", "9e"] 140 | 141 | ==== Subject DN 142 | Subject to use when generating a CSR or self-signed certificate. 143 | 144 | key:: subject 145 | type:: string 146 | registry key type:: REG_SZ 147 | default value:: "/CN=%USERNAME%" 148 | 149 | ==== Touch Policy 150 | When enabled, the YubiKey will require its button to be touched to perform any 151 | action with the private key of a slot. 152 | 153 | key:: touch_policy 154 | type:: bool 155 | registry key type:: REG_SZ 156 | default value:: "false" 157 | 158 | ==== Touch Policy Slots 159 | Which certificate slots to show the Touch Policy setting for. 160 | 161 | key:: touch_policy_slots 162 | type:: list of strings 163 | registry key type:: REG_MULTI_SZ 164 | valid options:: "9a", "9c", "9d", "9e" 165 | default value:: [] 166 | -------------------------------------------------------------------------------- /doc/development.adoc: -------------------------------------------------------------------------------- 1 | == Development 2 | 3 | === Dependencies 4 | YubiKey PIV Manager requires Python 2, PySide, pycrypto and yubico-piv-tool. 5 | 6 | 7 | === Vagrant VM for development 8 | 9 | A Vagrant VM definition for development is included. To use it: 10 | 11 | 1. Set up the VM: 12 | + 13 | $ cd vagrant/development 14 | $ vagrant up 15 | + 16 | After first startup, restart the VM as instructed on first login: 17 | + 18 | alice@work $ vagrant ssh 19 | ubuntu@ubuntu-xenial $ sudo poweroff 20 | 21 | 2. Restart the VM: 22 | + 23 | alice@work $ vagrant up 24 | 25 | 3. Log into the GUI as the user `ubuntu` (no password) 26 | 4. Start X: 27 | + 28 | ubuntu@ubuntu-xenial $ startx 29 | 30 | 5. Open a terminal and install and run YubiKey PIV Manager: 31 | + 32 | ubuntu@ubuntu-xenial $ cd /vagrant 33 | ubuntu@ubuntu-xenial $ sudo python setup.py qt_resources 34 | ubuntu@ubuntu-xenial $ sudo python setup.py install 35 | ubuntu@ubuntu-xenial $ pivman 36 | 37 | Repeat from (2) as necessary on subsequent uses of the VM. 38 | 39 | 40 | === Building binaries 41 | Binaries for Windows and OSX are built using PyInstaller. 42 | 43 | Get the source release file, yubikey-piv-manager-.tar.gz, and extract 44 | it. It should contain a single directory, henceforth refered to as the release 45 | directory. 46 | 47 | When building binaries for Windows or OS X, you will need to include 48 | .dll/.dylib files from the yubico-piv-tool project, as well as the 49 | yubico-piv-tool executable. Create a subdirectory called "lib" in the release 50 | directory. 51 | Download the correct binary release for your architecture from 52 | https://developers.yubico.com/yubico-piv-tool/Releases/ and extract the 53 | .dll/.dylib files and executable to the "lib" directory you created previously. 54 | 55 | ==== Windows 56 | For Windows you will need python, PySide, PyCrypto, PyInstaller and Pywin32 57 | installed (32 or 64-bit versions depending on the architecture of the binary 58 | your are building). 59 | 60 | To sign the executable you will need signtool.exe (from the Windows SDK) either 61 | copied into the root as well or in a location in your PATH, as well as a 62 | certificate in the Windows certificate store that you wish to sign with. 63 | 64 | Run "python setup.py executable" from the main release directory. 65 | 66 | With NSIS installed, a Windows installer will be built as well. 67 | 68 | ==== OSX 69 | For OSX you need python, pyside, pycrypto, and pyinstaller installed. One way 70 | to install these dependencies is by using Homebrew: 71 | 72 | brew install python 73 | brew install pyside 74 | pip install PyInstaller 75 | pip install pycrypto 76 | 77 | NOTE: Homebrew will build backwards-incompatible binaries, so the resulting 78 | build will not run on an older version of OSX. 79 | 80 | Run "python setup.py executable" from the main release directory. This 81 | will create an .app in the dist directory. 82 | 83 | Sign the code using codesign: 84 | 85 | codesign -s 'Developer ID Application' dist/YubiKey\ PIV\ Manager.app --deep 86 | 87 | There is also a project file for use with 88 | http://s.sudre.free.fr/Packaging.html[Packages] 89 | located at `resources/pivman.pkgproj`. 90 | This can be used to create an installer for distribution, which you should sign 91 | prior to distribution: 92 | 93 | packagesbuild resources/osx-installer.pkgproj 94 | productsign --sign 'Developer ID Installer' dist/YubiKey\ PIV\ Manager.pkg dist/yubikey-piv-manager-mac.pkg 95 | 96 | -------------------------------------------------------------------------------- /man/pivman.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2014 Yubico AB 2 | .\" All rights reserved. 3 | .\" 4 | .\" This program is free software: you can redistribute it and/or modify 5 | .\" it under the terms of the GNU General Public License as published by 6 | .\" the Free Software Foundation, either version 3 of the License, or 7 | .\" (at your option) any later version. 8 | .\" 9 | .\" This program is distributed in the hope that it will be useful, 10 | .\" but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | .\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | .\" GNU General Public License for more details. 13 | .\" 14 | .\" You should have received a copy of the GNU General Public License 15 | .\" along with this program. If not, see . 16 | .\" 17 | .\" Additional permission under GNU GPL version 3 section 7 18 | .\" 19 | .\" If you modify this program, or any covered work, by linking or 20 | .\" combining it with the OpenSSL project's OpenSSL library (or a 21 | .\" modified version of that library), containing parts covered by the 22 | .\" terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | .\" permission to convey the resulting work. Corresponding Source for a 24 | .\" non-source form of such a combination shall include the source code 25 | .\" for the parts of OpenSSL used as well as that of the covered work. 26 | .\" 27 | .\" The following commands are required for all man pages. 28 | .de URL 29 | \\$2 \(laURL: \\$1 \(ra\\$3 30 | .. 31 | .if \n[.g] .mso www.tmac 32 | .TH pivman "1" "Mar 2014" "yubikey-piv-manager" 33 | .SH NAME 34 | pivman - Tool for configuring your PIV-enabled YubiKey. 35 | .SH SYNOPSIS 36 | .B pivman 37 | 38 | .SH DESCRIPTION 39 | YubiKey PIV Manager is a graphical utility to manage keys and certificates 40 | stored in a PIV-enabled YubiKey.. 41 | .SH BUGS 42 | Report enroll bugs in 43 | .URL "https://github.com/Yubico/yubikey-piv-manager/issues" "the issue tracker" 44 | -------------------------------------------------------------------------------- /pivman/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | __version__ = "1.4.3-dev" 28 | -------------------------------------------------------------------------------- /pivman/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | from __future__ import print_function 28 | import sys 29 | import argparse 30 | import signal 31 | import pivman.qt_resources # noqa: F401 32 | from PySide import QtGui, QtCore 33 | from pivman.view.main import MainWidget 34 | from pivman import __version__ as version, messages as m 35 | from pivman.piv import YkPiv, libversion as ykpiv_version 36 | from pivman.controller import Controller 37 | from pivman.view.set_pin_dialog import SetPinDialog 38 | from pivman.view.settings_dialog import SettingsDialog 39 | from pivman.yubicommon import qt 40 | 41 | 42 | ABOUT_TEXT = """ 43 |

%s

44 | %s
45 | %s 46 |

%s

47 | %%s 48 |

49 | """ % (m.app_name, m.copyright, m.version_1, m.libraries) 50 | 51 | 52 | class PivtoolApplication(qt.Application): 53 | 54 | def __init__(self, argv): 55 | super(PivtoolApplication, self).__init__(m, version) 56 | 57 | QtCore.QCoreApplication.setOrganizationName(m.organization) 58 | QtCore.QCoreApplication.setOrganizationDomain(m.domain) 59 | QtCore.QCoreApplication.setApplicationName(m.app_name) 60 | 61 | args = self._parse_args() 62 | 63 | if args.check_only: 64 | self.check_pin() 65 | self.quit() 66 | return 67 | 68 | self.ensure_singleton() 69 | 70 | self._build_menu_bar() 71 | self._init_window() 72 | 73 | def check_pin(self): 74 | try: 75 | controller = Controller(YkPiv()) 76 | if controller.is_uninitialized(): 77 | print('Device not initialized') 78 | elif controller.is_pin_expired(): 79 | dialog = SetPinDialog(controller, None, True) 80 | if dialog.exec_(): 81 | QtGui.QMessageBox.information(None, m.pin_changed, 82 | m.pin_changed_desc) 83 | except: 84 | print('No YubiKey PIV applet detected') 85 | 86 | def _parse_args(self): 87 | parser = argparse.ArgumentParser(description='YubiKey PIV Manager', 88 | add_help=True) 89 | parser.add_argument('-c', '--check-only', action='store_true') 90 | return parser.parse_args() 91 | 92 | def _init_window(self): 93 | self.window.setWindowTitle(m.win_title_1 % self.version) 94 | self.window.setWindowIcon(QtGui.QIcon(':/pivman.png')) 95 | self.window.layout().setSizeConstraint(QtGui.QLayout.SetFixedSize) 96 | self.window.setCentralWidget(MainWidget()) 97 | self.window.show() 98 | self.window.raise_() 99 | 100 | def _build_menu_bar(self): 101 | file_menu = self.window.menuBar().addMenu(m.menu_file) 102 | settings_action = QtGui.QAction(m.action_settings, file_menu) 103 | settings_action.triggered.connect(self._show_settings) 104 | file_menu.addAction(settings_action) 105 | 106 | help_menu = self.window.menuBar().addMenu(m.menu_help) 107 | about_action = QtGui.QAction(m.action_about, help_menu) 108 | about_action.triggered.connect(self._about) 109 | help_menu.addAction(about_action) 110 | 111 | def _libversions(self): 112 | return 'ykpiv: %s' % ykpiv_version.decode('ascii') 113 | 114 | def _about(self): 115 | QtGui.QMessageBox.about( 116 | self.window, 117 | m.about_1 % m.app_name, 118 | ABOUT_TEXT % (self.version, self._libversions()) 119 | ) 120 | 121 | def _show_settings(self): 122 | dialog = SettingsDialog(self.window) 123 | if dialog.exec_(): 124 | self.window.centralWidget().refresh() 125 | 126 | 127 | def main(): 128 | signal.signal(signal.SIGINT, signal.SIG_DFL) 129 | app = PivtoolApplication(sys.argv) 130 | sys.exit(app.exec_()) 131 | 132 | 133 | if __name__ == '__main__': 134 | main() 135 | -------------------------------------------------------------------------------- /pivman/controller.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | from pivman.utils import test, der_read, is_macos_sierra_or_later 28 | from pivman.piv import PivError, WrongPinError 29 | from pivman.storage import settings, SETTINGS 30 | from pivman.view.utils import get_active_window, get_text 31 | from pivman import messages as m 32 | from pivman.yubicommon.compat import text_type, byte2int, int2byte 33 | from PySide import QtGui, QtNetwork 34 | from datetime import timedelta 35 | from hashlib import pbkdf2_hmac 36 | from binascii import a2b_hex 37 | import os 38 | import re 39 | import time 40 | import struct 41 | 42 | YKPIV_OBJ_PIVMAN_DATA = 0x5fff00 43 | 44 | TAG_PIVMAN_DATA = 0x80 # Wrapper for pivman data 45 | TAG_FLAGS_1 = 0x81 # Flags 1 46 | TAG_SALT = 0x82 # Salt used for management key derivation 47 | TAG_PIN_TIMESTAMP = 0x83 # When the PIN was last changed 48 | 49 | FLAG1_PUK_BLOCKED = 0x01 # PUK is blocked 50 | 51 | AUTH_SLOT = '9a' 52 | ENCRYPTION_SLOT = '9d' 53 | DEFAULT_AUTH_SUBJECT = "/CN=Yubico PIV Authentication" 54 | DEFAULT_ENCRYPTION_SUBJECT = "/CN=Yubico PIV Encryption" 55 | DEFAULT_VALID_DAYS = 10950 # 30 years 56 | 57 | NEO_MAX_CERT_LEN = 1024 * 2 - 23 58 | YK4_MAX_CERT_LEN = 1024 * 3 - 23 59 | 60 | 61 | def parse_pivtool_data(raw_data): 62 | rest, _ = der_read(raw_data, TAG_PIVMAN_DATA) 63 | data = {} 64 | while rest: 65 | t, v, rest = der_read(rest) 66 | data[t] = v 67 | return data 68 | 69 | 70 | def serialize_pivtool_data(data): # NOTE: Doesn't support values > 0x80 bytes. 71 | buf = b'' 72 | for k, v in sorted(data.items()): 73 | buf += int2byte(k) + int2byte(len(v)) + v 74 | return int2byte(TAG_PIVMAN_DATA) + int2byte(len(buf)) + buf 75 | 76 | 77 | def has_flag(data, flagkey, flagmask): 78 | flags = byte2int(data.get(flagkey, b'\0')[0]) 79 | return bool(flags & flagmask) 80 | 81 | 82 | def set_flag(data, flagkey, flagmask, value=True): 83 | flags = byte2int(data.get(flagkey, b'\0')[0]) 84 | if value: 85 | flags |= flagmask 86 | else: 87 | flags &= ~flagmask 88 | data[flagkey] = int2byte(flags) 89 | 90 | 91 | def derive_key(pin, salt): 92 | if pin is None: 93 | raise ValueError('PIN must not be None!') 94 | if isinstance(pin, text_type): 95 | pin = pin.encode('utf8') 96 | return pbkdf2_hmac('sha1', pin, salt, 10000, dklen=24) 97 | 98 | 99 | def is_hex_key(string): 100 | try: 101 | return bool(re.compile(r'^[a-fA-F0-9]{48}$').match(string)) 102 | except: 103 | return False 104 | 105 | 106 | class Controller(object): 107 | 108 | def __init__(self, key): 109 | self._key = key 110 | self._authenticated = False 111 | try: 112 | self._raw_data = self._key.fetch_object(YKPIV_OBJ_PIVMAN_DATA) 113 | # TODO: Remove in a few versions... 114 | if byte2int(self._raw_data[0]) != TAG_PIVMAN_DATA: 115 | self._data = {} 116 | self._data[TAG_PIN_TIMESTAMP] = self._raw_data 117 | self._data[TAG_SALT] = self._key.fetch_object( 118 | YKPIV_OBJ_PIVMAN_DATA + 1) 119 | else: 120 | # END legacy stuff 121 | self._data = parse_pivtool_data(self._raw_data) 122 | except PivError: 123 | self._raw_data = serialize_pivtool_data({}) 124 | self._data = {} 125 | 126 | def poll(self): 127 | return test(self._key._read_version) 128 | 129 | def reconnect(self): 130 | self._key.reconnect() 131 | 132 | def _save_data(self): 133 | raw_data = serialize_pivtool_data(self._data) 134 | if raw_data != self._raw_data: 135 | self.ensure_authenticated() 136 | self._key.save_object(YKPIV_OBJ_PIVMAN_DATA, raw_data) 137 | self._raw_data = raw_data 138 | 139 | @property 140 | def version(self): 141 | return self._key.version 142 | 143 | @property 144 | def version_tuple(self): 145 | return tuple(map(int, self.version.split(b'.'))) 146 | 147 | @property 148 | def authenticated(self): 149 | return self._authenticated 150 | 151 | @property 152 | def pin_is_key(self): 153 | return TAG_SALT in self._data 154 | 155 | @property 156 | def pin_blocked(self): 157 | return self._key.pin_blocked 158 | 159 | @property 160 | def puk_blocked(self): 161 | return has_flag(self._data, TAG_FLAGS_1, FLAG1_PUK_BLOCKED) 162 | 163 | def verify_pin(self, pin): 164 | if len(pin) > 8: 165 | raise ValueError('PIN must be no longer than 8 bytes!') 166 | self._key.verify_pin(pin) 167 | 168 | def ensure_pin(self, pin=None, window=None): 169 | if window is None: 170 | window = get_active_window() 171 | 172 | if pin is not None: 173 | try: 174 | self.verify_pin(pin) 175 | return pin 176 | except WrongPinError as e: 177 | if e.blocked: 178 | raise 179 | QtGui.QMessageBox.warning(window, m.error, str(e)) 180 | except ValueError as e: 181 | QtGui.QMessageBox.warning(window, m.error, str(e)) 182 | 183 | pin, status = get_text( 184 | window, m.enter_pin, m.pin_label, QtGui.QLineEdit.Password) 185 | if not status: 186 | raise ValueError('PIN entry aborted!') 187 | return self.ensure_pin(pin, window) 188 | 189 | def ensure_authenticated(self, key=None, window=None): 190 | if self.authenticated or test(self.authenticate, catches=ValueError): 191 | return 192 | 193 | if window is None: 194 | window = get_active_window() 195 | 196 | if self.pin_is_key: 197 | key = self.ensure_pin(key, window) 198 | self.authenticate(key) 199 | return 200 | elif key is not None: 201 | try: 202 | self.authenticate(key) 203 | return 204 | except ValueError: 205 | pass 206 | 207 | self._do_ensure_auth(None, window) 208 | 209 | def _do_ensure_auth(self, key, window): 210 | if key is not None: 211 | try: 212 | self.authenticate(key) 213 | return 214 | except ValueError as e: 215 | QtGui.QMessageBox.warning(window, m.error, str(e)) 216 | 217 | key, status = get_text(window, m.enter_key, m.key_label) 218 | if not status: 219 | raise ValueError('Key entry aborted!') 220 | self._do_ensure_auth(key, window) 221 | 222 | def reset_device(self): 223 | self._key.reset_device() 224 | 225 | def authenticate(self, key=None): 226 | salt = self._data.get(TAG_SALT) 227 | 228 | if key is not None and salt is not None: 229 | key = derive_key(key, salt) 230 | elif is_hex_key(key): 231 | key = a2b_hex(key) 232 | 233 | self._authenticated = False 234 | if test(self._key.authenticate, key, catches=PivError): 235 | self._authenticated = True 236 | else: 237 | raise ValueError(m.wrong_key) 238 | 239 | def is_uninitialized(self): 240 | return not self._data and test(self._key.authenticate) 241 | 242 | def _invalidate_puk(self): 243 | set_flag(self._data, TAG_FLAGS_1, FLAG1_PUK_BLOCKED) 244 | for i in range(8): # Invalidate the PUK 245 | test(self._key.set_puk, '', '000000', catches=ValueError) 246 | 247 | def initialize(self, pin, puk=None, key=None, old_pin='123456', 248 | old_puk='12345678'): 249 | 250 | if not self.authenticated: 251 | self.authenticate() 252 | 253 | if key is None: # Derive key from PIN 254 | self._data[TAG_SALT] = b'' # Used as a marker for change_pin 255 | else: 256 | self.set_authentication(key) 257 | if puk is None: 258 | self._invalidate_puk() 259 | else: 260 | self._key.set_puk(old_puk, puk) 261 | 262 | self.change_pin(old_pin, pin) 263 | 264 | def setup_for_macos(self, pin): 265 | 266 | """Generate self-signed certificates in slot 9a and 9d 267 | to allow pairing a YubiKey with a user account on macOS""" 268 | 269 | auth_key = self.generate_key(AUTH_SLOT, 'ECCP256') 270 | auth_cert = self.selfsign_certificate( 271 | AUTH_SLOT, pin, auth_key, 272 | DEFAULT_AUTH_SUBJECT, DEFAULT_VALID_DAYS) 273 | self.import_certificate(auth_cert, AUTH_SLOT) 274 | 275 | encryption_key = self.generate_key(ENCRYPTION_SLOT, 'ECCP256') 276 | encryption_cert = self.selfsign_certificate( 277 | ENCRYPTION_SLOT, pin, encryption_key, 278 | DEFAULT_ENCRYPTION_SUBJECT, DEFAULT_VALID_DAYS) 279 | self.import_certificate(encryption_cert, ENCRYPTION_SLOT) 280 | 281 | def set_authentication(self, new_key, is_pin=False): 282 | if not self.authenticated: 283 | raise ValueError('Not authenticated') 284 | 285 | if is_pin: 286 | self.verify_pin(new_key) 287 | salt = os.urandom(16) 288 | key = derive_key(new_key, salt) 289 | self._data[TAG_SALT] = salt 290 | self._key.set_authentication(key) 291 | 292 | # Make sure PUK is invalidated: 293 | if not has_flag(self._data, TAG_FLAGS_1, FLAG1_PUK_BLOCKED): 294 | self._invalidate_puk() 295 | else: 296 | if is_hex_key(new_key): 297 | new_key = a2b_hex(new_key) 298 | 299 | self._key.set_authentication(new_key) 300 | if self.pin_is_key: 301 | del self._data[TAG_SALT] 302 | 303 | self._save_data() 304 | 305 | def change_pin(self, old_pin, new_pin): 306 | if len(new_pin) < 6: 307 | raise ValueError('PIN must be at least 6 characters') 308 | self.verify_pin(old_pin) 309 | if self.pin_is_key or self.does_pin_expire(): 310 | self.ensure_authenticated(old_pin) 311 | self._key.set_pin(new_pin) 312 | # Update management key if needed: 313 | if self.pin_is_key: 314 | self.set_authentication(new_pin, True) 315 | 316 | if self.does_pin_expire(): 317 | self._data[TAG_PIN_TIMESTAMP] = struct.pack('i', int(time.time())) 318 | self._save_data() 319 | 320 | def reset_pin(self, puk, new_pin): 321 | if len(new_pin) < 6: 322 | raise ValueError('PIN must be at least 6 characters') 323 | try: 324 | self._key.reset_pin(puk, new_pin) 325 | except WrongPinError as e: 326 | if e.blocked: 327 | set_flag(self._data, TAG_FLAGS_1, FLAG1_PUK_BLOCKED) 328 | raise 329 | 330 | def change_puk(self, old_puk, new_puk): 331 | if self.puk_blocked: 332 | raise ValueError('PUK is disabled and cannot be changed') 333 | if len(new_puk) < 6: 334 | raise ValueError('PUK must be at least 6 characters') 335 | try: 336 | self._key.set_puk(old_puk, new_puk) 337 | except WrongPinError as e: 338 | if e.blocked: 339 | set_flag(self._data, TAG_FLAGS_1, FLAG1_PUK_BLOCKED) 340 | raise 341 | 342 | def update_chuid(self): 343 | if not self.authenticated: 344 | raise ValueError('Not authenticated') 345 | self._key.set_chuid() 346 | 347 | def generate_key(self, slot, algorithm='RSA2048', pin_policy=None, 348 | touch_policy=False): 349 | if not self.authenticated: 350 | raise ValueError('Not authenticated') 351 | 352 | if pin_policy == 'default': 353 | pin_policy = None 354 | 355 | if slot in self.certs: 356 | self.delete_certificate(slot) 357 | return self._key.generate(slot, algorithm, pin_policy, touch_policy) 358 | 359 | def create_csr(self, slot, pin, pubkey, subject): 360 | self.verify_pin(pin) 361 | if not self.authenticated: 362 | raise ValueError('Not authenticated') 363 | return self._key.create_csr(subject, pubkey, slot) 364 | 365 | def selfsign_certificate(self, slot, pin, pubkey, subject, valid_days=365): 366 | self.verify_pin(pin) 367 | if not self.authenticated: 368 | raise ValueError('Not authenticated') 369 | return self._key.create_selfsigned_cert( 370 | subject, pubkey, slot, valid_days) 371 | 372 | def does_pin_expire(self): 373 | return bool(settings[SETTINGS.PIN_EXPIRATION]) 374 | 375 | def get_pin_last_changed(self): 376 | data = self._data.get(TAG_PIN_TIMESTAMP) 377 | if data is not None: 378 | data = struct.unpack('i', data)[0] 379 | return data 380 | 381 | def get_pin_days_left(self): 382 | validity = settings[SETTINGS.PIN_EXPIRATION] 383 | if not validity: 384 | return -1 385 | last_changed = self.get_pin_last_changed() 386 | if last_changed is None: 387 | return 0 388 | time_passed = timedelta(seconds=time.time() - last_changed) 389 | time_left = timedelta(days=validity) - time_passed 390 | return max(time_left.days, 0) 391 | 392 | def is_pin_expired(self): 393 | if not self.does_pin_expire(): 394 | return False 395 | 396 | last_changed = self.get_pin_last_changed() 397 | if last_changed is None: 398 | return True 399 | delta = timedelta(seconds=time.time() - last_changed) 400 | return delta.days > 30 401 | 402 | @property 403 | def certs(self): 404 | return self._key.certs 405 | 406 | def get_certificate(self, slot): 407 | data = self._key.read_cert(slot) 408 | if data is None: 409 | return None 410 | return QtNetwork.QSslCertificate.fromData(data, QtNetwork.QSsl.Der)[0] 411 | 412 | def import_key(self, data, slot, frmt='PEM', password=None, pin_policy=None, 413 | touch_policy=False): 414 | if not self.authenticated: 415 | raise ValueError('Not authenticated') 416 | 417 | if pin_policy == 'default': 418 | pin_policy = None 419 | 420 | self._key.import_key(data, slot, frmt, password, pin_policy, 421 | touch_policy) 422 | 423 | def import_certificate(self, cert, slot, frmt='PEM', password=None): 424 | if not self.authenticated: 425 | raise ValueError('Not authenticated') 426 | try: 427 | self._key.import_cert(cert, slot, frmt, password) 428 | except ValueError: 429 | cert_len = len(cert) 430 | if cert_len > NEO_MAX_CERT_LEN and self.version_tuple < (4, 2, 7): 431 | raise ValueError( 432 | 'Certificate too large, maximum is ' 433 | + str(NEO_MAX_CERT_LEN) 434 | + ' bytes (was ' 435 | + str(cert_len) + ' bytes).') 436 | elif cert_len > YK4_MAX_CERT_LEN: 437 | raise ValueError( 438 | 'Certificate too large, maximum is ' 439 | + str(YK4_MAX_CERT_LEN) 440 | + ' bytes (was ' 441 | + str(cert_len) + ' bytes).') 442 | else: 443 | raise 444 | self.update_chuid() 445 | 446 | def delete_certificate(self, slot): 447 | if not self.authenticated: 448 | raise ValueError('Not authenticated') 449 | self._key.delete_cert(slot) 450 | 451 | def should_show_macos_dialog(self): 452 | return is_macos_sierra_or_later() \ 453 | and AUTH_SLOT not in self.certs \ 454 | and ENCRYPTION_SLOT not in self.certs 455 | -------------------------------------------------------------------------------- /pivman/libykpiv.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | from ctypes import (Structure, POINTER, c_int, c_ubyte, c_char_p, c_long, 28 | c_ulong, c_size_t) 29 | from pivman.yubicommon.ctypes import CLibrary 30 | 31 | 32 | ykpiv_state = type('ykpiv_state', (Structure,), {}) 33 | ykpiv_rc = c_int 34 | 35 | 36 | class YKPIV(object): 37 | OK = 0 38 | MEMORY_ERROR = -1 39 | PCSC_ERROR = -2 40 | SIZE_ERROR = -3 41 | APPLET_ERROR = -4 42 | AUTHENTICATION_ERROR = -5 43 | RANDOMNESS_ERROR = -6 44 | GENERIC_ERROR = -7 45 | KEY_ERROR = -8 46 | PARSE_ERROR = -9 47 | WRONG_PIN = -10 48 | INVALID_OBJECT = -11 49 | ALGORITHM_ERROR = -12 50 | 51 | class OBJ(object): 52 | CAPABILITY = 0x5fc107 53 | CHUID = 0x5fc102 54 | AUTHENTICATION = 0x5fc105 # cert for 9a key 55 | FINGERPRINTS = 0x5fc103 56 | SECURITY = 0x5fc106 57 | FACIAL = 0x5fc108 58 | PRINTED = 0x5fc109 59 | SIGNATURE = 0x5fc10a # cert for 9c key 60 | KEY_MANAGEMENT = 0x5fc10b # cert for 9d key 61 | CARD_AUTH = 0x5fc101 # cert for 9e key 62 | DISCOVERY = 0x7e 63 | KEY_HISTORY = 0x5fc10c 64 | IRIS = 0x5fc121 65 | 66 | class ALGO(object): 67 | TDEA = 0x03 68 | RSA1024 = 0x06 69 | RSA2048 = 0x07 70 | ECCP256 = 0x11 71 | ECCP384 = 0x14 72 | 73 | 74 | class LibYkPiv(CLibrary): 75 | ykpiv_strerror = [ykpiv_rc], c_char_p 76 | ykpiv_strerror_name = [ykpiv_rc], c_char_p 77 | 78 | ykpiv_init = [POINTER(POINTER(ykpiv_state)), c_int], ykpiv_rc 79 | ykpiv_done = [POINTER(ykpiv_state)], ykpiv_rc 80 | 81 | ykpiv_connect = [POINTER(ykpiv_state), c_char_p], ykpiv_rc 82 | ykpiv_disconnect = [POINTER(ykpiv_state)], ykpiv_rc 83 | ykpiv_transfer_data = [POINTER(ykpiv_state), POINTER(c_ubyte), 84 | POINTER(c_ubyte), c_long, POINTER(c_ubyte), 85 | POINTER(c_ulong), POINTER(c_int)], ykpiv_rc 86 | ykpiv_authenticate = [POINTER(ykpiv_state), POINTER(c_ubyte)], ykpiv_rc 87 | ykpiv_set_mgmkey = [POINTER(ykpiv_state), POINTER(c_ubyte)], ykpiv_rc 88 | ykpiv_hex_decode = [c_char_p, c_size_t, POINTER(c_ubyte), POINTER(c_size_t) 89 | ], ykpiv_rc 90 | ykpiv_sign_data = [POINTER(ykpiv_state), POINTER(c_ubyte), c_size_t, 91 | POINTER(c_ubyte), POINTER(c_size_t), c_ubyte, c_ubyte 92 | ], ykpiv_rc 93 | ykpiv_get_version = [POINTER(ykpiv_state), c_char_p, c_size_t], ykpiv_rc 94 | ykpiv_verify = [POINTER(ykpiv_state), c_char_p, POINTER(c_int)], ykpiv_rc 95 | ykpiv_fetch_object = [POINTER(ykpiv_state), c_int, POINTER(c_ubyte), 96 | POINTER(c_ulong)], ykpiv_rc 97 | ykpiv_save_object = [POINTER(ykpiv_state), c_int, POINTER(c_ubyte), 98 | c_size_t], ykpiv_rc 99 | 100 | ykpiv_check_version = [c_char_p], c_char_p 101 | 102 | 103 | ykpiv = LibYkPiv('ykpiv', '1') 104 | -------------------------------------------------------------------------------- /pivman/messages.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | """ 28 | Strings for YubiKey PIV Manager. 29 | 30 | Note: String names must not start with underscore (_). 31 | 32 | """ 33 | 34 | organization = "Yubico" 35 | domain = "yubico.com" 36 | app_name = "YubiKey PIV Manager" 37 | win_title_1 = "YubiKey PIV Manager (%s)" 38 | about_1 = "About: %s" 39 | copyright = "Copyright © Yubico" 40 | libraries = "Library versions" 41 | version_1 = "Version: %s" 42 | menu_file = "&File" 43 | menu_help = "&Help" 44 | action_about = "&About" 45 | action_settings = "&Settings" 46 | settings = "Settings" 47 | general = "General" 48 | misc = "Miscellaneous" 49 | certificates = "Certificates" 50 | active_directory = "Active Directory" 51 | active_directory_desc = "The following options are used when requesting a " \ 52 | "certificate from the Windows CA" 53 | reader_name = "Card reader name" 54 | no = "no" 55 | ok = "OK" 56 | cancel = "Cancel" 57 | error = "Error" 58 | refresh = "Refresh" 59 | no_key = "No YubiKey found. Please insert a PIV enabled YubiKey..." 60 | key_with_applet_1 = "YubiKey present with applet version: %s." 61 | name = "Name" 62 | name_1 = "Name: %s" 63 | wait = "Please wait..." 64 | device_unplugged = "Unable to communicate with the device, has it been removed?" 65 | certs_loaded_1 = "You have %s certificate(s) loaded." 66 | change_name = "Change name" 67 | change_name_desc = "Change the name of the device." 68 | current_pin_label = "Current PIN:" 69 | current_puk_label = "Current PUK:" 70 | current_key_label = "Current Management Key:" 71 | new_pin_label = "New PIN (6-8 characters):" 72 | new_key_label = "New Management Key:" 73 | verify_pin_label = "Repeat new PIN:" 74 | pin = "PIN" 75 | pin_label = "PIN:" 76 | pin_days_left_1 = "PIN expires in %s days." 77 | puk = "PUK" 78 | puk_label = "PUK:" 79 | new_puk_label = "PUK (6-8 characters):" 80 | verify_puk_label = "Repeat PUK:" 81 | puk_confirm_mismatch = "PUKs don't match!" 82 | no_puk = "No PUK set" 83 | no_puk_warning = "If you do not set a PUK you will not be able to reset your " \ 84 | "PIN in case it is ever lost. Continue without setting a PUK?" 85 | puk_not_complex = "PUK doesn't meet complexity rules" 86 | initialize = "Device Initialization" 87 | key_type_pin = "PIN (same as above)" 88 | key_type_key = "Key" 89 | key_invalid = "Invalid management key" 90 | key_invalid_desc = "The key you have provided is invalid. It should contain " \ 91 | "exactly 48 hexadecimal characters." 92 | management_key = "Management key" 93 | key_type_label = "Key type:" 94 | key_label = "Management Key:" 95 | use_pin_as_key = "Use PIN as key" 96 | use_separate_key = "Use a separate key" 97 | randomize = "Randomize" 98 | copy_clipboard = "Copy to clipboard" 99 | change_pin = "Change PIN" 100 | reset_pin = "Reset PIN" 101 | reset_device = "Reset device" 102 | reset_device_warning = "This will erase all data including keys and " \ 103 | "certificates from the device. Your PIN, PUK and Management Key will be " \ 104 | "reset to the factory defaults." 105 | resetting_device = "Resetting device..." 106 | device_resetted = "Device reset complete" 107 | device_resetted_desc = "Your device has now been reset, and will require " \ 108 | "initialization." 109 | change_puk = "Change PUK" 110 | change_key = "Change Management Key" 111 | change_pin_desc = "Change your PIN" 112 | change_pin_forced_desc = "Your PIN has expired and must now be changed." 113 | changing_pin = "Setting PIN..." 114 | changing_puk = "Setting PUK..." 115 | changing_key = "Setting Management Key..." 116 | initializing = "Initializing..." 117 | pin_changed = "PIN changed" 118 | pin_changed_desc = "Your PIN has been successfully changed." 119 | puk_changed = "PUK changed" 120 | puk_changed_desc = "Your PUK has been successfully changed." 121 | key_changed = "Management key changed" 122 | key_changed_desc = "Your management key has been successfully changed." 123 | pin_not_changed = "PIN not changed" 124 | pin_not_changed_desc = "New PIN must be different from old PIN" 125 | puk_not_changed = "PUK not changed" 126 | puk_not_changed_desc = "New PUK must be different from old PUK" 127 | pin_puk_same = "PIN and PUK the same" 128 | pin_puk_same_desc = "PIN and PUK must be different" 129 | puk_blocked = "PUK is blocked." 130 | block_puk = "PUK will be blocked" 131 | block_puk_desc = "Using your PIN as Management Key will block your PUK. " \ 132 | "You will not be able to recover your PIN if it is lost. A blocked PUK " \ 133 | "cannot be unblocked, even by setting a new Management Key." 134 | pin_confirm_mismatch = "PINs don't match!" 135 | pin_empty = "PIN is empty" 136 | pin_not_complex = "PIN doesn't meet complexity rules" 137 | pin_complexity_desc = """Your PIN/PUK must: 138 | 139 | * Not contain all or part of the user's account name 140 | * Be at least six characters in length 141 | * Contain characters from three of the following four categories: 142 | * English uppercase characters (A through Z) 143 | * English lowercase characters (a through z) 144 | * Base 10 digits (0 through 9) 145 | * Nonalphanumeric characters (e.g., !, $, #, %) 146 | """ 147 | enter_pin = "Enter PIN" 148 | enter_key = "Enter management key" 149 | manage_pin = "Manage device PINs" 150 | pin_is_key = "PIN is management key." 151 | enter_file_password = "Enter password to unlock file." 152 | password_label = "Password:" 153 | unknown = "Unknown" 154 | change_cert = "Request certificate" 155 | change_cert_warning_1 = "This will generate a new private key and request a " \ 156 | "certificate from the Windows CA, overwriting any previously stored " \ 157 | "credential in slot '%s' of your YubiKey's PIV applet. This action " \ 158 | "cannot be undone." 159 | changing_cert = "Requesting certificate..." 160 | export_to_file = "Export certificate..." 161 | export_cert = "Export certificate" 162 | save_pk = "Save Public Key as..." 163 | save_csr = "Save Certificate Signing Request as..." 164 | generate_key = "Generate new key..." 165 | generate_key_warning_1 = "A new private key will be generated and stored in " \ 166 | "slot '%s'." 167 | generating_key = "Generating new key..." 168 | generated_key = "New key generated" 169 | generated_key_desc_1 = "A new private key has been generated in slot '%s'." 170 | gen_out_pk_1 = "The corresponding public key has been saved to:\n%s" 171 | gen_out_csr_1 = "A certificate signing request has been saved to:\n%s" 172 | gen_out_ssc = "A self-signed certificate has been loaded." 173 | gen_out_ca = "A certificate from the CA has been loaded." 174 | import_from_file = "Import from file..." 175 | import_from_file_warning_1 = "Anything currently in slot '%s' will be " \ 176 | "overwritten by the imported content. This action cannot be undone." 177 | importing_file = "Importing from file..." 178 | unsupported_file = "Unsupported file type" 179 | delete_cert = "Delete certificate" 180 | delete_cert_warning_1 = "This will delete the certificate stored in " \ 181 | "slot '%s' of your YubiKey, and cannot be undone. Note that the "\ 182 | "private key is not deleted." 183 | deleting_cert = "Deleting certificate..." 184 | cert_exported = "Certificate exported" 185 | cert_exported_desc_1 = "Certificate exported to file: %s" 186 | cert_deleted = "Certificate deleted" 187 | cert_deleted_desc = "Certificate deleted successfully" 188 | cert_not_loaded = "No certificate loaded." 189 | cert_expires_1 = "Certificate expires: %s" 190 | cert_installed = "Certificate installed" 191 | cert_installed_desc = "A new certificate has been installed. You may need to " \ 192 | "unplug and re-insert your YubiKey before it can be used." 193 | cert_tmpl = "Certificate Template" 194 | subject = "Subject" 195 | error = "Error" 196 | wrong_key = "Incorrect management key" 197 | communication_error = "Communication error with the device" 198 | ykpiv_error_2 = "YkPiv error %d: %s" 199 | wrong_pin_tries_1 = "PIN verification failed. %d tries remaining" 200 | wrong_puk_tries_1 = "PUK verification failed. %d tries remaining" 201 | pin_blocked = "Your PIN has been blocked due to too many incorrect attempts." 202 | pin_too_long = "PIN must be no more than 8 characters long.\n" \ 203 | "NOTE: Special characters may be counted more than once." 204 | puk_too_long = "PUK must be no more than 8 characters long.\n" \ 205 | "NOTE: Special characters may be counted more than once." 206 | certreq_error = "There was an error requesting a certificate." 207 | certreq_error_1 = "Error running certreq: %s" 208 | ca_not_connected = "You currently do not have a connection to a " \ 209 | "Certification Authority." 210 | authentication_error = "Unable to authenticate to device" 211 | use_complex_pins = "Enforce complex PIN/PUKs" 212 | pin_expires = "Force periodic PIN change" 213 | pin_expires_days = "How often (days)?" 214 | issued_to_label = "Issued to:" 215 | issued_by_label = "Issued by:" 216 | valid_from_label = "Valid from:" 217 | valid_to_label = "Valid to:" 218 | usage_9a = "The X.509 Certificate for PIV Authentication and its associated " \ 219 | "private key, as defined in FIPS 201, is used to authenticate the card " \ 220 | "and the cardholder." 221 | usage_9c = "The X.509 Certificate for Digital Signature and its associated " \ 222 | "private key, as defined in FIPS 201, support the use of digital " \ 223 | "signatures for the purpose of document signing. " 224 | usage_9d = "The X.509 Certificate for Key Management and its associated " \ 225 | "private key, as defined in FIPS 201, support the use of encryption for " \ 226 | "the purpose of confidentiality." 227 | usage_9e = "FIPS 201 specifies the optional Card Authentication Key (CAK) as " \ 228 | "an asymmetric or symmetric key that is used to support additional " \ 229 | "physical access applications. " 230 | algorithm = "Algorithm" 231 | alg_rsa_1024 = "RSA (1024 bits)" 232 | alg_rsa_2048 = "RSA (2048 bits)" 233 | alg_ecc_p256 = "ECC (P-256)" 234 | alg_ecc_p384 = "ECC (P-384)" 235 | algorithm_1 = "Algorithm: %s" 236 | output = "Output" 237 | out_pk = "Public key" 238 | out_csr = "Certificate Signing Request (CSR)" 239 | out_ssc = "Create a self-signed certificate" 240 | out_ca = "Request a certificate from a Windows CA" 241 | no_output = "Your configuration does not allow any valid output format." 242 | invalid_subject = "Invalid subject" 243 | invalid_subject_desc = """The subject must be written as: 244 | /CN=host.example.com/OU=test/O=example.com""" 245 | usage_policy = "Usage policy" 246 | pin_policy = "Require PIN" 247 | pin_policy_1 = "Require PIN: %s" 248 | pin_policy_default = "Slot default" 249 | pin_policy_never = "Never" 250 | pin_policy_once = "Once" 251 | pin_policy_always = "Always" 252 | touch_policy = "Require button touch" 253 | touch_needed = "User action needed" 254 | touch_needed_desc = "You have chosen to require user interaction to use this " \ 255 | "certificate. Once you close this dialog, the light on your YubiKey " \ 256 | "will start slowly blinking. At that point please touch the button on " \ 257 | "your YubiKey." 258 | touch_prompt = "Touch the button now..." 259 | expiration_date = "Expiration date" 260 | setting_up_macos = "Setting up for macOS..." 261 | macos_pairing_title = "Set Up YubiKey for macOS" 262 | macos_pairing_desc = "

This version of macOS allows you to pair your " \ 263 | "YubiKey with your user account. When you have completed the pairing, " \ 264 | "you can use your YubiKey to log in to your Mac.

" \ 265 | "Do you want to generate certificates for this purpose (recommended)?

" 266 | setup_for_macos = "Setup for macOS" 267 | setup_macos_compl = "Setup for macOS completed" 268 | setup_macos_compl_desc = "Your YubiKey is now ready to be paired with your " \ 269 | "user account. To start the pairing process, remove and re-insert " \ 270 | "your YubiKey." 271 | non_numeric_pin_warning = "For cross-platform compatibility, " \ 272 | "we recommend " \ 273 | "you enter a PIN of 6-8 numeric digits." 274 | non_numeric_pin = "Pairing your YubiKey with macOS requires your PIN to only "\ 275 | "contain numeric characters. Do you want to change your PIN?" 276 | overwrite_slot_warning = "Overwrite slot?" 277 | overwrite_slot_warning_desc = "This will overwrite all data currently " \ 278 | "stored in slot '%s'. This action cannot be undone. " \ 279 | "Do you want to continue?" 280 | overwrite_slot_warning_macos = "This will overwrite all data currently " \ 281 | "stored in slot '9a' and '9d'. This action cannot be undone. "\ 282 | "Do you want to continue?" 283 | not_default_pin = "Your credentials for the YubiKey are not the default " \ 284 | "values. This may occur when the PIN or PUK has been changed with a " \ 285 | "different tool. Use the 'Manage device PINs' option to change the " \ 286 | "credentials." 287 | -------------------------------------------------------------------------------- /pivman/piv.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | from pivman.libykpiv import YKPIV, ykpiv, ykpiv_state 28 | from pivman.piv_cmd import YkPivCmd 29 | from pivman import messages as m 30 | from pivman.utils import der_read 31 | from pivman.yubicommon.compat import text_type, int2byte 32 | from ctypes import (POINTER, byref, create_string_buffer, sizeof, c_ubyte, 33 | c_size_t, c_int) 34 | from binascii import a2b_hex, b2a_hex 35 | import re 36 | 37 | 38 | _YKPIV_MIN_VERSION = b'1.2.0' 39 | 40 | libversion = ykpiv.ykpiv_check_version(_YKPIV_MIN_VERSION) 41 | if not libversion: 42 | raise Exception('libykpiv >= %s required' % _YKPIV_MIN_VERSION) 43 | 44 | 45 | class DeviceGoneError(Exception): 46 | 47 | def __init__(self): 48 | super(DeviceGoneError, self).__init__(m.communication_error) 49 | 50 | 51 | class PivError(Exception): 52 | 53 | def __init__(self, code): 54 | message = ykpiv.ykpiv_strerror(code) 55 | super(PivError, self).__init__(code, message) 56 | self.code = code 57 | self.message = message 58 | 59 | def __str__(self): 60 | return m.ykpiv.ykpiv_error_2 % (self.code, self.message) 61 | 62 | 63 | class WrongPinError(ValueError): 64 | m_tries_1 = m.wrong_pin_tries_1 65 | m_blocked = m.pin_blocked 66 | 67 | def __init__(self, tries): 68 | super(WrongPinError, self).__init__(self.m_tries_1 % tries 69 | if tries > 0 else self.m_blocked) 70 | self.tries = tries 71 | 72 | @property 73 | def blocked(self): 74 | return self.tries == 0 75 | 76 | 77 | class WrongPukError(WrongPinError): 78 | m_tries_1 = m.wrong_puk_tries_1 79 | m_blocked = m.puk_blocked 80 | 81 | 82 | def check(rc): 83 | if rc == YKPIV.PCSC_ERROR: 84 | raise DeviceGoneError() 85 | elif rc != YKPIV.OK: 86 | raise PivError(rc) 87 | 88 | 89 | def wrap_puk_error(error): 90 | match = TRIES_PATTERN.search(str(error)) 91 | if match: 92 | raise WrongPukError(int(match.group(1))) 93 | raise WrongPukError(0) 94 | 95 | 96 | KEY_LEN = 24 97 | DEFAULT_KEY = a2b_hex(b'010203040506070801020304050607080102030405060708') 98 | 99 | CERT_SLOTS = { 100 | '9a': YKPIV.OBJ.AUTHENTICATION, 101 | '9c': YKPIV.OBJ.SIGNATURE, 102 | '9d': YKPIV.OBJ.KEY_MANAGEMENT, 103 | '9e': YKPIV.OBJ.CARD_AUTH 104 | } 105 | 106 | ATTR_NAME = 'name' 107 | 108 | TRIES_PATTERN = re.compile(r'now (\d+) tries') 109 | 110 | 111 | class YkPiv(object): 112 | 113 | def __init__(self, verbosity=0, reader=None): 114 | self._cmd = YkPivCmd(verbosity=verbosity, reader=reader) 115 | 116 | self._state = POINTER(ykpiv_state)() 117 | if not reader: 118 | reader = 'Yubikey' 119 | 120 | self._chuid = None 121 | self._ccc = None 122 | self._pin_blocked = False 123 | self._verbosity = verbosity 124 | self._reader = reader 125 | self._certs = {} 126 | 127 | check(ykpiv.ykpiv_init(byref(self._state), self._verbosity)) 128 | self._connect() 129 | self._read_status() 130 | 131 | if not self.chuid: 132 | try: 133 | self.set_chuid() 134 | except ValueError: 135 | pass # Not autheniticated, perhaps? 136 | 137 | if not self.ccc: 138 | try: 139 | self.set_ccc() 140 | except ValueError: 141 | pass # Not autheniticated, perhaps? 142 | 143 | def reconnect(self): 144 | check(ykpiv.ykpiv_disconnect(self._state)) 145 | self._reset() 146 | 147 | def _connect(self): 148 | check(ykpiv.ykpiv_connect(self._state, self._reader.encode('utf8'))) 149 | 150 | self._read_version() 151 | self._read_chuid() 152 | 153 | def _read_status(self): 154 | try: 155 | check(ykpiv.ykpiv_disconnect(self._state)) 156 | data = self._cmd.run('-a', 'status') 157 | lines = data.splitlines() 158 | chunk = [] 159 | while lines: 160 | line = lines.pop(0) 161 | if chunk and not line.startswith(b'\t'): 162 | self._parse_status(chunk) 163 | chunk = [] 164 | chunk.append(line) 165 | if chunk: 166 | self._parse_status(chunk) 167 | self._status = data 168 | finally: 169 | self._reset() 170 | 171 | def _parse_status(self, chunk): 172 | parts, rest = chunk[0].split(), chunk[1:] 173 | if parts[0] == b'Slot' and rest: 174 | self._parse_slot(parts[1][:-1], rest) 175 | elif parts[0] == b'PIN': 176 | self._pin_blocked = parts[-1] == '0' 177 | 178 | def _parse_slot(self, slot, lines): 179 | slot = slot.decode('ascii') 180 | self._certs[slot] = dict(l.strip().split(b':\t', 1) for l in lines) 181 | 182 | def _read_version(self): 183 | v = create_string_buffer(10) 184 | check(ykpiv.ykpiv_get_version(self._state, v, sizeof(v))) 185 | self._version = v.value 186 | 187 | def _read_chuid(self): 188 | try: 189 | chuid_data = self.fetch_object(YKPIV.OBJ.CHUID)[29:29 + 16] 190 | self._chuid = b2a_hex(chuid_data) 191 | except PivError: # No chuid set? 192 | self._chuid = None 193 | 194 | def _read_ccc(self): 195 | try: 196 | ccc_data = self.fetch_object(YKPIV.OBJ.CAPABILITY)[29:29 + 16] 197 | self._ccc = b2a_hex(ccc_data) 198 | except PivError: # No ccc set? 199 | self._ccc = None 200 | 201 | def __del__(self): 202 | check(ykpiv.ykpiv_done(self._state)) 203 | 204 | def _reset(self): 205 | self._connect() 206 | if self._cmd._pin is not None: 207 | self.verify_pin(self._cmd._pin) 208 | if self._cmd._key is not None: 209 | self.authenticate(a2b_hex(self._cmd._key)) 210 | 211 | @property 212 | def version(self): 213 | return self._version 214 | 215 | @property 216 | def chuid(self): 217 | return self._chuid 218 | 219 | @property 220 | def ccc(self): 221 | return self._ccc 222 | 223 | @property 224 | def pin_blocked(self): 225 | return self._pin_blocked 226 | 227 | @property 228 | def certs(self): 229 | return dict(self._certs) 230 | 231 | def set_chuid(self): 232 | try: 233 | check(ykpiv.ykpiv_disconnect(self._state)) 234 | self._cmd.set_chuid() 235 | finally: 236 | self._reset() 237 | 238 | def set_ccc(self): 239 | try: 240 | check(ykpiv.ykpiv_disconnect(self._state)) 241 | self._cmd.run('-a', 'set-ccc') 242 | finally: 243 | self._reset() 244 | 245 | def authenticate(self, key=None): 246 | if key is None: 247 | key = DEFAULT_KEY 248 | elif len(key) != KEY_LEN: 249 | raise ValueError('Key must be %d bytes' % KEY_LEN) 250 | c_key = (c_ubyte * KEY_LEN).from_buffer_copy(key) 251 | check(ykpiv.ykpiv_authenticate(self._state, c_key)) 252 | self._cmd._key = b2a_hex(key) 253 | if not self.chuid: 254 | self.set_chuid() 255 | 256 | def set_authentication(self, key): 257 | if len(key) != KEY_LEN: 258 | raise ValueError('Key must be %d bytes' % KEY_LEN) 259 | c_key = (c_ubyte * len(key)).from_buffer_copy(key) 260 | check(ykpiv.ykpiv_set_mgmkey(self._state, c_key)) 261 | self._cmd._key = b2a_hex(key) 262 | 263 | def verify_pin(self, pin): 264 | if isinstance(pin, text_type): 265 | pin = pin.encode('utf8') 266 | buf = create_string_buffer(pin) 267 | tries = c_int(-1) 268 | rc = ykpiv.ykpiv_verify(self._state, buf, byref(tries)) 269 | 270 | if rc == YKPIV.WRONG_PIN: 271 | if tries.value == 0: 272 | self._pin_blocked = True 273 | self._cmd._pin = None 274 | raise WrongPinError(tries.value) 275 | check(rc) 276 | self._cmd._pin = pin 277 | 278 | def set_pin(self, pin): 279 | if isinstance(pin, text_type): 280 | pin = pin.encode('utf8') 281 | if len(pin) > 8: 282 | raise ValueError(m.pin_too_long) 283 | try: 284 | check(ykpiv.ykpiv_disconnect(self._state)) 285 | self._cmd.change_pin(pin) 286 | finally: 287 | self._reset() 288 | 289 | def reset_pin(self, puk, new_pin): 290 | if isinstance(new_pin, text_type): 291 | new_pin = new_pin.encode('utf8') 292 | if len(new_pin) > 8: 293 | raise ValueError(m.pin_too_long) 294 | if isinstance(puk, text_type): 295 | puk = puk.encode('utf8') 296 | try: 297 | check(ykpiv.ykpiv_disconnect(self._state)) 298 | self._cmd.reset_pin(puk, new_pin) 299 | except ValueError as e: 300 | wrap_puk_error(e) 301 | finally: 302 | self._reset() 303 | self._read_status() 304 | 305 | def set_puk(self, puk, new_puk): 306 | if isinstance(puk, text_type): 307 | puk = puk.encode('utf8') 308 | if isinstance(new_puk, text_type): 309 | new_puk = new_puk.encode('utf8') 310 | if len(new_puk) > 8: 311 | raise ValueError(m.puk_too_long) 312 | 313 | try: 314 | check(ykpiv.ykpiv_disconnect(self._state)) 315 | self._cmd.change_puk(puk, new_puk) 316 | except ValueError as e: 317 | wrap_puk_error(e) 318 | finally: 319 | self._reset() 320 | 321 | def reset_device(self): 322 | try: 323 | check(ykpiv.ykpiv_disconnect(self._state)) 324 | self._cmd.run('-a', 'reset') 325 | finally: 326 | del self._cmd 327 | 328 | def fetch_object(self, object_id): 329 | buf = (c_ubyte * 4096)() 330 | buf_len = c_size_t(sizeof(buf)) 331 | 332 | check(ykpiv.ykpiv_fetch_object(self._state, object_id, buf, 333 | byref(buf_len))) 334 | return b''.join(map(int2byte, buf[:buf_len.value])) 335 | 336 | def save_object(self, object_id, data): 337 | c_data = (c_ubyte * len(data)).from_buffer_copy(data) 338 | check(ykpiv.ykpiv_save_object(self._state, object_id, c_data, 339 | len(data))) 340 | 341 | def generate(self, slot, algorithm, pin_policy, touch_policy): 342 | try: 343 | check(ykpiv.ykpiv_disconnect(self._state)) 344 | return self._cmd.generate(slot, algorithm, pin_policy, touch_policy) 345 | finally: 346 | self._reset() 347 | 348 | def create_csr(self, subject, pubkey_pem, slot): 349 | try: 350 | check(ykpiv.ykpiv_disconnect(self._state)) 351 | return self._cmd.create_csr(subject, pubkey_pem, slot) 352 | finally: 353 | self._reset() 354 | 355 | def create_selfsigned_cert(self, subject, pubkey_pem, slot, valid_days=365): 356 | try: 357 | check(ykpiv.ykpiv_disconnect(self._state)) 358 | return self._cmd.create_ssc(subject, pubkey_pem, slot, valid_days) 359 | finally: 360 | self._reset() 361 | 362 | def import_cert(self, cert_pem, slot, frmt='PEM', password=None): 363 | try: 364 | check(ykpiv.ykpiv_disconnect(self._state)) 365 | return self._cmd.import_cert(cert_pem, slot, frmt, password) 366 | finally: 367 | self._reset() 368 | self._read_status() 369 | 370 | def import_key(self, cert_pem, slot, frmt, password, pin_policy, 371 | touch_policy): 372 | try: 373 | check(ykpiv.ykpiv_disconnect(self._state)) 374 | return self._cmd.import_key(cert_pem, slot, frmt, password, 375 | pin_policy, touch_policy) 376 | finally: 377 | self._reset() 378 | 379 | def sign_data(self, slot, hashed, algorithm=YKPIV.ALGO.RSA2048): 380 | c_hashed = (c_ubyte * len(hashed)).from_buffer_copy(hashed) 381 | buf = (c_ubyte * 4096)() 382 | buf_len = c_size_t(sizeof(buf)) 383 | check(ykpiv.ykpiv_sign_data(self._state, c_hashed, len(hashed), buf, 384 | byref(buf_len), algorithm, int(slot, 16))) 385 | return ''.join(map(int2byte, buf[:buf_len.value])) 386 | 387 | def read_cert(self, slot): 388 | try: 389 | data = self.fetch_object(CERT_SLOTS[slot]) 390 | except PivError: 391 | return None 392 | cert, rest = der_read(data, 0x70) 393 | zipped, rest = der_read(rest, 0x71) 394 | if zipped != b'\0': 395 | pass # TODO: cert is compressed, uncompress. 396 | return cert 397 | 398 | def delete_cert(self, slot): 399 | if slot not in self._certs: 400 | raise ValueError('No certificate loaded in slot: %s' % slot) 401 | 402 | try: 403 | check(ykpiv.ykpiv_disconnect(self._state)) 404 | self._cmd.delete_cert(slot) 405 | del self._certs[slot] 406 | finally: 407 | self._reset() 408 | -------------------------------------------------------------------------------- /pivman/piv_cmd.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | 28 | import subprocess 29 | import sys 30 | import os 31 | 32 | 33 | CMD = 'yubico-piv-tool' 34 | 35 | if getattr(sys, 'frozen', False): 36 | # we are running in a PyInstaller bundle 37 | basedir = sys._MEIPASS 38 | else: 39 | # we are running in a normal Python environment 40 | basedir = os.path.dirname(__file__) 41 | 42 | 43 | def find_cmd(): 44 | def is_exe(fpath): 45 | return os.path.isfile(fpath) and os.access(fpath, os.X_OK) 46 | 47 | cmd = CMD + '.exe' if sys.platform == 'win32' else CMD 48 | paths = [basedir] + os.environ.get('PATH', '').split(os.pathsep) 49 | for path in paths: 50 | path = path.strip('"') 51 | fpath = os.path.join(path, cmd) 52 | if is_exe(fpath): 53 | return fpath 54 | return None 55 | 56 | 57 | def check(status, err): 58 | if status != 0: 59 | raise ValueError('Error: %s' % err) 60 | 61 | 62 | def set_arg(args, opt, value): 63 | args = list(args) 64 | if opt != '-a' and opt in args: 65 | index = args.index(opt) 66 | if value is None: 67 | del args[index] 68 | del args[index] 69 | else: 70 | args[index + 1] = value 71 | elif value is not None: 72 | args.extend([opt, value]) 73 | return args 74 | 75 | 76 | class YkPivCmd(object): 77 | 78 | _pin = None 79 | _key = None 80 | 81 | def __init__(self, cmd=find_cmd(), verbosity=0, reader=None, key=None): 82 | self._base_args = [cmd] 83 | if verbosity > 0: 84 | self._base_args.extend(['-v', str(verbosity)]) 85 | if reader: 86 | self._base_args.extend(['-r', reader]) 87 | if key: 88 | self._key = key 89 | 90 | def set_arg(self, opt, value): 91 | if isinstance(value, bytes): 92 | value = value.decode('utf8') 93 | self._base_args = set_arg(self._base_args, opt, value) 94 | 95 | def run(self, *args, **kwargs): 96 | full_args = list(self._base_args) 97 | new_args = list(args) 98 | while new_args: 99 | full_args = set_arg(full_args, new_args.pop(0), new_args.pop(0)) 100 | 101 | if '-k' in full_args: # Workaround for passing key in 1.1.0 102 | i = full_args.index('-k') 103 | full_args = full_args[:i] + ['-k' + full_args[i+1]] \ 104 | + full_args[i+2:] 105 | p = subprocess.Popen(full_args, stdin=subprocess.PIPE, 106 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 107 | startupinfo=self.get_startup_info()) 108 | out, err = p.communicate(**kwargs) 109 | check(p.returncode, err) 110 | 111 | return out 112 | 113 | def get_startup_info(self): 114 | if sys.platform == 'win32': # Avoid showing console window on Windows 115 | startupinfo = subprocess.STARTUPINFO() 116 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 117 | else: 118 | startupinfo = None 119 | return startupinfo 120 | 121 | def status(self): 122 | return self.run('-a', 'status') 123 | 124 | def change_pin(self, new_pin): 125 | if self._pin is None: 126 | raise ValueError('PIN has not been verified') 127 | full_args = list(self._base_args) 128 | full_args = set_arg(full_args, '-a', 'change-pin') 129 | full_args.append('--stdin-input') 130 | p = subprocess.Popen(full_args, stdin=subprocess.PIPE, 131 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 132 | startupinfo=self.get_startup_info()) 133 | p.stdin.write(self._pin + '\n') 134 | p.stdin.write(new_pin + '\n') 135 | p.stdin.flush() 136 | out, err = p.communicate() 137 | check(p.returncode, err) 138 | self._pin = new_pin 139 | 140 | def change_puk(self, old_puk, new_puk): 141 | full_args = list(self._base_args) 142 | full_args = set_arg(full_args, '-a', 'change-puk') 143 | full_args.append('--stdin-input') 144 | p = subprocess.Popen(full_args, stdin=subprocess.PIPE, 145 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 146 | startupinfo=self.get_startup_info()) 147 | p.stdin.write(old_puk + '\n') 148 | p.stdin.write(new_puk + '\n') 149 | p.stdin.flush() 150 | out, err = p.communicate() 151 | check(p.returncode, err) 152 | 153 | def reset_pin(self, puk, new_pin): 154 | full_args = list(self._base_args) 155 | full_args = set_arg(full_args, '-a', 'unblock-pin') 156 | full_args.append('--stdin-input') 157 | p = subprocess.Popen(full_args, stdin=subprocess.PIPE, 158 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 159 | startupinfo=self.get_startup_info()) 160 | p.stdin.write(puk + '\n') 161 | p.stdin.write(new_pin + '\n') 162 | p.stdin.flush() 163 | out, err = p.communicate() 164 | check(p.returncode, err) 165 | 166 | def set_chuid(self): 167 | full_args = list(self._base_args) 168 | full_args = set_arg(full_args, '-a', 'set-chuid') 169 | if self._key is not None: 170 | full_args.append('-k') 171 | full_args.append('--stdin-input') 172 | p = subprocess.Popen(full_args, stdin=subprocess.PIPE, 173 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 174 | startupinfo=self.get_startup_info()) 175 | if self._key is not None: 176 | p.stdin.write(self._key + '\n') 177 | p.stdin.flush() 178 | out, err = p.communicate() 179 | check(p.returncode, err) 180 | 181 | def generate(self, slot, algorithm, pin_policy, touch_policy): 182 | full_args = list(self._base_args) 183 | full_args = set_arg(full_args, '-s', slot) 184 | full_args = set_arg(full_args, '-a', 'generate') 185 | full_args = set_arg(full_args, '-A', algorithm) 186 | full_args = set_arg(full_args, '--pin-policy', pin_policy) 187 | full_args = set_arg( 188 | full_args, '--touch-policy', 'always' if touch_policy else 'never') 189 | full_args.append('-k') 190 | full_args.append('--stdin-input') 191 | p = subprocess.Popen(full_args, stdin=subprocess.PIPE, 192 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 193 | startupinfo=self.get_startup_info()) 194 | p.stdin.write(self._key + '\n') 195 | p.stdin.flush() 196 | out, err = p.communicate() 197 | check(p.returncode, err) 198 | return out 199 | 200 | def create_csr(self, subject, pem, slot): 201 | if self._pin is None: 202 | raise ValueError('PIN has not been verified') 203 | full_args = list(self._base_args) 204 | full_args = set_arg(full_args, '-a', 'verify-pin') 205 | full_args = set_arg(full_args, '-s', slot) 206 | full_args = set_arg(full_args, '-a', 'request-certificate') 207 | full_args = set_arg(full_args, '-S', subject) 208 | full_args.append('--stdin-input') 209 | p = subprocess.Popen(full_args, stdin=subprocess.PIPE, 210 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 211 | startupinfo=self.get_startup_info()) 212 | p.stdin.write(self._pin + '\n') 213 | p.stdin.flush() 214 | out, err = p.communicate(input=pem) 215 | check(p.returncode, err) 216 | return out 217 | 218 | def create_ssc(self, subject, pem, slot, valid_days=365): 219 | if self._pin is None: 220 | raise ValueError('PIN has not been verified') 221 | full_args = list(self._base_args) 222 | full_args = set_arg(full_args, '-a', 'verify-pin') 223 | full_args = set_arg(full_args, '-s', slot) 224 | full_args = set_arg(full_args, '-a', 'selfsign-certificate') 225 | full_args = set_arg(full_args, '-S', subject) 226 | full_args = set_arg(full_args, '--valid-days', str(valid_days)) 227 | full_args.append('-k') 228 | full_args.append('--stdin-input') 229 | p = subprocess.Popen(full_args, stdin=subprocess.PIPE, 230 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 231 | startupinfo=self.get_startup_info()) 232 | p.stdin.write(self._pin + '\n') 233 | p.stdin.write(self._key + '\n') 234 | p.stdin.flush() 235 | out, err = p.communicate(input=pem) 236 | check(p.returncode, err) 237 | return out 238 | 239 | def import_cert(self, data, slot, frmt='PEM', password=None): 240 | return self._do_import('import-cert', data, slot, frmt, password) 241 | 242 | def import_key(self, data, slot, frmt, password, pin_policy, touch_policy): 243 | return self._do_import('import-key', data, slot, frmt, password, 244 | '--pin-policy', pin_policy, '--touch-policy', 245 | 'always' if touch_policy else 'never') 246 | 247 | def _do_import(self, action, data, slot, frmt, password, *args): 248 | if self._key is None: 249 | raise ValueError('Management key has not been provided') 250 | full_args = list(self._base_args) 251 | full_args = set_arg(full_args, '-s', slot) 252 | full_args = set_arg(full_args, '-K', frmt) 253 | full_args = set_arg(full_args, '-a', action) 254 | new_args = list(args) 255 | while new_args: 256 | full_args = set_arg(full_args, new_args.pop(0), new_args.pop(0)) 257 | full_args.append('-k') 258 | full_args.append('--stdin-input') 259 | p = subprocess.Popen(full_args, stdin=subprocess.PIPE, 260 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 261 | startupinfo=self.get_startup_info()) 262 | if password is not None: 263 | p.stdin.write(password + '\n') 264 | p.stdin.write(self._key + '\n') 265 | p.stdin.flush() 266 | # A small sleep is needed to get yubico-piv-tool 267 | # to read properly in all cases. 268 | import time 269 | time.sleep(0.1) 270 | out, err = p.communicate(input=data) 271 | check(p.returncode, err) 272 | return out 273 | 274 | def delete_cert(self, slot): 275 | if self._key is None: 276 | raise ValueError('Management key has not been provided') 277 | full_args = list(self._base_args) 278 | full_args = set_arg(full_args, '-a', 'delete-certificate') 279 | full_args = set_arg(full_args, '-s', slot) 280 | full_args.append('-k') 281 | full_args.append('--stdin-input') 282 | p = subprocess.Popen(full_args, stdin=subprocess.PIPE, 283 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 284 | startupinfo=self.get_startup_info()) 285 | p.stdin.write(self._key + '\n') 286 | p.stdin.flush() 287 | out, err = p.communicate() 288 | check(p.returncode, err) 289 | return out 290 | -------------------------------------------------------------------------------- /pivman/storage.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | import os 28 | from pivman import messages as m 29 | from pivman.piv import CERT_SLOTS 30 | from pivman.yubicommon import qt 31 | from PySide import QtCore 32 | from collections import namedtuple 33 | from getpass import getuser 34 | from sys import platform 35 | 36 | __all__ = [ 37 | 'CONFIG_HOME', 38 | 'SETTINGS', 39 | 'settings' 40 | ] 41 | 42 | CONFIG_HOME = os.path.join(os.path.expanduser('~'), '.pivman') 43 | 44 | 45 | Setting = namedtuple('Setting', 'key default type') 46 | 47 | win = platform == 'win32' 48 | 49 | 50 | def default_outs(): 51 | if win: 52 | return ['ssc', 'csr', 'ca'] 53 | else: 54 | return ['ssc', 'csr'] 55 | 56 | 57 | class SETTINGS: 58 | ALGORITHM = Setting('algorithm', 'RSA2048', str) 59 | CARD_READER = Setting('card_reader', None, str) 60 | CERTREQ_TEMPLATE = Setting('certreq_template', None, str) 61 | COMPLEX_PINS = Setting('complex_pins', False, bool) 62 | ENABLE_IMPORT = Setting('enable_import', True, bool) 63 | OUT_TYPE = Setting('out_type', 'ca' if win else 'ssc', str) 64 | PIN_AS_KEY = Setting('pin_as_key', True, bool) 65 | PIN_EXPIRATION = Setting('pin_expiration', 0, int) 66 | PIN_POLICY = Setting('pin_policy', None, str) 67 | PIN_POLICY_SLOTS = Setting('pin_policy_slots', [], list) 68 | SHOWN_OUT_FORMS = Setting('shown_outs', default_outs(), list) 69 | SHOWN_SLOTS = Setting('shown_slots', sorted(CERT_SLOTS.keys()), list) 70 | SUBJECT = Setting('subject', '/CN=%s' % getuser(), str) 71 | TOUCH_POLICY = Setting('touch_policy', False, bool) 72 | TOUCH_POLICY_SLOTS = Setting('touch_policy_slots', [], list) 73 | 74 | 75 | class SettingsOverlay(object): 76 | 77 | def __init__(self, master, overlay): 78 | self._master = master 79 | self._overlay = overlay 80 | 81 | def __getattr__(self, method_name): 82 | return getattr(self._overlay, method_name) 83 | 84 | def rename(self, new_name): 85 | raise NotImplementedError() 86 | 87 | def value(self, setting, default=None): 88 | """Give preference to master.""" 89 | key, default, d_type = setting 90 | val = self._master.value(key, self._overlay.value(key, default)) 91 | if not isinstance(val, d_type): 92 | val = qt.convert_to(val, d_type) 93 | return val 94 | 95 | def setValue(self, setting, value): 96 | self._overlay.setValue(setting.key, value) 97 | 98 | def remove(self, setting): 99 | self._overlay.remove(setting.key) 100 | 101 | def childKeys(self): 102 | """Combine keys of master and overlay.""" 103 | return list(set(self._master.childKeys() + self._overlay.childKeys())) 104 | 105 | def is_locked(self, setting): 106 | return self._master.contains(setting.key) 107 | 108 | def __repr__(self): 109 | return 'Overlay(%s, %s)' % (self._master, self._overlay) 110 | 111 | 112 | settings = qt.PySettings(SettingsOverlay( 113 | QtCore.QSettings(m.organization, m.app_name), 114 | qt.Settings.wrap(os.path.join(CONFIG_HOME, 'settings.ini'), 115 | QtCore.QSettings.IniFormat).get_group('settings') 116 | )) 117 | -------------------------------------------------------------------------------- /pivman/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | from getpass import getuser 28 | from pivman import messages as m 29 | from pivman.yubicommon.compat import byte2int 30 | import re 31 | import subprocess 32 | import os 33 | import tempfile 34 | import sys 35 | 36 | 37 | def has_ca(): 38 | try: 39 | if sys.platform == 'win32': 40 | startupinfo = subprocess.STARTUPINFO() 41 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 42 | p = subprocess.Popen( 43 | ['certutil', '-dump'], stdout=subprocess.PIPE, 44 | startupinfo=startupinfo) 45 | output, _ = p.communicate() 46 | # 'certutil -dump' returns one line when no CA is available. 47 | return len(str.splitlines(output)) > 1 48 | 49 | except OSError: 50 | pass 51 | return False 52 | 53 | 54 | def request_cert_from_ca(csr, cert_tmpl): 55 | try: 56 | with tempfile.NamedTemporaryFile(delete=False) as f: 57 | f.write(csr) 58 | csr_fn = f.name 59 | 60 | with tempfile.NamedTemporaryFile() as f: 61 | cert_fn = f.name 62 | 63 | p = subprocess.Popen(['certreq', '-submit', '-attrib', 64 | 'CertificateTemplate:%s' % cert_tmpl, csr_fn, 65 | cert_fn], stdin=subprocess.PIPE, 66 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 67 | out, _ = p.communicate() 68 | if p.returncode != 0: 69 | raise ValueError(m.certreq_error_1 % out) 70 | 71 | with open(cert_fn, 'r') as cert: 72 | return cert.read() 73 | except OSError as e: 74 | raise ValueError(m.certreq_error_1 % e) 75 | finally: 76 | os.remove(csr_fn) 77 | if os.path.isfile(cert_fn): 78 | os.remove(cert_fn) 79 | 80 | 81 | def test(fn, *args, **kwargs): 82 | e_type = kwargs.pop('catches', Exception) 83 | try: 84 | fn(*args, **kwargs) 85 | return True 86 | except e_type: 87 | return False 88 | 89 | 90 | # https://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/504.mspx?mfr=true 91 | 92 | # Password must contain characters from three of the following four categories: 93 | CATEGORIES = [ 94 | lambda c: c.isupper(), # English uppercase characters (A through Z) 95 | lambda c: c.islower(), # English lowercase characters (a through z) 96 | re.compile(r'[0-9]').match, # Base 10 digits (0 through 9) 97 | re.compile(r'\W', re.UNICODE).match 98 | # Nonalphanumeric characters (e.g., !, $, #, %) 99 | ] 100 | 101 | 102 | def contains_category(password, category): 103 | return any(category(p) for p in password) 104 | 105 | 106 | def complexity_check(password): 107 | # Be at least six characters in length 108 | if len(password) < 6: 109 | return False 110 | 111 | # Contain characters from at least 3 groups: 112 | if sum(contains_category(password, c) for c in CATEGORIES) < 3: 113 | return False 114 | 115 | # Not contain all or part of the user's account name 116 | parts = [p for p in re.split(r'\W', getuser().lower()) if len(p) >= 3] 117 | if any(part in password.lower() for part in parts): 118 | return False 119 | 120 | return True 121 | 122 | 123 | def der_read(der_data, expected_t=None): 124 | t = byte2int(der_data[0]) 125 | if expected_t is not None and expected_t != t: 126 | raise ValueError('Wrong tag. Expected: %x, got: %x' % (expected_t, t)) 127 | l = byte2int(der_data[1]) 128 | offs = 2 129 | if l > 0x80: 130 | n_bytes = l - 0x80 131 | l = b2len(der_data[offs:offs + n_bytes]) 132 | offs = offs + n_bytes 133 | v = der_data[offs:offs + l] 134 | rest = der_data[offs + l:] 135 | if expected_t is None: 136 | return t, v, rest 137 | return v, rest 138 | 139 | 140 | def b2len(bs): 141 | l = 0 142 | for b in bs: 143 | l *= 256 144 | l += byte2int(b) 145 | return l 146 | 147 | 148 | def is_macos_sierra_or_later(): 149 | if sys.platform == 'darwin': 150 | from platform import mac_ver 151 | mac_version = tuple(int(x) for x in mac_ver()[0].split('.')) 152 | return mac_version >= (10, 12) 153 | return False 154 | -------------------------------------------------------------------------------- /pivman/view/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | -------------------------------------------------------------------------------- /pivman/view/cert.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | from PySide import QtGui, QtCore, QtNetwork 28 | from pivman import messages as m 29 | from pivman.piv import PivError, DeviceGoneError 30 | from pivman.storage import settings, SETTINGS 31 | from pivman.view.utils import Dialog, get_text 32 | from pivman.view.generate_dialog import GenerateKeyDialog 33 | from pivman.view.usage_policy_dialog import UsagePolicyDialog 34 | from datetime import datetime 35 | from functools import partial 36 | 37 | SLOTS = { 38 | '9a': 'Authentication', 39 | '9c': 'Digital Signature', 40 | '9d': 'Key Management', 41 | '9e': 'Card Authentication', 42 | } 43 | 44 | USAGES = { 45 | '9a': m.usage_9a, 46 | '9c': m.usage_9c, 47 | '9d': m.usage_9d, 48 | '9e': m.usage_9e, 49 | } 50 | 51 | FILE_FILTER = 'Certificate/key files ' \ 52 | '(*.pfx *.p12 *.cer *.crt *.key *.pem *.der)' 53 | 54 | 55 | def detect_type(data, fn): 56 | suffix = '.' in fn and fn.lower().rsplit('.', 1)[1] 57 | f_format = None # pfx, pem or der 58 | f_type = 0 # 1 for certificate, 2 for key, 3 for both 59 | needs_password = False 60 | if suffix in ['pfx', 'p12']: 61 | f_format = 'pfx' 62 | needs_password = True 63 | else: 64 | f_format = 'pem' if data.startswith(b'-----') else 'der' 65 | if f_format == 'pem': 66 | if b'CERTIFICATE' in data and b'PRIVATE KEY' in data: 67 | f_type = 3 68 | elif b'PRIVATE KEY' in data: 69 | f_type = 2 70 | elif b'CERTIFICATE' in data: 71 | f_type = 1 72 | needs_password = b'ENCRYPTED' in data 73 | elif suffix in ['cer', 'crt']: 74 | f_type = 1 75 | elif suffix in ['key']: 76 | f_type = 2 77 | else: 78 | certs = QtNetwork.QSslCertificate.fromData( 79 | data, QtNetwork.QSsl.Der) 80 | f_type = 1 if certs else 2 81 | return f_type, f_format, needs_password 82 | 83 | 84 | def import_file(controller, slot, fn): 85 | with open(fn, 'rb') as f: 86 | data = f.read() 87 | 88 | f_type, f_format, needs_password = detect_type(data, fn) 89 | 90 | if f_type == 2 and f_format == 'der': 91 | return None, None, False # We don't know what type of key this is. 92 | 93 | def func(password=None, pin_policy=None, touch_policy=False): 94 | if f_format == 'pfx': 95 | controller.import_key(data, slot, 'PKCS12', password, pin_policy, 96 | touch_policy) 97 | controller.import_certificate(data, slot, 'PKCS12', password) 98 | elif f_format == 'pem': 99 | if f_type == 1: 100 | controller.import_certificate(data, slot, 'PEM', password) 101 | elif f_type == 2: 102 | controller.import_key(data, slot, 'PEM', password, pin_policy, 103 | touch_policy) 104 | elif f_type == 3: 105 | controller.import_certificate(data, slot, 'PEM', password) 106 | controller.import_key(data, slot, 'PEM', password, pin_policy, 107 | touch_policy) 108 | else: 109 | controller.import_certificate(data, slot, 'DER') 110 | 111 | return func, needs_password, f_type != 1 112 | 113 | 114 | class CertPanel(QtGui.QWidget): 115 | 116 | def __init__(self, controller, slot, parent=None): 117 | super(CertPanel, self).__init__(parent) 118 | 119 | self._controller = controller 120 | self._slot = slot 121 | controller.use(self._build_ui) 122 | 123 | def _build_ui(self, controller): 124 | cert = controller.get_certificate(self._slot) 125 | 126 | layout = QtGui.QVBoxLayout(self) 127 | layout.setContentsMargins(0, 0, 0, 0) 128 | 129 | status = QtGui.QGridLayout() 130 | status.addWidget(QtGui.QLabel(m.issued_to_label), 0, 0) 131 | issued_to = cert.subjectInfo(QtNetwork.QSslCertificate.CommonName) 132 | status.addWidget(QtGui.QLabel(issued_to), 0, 1) 133 | status.addWidget(QtGui.QLabel(m.issued_by_label), 0, 2) 134 | issued_by = cert.issuerInfo(QtNetwork.QSslCertificate.CommonName) 135 | status.addWidget(QtGui.QLabel(issued_by), 0, 3) 136 | status.addWidget(QtGui.QLabel(m.valid_from_label), 1, 0) 137 | valid_from = QtGui.QLabel(cert.effectiveDate().toString()) 138 | now = datetime.utcnow() 139 | if cert.effectiveDate().toPython() > now: 140 | valid_from.setStyleSheet('QLabel { color: red; }') 141 | status.addWidget(valid_from, 1, 1) 142 | status.addWidget(QtGui.QLabel(m.valid_to_label), 1, 2) 143 | valid_to = QtGui.QLabel(cert.expiryDate().toString()) 144 | if cert.expiryDate().toPython() < now: 145 | valid_to.setStyleSheet('QLabel { color: red; }') 146 | status.addWidget(valid_to, 1, 3) 147 | 148 | layout.addLayout(status) 149 | buttons = QtGui.QHBoxLayout() 150 | 151 | export_btn = QtGui.QPushButton(m.export_to_file) 152 | export_btn.clicked.connect(partial(self._export_cert, cert)) 153 | buttons.addWidget(export_btn) 154 | 155 | delete_btn = QtGui.QPushButton(m.delete_cert) 156 | delete_btn.clicked.connect( 157 | self._controller.wrap(self._delete_cert, True)) 158 | buttons.addWidget(delete_btn) 159 | layout.addStretch() 160 | layout.addLayout(buttons) 161 | 162 | def _export_cert(self, cert): 163 | fn, fn_filter = QtGui.QFileDialog.getSaveFileName( 164 | self, m.export_cert, filter='Certificate (*.pem *.crt)') 165 | if not fn: 166 | return 167 | 168 | with open(fn, 'wb') as f: 169 | f.write(cert.toPem().data()) 170 | QtGui.QMessageBox.information(self, m.cert_exported, 171 | m.cert_exported_desc_1 % fn) 172 | 173 | def _delete_cert(self, controller, release): 174 | res = QtGui.QMessageBox.warning(self, m.delete_cert, 175 | m.delete_cert_warning_1 % self._slot, 176 | QtGui.QMessageBox.Ok, 177 | QtGui.QMessageBox.Cancel) 178 | if res == QtGui.QMessageBox.Ok: 179 | try: 180 | controller.ensure_authenticated() 181 | worker = QtCore.QCoreApplication.instance().worker 182 | worker.post( 183 | m.deleting_cert, 184 | (controller.delete_certificate, self._slot), 185 | partial(self._delete_cert_callback, controller, release), 186 | True) 187 | except (DeviceGoneError, PivError, ValueError) as e: 188 | QtGui.QMessageBox.warning(self, m.error, str(e)) 189 | 190 | def _delete_cert_callback(self, controller, release, result): 191 | if isinstance(result, DeviceGoneError): 192 | QtGui.QMessageBox.warning(self, m.error, m.device_unplugged) 193 | self.window().accept() 194 | elif isinstance(result, Exception): 195 | QtGui.QMessageBox.warning(self, m.error, str(result)) 196 | else: 197 | self.parent().refresh(controller) 198 | QtGui.QMessageBox.information(self, m.cert_deleted, 199 | m.cert_deleted_desc) 200 | 201 | 202 | class CertWidget(QtGui.QWidget): 203 | 204 | def __init__(self, controller, slot): 205 | super(CertWidget, self).__init__() 206 | 207 | self._controller = controller 208 | self._slot = slot 209 | 210 | self._build_ui() 211 | 212 | controller.use(self.refresh) 213 | 214 | def _build_ui(self): 215 | layout = QtGui.QVBoxLayout(self) 216 | 217 | self._status = QtGui.QLabel(m.cert_not_loaded) 218 | layout.addWidget(self._status) 219 | 220 | buttons = QtGui.QHBoxLayout() 221 | 222 | from_file_btn = QtGui.QPushButton(m.import_from_file) 223 | from_file_btn.clicked.connect( 224 | self._controller.wrap(self._import_file, True)) 225 | if settings[SETTINGS.ENABLE_IMPORT]: 226 | buttons.addWidget(from_file_btn) 227 | 228 | generate_btn = QtGui.QPushButton(m.generate_key) 229 | generate_btn.clicked.connect( 230 | self._controller.wrap(self._generate_key, True)) 231 | buttons.addWidget(generate_btn) 232 | 233 | layout.addLayout(buttons) 234 | 235 | def refresh(self, controller): 236 | if controller.pin_blocked: 237 | self.window().accept() 238 | return 239 | 240 | self.layout().removeWidget(self._status) 241 | self._status.hide() 242 | if self._slot in controller.certs: 243 | self._status = CertPanel(self._controller, self._slot, self) 244 | else: 245 | self._status = QtGui.QLabel('%s

%s' % ( 246 | USAGES[self._slot], m.cert_not_loaded)) 247 | self._status.setWordWrap(True) 248 | self.layout().insertWidget(0, self._status) 249 | 250 | def _import_file(self, controller, release): 251 | 252 | fn, fn_filter = QtGui.QFileDialog.getOpenFileName( 253 | self, m.import_from_file, filter=FILE_FILTER) 254 | if not fn: 255 | return 256 | 257 | func, needs_password, is_key = import_file(controller, self._slot, fn) 258 | if func is None: 259 | QtGui.QMessageBox.warning(self, m.error, m.unsupported_file) 260 | return 261 | if is_key: 262 | dialog = UsagePolicyDialog(controller, self._slot, self) 263 | if dialog.has_content and dialog.exec_(): 264 | func = partial(func, pin_policy=dialog.pin_policy, 265 | touch_policy=dialog.touch_policy) 266 | settings[SETTINGS.TOUCH_POLICY] = dialog.touch_policy 267 | 268 | if needs_password: 269 | password, status = get_text( 270 | self, m.enter_file_password, m.password_label, 271 | QtGui.QLineEdit.Password) 272 | if not status: 273 | return 274 | func = partial(func, password=password) 275 | 276 | try: 277 | if not controller.poll(): 278 | controller.reconnect() 279 | controller.ensure_authenticated() 280 | except Exception as e: 281 | QtGui.QMessageBox.warning(self, m.error, str(e)) 282 | 283 | # User confirmation for overwriting slot data 284 | if self._slot in controller.certs: 285 | res = QtGui.QMessageBox.warning( 286 | self, 287 | m.overwrite_slot_warning, 288 | m.overwrite_slot_warning_desc % self._slot, 289 | QtGui.QMessageBox.Ok, 290 | QtGui.QMessageBox.Cancel) 291 | if res == QtGui.QMessageBox.Cancel: 292 | return 293 | 294 | try: 295 | worker = QtCore.QCoreApplication.instance().worker 296 | worker.post(m.importing_file, func, partial( 297 | self._import_file_callback, controller, release), True) 298 | except (DeviceGoneError, PivError, ValueError) as e: 299 | QtGui.QMessageBox.warning(self, m.error, str(e)) 300 | 301 | def _import_file_callback(self, controller, release, result): 302 | if isinstance(result, DeviceGoneError): 303 | QtGui.QMessageBox.warning(self, m.error, m.device_unplugged) 304 | self.window().accept() 305 | elif isinstance(result, Exception): 306 | QtGui.QMessageBox.warning(self, m.error, str(result)) 307 | else: 308 | self.refresh(controller) 309 | QtGui.QMessageBox.information(self, m.cert_installed, 310 | m.cert_installed_desc) 311 | 312 | def _generate_key(self, controller, release): 313 | dialog = GenerateKeyDialog(controller, self._slot, self) 314 | if dialog.exec_(): 315 | self.refresh(controller) 316 | 317 | 318 | class CertDialog(Dialog): 319 | 320 | def __init__(self, controller, parent=None): 321 | super(CertDialog, self).__init__(parent) 322 | self.setWindowTitle(m.certificates) 323 | 324 | self._complex = settings[SETTINGS.COMPLEX_PINS] 325 | self._controller = controller 326 | controller.on_lost(self.accept) 327 | self._build_ui() 328 | 329 | def _build_ui(self): 330 | layout = QtGui.QVBoxLayout(self) 331 | # This unfortunately causes the window to resize when switching tabs. 332 | # layout.setSizeConstraint(QtGui.QLayout.SetFixedSize) 333 | 334 | self._cert_tabs = QtGui.QTabWidget() 335 | self._cert_tabs.setMinimumSize(540, 160) 336 | shown_slots = settings[SETTINGS.SHOWN_SLOTS] 337 | selected = False 338 | for (slot, label) in sorted(SLOTS.items()): 339 | if slot in shown_slots: 340 | index = self._cert_tabs.addTab( 341 | CertWidget(self._controller, slot), label) 342 | if not selected: 343 | self._cert_tabs.setCurrentIndex(index) 344 | selected = True 345 | elif not settings.is_locked(SETTINGS.SHOWN_SLOTS): 346 | index = self._cert_tabs.addTab(QtGui.QLabel(), label) 347 | self._cert_tabs.setTabEnabled(index, False) 348 | layout.addWidget(self._cert_tabs) 349 | -------------------------------------------------------------------------------- /pivman/view/generate_dialog.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | from PySide import QtGui, QtCore 28 | from pivman import messages as m 29 | from pivman.utils import has_ca, request_cert_from_ca 30 | from pivman.storage import settings, SETTINGS 31 | from pivman.view.usage_policy_dialog import UsagePolicyDialog 32 | from pivman.view.utils import SUBJECT_VALIDATOR 33 | 34 | 35 | def save_file_as(parent, title, fn_filter): 36 | return QtGui.QFileDialog.getSaveFileName(parent, title, filter=fn_filter)[0] 37 | 38 | 39 | def needs_subject(forms): 40 | return bool({'csr', 'ssc', 'ca'}.intersection(forms)) 41 | 42 | 43 | class GenerateKeyDialog(UsagePolicyDialog): 44 | 45 | def __init__(self, controller, slot, parent=None): 46 | super(GenerateKeyDialog, self).__init__(controller, slot, parent) 47 | 48 | def _build_ui(self): 49 | self.setWindowTitle(m.generate_key) 50 | self.setFixedWidth(400) 51 | 52 | layout = QtGui.QVBoxLayout(self) 53 | 54 | warning = QtGui.QLabel(m.generate_key_warning_1 % self._slot) 55 | warning.setWordWrap(True) 56 | layout.addWidget(warning) 57 | 58 | self._build_algorithms(layout) 59 | self._build_usage_policy(layout) 60 | self._build_output(layout) 61 | 62 | def _build_algorithms(self, layout): 63 | self._alg_type = QtGui.QButtonGroup(self) 64 | self._alg_rsa_1024 = QtGui.QRadioButton(m.alg_rsa_1024) 65 | self._alg_rsa_1024.setProperty('value', 'RSA1024') 66 | self._alg_rsa_2048 = QtGui.QRadioButton(m.alg_rsa_2048) 67 | self._alg_rsa_2048.setProperty('value', 'RSA2048') 68 | self._alg_ecc_p256 = QtGui.QRadioButton(m.alg_ecc_p256) 69 | self._alg_ecc_p256.setProperty('value', 'ECCP256') 70 | self._alg_ecc_p384 = QtGui.QRadioButton(m.alg_ecc_p384) 71 | self._alg_ecc_p384.setProperty('value', 'ECCP384') 72 | self._alg_type.addButton(self._alg_rsa_1024) 73 | self._alg_type.addButton(self._alg_rsa_2048) 74 | self._alg_type.addButton(self._alg_ecc_p256) 75 | if self._controller.version_tuple >= (4, 0, 0): 76 | self._alg_type.addButton(self._alg_ecc_p384) 77 | algo = settings[SETTINGS.ALGORITHM] 78 | if settings.is_locked(SETTINGS.ALGORITHM): 79 | layout.addWidget(QtGui.QLabel(m.algorithm_1 % algo)) 80 | else: 81 | layout.addWidget(self.section(m.algorithm)) 82 | for button in self._alg_type.buttons(): 83 | layout.addWidget(button) 84 | if button.property('value') == algo: 85 | button.setChecked(True) 86 | button.setFocus() 87 | if not self._alg_type.checkedButton(): 88 | button = self._alg_type.buttons()[0] 89 | button.setChecked(True) 90 | 91 | def _build_output(self, layout): 92 | layout.addWidget(self.section(m.output)) 93 | self._out_type = QtGui.QButtonGroup(self) 94 | self._out_pk = QtGui.QRadioButton(m.out_pk) 95 | self._out_pk.setProperty('value', 'pk') 96 | self._out_ssc = QtGui.QRadioButton(m.out_ssc) 97 | self._out_ssc.setProperty('value', 'ssc') 98 | self._out_csr = QtGui.QRadioButton(m.out_csr) 99 | self._out_csr.setProperty('value', 'csr') 100 | self._out_ca = QtGui.QRadioButton(m.out_ca) 101 | self._out_ca.setProperty('value', 'ca') 102 | self._out_type.addButton(self._out_pk) 103 | self._out_type.addButton(self._out_ssc) 104 | self._out_type.addButton(self._out_csr) 105 | out_btns = [] 106 | for button in self._out_type.buttons(): 107 | value = button.property('value') 108 | if value in settings[SETTINGS.SHOWN_OUT_FORMS]: 109 | layout.addWidget(button) 110 | out_btns.append(button) 111 | if value == settings[SETTINGS.OUT_TYPE]: 112 | button.setChecked(True) 113 | 114 | self._cert_tmpl = QtGui.QLineEdit(settings[SETTINGS.CERTREQ_TEMPLATE]) 115 | if 'ca' in settings[SETTINGS.SHOWN_OUT_FORMS]: 116 | if has_ca(): 117 | out_btns.append(self._out_ca) 118 | self._out_type.addButton(self._out_ca) 119 | self._out_ca.setChecked(True) 120 | layout.addWidget(self._out_ca) 121 | if not settings.is_locked(SETTINGS.CERTREQ_TEMPLATE): 122 | cert_box = QtGui.QHBoxLayout() 123 | cert_box.addWidget(QtGui.QLabel(m.cert_tmpl)) 124 | cert_box.addWidget(self._cert_tmpl) 125 | layout.addLayout(cert_box) 126 | else: 127 | layout.addWidget(QtGui.QLabel(m.ca_not_connected)) 128 | 129 | self._out_type.buttonClicked.connect(self._output_changed) 130 | 131 | buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | 132 | QtGui.QDialogButtonBox.Cancel) 133 | 134 | self._subject = QtGui.QLineEdit(settings[SETTINGS.SUBJECT]) 135 | self._subject.setValidator(SUBJECT_VALIDATOR) 136 | 137 | today = QtCore.QDate.currentDate() 138 | self._expire_date = QtGui.QDateTimeEdit(today.addYears(1)) 139 | self._expire_date.setDisplayFormat("yyyy-MM-dd") 140 | self._expire_date.setMinimumDate(today.addDays(1)) 141 | 142 | if not out_btns: 143 | layout.addWidget(QtGui.QLabel(m.no_output)) 144 | buttons.button(QtGui.QDialogButtonBox.Ok).setDisabled(True) 145 | else: 146 | if not settings.is_locked(SETTINGS.SUBJECT) and \ 147 | needs_subject([b.property('value') for b in out_btns]): 148 | subject_box = QtGui.QHBoxLayout() 149 | subject_box.addWidget(QtGui.QLabel(m.subject)) 150 | subject_box.addWidget(self._subject) 151 | layout.addLayout(subject_box) 152 | expire_date = QtGui.QHBoxLayout() 153 | expire_date.addWidget(QtGui.QLabel(m.expiration_date)) 154 | expire_date.addWidget(self._expire_date) 155 | layout.addLayout(expire_date) 156 | 157 | out_btn = self._out_type.checkedButton() 158 | if out_btn is None: 159 | out_btn = out_btns[0] 160 | out_btn.setChecked(True) 161 | self._output_changed(out_btn) 162 | buttons.accepted.connect(self._generate) 163 | buttons.rejected.connect(self.reject) 164 | layout.addWidget(buttons) 165 | 166 | def _output_changed(self, btn): 167 | self._cert_tmpl.setEnabled(btn is self._out_ca) 168 | self._subject.setDisabled(btn is self._out_pk) 169 | self._expire_date.setDisabled(btn is not self._out_ssc) 170 | 171 | @property 172 | def algorithm(self): 173 | if settings.is_locked(SETTINGS.ALGORITHM): 174 | return settings[SETTINGS.ALGORITHM] 175 | return self._alg_type.checkedButton().property('value') 176 | 177 | @property 178 | def out_format(self): 179 | return self._out_type.checkedButton().property('value') 180 | 181 | def _generate(self): 182 | 183 | if self.out_format != 'pk' and not \ 184 | self._subject.hasAcceptableInput(): 185 | QtGui.QMessageBox.warning(self, m.invalid_subject, 186 | m.invalid_subject_desc) 187 | self._subject.setFocus() 188 | self._subject.selectAll() 189 | return 190 | 191 | if self.out_format == 'pk': 192 | out_fn = save_file_as(self, m.save_pk, 'Public Key (*.pem)') 193 | if not out_fn: 194 | return 195 | elif self.out_format == 'csr': 196 | out_fn = save_file_as(self, m.save_csr, 197 | 'Certificate Signing Reqest (*.csr)') 198 | if not out_fn: 199 | return 200 | else: 201 | out_fn = None 202 | 203 | try: 204 | if not self._controller.poll(): 205 | self._controller.reconnect() 206 | 207 | if self.out_format != 'pk': 208 | pin = self._controller.ensure_pin() 209 | else: 210 | pin = None 211 | self._controller.ensure_authenticated(pin) 212 | except Exception as e: 213 | QtGui.QMessageBox.warning(self, m.error, str(e)) 214 | return 215 | 216 | valid_days = QtCore.QDate.currentDate().daysTo(self._expire_date.date()) 217 | 218 | # User confirmation for overwriting slot data 219 | if self._slot in self._controller.certs: 220 | res = QtGui.QMessageBox.warning( 221 | self, 222 | m.overwrite_slot_warning, 223 | m.overwrite_slot_warning_desc % self._slot, 224 | QtGui.QMessageBox.Ok, 225 | QtGui.QMessageBox.Cancel) 226 | if res == QtGui.QMessageBox.Cancel: 227 | return 228 | 229 | worker = QtCore.QCoreApplication.instance().worker 230 | worker.post( 231 | m.generating_key, (self._do_generate, pin, out_fn, valid_days), 232 | self._generate_callback, True) 233 | 234 | def _do_generate(self, pin=None, out_fn=None, valid_days=365): 235 | data = self._controller.generate_key(self._slot, self.algorithm, 236 | self.pin_policy, 237 | self.touch_policy) 238 | return (self._do_generate2, data, pin, out_fn, valid_days) 239 | 240 | def _generate_callback(self, result): 241 | if isinstance(result, Exception): 242 | QtGui.QMessageBox.warning(self, m.error, str(result)) 243 | else: 244 | busy_message = m.generating_key 245 | if self.touch_policy and self.out_format in ['ssc', 'csr', 'ca']: 246 | QtGui.QMessageBox.information(self, m.touch_needed, 247 | m.touch_needed_desc) 248 | busy_message = m.touch_prompt 249 | worker = QtCore.QCoreApplication.instance().worker 250 | worker.post(busy_message, result, self._generate_callback2, True) 251 | 252 | def _do_generate2(self, data, pin, out_fn, valid_days=365): 253 | subject = self._subject.text() 254 | if self.out_format in ['csr', 'ca']: 255 | data = self._controller.create_csr(self._slot, pin, data, subject) 256 | 257 | if self.out_format in ['pk', 'csr']: 258 | with open(out_fn, 'w') as f: 259 | f.write(data) 260 | return out_fn 261 | else: 262 | if self.out_format == 'ssc': 263 | cert = self._controller.selfsign_certificate( 264 | self._slot, pin, data, subject, valid_days) 265 | elif self.out_format == 'ca': 266 | cert = request_cert_from_ca(data, self._cert_tmpl.text()) 267 | self._controller.import_certificate(cert, self._slot) 268 | 269 | def _generate_callback2(self, result): 270 | if isinstance(result, Exception): 271 | QtGui.QMessageBox.warning(self, m.error, str(result)) 272 | else: 273 | settings[SETTINGS.ALGORITHM] = self.algorithm 274 | if self._controller.version_tuple >= (4, 0, 0): 275 | settings[SETTINGS.TOUCH_POLICY] = self.touch_policy 276 | settings[SETTINGS.OUT_TYPE] = self.out_format 277 | if self.out_format != 'pk' and not \ 278 | settings.is_locked(SETTINGS.SUBJECT): 279 | subject = self._subject.text() 280 | # Only save if different: 281 | if subject != settings[SETTINGS.SUBJECT]: 282 | settings[SETTINGS.SUBJECT] = subject 283 | if self.out_format == 'ca': 284 | settings[SETTINGS.CERTREQ_TEMPLATE] = self._cert_tmpl.text() 285 | 286 | message = m.generated_key_desc_1 % self._slot 287 | if self.out_format == 'pk': 288 | message += '\n' + m.gen_out_pk_1 % result 289 | elif self.out_format == 'csr': 290 | message += '\n' + m.gen_out_csr_1 % result 291 | elif self.out_format == 'ssc': 292 | message += '\n' + m.gen_out_ssc 293 | elif self.out_format == 'ca': 294 | message += '\n' + m.gen_out_ca 295 | 296 | QtGui.QMessageBox.information(self, m.generated_key, message) 297 | self.accept() 298 | -------------------------------------------------------------------------------- /pivman/view/init_dialog.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | from PySide import QtGui, QtCore 28 | from pivman import messages as m 29 | from pivman.piv import DeviceGoneError, PivError, WrongPinError, KEY_LEN 30 | from pivman.view.set_pin_dialog import SetPinDialog 31 | from pivman.view.utils import KEY_VALIDATOR, pin_field 32 | from pivman.utils import complexity_check 33 | from pivman.storage import settings, SETTINGS 34 | from pivman.yubicommon import qt 35 | from binascii import b2a_hex 36 | from pivman.controller import AUTH_SLOT, ENCRYPTION_SLOT 37 | import os 38 | import re 39 | 40 | NUMERIC_PATTERN = re.compile("^[0-9]+$") 41 | 42 | 43 | class PinPanel(QtGui.QWidget): 44 | 45 | def __init__(self, headers): 46 | super(PinPanel, self).__init__() 47 | 48 | self._complex = settings[SETTINGS.COMPLEX_PINS] 49 | layout = QtGui.QVBoxLayout(self) 50 | layout.setContentsMargins(0, 0, 0, 0) 51 | rowLayout = QtGui.QFormLayout() 52 | rowLayout.addRow(headers.section(m.pin)) 53 | self._new_pin = pin_field() 54 | rowLayout.addRow(m.new_pin_label, self._new_pin) 55 | self._confirm_pin = pin_field() 56 | rowLayout.addRow(m.verify_pin_label, self._confirm_pin) 57 | layout.addLayout(rowLayout) 58 | self._non_numeric_pin_warning = QtGui.QLabel(m.non_numeric_pin_warning) 59 | self._non_numeric_pin_warning.setWordWrap(True) 60 | layout.addWidget(self._non_numeric_pin_warning) 61 | 62 | @property 63 | def pin(self): 64 | error = None 65 | 66 | new_pin = self._new_pin.text() 67 | if not new_pin: 68 | error = m.pin_empty 69 | elif new_pin != self._confirm_pin.text(): 70 | error = m.pin_confirm_mismatch 71 | elif self._complex and not complexity_check(new_pin): 72 | error = m.pin_complexity_desc 73 | 74 | if error: 75 | self._new_pin.setText('') 76 | self._confirm_pin.setText('') 77 | self._new_pin.setFocus() 78 | raise ValueError(error) 79 | 80 | return new_pin 81 | 82 | 83 | class KeyPanel(QtGui.QWidget): 84 | 85 | def __init__(self, headers): 86 | super(KeyPanel, self).__init__() 87 | 88 | layout = QtGui.QVBoxLayout(self) 89 | layout.setContentsMargins(0, 0, 0, 0) 90 | 91 | layout.addWidget(headers.section(m.management_key)) 92 | 93 | self._key_type = QtGui.QButtonGroup(self) 94 | self._kt_pin = QtGui.QRadioButton(m.use_pin_as_key, self) 95 | self._kt_key = QtGui.QRadioButton(m.use_separate_key, self) 96 | self._key_type.addButton(self._kt_pin) 97 | self._key_type.addButton(self._kt_key) 98 | self._key_type.buttonClicked.connect(self._change_key_type) 99 | layout.addWidget(self._kt_pin) 100 | layout.addWidget(self._kt_key) 101 | 102 | self._adv_panel = AdvancedPanel(headers) 103 | 104 | if settings[SETTINGS.PIN_AS_KEY]: 105 | self._kt_pin.setChecked(True) 106 | else: 107 | self._kt_key.setChecked(True) 108 | self.layout().addWidget(self._adv_panel) 109 | 110 | def _change_key_type(self, btn): 111 | if btn == self._kt_pin: 112 | self.layout().removeWidget(self._adv_panel) 113 | self._adv_panel.hide() 114 | else: 115 | self._adv_panel.reset() 116 | self.layout().addWidget(self._adv_panel) 117 | self._adv_panel.show() 118 | self.adjustSize() 119 | self.parentWidget().adjustSize() 120 | 121 | @property 122 | def use_pin(self): 123 | return self._key_type.checkedButton() == self._kt_pin 124 | 125 | @property 126 | def puk(self): 127 | return self._adv_panel.puk if not self.use_pin else None 128 | 129 | @property 130 | def key(self): 131 | return self._adv_panel.key if not self.use_pin else None 132 | 133 | 134 | class AdvancedPanel(QtGui.QWidget): 135 | 136 | def __init__(self, headers): 137 | super(AdvancedPanel, self).__init__() 138 | 139 | self._complex = settings[SETTINGS.COMPLEX_PINS] 140 | 141 | layout = QtGui.QFormLayout(self) 142 | layout.setContentsMargins(0, 0, 0, 0) 143 | 144 | layout.addRow(QtGui.QLabel(m.key_label)) 145 | self._key = QtGui.QLineEdit() 146 | self._key.setValidator(KEY_VALIDATOR) 147 | self._key.textChanged.connect(self._validate_key) 148 | layout.addRow(self._key) 149 | 150 | buttons = QtGui.QDialogButtonBox() 151 | self._randomize_btn = QtGui.QPushButton(m.randomize) 152 | self._randomize_btn.clicked.connect(self.randomize) 153 | self._copy_btn = QtGui.QPushButton(m.copy_clipboard) 154 | self._copy_btn.clicked.connect(self._copy) 155 | buttons.addButton(self._randomize_btn, 156 | QtGui.QDialogButtonBox.ActionRole) 157 | buttons.addButton(self._copy_btn, QtGui.QDialogButtonBox.ActionRole) 158 | layout.addRow(buttons) 159 | 160 | layout.addRow(headers.section(m.puk)) 161 | self._puk = pin_field() 162 | layout.addRow(m.new_puk_label, self._puk) 163 | self._confirm_puk = pin_field() 164 | layout.addRow(m.verify_puk_label, self._confirm_puk) 165 | 166 | def reset(self): 167 | self.randomize() 168 | self._puk.setText('') 169 | self._confirm_puk.setText('') 170 | 171 | def randomize(self): 172 | self._key.setText(b2a_hex(os.urandom(KEY_LEN)).decode('ascii')) 173 | 174 | def _validate_key(self): 175 | self._copy_btn.setDisabled(not self._key.hasAcceptableInput()) 176 | 177 | def _copy(self): 178 | self._key.selectAll() 179 | self._key.copy() 180 | self._key.deselect() 181 | 182 | @property 183 | def key(self): 184 | if not self._key.hasAcceptableInput(): 185 | self._key.setText('') 186 | self._key.setFocus() 187 | raise ValueError(m.key_invalid_desc) 188 | 189 | return self._key.text() 190 | 191 | @property 192 | def puk(self): 193 | error = None 194 | 195 | puk = self._puk.text() 196 | if not puk: 197 | return None 198 | elif self._complex and not complexity_check(puk): 199 | error = m.puk_not_complex 200 | elif puk != self._confirm_puk.text(): 201 | error = m.puk_confirm_mismatch 202 | 203 | if error: 204 | self._puk.setText('') 205 | self._confirm_puk.setText('') 206 | self._puk.setFocus() 207 | raise ValueError(error) 208 | 209 | return puk 210 | 211 | 212 | class InitDialog(qt.Dialog): 213 | 214 | def __init__(self, controller, parent=None): 215 | super(InitDialog, self).__init__(parent) 216 | self.setWindowTitle(m.initialize) 217 | self.setMinimumWidth(400) 218 | self._controller = controller 219 | self._build_ui() 220 | 221 | def _build_ui(self): 222 | layout = QtGui.QVBoxLayout(self) 223 | 224 | self._pin_panel = PinPanel(self.headers) 225 | layout.addWidget(self._pin_panel) 226 | self._key_panel = KeyPanel(self.headers) 227 | if not settings.is_locked(SETTINGS.PIN_AS_KEY) or \ 228 | not settings[SETTINGS.PIN_AS_KEY]: 229 | layout.addWidget(self._key_panel) 230 | 231 | layout.addStretch() 232 | 233 | buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok) 234 | self._ok_btn = buttons.button(QtGui.QDialogButtonBox.Ok) 235 | buttons.accepted.connect(self._initialize) 236 | layout.addWidget(buttons) 237 | 238 | def _initialize(self): 239 | try: 240 | pin = self._pin_panel.pin 241 | key = self._key_panel.key 242 | puk = self._key_panel.puk 243 | 244 | if key is not None and puk is None: 245 | res = QtGui.QMessageBox.warning(self, m.no_puk, 246 | m.no_puk_warning, 247 | QtGui.QMessageBox.Ok, 248 | QtGui.QMessageBox.Cancel) 249 | if res != QtGui.QMessageBox.Ok: 250 | return 251 | 252 | if not self._controller.poll(): 253 | self._controller.reconnect() 254 | 255 | self._controller.ensure_authenticated() 256 | worker = QtCore.QCoreApplication.instance().worker 257 | worker.post( 258 | m.initializing, 259 | (self._controller.initialize, pin, puk, key), 260 | self._init_callback, 261 | True 262 | ) 263 | except DeviceGoneError: 264 | QtGui.QMessageBox.warning(self, m.error, m.device_unplugged) 265 | self.close() 266 | except (PivError, ValueError) as e: 267 | QtGui.QMessageBox.warning(self, m.error, str(e)) 268 | 269 | def _init_callback(self, result): 270 | if isinstance(result, DeviceGoneError): 271 | QtGui.QMessageBox.warning(self, m.error, m.device_unplugged) 272 | self.close() 273 | if isinstance(result, WrongPinError): 274 | QtGui.QMessageBox.warning(self, m.error, m.not_default_pin) 275 | self.close() 276 | elif isinstance(result, Exception): 277 | QtGui.QMessageBox.warning(self, m.error, str(result)) 278 | else: 279 | if not settings.is_locked(SETTINGS.PIN_AS_KEY): 280 | settings[SETTINGS.PIN_AS_KEY] = self._key_panel.use_pin 281 | self.accept() 282 | 283 | 284 | class MacOSPairingDialog(qt.Dialog): 285 | 286 | def __init__(self, controller, parent=None): 287 | super(MacOSPairingDialog, self).__init__(parent) 288 | self.setWindowTitle(m.macos_pairing_title) 289 | self._controller = controller 290 | self._build_ui() 291 | 292 | def _build_ui(self): 293 | layout = QtGui.QVBoxLayout(self) 294 | lbl = QtGui.QLabel(m.macos_pairing_desc) 295 | lbl.setWordWrap(True) 296 | layout.addWidget(lbl) 297 | buttons = QtGui.QDialogButtonBox() 298 | yes_btn = buttons.addButton(QtGui.QDialogButtonBox.Yes) 299 | yes_btn.setDefault(True) 300 | no_btn = buttons.addButton(QtGui.QDialogButtonBox.No) 301 | no_btn.setAutoDefault(False) 302 | no_btn.setDefault(False) 303 | buttons.accepted.connect(self._setup) 304 | buttons.rejected.connect(self.close) 305 | layout.addWidget(buttons) 306 | 307 | def _setup(self): 308 | try: 309 | if not self._controller.poll(): 310 | self._controller.reconnect() 311 | 312 | pin = self._controller.ensure_pin() 313 | if NUMERIC_PATTERN.match(pin): 314 | self._controller.ensure_authenticated(pin) 315 | 316 | # User confirmation for overwriting slot data 317 | if (AUTH_SLOT in self._controller.certs 318 | or ENCRYPTION_SLOT in self._controller.certs): 319 | res = QtGui.QMessageBox.warning( 320 | self, 321 | m.overwrite_slot_warning, 322 | m.overwrite_slot_warning_macos, 323 | QtGui.QMessageBox.Cancel, 324 | QtGui.QMessageBox.Ok) 325 | if res == QtGui.QMessageBox.Cancel: 326 | return 327 | 328 | worker = QtCore.QCoreApplication.instance().worker 329 | worker.post( 330 | m.setting_up_macos, 331 | (self._controller.setup_for_macos, pin), 332 | self.setup_callback, 333 | True 334 | ) 335 | else: 336 | res = QtGui.QMessageBox.warning( 337 | self, 338 | m.error, 339 | m.non_numeric_pin, 340 | QtGui.QMessageBox.Yes, 341 | QtGui.QMessageBox.No) 342 | 343 | if res == QtGui.QMessageBox.Yes: 344 | SetPinDialog(self._controller, self).exec_() 345 | 346 | except DeviceGoneError: 347 | QtGui.QMessageBox.warning(self, m.error, m.device_unplugged) 348 | self.close() 349 | except Exception as e: 350 | QtGui.QMessageBox.warning(self, m.error, str(e)) 351 | 352 | def setup_callback(self, result): 353 | if isinstance(result, DeviceGoneError): 354 | QtGui.QMessageBox.warning(self, m.error, m.device_unplugged) 355 | self.close() 356 | elif isinstance(result, Exception): 357 | QtGui.QMessageBox.warning(self, m.error, str(result)) 358 | else: 359 | QtGui.QMessageBox.information( 360 | self, m.setup_macos_compl, m.setup_macos_compl_desc) 361 | self.accept() 362 | -------------------------------------------------------------------------------- /pivman/view/main.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | from PySide import QtGui 28 | from PySide import QtCore 29 | from pivman import messages as m 30 | from pivman.utils import is_macos_sierra_or_later 31 | from pivman.watcher import ControllerWatcher 32 | from pivman.view.utils import IMPORTANT 33 | from pivman.view.init_dialog import InitDialog, MacOSPairingDialog 34 | from pivman.view.set_pin_dialog import SetPinDialog 35 | from pivman.view.manage import ManageDialog 36 | from pivman.view.cert import CertDialog 37 | 38 | 39 | class MainWidget(QtGui.QWidget): 40 | 41 | def __init__(self): 42 | super(MainWidget, self).__init__() 43 | 44 | self._lock = QtCore.QMutex() 45 | self._controller = ControllerWatcher() 46 | self._build_ui() 47 | self._controller.on_found(self._refresh_controller) 48 | self._controller.on_lost(self._no_controller) 49 | self._no_controller() 50 | 51 | def showEvent(self, event): 52 | self.refresh() 53 | event.accept() 54 | 55 | def _build_ui(self): 56 | layout = QtGui.QVBoxLayout(self) 57 | 58 | btns = QtGui.QHBoxLayout() 59 | self._cert_btn = QtGui.QPushButton(m.certificates) 60 | self._cert_btn.clicked.connect(self._manage_certs) 61 | btns.addWidget(self._cert_btn) 62 | self._pin_btn = QtGui.QPushButton(m.manage_pin) 63 | self._pin_btn.clicked.connect(self._manage_pin) 64 | btns.addWidget(self._pin_btn) 65 | self._setup_macos_btn = QtGui.QPushButton(m.setup_for_macos) 66 | if is_macos_sierra_or_later(): 67 | self._setup_macos_btn.clicked.connect( 68 | self._controller.wrap(self._setup_for_macos)) 69 | btns.addWidget(self._setup_macos_btn) 70 | layout.addLayout(btns) 71 | 72 | self._messages = QtGui.QTextEdit() 73 | self._messages.setFixedSize(480, 100) 74 | self._messages.setReadOnly(True) 75 | layout.addWidget(self._messages) 76 | 77 | def _manage_pin(self): 78 | ManageDialog(self._controller, self).exec_() 79 | self.refresh() 80 | 81 | def _manage_certs(self): 82 | CertDialog(self._controller, self).exec_() 83 | self.refresh() 84 | 85 | def _setup_for_macos(self, controller): 86 | MacOSPairingDialog(controller, self).exec_() 87 | self.refresh() 88 | 89 | def refresh(self): 90 | self._controller.use(self._refresh_controller) 91 | 92 | def _no_controller(self): 93 | self._pin_btn.setEnabled(False) 94 | self._cert_btn.setEnabled(False) 95 | self._setup_macos_btn.setEnabled(False) 96 | self._messages.setHtml(m.no_key) 97 | 98 | def _refresh_controller(self, controller): 99 | if not controller.poll(): 100 | self._no_controller() 101 | return 102 | 103 | self._pin_btn.setEnabled(True) 104 | self._cert_btn.setDisabled(controller.pin_blocked) 105 | self._setup_macos_btn.setDisabled(controller.pin_blocked) 106 | 107 | messages = [] 108 | if controller.pin_blocked: 109 | messages.append(IMPORTANT % m.pin_blocked) 110 | messages.append(m.key_with_applet_1 111 | % controller.version.decode('ascii')) 112 | n_certs = len(controller.certs) 113 | messages.append(m.certs_loaded_1 % n_certs or m.no) 114 | 115 | self._messages.setHtml('
'.join(messages)) 116 | 117 | if controller.is_uninitialized(): 118 | dialog = InitDialog(controller, self) 119 | if dialog.exec_(): 120 | if controller.should_show_macos_dialog(): 121 | self._setup_for_macos(controller) 122 | else: 123 | self.refresh() 124 | elif controller.is_pin_expired() and not controller.pin_blocked: 125 | dialog = SetPinDialog(controller, self, True) 126 | if dialog.exec_(): 127 | self.refresh() 128 | -------------------------------------------------------------------------------- /pivman/view/manage.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | from PySide import QtCore, QtGui 28 | from pivman import messages as m 29 | from pivman.view.set_pin_dialog import (SetPinDialog, SetPukDialog, 30 | ResetPinDialog) 31 | from pivman.view.set_key_dialog import SetKeyDialog 32 | from pivman.view.utils import IMPORTANT, Dialog 33 | from pivman.storage import settings, SETTINGS 34 | from functools import partial 35 | 36 | 37 | class ManageDialog(Dialog): 38 | 39 | def __init__(self, controller, parent=None): 40 | super(ManageDialog, self).__init__(parent) 41 | self.setWindowTitle(m.manage_pin) 42 | # self.setFixedSize(480, 180) 43 | 44 | self._controller = controller 45 | self._build_ui() 46 | self._controller.on_found(self.refresh) 47 | self._controller.on_lost(self.accept) 48 | self._controller.use(self.refresh) 49 | 50 | def _build_ui(self): 51 | layout = QtGui.QVBoxLayout(self) 52 | layout.setSizeConstraint(QtGui.QLayout.SetFixedSize) 53 | 54 | btns = QtGui.QHBoxLayout() 55 | self._pin_btn = QtGui.QPushButton(m.change_pin) 56 | self._pin_btn.clicked.connect(self._controller.wrap(self._change_pin, 57 | True)) 58 | btns.addWidget(self._pin_btn) 59 | 60 | self._puk_btn = QtGui.QPushButton(m.change_puk) 61 | self._puk_btn.clicked.connect(self._controller.wrap(self._change_puk, 62 | True)) 63 | 64 | self._key_btn = QtGui.QPushButton(m.change_key) 65 | self._key_btn.clicked.connect(self._controller.wrap(self._change_key, 66 | True)) 67 | if not settings.is_locked(SETTINGS.PIN_AS_KEY) or \ 68 | not settings[SETTINGS.PIN_AS_KEY]: 69 | btns.addWidget(self._puk_btn) 70 | btns.addWidget(self._key_btn) 71 | layout.addLayout(btns) 72 | 73 | self._messages = QtGui.QTextEdit() 74 | self._messages.setFixedSize(480, 100) 75 | self._messages.setReadOnly(True) 76 | layout.addWidget(self._messages) 77 | 78 | def refresh(self, controller): 79 | messages = [] 80 | if controller.pin_blocked: 81 | messages.append(IMPORTANT % m.pin_blocked) 82 | elif controller.does_pin_expire(): 83 | days_left = controller.get_pin_days_left() 84 | message = m.pin_days_left_1 % days_left 85 | if days_left < 7: 86 | message = IMPORTANT % message 87 | 88 | messages.append(message) 89 | if controller.pin_is_key: 90 | messages.append(m.pin_is_key) 91 | if controller.puk_blocked: 92 | messages.append(m.puk_blocked) 93 | 94 | if controller.pin_blocked: 95 | if controller.puk_blocked: 96 | self._pin_btn.setText(m.reset_device) 97 | else: 98 | self._pin_btn.setText(m.reset_pin) 99 | else: 100 | self._pin_btn.setText(m.change_pin) 101 | 102 | self._puk_btn.setDisabled(controller.puk_blocked) 103 | self._key_btn.setDisabled(controller.pin_is_key and 104 | controller.pin_blocked) 105 | self._messages.setHtml('
'.join(messages)) 106 | 107 | def _change_pin(self, controller, release): 108 | if controller.pin_blocked: 109 | if controller.puk_blocked: 110 | res = QtGui.QMessageBox.warning( 111 | self, m.reset_device, m.reset_device_warning, 112 | QtGui.QMessageBox.Ok, QtGui.QMessageBox.Cancel) 113 | if res == QtGui.QMessageBox.Ok: 114 | worker = QtCore.QCoreApplication.instance().worker 115 | worker.post(m.resetting_device, controller.reset_device, 116 | partial(self._reset_callback, release), True) 117 | return 118 | else: 119 | dialog = ResetPinDialog(controller, self) 120 | else: 121 | dialog = SetPinDialog(controller, self) 122 | if dialog.exec_(): 123 | self.refresh(controller) 124 | 125 | def _change_puk(self, controller, release): 126 | dialog = SetPukDialog(controller, self) 127 | if dialog.exec_(): 128 | self.refresh(controller) 129 | 130 | def _change_key(self, controller, release): 131 | dialog = SetKeyDialog(controller, self) 132 | if dialog.exec_(): 133 | QtGui.QMessageBox.information(self, m.key_changed, 134 | m.key_changed_desc) 135 | self.refresh(controller) 136 | 137 | def _reset_callback(self, release, result): 138 | QtGui.QMessageBox.information(self, m.device_resetted, 139 | m.device_resetted_desc) 140 | self.accept() 141 | -------------------------------------------------------------------------------- /pivman/view/set_key_dialog.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | from PySide import QtGui, QtCore 28 | from pivman import messages as m 29 | from pivman.piv import DeviceGoneError, PivError, KEY_LEN 30 | from pivman.view.utils import KEY_VALIDATOR 31 | from pivman.yubicommon import qt 32 | from binascii import b2a_hex 33 | import os 34 | 35 | 36 | class SetKeyDialog(qt.Dialog): 37 | 38 | def __init__(self, controller, parent=None): 39 | super(SetKeyDialog, self).__init__(parent) 40 | 41 | self._controller = controller 42 | self._build_ui() 43 | 44 | kt = self._kt_pin if self._controller.pin_is_key else self._kt_key 45 | kt.setChecked(True) 46 | self._change_key_type(kt) 47 | 48 | def _build_ui(self): 49 | self.setWindowTitle(m.change_key) 50 | self.setMinimumWidth(400) 51 | 52 | layout = QtGui.QVBoxLayout(self) 53 | 54 | self._current_key = QtGui.QLineEdit() 55 | self._current_key.setValidator(KEY_VALIDATOR) 56 | self._current_key.textChanged.connect(self._validate) 57 | if not self._controller.pin_is_key: 58 | layout.addWidget(QtGui.QLabel(m.current_key_label)) 59 | layout.addWidget(self._current_key) 60 | 61 | self._key_type = QtGui.QButtonGroup(self) 62 | self._kt_pin = QtGui.QRadioButton(m.use_pin_as_key, self) 63 | self._kt_key = QtGui.QRadioButton(m.use_separate_key, self) 64 | self._key_type.addButton(self._kt_pin) 65 | self._key_type.addButton(self._kt_key) 66 | self._key_type.buttonClicked.connect(self._change_key_type) 67 | layout.addWidget(self._kt_pin) 68 | layout.addWidget(self._kt_key) 69 | 70 | layout.addWidget(QtGui.QLabel(m.new_key_label)) 71 | self._key = QtGui.QLineEdit() 72 | self._key.setValidator(KEY_VALIDATOR) 73 | self._key.textChanged.connect(self._validate) 74 | layout.addWidget(self._key) 75 | 76 | buttons = QtGui.QDialogButtonBox() 77 | self._randomize_btn = QtGui.QPushButton(m.randomize) 78 | self._randomize_btn.clicked.connect(self.randomize) 79 | self._copy_btn = QtGui.QPushButton(m.copy_clipboard) 80 | self._copy_btn.clicked.connect(self._copy) 81 | buttons.addButton(self._randomize_btn, 82 | QtGui.QDialogButtonBox.ActionRole) 83 | buttons.addButton(self._copy_btn, QtGui.QDialogButtonBox.ActionRole) 84 | layout.addWidget(buttons) 85 | 86 | buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | 87 | QtGui.QDialogButtonBox.Cancel) 88 | self._ok_btn = buttons.button(QtGui.QDialogButtonBox.Ok) 89 | self._ok_btn.setDisabled(True) 90 | buttons.accepted.connect(self._set_key) 91 | buttons.rejected.connect(self.reject) 92 | layout.addWidget(buttons) 93 | 94 | @property 95 | def use_pin(self): 96 | return self._key_type.checkedButton() == self._kt_pin 97 | 98 | def _change_key_type(self, btn): 99 | if btn == self._kt_pin: 100 | self._key.setText('') 101 | self._key.setEnabled(False) 102 | self._randomize_btn.setEnabled(False) 103 | self._copy_btn.setEnabled(False) 104 | else: 105 | self.randomize() 106 | self._key.setEnabled(True) 107 | self._randomize_btn.setEnabled(True) 108 | self._copy_btn.setEnabled(True) 109 | self._validate() 110 | 111 | def randomize(self): 112 | self._key.setText(b2a_hex(os.urandom(KEY_LEN)).decode('ascii')) 113 | 114 | def _copy(self): 115 | self._key.selectAll() 116 | self._key.copy() 117 | self._key.deselect() 118 | 119 | def _validate(self): 120 | old_ok = self._controller.pin_is_key \ 121 | or self._current_key.hasAcceptableInput() 122 | new_ok = self.use_pin or self._key.hasAcceptableInput() 123 | 124 | self._copy_btn.setEnabled(not self.use_pin and new_ok) 125 | self._ok_btn.setEnabled(old_ok and new_ok) 126 | 127 | def _set_key(self): 128 | if self.use_pin and self._controller.pin_is_key: 129 | self.reject() 130 | return 131 | 132 | if not self._controller.puk_blocked and self.use_pin: 133 | res = QtGui.QMessageBox.warning(self, m.block_puk, 134 | m.block_puk_desc, 135 | QtGui.QMessageBox.Ok, 136 | QtGui.QMessageBox.Cancel) 137 | if res != QtGui.QMessageBox.Ok: 138 | return 139 | 140 | try: 141 | if not self._controller.poll(): 142 | self._controller.reconnect() 143 | 144 | if self._controller.pin_is_key or self.use_pin: 145 | pin = self._controller.ensure_pin() 146 | else: 147 | pin = None 148 | 149 | current_key = pin \ 150 | if self._controller.pin_is_key else self._current_key.text() 151 | new_key = pin if self.use_pin else self._key.text() 152 | 153 | self._controller.ensure_authenticated(current_key) 154 | worker = QtCore.QCoreApplication.instance().worker 155 | worker.post(m.changing_key, (self._controller.set_authentication, 156 | new_key, self.use_pin), 157 | self._set_key_callback, True) 158 | except (DeviceGoneError, PivError, ValueError) as e: 159 | QtGui.QMessageBox.warning(self, m.error, str(e)) 160 | self.reject() 161 | 162 | def _set_key_callback(self, result): 163 | if isinstance(result, DeviceGoneError): 164 | QtGui.QMessageBox.warning(self, m.error, m.device_unplugged) 165 | self.accept() 166 | elif isinstance(result, Exception): 167 | QtGui.QMessageBox.warning(self, m.error, str(result)) 168 | else: 169 | self.accept() 170 | -------------------------------------------------------------------------------- /pivman/view/set_pin_dialog.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | from PySide import QtGui, QtCore 28 | from pivman import messages as m 29 | from pivman.piv import WrongPinError 30 | from pivman.storage import settings, SETTINGS 31 | from pivman.utils import complexity_check 32 | from pivman.view.utils import pin_field 33 | from pivman.yubicommon import qt 34 | 35 | 36 | class SetPinDialog(qt.Dialog): 37 | window_title = m.change_pin 38 | label_current = m.current_pin_label 39 | label_new = m.new_pin_label 40 | label_verify = m.verify_pin_label 41 | warn_not_changed = m.pin_not_changed 42 | desc_not_changed = m.pin_not_changed_desc 43 | warn_not_complex = m.pin_not_complex 44 | busy = m.changing_pin 45 | info_changed = m.pin_changed 46 | desc_changed = m.pin_changed_desc 47 | 48 | def __init__(self, controller, parent=None, forced=False): 49 | super(SetPinDialog, self).__init__(parent) 50 | 51 | self._complex = settings[SETTINGS.COMPLEX_PINS] 52 | self._controller = controller 53 | self._build_ui(forced) 54 | 55 | def _build_ui(self, forced): 56 | self.setWindowTitle(self.window_title) 57 | 58 | layout = QtGui.QVBoxLayout(self) 59 | if forced: 60 | layout.addWidget(QtGui.QLabel(m.change_pin_forced_desc)) 61 | 62 | layout.addWidget(QtGui.QLabel(self.label_current)) 63 | self._old_pin = pin_field() 64 | layout.addWidget(self._old_pin) 65 | layout.addWidget(QtGui.QLabel(self.label_new)) 66 | 67 | self._new_pin = pin_field() 68 | layout.addWidget(self._new_pin) 69 | layout.addWidget(QtGui.QLabel(self.label_verify)) 70 | self._confirm_pin = pin_field() 71 | layout.addWidget(self._confirm_pin) 72 | self._new_pin.textChanged.connect(self._check_confirm) 73 | self._confirm_pin.textChanged.connect(self._check_confirm) 74 | 75 | if forced: 76 | buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok) 77 | else: 78 | buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | 79 | QtGui.QDialogButtonBox.Cancel) 80 | self._ok_btn = buttons.button(QtGui.QDialogButtonBox.Ok) 81 | self._ok_btn.setDisabled(True) 82 | buttons.accepted.connect(self._set_pin) 83 | buttons.rejected.connect(self.reject) 84 | layout.addWidget(buttons) 85 | 86 | def _check_confirm(self): 87 | new_pin = self._new_pin.text() 88 | if len(new_pin) > 0 and new_pin == self._confirm_pin.text(): 89 | self._ok_btn.setDisabled(False) 90 | else: 91 | self._ok_btn.setDisabled(True) 92 | 93 | def _invalid_pin(self, title, reason): 94 | QtGui.QMessageBox.warning(self, title, reason) 95 | self._new_pin.setText('') 96 | self._confirm_pin.setText('') 97 | self._new_pin.setFocus() 98 | 99 | def _prepare_fn(self, old_pin, new_pin): 100 | self._controller.verify_pin(old_pin) 101 | if self._controller.does_pin_expire(): 102 | self._controller.ensure_authenticated(old_pin) 103 | return (self._controller.change_pin, old_pin, new_pin) 104 | 105 | def _set_pin(self): 106 | old_pin = self._old_pin.text() 107 | new_pin = self._new_pin.text() 108 | 109 | if old_pin == new_pin: 110 | self._invalid_pin(self.warn_not_changed, self.desc_not_changed) 111 | elif self._complex and not complexity_check(new_pin): 112 | self._invalid_pin(self.warn_not_complex, m.pin_complexity_desc) 113 | else: 114 | try: 115 | if not self._controller.poll(): 116 | self._controller.reconnect() 117 | fn = self._prepare_fn(old_pin, new_pin) 118 | worker = QtCore.QCoreApplication.instance().worker 119 | worker.post(self.busy, fn, self._change_pin_callback, True) 120 | except Exception as e: 121 | self._change_pin_callback(e) 122 | 123 | def _change_pin_callback(self, result): 124 | if isinstance(result, Exception): 125 | QtGui.QMessageBox.warning(self, m.error, str(result)) 126 | if isinstance(result, WrongPinError): 127 | self._old_pin.setText('') 128 | self._old_pin.setFocus() 129 | if result.blocked: 130 | self.accept() 131 | else: 132 | self.reject() 133 | else: 134 | QtGui.QMessageBox.information(self, self.info_changed, 135 | self.desc_changed) 136 | self.accept() 137 | 138 | 139 | class SetPukDialog(SetPinDialog): 140 | window_title = m.change_puk 141 | label_current = m.current_puk_label 142 | label_new = m.new_puk_label 143 | label_verify = m.verify_puk_label 144 | warn_not_changed = m.puk_not_changed 145 | desc_not_changed = m.puk_not_changed_desc 146 | warn_not_complex = m.puk_not_complex 147 | busy = m.changing_puk 148 | info_changed = m.puk_changed 149 | desc_changed = m.puk_changed_desc 150 | 151 | def _prepare_fn(self, old_puk, new_puk): 152 | return (self._controller.change_puk, old_puk, new_puk) 153 | 154 | 155 | class ResetPinDialog(SetPinDialog): 156 | window_title = m.reset_pin 157 | label_current = m.puk_label 158 | label_new = m.new_pin_label 159 | label_verify = m.verify_pin_label 160 | warn_not_changed = m.pin_puk_same 161 | desc_not_changed = m.pin_puk_same_desc 162 | warn_not_complex = m.pin_not_complex 163 | busy = m.changing_pin 164 | info_changed = m.pin_changed 165 | desc_changed = m.pin_changed_desc 166 | 167 | def _prepare_fn(self, puk, new_pin): 168 | return (self._controller.reset_pin, puk, new_pin) 169 | -------------------------------------------------------------------------------- /pivman/view/settings_dialog.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | from PySide import QtGui 28 | from pivman import messages as m 29 | from pivman.yubicommon import qt 30 | from pivman.storage import settings, SETTINGS 31 | 32 | 33 | class SettingsDialog(qt.Dialog): 34 | 35 | def __init__(self, parent=None): 36 | super(SettingsDialog, self).__init__(parent) 37 | self.setWindowTitle(m.settings) 38 | 39 | self._build_ui() 40 | 41 | def _build_ui(self): 42 | layout = QtGui.QFormLayout(self) 43 | 44 | layout.addRow(self.section(m.pin)) 45 | 46 | self._complex_pins = QtGui.QCheckBox(m.use_complex_pins) 47 | self._complex_pins.setChecked(settings[SETTINGS.COMPLEX_PINS]) 48 | self._complex_pins.setDisabled( 49 | settings.is_locked(SETTINGS.COMPLEX_PINS)) 50 | layout.addRow(self._complex_pins) 51 | 52 | self._pin_expires = QtGui.QCheckBox(m.pin_expires) 53 | self._pin_expires_days = QtGui.QSpinBox() 54 | self._pin_expires_days.setMinimum(30) 55 | 56 | pin_expires = settings[SETTINGS.PIN_EXPIRATION] 57 | pin_expiry_locked = settings.is_locked(SETTINGS.PIN_EXPIRATION) 58 | self._pin_expires.setChecked(bool(pin_expires)) 59 | self._pin_expires_days.setValue(pin_expires) 60 | self._pin_expires.setDisabled(pin_expiry_locked) 61 | self._pin_expires_days.setDisabled( 62 | pin_expiry_locked or not pin_expires) 63 | self._pin_expires.stateChanged.connect( 64 | self._pin_expires_days.setEnabled) 65 | layout.addRow(self._pin_expires) 66 | layout.addRow(m.pin_expires_days, self._pin_expires_days) 67 | 68 | layout.addRow(self.section(m.misc)) 69 | reader_pattern = settings[SETTINGS.CARD_READER] 70 | self._reader_pattern = QtGui.QLineEdit(reader_pattern) 71 | layout.addRow(m.reader_name, self._reader_pattern) 72 | 73 | buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | 74 | QtGui.QDialogButtonBox.Cancel) 75 | buttons.accepted.connect(self._save) 76 | buttons.rejected.connect(self.reject) 77 | layout.addWidget(buttons) 78 | 79 | def _pin_expires_changed(self, val): 80 | self._pin_expires_days.setEnabled(val) 81 | 82 | def _save(self): 83 | settings[SETTINGS.COMPLEX_PINS] = self._complex_pins.isChecked() 84 | settings[SETTINGS.CARD_READER] = self._reader_pattern.text() 85 | if self._pin_expires.isChecked(): 86 | settings[SETTINGS.PIN_EXPIRATION] = self._pin_expires_days.value() 87 | else: 88 | settings[SETTINGS.PIN_EXPIRATION] = 0 89 | self.accept() 90 | -------------------------------------------------------------------------------- /pivman/view/usage_policy_dialog.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | from PySide import QtGui 28 | from pivman import messages as m 29 | from pivman.storage import settings, SETTINGS 30 | from pivman.yubicommon import qt 31 | 32 | 33 | class UsagePolicyDialog(qt.Dialog): 34 | 35 | def __init__(self, controller, slot, parent=None): 36 | super(UsagePolicyDialog, self).__init__(parent) 37 | 38 | self._controller = controller 39 | self._slot = slot 40 | self.has_content = False 41 | self._build_ui() 42 | 43 | def _build_ui(self): 44 | self.setWindowTitle(m.usage_policy) 45 | self.setFixedWidth(400) 46 | 47 | layout = QtGui.QVBoxLayout(self) 48 | 49 | self._build_usage_policy(layout) 50 | 51 | buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | 52 | QtGui.QDialogButtonBox.Cancel) 53 | buttons.accepted.connect(self.accept) 54 | buttons.rejected.connect(self.reject) 55 | layout.addWidget(buttons) 56 | 57 | def _build_usage_policy(self, layout): 58 | self._pin_policy = QtGui.QComboBox() 59 | self._pin_policy.addItem(m.pin_policy_default, None) 60 | self._pin_policy.addItem(m.pin_policy_never, 'never') 61 | self._pin_policy.addItem(m.pin_policy_once, 'once') 62 | self._pin_policy.addItem(m.pin_policy_always, 'always') 63 | 64 | self._touch_policy = QtGui.QCheckBox(m.touch_policy) 65 | if self._controller.version_tuple < (4, 0, 0): 66 | return 67 | 68 | use_pin_policy = self._slot in settings[SETTINGS.PIN_POLICY_SLOTS] 69 | use_touch_policy = self._slot in settings[SETTINGS.TOUCH_POLICY_SLOTS] 70 | 71 | if use_pin_policy or use_touch_policy: 72 | self.has_content = True 73 | layout.addWidget(self.section(m.usage_policy)) 74 | 75 | if use_pin_policy: 76 | pin_policy = settings[SETTINGS.PIN_POLICY] 77 | for index in range(self._pin_policy.count()): 78 | if self._pin_policy.itemData(index) == pin_policy: 79 | pin_policy_text = self._pin_policy.itemText(index) 80 | self._pin_policy.setCurrentIndex(index) 81 | break 82 | else: 83 | pin_policy = None 84 | pin_policy_text = m.pin_policy_default 85 | 86 | if settings.is_locked(SETTINGS.PIN_POLICY): 87 | layout.addWidget(QtGui.QLabel(m.pin_policy_1 % pin_policy_text)) 88 | else: 89 | pin_policy_box = QtGui.QHBoxLayout() 90 | pin_policy_box.addWidget(QtGui.QLabel(m.pin_policy)) 91 | pin_policy_box.addWidget(self._pin_policy) 92 | layout.addLayout(pin_policy_box) 93 | 94 | if use_touch_policy: 95 | self._touch_policy.setChecked(settings[SETTINGS.TOUCH_POLICY]) 96 | self._touch_policy.setDisabled( 97 | settings.is_locked(SETTINGS.TOUCH_POLICY)) 98 | layout.addWidget(self._touch_policy) 99 | 100 | @property 101 | def pin_policy(self): 102 | if settings.is_locked(SETTINGS.PIN_POLICY): 103 | return settings[SETTINGS.PIN_POLICY] 104 | return self._pin_policy.itemData(self._pin_policy.currentIndex()) 105 | 106 | @property 107 | def touch_policy(self): 108 | if self._controller.version_tuple < (4, 0, 0): 109 | return False 110 | if settings.is_locked(SETTINGS.TOUCH_POLICY): 111 | return settings[SETTINGS.TOUCH_POLICY] 112 | return self._touch_policy.isChecked() 113 | -------------------------------------------------------------------------------- /pivman/view/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | from PySide import QtGui, QtCore 28 | 29 | TOP_SECTION = '%s' 30 | SECTION = '
%s' 31 | IMPORTANT = '%s' 32 | 33 | PIN_VALIDATOR = QtGui.QRegExpValidator(QtCore.QRegExp(r'.{6,8}')) 34 | NUMERIC_PIN_VALIDATOR = QtGui.QRegExpValidator(QtCore.QRegExp(r'[0-9]{6,8}')) 35 | KEY_VALIDATOR = QtGui.QRegExpValidator(QtCore.QRegExp(r'[0-9a-fA-F]{48}')) 36 | SUBJECT_VALIDATOR = QtGui.QRegExpValidator(QtCore.QRegExp( 37 | r'^(/[a-zA-Z]+=[^/]+)+/?$')) 38 | 39 | 40 | class Dialog(QtGui.QDialog): 41 | 42 | def __init__(self, *args, **kwargs): 43 | super(Dialog, self).__init__(*args, **kwargs) 44 | self.setWindowFlags(self.windowFlags() 45 | ^ QtCore.Qt.WindowContextHelpButtonHint) 46 | self._headers = Headers() 47 | 48 | @property 49 | def headers(self): 50 | return self._headers 51 | 52 | def section(self, title): 53 | return self._headers.section(title) 54 | 55 | 56 | class Headers(object): 57 | 58 | def __init__(self): 59 | self._first = True 60 | 61 | def section(self, title): 62 | if self._first: 63 | self._first = False 64 | section = TOP_SECTION % title 65 | else: 66 | section = SECTION % title 67 | return QtGui.QLabel(section) 68 | 69 | 70 | def get_text(*args, **kwargs): 71 | flags = ( 72 | QtCore.Qt.WindowTitleHint | 73 | QtCore.Qt.WindowSystemMenuHint 74 | ) 75 | kwargs['flags'] = flags 76 | return QtGui.QInputDialog.getText(*args, **kwargs) 77 | 78 | 79 | def pin_field(): 80 | field = QtGui.QLineEdit() 81 | field.setEchoMode(QtGui.QLineEdit.Password) 82 | field.setMaxLength(8) 83 | return field 84 | 85 | 86 | def get_active_window(): 87 | active_win = QtGui.QApplication.activeWindow() 88 | if active_win is not None: 89 | return active_win 90 | 91 | wins = [w for w in QtGui.QApplication.topLevelWidgets() 92 | if isinstance(w, Dialog) and w.isVisible()] 93 | 94 | if not wins: 95 | return QtCore.QCoreApplication.instance().window 96 | 97 | return wins[0] # TODO: If more than one candidates remain, find best one. 98 | -------------------------------------------------------------------------------- /pivman/watcher.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | from __future__ import print_function 28 | from PySide import QtGui, QtCore 29 | from pivman.controller import Controller 30 | from pivman.piv import YkPiv, PivError, DeviceGoneError 31 | from pivman.storage import settings, SETTINGS 32 | from functools import partial 33 | try: 34 | from Queue import Queue 35 | except ImportError: 36 | from queue import Queue 37 | 38 | 39 | class Release(object): 40 | 41 | def __init__(self, fn): 42 | self._fn = fn 43 | 44 | def __del__(self): 45 | self._fn() 46 | 47 | def __call__(self): 48 | self._fn() 49 | self._fn = lambda: None 50 | 51 | 52 | class ControllerWatcher(QtCore.QObject): 53 | _device_found = QtCore.Signal() 54 | _device_lost = QtCore.Signal() 55 | 56 | def __init__(self): 57 | super(ControllerWatcher, self).__init__() 58 | 59 | self._waiters = Queue() 60 | self._controller = None 61 | 62 | self._lock = QtCore.QMutex() 63 | self._lock.lock() 64 | self._worker = QtCore.QCoreApplication.instance().worker 65 | self._worker.post_bg(self._poll, self._release, True) 66 | 67 | self.startTimer(2000) 68 | 69 | def timerEvent(self, event): 70 | if QtGui.QApplication.activeWindow() and self._lock.tryLock(): 71 | self._worker.post_bg(self._poll, self._release, True) 72 | event.accept() 73 | 74 | def _release(self, result=None): 75 | if self._controller and not self._waiters.empty(): 76 | waiter = self._waiters.get_nowait() 77 | waiter(self._controller, Release(self._release)) 78 | else: 79 | self._lock.unlock() 80 | 81 | def _poll(self): 82 | reader = settings[SETTINGS.CARD_READER] 83 | if self._controller: 84 | if self._controller.poll(): 85 | return 86 | self._controller = None 87 | self._device_lost.emit() 88 | 89 | try: 90 | self._controller = Controller(YkPiv(reader=reader)) 91 | self._device_found.emit() 92 | except (PivError, DeviceGoneError) as e: 93 | print(e) 94 | 95 | def on_found(self, fn, hold_lock=False): 96 | self._device_found.connect(self.wrap(fn, hold_lock)) 97 | 98 | def on_lost(self, fn): 99 | self._device_lost.connect(fn) 100 | 101 | def use(self, fn, hold_lock=False): 102 | if not hold_lock: 103 | def waiter(controller, release): 104 | fn(controller) 105 | else: 106 | waiter = fn 107 | 108 | if self._controller and self._lock.tryLock(): 109 | waiter(self._controller, Release(self._release)) 110 | else: 111 | self._waiters.put(waiter) 112 | 113 | def wrap(self, fn, hold_lock=False): 114 | return partial(self.use, fn, hold_lock) 115 | -------------------------------------------------------------------------------- /pivman/yubicommon: -------------------------------------------------------------------------------- 1 | ../vendor/yubicommon/yubicommon -------------------------------------------------------------------------------- /qt_resources/pivman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yubico/yubikey-piv-manager/788ccad1bb6f7e83aafe7b64f9442172fb4395c1/qt_resources/pivman.png -------------------------------------------------------------------------------- /release-windows.bat: -------------------------------------------------------------------------------- 1 | SET VERSION="%1" 2 | 3 | ECHO "Building release of version: %VERSION%" 4 | 5 | SET RELEASE_DIR=".\dist" 6 | 7 | SET "PATH=%PATH%;C:\Program Files (x86)\NSIS" 8 | SET "PATH=%PATH%;C:\Program Files (x86)\Common Files\Microsoft\Visual C++ for Python\9.0\WinSDK\Bin" 9 | 10 | signtool sign /fd SHA256 /t http://timestamp.verisign.com/scripts/timstamp.dll "%RELEASE_DIR%\YubiKey PIV Manager"\pivman.exe 11 | makensis -D"VERSION=%VERSION%" resources\win-installer.nsi 12 | signtool sign /fd SHA256 /t http://timestamp.verisign.com/scripts/timstamp.dll "%RELEASE_DIR%\yubikey-piv-manager-%VERSION%-win.exe" 13 | gpg --detach-sign "yubikey-piv-manager-%VERSION%-win.exe" 14 | -------------------------------------------------------------------------------- /resources/installer_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yubico/yubikey-piv-manager/788ccad1bb6f7e83aafe7b64f9442172fb4395c1/resources/installer_bg.png -------------------------------------------------------------------------------- /resources/pivman.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=YubiKey PIV Manager 3 | GenericName=YubiKey PIV Manager 4 | Comment=Tool for configuring your PIV-enabled YubiKey 5 | Exec=pivman 6 | Icon=pivman 7 | StartupNotify=false 8 | Terminal=false 9 | Type=Application 10 | Categories=Utility; 11 | Keywords=YubiKey;PIV;Manager; 12 | -------------------------------------------------------------------------------- /resources/pivman.xpm: -------------------------------------------------------------------------------- 1 | /* XPM */ 2 | static char * neoman_xpm[] = { 3 | "32 32 81 1", 4 | " c None", 5 | ". c #8CC041", 6 | "+ c #8BC040", 7 | "@ c #89BE3C", 8 | "# c #88BE3A", 9 | "$ c #8CC040", 10 | "% c #87BD39", 11 | "& c #A9D071", 12 | "* c #CCE3AA", 13 | "= c #E1EFCD", 14 | "- c #EEF6E2", 15 | "; c #E1EECE", 16 | "> c #CCE4AB", 17 | ", c #88BE3B", 18 | "' c #C1DD98", 19 | ") c #FAFDF7", 20 | "! c #FFFFFF", 21 | "~ c #FAFCF8", 22 | "{ c #C0DD97", 23 | "] c #98C755", 24 | "^ c #F3F8EA", 25 | "/ c #F2F8EA", 26 | "( c #A3CC66", 27 | "_ c #A2CD66", 28 | ": c #FBFDF8", 29 | "< c #F1F8E8", 30 | "[ c #F1F8E9", 31 | "} c #F8FBF4", 32 | "| c #FAFCF7", 33 | "1 c #F9FCF6", 34 | "2 c #82BA30", 35 | "3 c #A1CC63", 36 | "4 c #B2D580", 37 | "5 c #F6FAEF", 38 | "6 c #AED378", 39 | "7 c #86BD37", 40 | "8 c #8ABF3E", 41 | "9 c #AAD173", 42 | "0 c #C4DF9E", 43 | "a c #DAEBC2", 44 | "b c #8ABF3D", 45 | "c c #E0EECB", 46 | "d c #92C44B", 47 | "e c #94C44E", 48 | "f c #ACD276", 49 | "g c #92C34A", 50 | "h c #AAD072", 51 | "i c #E6F1D5", 52 | "j c #88BE39", 53 | "k c #C8E1A4", 54 | "l c #CCE3AB", 55 | "m c #F7FBF2", 56 | "n c #86BD38", 57 | "o c #B6D787", 58 | "p c #D5E8B9", 59 | "q c #8BBF3F", 60 | "r c #E1EFCE", 61 | "s c #8EC145", 62 | "t c #A7CF6E", 63 | "u c #AFD37B", 64 | "v c #EDF5E1", 65 | "w c #DCECC4", 66 | "x c #E3F0D1", 67 | "y c #96C551", 68 | "z c #E2EFCE", 69 | "A c #C0DC97", 70 | "B c #87BE39", 71 | "C c #FEFEFC", 72 | "D c #E2EFD0", 73 | "E c #B4D683", 74 | "F c #AAD073", 75 | "G c #E8F2D9", 76 | "H c #C0DD96", 77 | "I c #F6FAF0", 78 | "J c #9AC858", 79 | "K c #B8D98A", 80 | "L c #85BD36", 81 | "M c #84BC34", 82 | "N c #CFE5B0", 83 | "O c #98C756", 84 | "P c #FAFCF6", 85 | " ", 86 | " .......... ", 87 | " .............. ", 88 | " ......+@##@+...... ", 89 | " ....$%&*=--;>&%$.... ", 90 | " ....,')!!!!!!!!~{,.... ", 91 | " ....]^!!!!!!!!!!!!/].... ", 92 | " ....(!!!!!!!!!!!!!!!!_.... ", 93 | " ...]!!:<[[}!!!!|<[<|!!].... ", 94 | " ...,^!!12%%3!!!!4%%25!!^#... ", 95 | " ..$'!!!!6..75!!!8..9!!!!{$.. ", 96 | " ...%)!!!!=@.$0!!ab.@c!!!!:%... ", 97 | " ...&!!!!!!d..e!!f..g!!!!!!h... ", 98 | " ..+*!!!!!!0$.,i!j.+k!!!!!!l+.. ", 99 | " ..@=!!!!!!mn..opq.%:!!!!!!r@.. ", 100 | " ..#-!!!!!!!&..st..u!!!!!!!v#.. ", 101 | " ..#-!!!!!!!wb..+.,x!!!!!!!v#.. ", 102 | " ..@;!!!!!!!!s....y!!!!!!!!z@.. ", 103 | " ..+>!!!!!!!!A...ql!!!!!!!!l+.. ", 104 | " ...&!!!!!!!!^B..#C!!!!!!!!h... ", 105 | " ...%~!!!!!!!D@..E!!!!!!!!:%... ", 106 | " ..${!!!!!!!F..#G!!!!!!!!H$.. ", 107 | " ...,/!!!!!In..J!!!!!!!!/,... ", 108 | " ...]!!!!!KL7MN!!!!!!!!O.... ", 109 | " ...._!!!!PPPP!!!!!!!!_.... ", 110 | " ....]^!!!!!!!!!!!!/O.... ", 111 | " ....#{:!!!!!!!!:H,.... ", 112 | " ....$%hlrvvzlh%$.... ", 113 | " ......+@##@+...... ", 114 | " ................ ", 115 | " .......... ", 116 | " "}; 117 | -------------------------------------------------------------------------------- /resources/win-installer.nsi: -------------------------------------------------------------------------------- 1 | !include "MUI2.nsh" 2 | 3 | !define MUI_ICON "yubikey-piv-manager.ico" 4 | 5 | ; The name of the installer 6 | Name "YubiKey PIV Manager" 7 | 8 | ; The file to write 9 | OutFile "../dist/yubikey-piv-manager-${VERSION}-win.exe" 10 | 11 | ; The default installation directory 12 | InstallDir "$PROGRAMFILES\Yubico\YubiKey PIV Manager" 13 | 14 | ; Registry key to check for directory (so if you install again, it will 15 | ; overwrite the old one automatically) 16 | InstallDirRegKey HKLM "Software\Yubico\YubiKey PIV Manager" "Install_Dir" 17 | 18 | SetCompressor /SOLID lzma 19 | ShowInstDetails show 20 | 21 | Var MUI_TEMP 22 | Var STARTMENU_FOLDER 23 | 24 | ;Interface Settings 25 | 26 | !define MUI_ABORTWARNING 27 | 28 | ;-------------------------------- 29 | 30 | ; Pages 31 | !insertmacro MUI_PAGE_WELCOME 32 | !insertmacro MUI_PAGE_DIRECTORY 33 | ;Start Menu Folder Page Configuration 34 | !define MUI_STARTMENUPAGE_DEFAULTFOLDER "Yubico\YubiKey PIV Manager" 35 | !define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKCU" 36 | !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\Yubico\YubiKey PIV Manager" 37 | !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" 38 | !insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER 39 | !insertmacro MUI_PAGE_INSTFILES 40 | !insertmacro MUI_PAGE_FINISH 41 | 42 | !insertmacro MUI_UNPAGE_CONFIRM 43 | !insertmacro MUI_UNPAGE_INSTFILES 44 | 45 | ;Languages 46 | !insertmacro MUI_LANGUAGE "English" 47 | 48 | ;-------------------------------- 49 | 50 | Section "YubiKey PIV Manager" 51 | ; Remove all 52 | DELETE "$INSTDIR\*" 53 | 54 | SectionIn RO 55 | SetOutPath $INSTDIR 56 | FILE "..\dist\YubiKey PIV Manager\*" 57 | SectionEnd 58 | 59 | Var MYTMP 60 | 61 | # Last section is a hidden one. 62 | Section 63 | WriteUninstaller "$INSTDIR\uninstall.exe" 64 | 65 | ; Write the installation path into the registry 66 | WriteRegStr HKLM "Software\Yubico\YubiKey PIV Manager" "Install_Dir" "$INSTDIR" 67 | 68 | # Windows Add/Remove Programs support 69 | StrCpy $MYTMP "Software\Microsoft\Windows\CurrentVersion\Uninstall\YubiKey PIV Manager" 70 | WriteRegStr HKLM $MYTMP "DisplayName" "YubiKey PIV Manager" 71 | WriteRegExpandStr HKLM $MYTMP "UninstallString" '"$INSTDIR\uninstall.exe"' 72 | WriteRegExpandStr HKLM $MYTMP "InstallLocation" "$INSTDIR" 73 | WriteRegStr HKLM $MYTMP "DisplayVersion" "${VERSION}" 74 | WriteRegStr HKLM $MYTMP "Publisher" "Yubico AB" 75 | WriteRegStr HKLM $MYTMP "URLInfoAbout" "http://www.yubico.com" 76 | WriteRegDWORD HKLM $MYTMP "NoModify" "1" 77 | WriteRegDWORD HKLM $MYTMP "NoRepair" "1" 78 | 79 | !insertmacro MUI_STARTMENU_WRITE_BEGIN Application 80 | 81 | ;Create shortcuts 82 | SetShellVarContext all 83 | SetOutPath "$SMPROGRAMS\$STARTMENU_FOLDER" 84 | CreateDirectory "$SMPROGRAMS\$STARTMENU_FOLDER" 85 | CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\YubiKey PIV Manager.lnk" "$INSTDIR\pivman.exe" "" "$INSTDIR\pivman.exe" 0 86 | CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Uninstall YubiKey PIV Manager.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 1 87 | !insertmacro MUI_STARTMENU_WRITE_END 88 | 89 | CreateShortCut "$SMSTARTUP\YubiKey PIV Manager PIN-check.lnk" "$INSTDIR\pivman.exe" "-c" 90 | SectionEnd 91 | 92 | ; Uninstaller 93 | 94 | Section "Uninstall" 95 | 96 | ; Remove registry keys 97 | DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\pivman" 98 | DeleteRegKey HKLM "Software\Yubico\YubiKey PIV Manager" 99 | 100 | ; Remove all 101 | DELETE "$INSTDIR\*" 102 | 103 | ; Remove shortcuts, if any 104 | !insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP 105 | SetShellVarContext all 106 | 107 | Delete "$SMPROGRAMS\$MUI_TEMP\Uninstall YubiKey PIV Manager.lnk" 108 | Delete "$SMPROGRAMS\$MUI_TEMP\YubiKey PIV Manager.lnk" 109 | Delete "$SMSTARTUP\YubiKey PIV Manager PIN-check.lnk" 110 | 111 | ;Delete empty start menu parent diretories 112 | StrCpy $MUI_TEMP "$SMPROGRAMS\$MUI_TEMP" 113 | 114 | startMenuDeleteLoop: 115 | ClearErrors 116 | RMDir $MUI_TEMP 117 | GetFullPathName $MUI_TEMP "$MUI_TEMP\.." 118 | 119 | IfErrors startMenuDeleteLoopDone 120 | 121 | StrCmp $MUI_TEMP $SMPROGRAMS startMenuDeleteLoopDone startMenuDeleteLoop 122 | startMenuDeleteLoopDone: 123 | 124 | DeleteRegKey /ifempty HKCU "Software\Yubico\YubiKey PIV Manager" 125 | 126 | ; Remove directories used 127 | RMDir "$INSTDIR" 128 | SectionEnd 129 | -------------------------------------------------------------------------------- /resources/yubikey-piv-manager.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yubico/yubikey-piv-manager/788ccad1bb6f7e83aafe7b64f9442172fb4395c1/resources/yubikey-piv-manager.icns -------------------------------------------------------------------------------- /resources/yubikey-piv-manager.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yubico/yubikey-piv-manager/788ccad1bb6f7e83aafe7b64f9442172fb4395c1/resources/yubikey-piv-manager.ico -------------------------------------------------------------------------------- /resources/yubikey-piv-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yubico/yubikey-piv-manager/788ccad1bb6f7e83aafe7b64f9442172fb4395c1/resources/yubikey-piv-manager.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yubico/yubikey-piv-manager/788ccad1bb6f7e83aafe7b64f9442172fb4395c1/screenshot.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 80 3 | exclude = 4 | .*/, 5 | vendor/, 6 | pivman/qt_resources.py 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Yubico AB 2 | # All rights reserved. 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | # Additional permission under GNU GPL version 3 section 7 18 | # 19 | # If you modify this program, or any covered work, by linking or 20 | # combining it with the OpenSSL project's OpenSSL library (or a 21 | # modified version of that library), containing parts covered by the 22 | # terms of the OpenSSL or SSLeay licenses, We grant you additional 23 | # permission to convey the resulting work. Corresponding Source for a 24 | # non-source form of such a combination shall include the source code 25 | # for the parts of OpenSSL used as well as that of the covered work. 26 | 27 | from pivman.yubicommon.setup.exe import executable 28 | from pivman.yubicommon.setup.qt import qt_resources 29 | from pivman.yubicommon.setup import setup 30 | 31 | 32 | setup( 33 | name='yubikey-piv-manager', 34 | long_name='YubiKey PIV Manager', 35 | author='Dain Nilsson', 36 | author_email='dain@yubico.com', 37 | maintainer='Yubico Open Source Maintainers', 38 | maintainer_email='ossmaint@yubico.com', 39 | url='https://github.com/Yubico/yubikey-piv-manager', 40 | description='Tool for configuring your PIV-enabled YubiKey.', 41 | license='GPLv3+', 42 | entry_points={ 43 | 'gui_scripts': ['pivman=pivman.__main__:main'] 44 | }, 45 | install_requires=['PySide'], 46 | yc_requires=['ctypes', 'qt'], 47 | test_suite='test', 48 | tests_require=[''], 49 | cmdclass={'executable': executable, 'qt_resources': qt_resources('pivman')}, 50 | classifiers=[ 51 | 'License :: OSI Approved :: ' + 52 | 'GNU General Public License v3 or later (GPLv3+)', 53 | 'Operating System :: OS Independent', 54 | 'Programming Language :: Python', 55 | 'Development Status :: 5 - Production/Stable', 56 | 'Environment :: X11 Applications :: Qt', 57 | 'Intended Audience :: End Users/Desktop', 58 | 'Topic :: Security :: Cryptography', 59 | 'Topic :: Utilities' 60 | ] 61 | ) 62 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yubico/yubikey-piv-manager/788ccad1bb6f7e83aafe7b64f9442172fb4395c1/test/__init__.py -------------------------------------------------------------------------------- /vagrant/build-windows/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | 6 | # Use Windows 10 as a base box. 7 | config.vm.box = "senglin/win-10-enterprise-vs2015community" 8 | config.vm.box_version = "1.0.0" 9 | 10 | 11 | # Install dependencies needed for yubikey-piv-manager development. 12 | #config.vm.provision "file", source: "qt-installer-noninteractive.qs", destination: "C:\Users\vagrant\qt-installer-noninteractive.qs" 13 | config.vm.provision "shell", path: "provision.bat" 14 | 15 | # Sync repository to /vagrant 16 | config.vm.synced_folder '../..', '/vagrant' 17 | 18 | config.vm.communicator = "winrm" 19 | 20 | # VirtualBox configuration 21 | config.vm.provider "virtualbox" do |vb| 22 | vb.name = "yubikey-piv-mananger_windows-build" 23 | 24 | # Enable GUI 25 | vb.gui = true 26 | # Set memory 27 | vb.memory = 2048 28 | # Enable shared clipboard 29 | vb.customize ["modifyvm", :id, "--clipboard", "bidirectional"] 30 | # Enable drag-n-drop 31 | vb.customize ["modifyvm", :id, "--draganddrop", "bidirectional"] 32 | end 33 | 34 | # This will connect the YubiKey to the VM when re-inserted. 35 | # This filter uses VirtualBox as provider. 36 | # Modify the paramters as needed depending on the device. 37 | 38 | FILTER_NAME="YubiKey" 39 | MANUFACTURER="Yubico" 40 | VENDOR_ID="0x1050" 41 | PRODUCT_ID="0x0407" 42 | PRODUCT="Yubikey 4 OTP+U2F+CCID" 43 | 44 | config.vm.provider "virtualbox" do |vb| 45 | vb.customize ['modifyvm', :id, '--usb', 'on'] 46 | vb.customize ['usbfilter', 'add', '0', 47 | '--target', :id, 48 | '--name', FILTER_NAME, 49 | '--manufacturer', MANUFACTURER, 50 | '--vendorid', VENDOR_ID, 51 | '--productid', PRODUCT_ID, 52 | '--product', PRODUCT] 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /vagrant/build-windows/provision.bat: -------------------------------------------------------------------------------- 1 | net use Z: \\VBOXSVR\vagrant 2 | 3 | REM Install Chocolatey 4 | @"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin" 5 | 6 | choco install python2 -y 7 | choco install vcpython27 -y 8 | choco install nsis -y 9 | choco install 7zip -y 10 | 11 | pip install --only-binary pyside pyside pycrypto pyinstaller 12 | -------------------------------------------------------------------------------- /vagrant/development/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | 6 | # Use Ubuntu 16.04 Xenial as a base box. 7 | config.vm.box = "ubuntu/xenial64" 8 | 9 | # Install dependencies needed for yubikey-piv-manager development. 10 | config.vm.provision "shell", path: "provision.sh" 11 | 12 | # Sync repository to /vagrant 13 | config.vm.synced_folder '../..', '/vagrant' 14 | 15 | # VirtualBox configuration 16 | config.vm.provider "virtualbox" do |vb| 17 | # Enable GUI 18 | vb.gui = true 19 | # Set memory 20 | vb.memory = 2048 21 | # Enable shared clipboard 22 | vb.customize ["modifyvm", :id, "--clipboard", "bidirectional"] 23 | # Enable drag-n-drop 24 | vb.customize ["modifyvm", :id, "--draganddrop", "bidirectional"] 25 | end 26 | 27 | # Uncomment this to add a USB filter for YubiKeys. 28 | # This will connect the YubiKey to the VM when re-inserted. 29 | # This filter uses VirtualBox as provider. 30 | # Modify the paramters as needed depending on the device. 31 | 32 | FILTER_NAME="YubiKey" 33 | MANUFACTURER="Yubico" 34 | VENDOR_ID="0x1050" 35 | PRODUCT_ID="0x0407" 36 | PRODUCT="Yubikey 4 OTP+U2F+CCID" 37 | 38 | config.vm.provider "virtualbox" do |vb| 39 | vb.customize ['modifyvm', :id, '--usb', 'on'] 40 | vb.customize ['usbfilter', 'add', '0', 41 | '--target', :id, 42 | '--name', FILTER_NAME, 43 | '--manufacturer', MANUFACTURER, 44 | '--vendorid', VENDOR_ID, 45 | '--productid', PRODUCT_ID, 46 | '--product', PRODUCT] 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /vagrant/development/provision.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # Install development dependencies 4 | sudo apt-get update -qq 5 | sudo apt-get install -qq software-properties-common 6 | sudo add-apt-repository -y ppa:yubico/stable 7 | sudo apt-get update -qq && apt-get -qq upgrade 8 | sudo apt-get install -qq \ 9 | virtualbox-guest-dkms \ 10 | cmake \ 11 | libqt4-dev \ 12 | qt4-default \ 13 | qt4-qmake \ 14 | python-dev \ 15 | python-pip \ 16 | python-pyside=1.2.2-2build2 \ 17 | python-setuptools \ 18 | pyside-tools \ 19 | libykpiv1 \ 20 | xfce4 \ 21 | yubico-piv-tool 22 | pip install --upgrade pip 23 | 24 | # Install flake8 for linting 25 | pip install pre-commit flake8 26 | 27 | # Fix permissions in repo, install pre-commit hook 28 | cd /vagrant && chown -R ubuntu . && pre-commit install 29 | 30 | # Set a root password to enable login from GUI 31 | # Do startx after login to launch xfce4 32 | sudo echo "root:root" | sudo chpasswd 33 | 34 | # Make ubuntu user passwordless 35 | sudo passwd -d ubuntu 36 | 37 | # Add manifest file missing from PySide package 38 | 39 | cat << EOF | sudo tee /usr/lib/python2.7/dist-packages/PySide-1.2.2-2build2.egg-info 40 | Metadata-Version: 1.0 41 | Name: PySide 42 | Version: 1.2.2-2build2 43 | Summary: UNKNOWN 44 | Home-page: http://www.pyside.org/ 45 | Author: UNKNOWN 46 | Author-email: UNKNOWN 47 | License: UNKNOWN 48 | Description: Python bindings for Qt4 49 | Platform: UNKNOWN 50 | Summary: UNKNOWN 51 | EOF 52 | -------------------------------------------------------------------------------- /vagrant/windows-user/README.md: -------------------------------------------------------------------------------- 1 | Windows VM for testing releases 2 | === 3 | 4 | This is a blank Windows 10 VM with Edge installed. 5 | 6 | 7 | Usage 8 | --- 9 | 10 | Go to https://github.com/Yubico/yubikey-piv-manager/releases , download and 11 | install a Windows release, run 12 | `C:\Program Files (x86)\Yubico\YubiKey PIV Manager\pivman.exe` and connect a 13 | YubiKey. 14 | 15 | This `Vagrantfile` sets up USB forwarding in VirtualBox of YubiKey 4s with _all_ 16 | of the OTP+U2F+CCID transports enabled (device ID `1050:0407`). If you use a 17 | different VM engine or YubiKey, adjust these settings as necessary. In Linux, 18 | you can find the device ID using `lsusb`: 19 | 20 | $ lsusb -d 1050: 21 | -------------------------------------------------------------------------------- /vagrant/windows-user/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | 6 | # Use Windows 10 as a base box. 7 | config.vm.box = "Microsoft/EdgeOnWindows10" 8 | config.vm.box_version = "1.0.0" 9 | 10 | config.vm.communicator = "winrm" 11 | 12 | # VirtualBox configuration 13 | config.vm.provider "virtualbox" do |vb| 14 | vb.name = "yubikey-piv-mananger_windows-user" 15 | 16 | # Enable GUI 17 | vb.gui = true 18 | # Set memory 19 | vb.memory = 2048 20 | # Enable shared clipboard 21 | vb.customize ["modifyvm", :id, "--clipboard", "bidirectional"] 22 | # Enable drag-n-drop 23 | vb.customize ["modifyvm", :id, "--draganddrop", "bidirectional"] 24 | end 25 | 26 | # This will connect the YubiKey to the VM when re-inserted. 27 | # This filter uses VirtualBox as provider. 28 | # Modify the paramters as needed depending on the device. 29 | 30 | FILTER_NAME="YubiKey" 31 | MANUFACTURER="Yubico" 32 | VENDOR_ID="0x1050" 33 | PRODUCT_ID="0x0407" 34 | PRODUCT="Yubikey 4 OTP+U2F+CCID" 35 | 36 | config.vm.provider "virtualbox" do |vb| 37 | vb.customize ['modifyvm', :id, '--usb', 'on'] 38 | vb.customize ['usbfilter', 'add', '0', 39 | '--target', :id, 40 | '--name', FILTER_NAME, 41 | '--manufacturer', MANUFACTURER, 42 | '--vendorid', VENDOR_ID, 43 | '--productid', PRODUCT_ID, 44 | '--product', PRODUCT] 45 | end 46 | 47 | end 48 | --------------------------------------------------------------------------------