├── AUTHORS ├── BLURB ├── COPYING ├── MANIFEST.in ├── NEWS ├── README ├── README.adoc ├── doc ├── Database_Usage.adoc ├── Intro.adoc ├── README ├── Running_Hardware_Tests.adoc ├── YubiHSM_if.h ├── YubiKey_KSM.adoc └── db_schema ├── examples ├── yhsm-monitor-exit.py ├── yhsm-password-auth.py └── yhsm-sysinfo.py ├── man ├── yhsm-daemon.1 ├── yhsm-db-export.1 ├── yhsm-db-import.1 ├── yhsm-decrypt-aead.1 ├── yhsm-generate-keys.1 ├── yhsm-import-keys.1 ├── yhsm-init-oath-token.1 ├── yhsm-keystore-unlock.1 ├── yhsm-linux-add-entropy.1 ├── yhsm-validate-otp.1 ├── yhsm-validation-server.1 └── yhsm-yubikey-ksm.1 ├── pyhsm ├── __init__.py ├── aead_cmd.py ├── aes_ecb_cmd.py ├── base.py ├── basic_cmd.py ├── buffer_cmd.py ├── cmd.py ├── db_cmd.py ├── debug_cmd.py ├── defines.py ├── exception.py ├── hmac_cmd.py ├── ksm │ ├── __init__.py │ ├── db_export.py │ ├── db_import.py │ ├── import_keys.py │ └── yubikey_ksm.py ├── oath_hotp.py ├── oath_totp.py ├── soft_hsm.py ├── stick.py ├── stick_client.py ├── stick_daemon.py ├── tools │ ├── __init__.py │ ├── decrypt_aead.py │ ├── generate_keys.py │ ├── keystore_unlock.py │ └── linux_add_entropy.py ├── util.py ├── val │ ├── __init__.py │ ├── init_oath_token.py │ ├── validate_otp.py │ └── validation_server.py ├── validate_cmd.py ├── version.py └── yubikey.py ├── setup.py └── test ├── README.adoc ├── __init__.py ├── configure_hsm.py ├── test_aead.py ├── test_aes_ecb.py ├── test_basics.py ├── test_buffer.py ├── test_common.py ├── test_db.py ├── test_hmac.py ├── test_init.py ├── test_misc.py ├── test_oath.py ├── test_otp_validate.py ├── test_soft_hsm.py ├── test_stick.py ├── test_util.py └── test_yubikey_validate.py /AUTHORS: -------------------------------------------------------------------------------- 1 | Fredrik Thulin 2 | Created the project, design and implementation of most of it. 3 | 4 | Dain Nilsson 5 | Added yhsm-daemon, some other fixes. 6 | 7 | Tommaso De Orchi 8 | Added database support. 9 | 10 | Klas Lindfors 11 | Maintenance work. 12 | 13 | Simon Josefsson 14 | Maintenance work. 15 | -------------------------------------------------------------------------------- /BLURB: -------------------------------------------------------------------------------- 1 | Author: Yubico 2 | Basename: python-pyhsm 3 | Homepage: https://developers.yubico.com/python-pyhsm/ 4 | License: BSD-2-Clause 5 | Name: python-pyhsm 6 | Project: python-pyhsm 7 | Summary: Python code for YubiHSM 8 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2014 Yubico AB 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or 5 | without modification, are permitted provided that the following 6 | conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include COPYING 2 | include ChangeLog 3 | include NEWS 4 | include doc/* 5 | include man/* 6 | include examples/*.py 7 | include test/*.py 8 | include test/README.adoc 9 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | * Version 1.2.2 (unreleased) 2 | 3 | * Version 1.2.1 (released 2016-11-03) 4 | ** Fixup release: Remove minimum version for sqlalchemy dependency which was 5 | set too high. 6 | ** yhsm-yubikey-ksm: Add --proxy/--proxies argument for logging proxies 7 | requests. 8 | 9 | * Version 1.2.0 (released 2016-11-02) 10 | ** yhsm-validation-server: Support OATH TOTP. 11 | ** yhsm-init-oath-token: Handle keys with length != 20. 12 | ** yhsm-yubikey-ksm: Allow passing soft-HSM keys via stdin by passing "-" as 13 | device argument. 14 | ** yhsm-yubikey-ksm: Allow passing --db-url via environment variable. 15 | ** Moved utils, yubikey-ksm and validation-server to be included when 16 | installing using pip. 17 | ** Use entry_point scripts generated by setuptools. 18 | ** Moved man pages to man/ directory. 19 | ** Bugfix: Fix AEAD generation on Windows by writing in binary mode. 20 | ** Bugfix: Support AEADs generated on Windows using pyhsm <= 1.1.1. 21 | ** Bugfix: Avoid installing unit test package. 22 | ** Bugfix: yhsm-import-keys: Fix --aes-key argument used when importing 23 | without a YubiHSM. 24 | 25 | * Version 1.1.1 (released 2016-01-08) 26 | ** Fixup release. 27 | 28 | * Version 1.1.0 (released 2016-01-08) 29 | ** Restructured the repository and build process. 30 | ** Use Semantic Versioning (semver.org). 31 | ** Added support for a "soft" HSM in yhsm-yubikey-ksm, yhsm-import-keys 32 | and yhsm-generate-keys. 33 | 34 | * Version 1.0.4l (released 2015-08-24) 35 | ** Documentation is now in asciidoc format. 36 | ** yhsm-yubikey-ksm: Fix bug when the same public ID occured for multiple 37 | keyhandles. 38 | 39 | * Version 1.0.4k (released 2014-09-18) 40 | ** yhsm-db-import, yhsm-db-export: Fix syntax error. 41 | 42 | * Version 1.0.4j (released 2014-09-16) 43 | ** yhsm-yubikey-ksm: Fix syntax error. 44 | 45 | * Version 1.0.4i (released 2014-09-16) 46 | ** yhsm-yubikey-ksm: Add --daemon. 47 | ** yhsm-yubikey-ksm: Add --db-url to specify SQL database path to AEAD store. 48 | ** yhsm-db-import, yhsm-db-export: New tools to do database import/export. 49 | ** Documentation cleanup. 50 | 51 | * Version 1.0.4h (released 2014-01-09) 52 | ** yhsm-daemon: Use JSON messages instead of Python pickling. 53 | (as suggested by Rogdham) 54 | 55 | * Version 1.0.4g (released 2013-05-06) 56 | ** yhsm-daemon: Support listening to non-loopback interfaces. 57 | ** yhsm-daemon: Forward exceptions to the client. 58 | ** yhsm-daemon: Handle device unavailable, and attempt to recover. 59 | 60 | * Version 1.0.4f (released 2013-04-12) 61 | ** Fix failing test. 62 | ** Support URLs in device field, for more info see: 63 | http://pyserial.sourceforge.net/pyserial_api.html#serial.serial_for_url 64 | ** Added yhsm-daemon. 65 | 66 | * Version 1.0.4e (released 2013-04-06) 67 | ** yhsm-decrypt-aead: For yubikey-csv output, fix prefix field. 68 | Before the prefix was set to the AEAD nonce which is only correct for 69 | old style AEAD files. Now it uses the AEAD filename for the prefix 70 | field. 71 | ** yhsm-decrypt-aead: Improve diagnostic messages. 72 | Errors and warnings are now printed to standard error. The count of 73 | number of failed and processsed AEADs should now be accurate. 74 | 75 | * Version 1.0.4d (released 2013-03-18) 76 | ** Fix so that yhsm-yubikey-ksm can work with the older format. 77 | 78 | * Version 1.0.4c (released 2013-03-18) 79 | ** When doing OTP verification, if a nonce is written to the AEAD file 80 | use that for decryption, not public id. 81 | ** yhsm-import-keys: add --random-nonce for using hsm generated nonce. 82 | ** yhsm-generate-keys: add --random-nonce for using hsm generated nonce. 83 | 84 | * Version 1.0.4b (released 2013-02-11) 85 | ** yhsm-import-keys: Support soft HSM AEAD generation. 86 | ** yhsm-import-keys: Ignore lines starting with #. 87 | ** yhsm-import-keys: Block all-zero (ccc...c) keys. 88 | 89 | ** yhsm-decrypt-keys: Support generating AEADs. 90 | ** yhsm-decrypt-keys: Ignores non-modhex files in AEAD directory trees. 91 | 92 | ** yhsm-generate-keys: Bugfix that caused AEAD generation to fail. 93 | ** yhsm-generate-keys: Bugfix that caused wrong nonce to be used. 94 | ** yhsm-generate-keys: Prevent generating all-zero (ccc...c) keys. 95 | 96 | ** Added this NEWS file, based on debian/changelog in the Debian package. 97 | 98 | * Version 1.0.4a (released 2012-06-26) 99 | ** Enable IPv6 --addr for network servers. 100 | ** Verifies communication with YubiHSM on initialization. 101 | 102 | * Version 1.0.4 (released 2012-06-21) 103 | ** Match firmware 1.0.4. 104 | Firmware adds flag YSM_USER_NONCE to address security problem 105 | for some usages where AEADs could be decrypted by an attacker 106 | capable of generating new AEADs. 107 | 108 | ** New file format for stored AEADs (code loading AEADs is backwards 109 | ** compatible), including key handle and nonce. 110 | 111 | ** AES CCM implementation compatible with YubiHSM in software, for 112 | ** transparency and to enable willfull decryption of AEADs. 113 | 114 | ** Tools to generate YubiKey secrets into AEADs as well as decrypt 115 | ** them to enable provisioning YubiKeys with the secrets. 116 | 117 | * Version 1.0.3c (released 2012-01-05) 118 | ** First public release. 119 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | == PyHSM - Python YubiHSM Project 2 | 3 | === Introduction 4 | PyHSM is a Python package to talk to a YubiHSM. The YubiHSM is Yubico's take 5 | on the Hardware Security Module (HSM), designed for protecting secrets on 6 | authentication servers, including cryptographic keys and passwords, at 7 | unmatched simplicity and low cost. 8 | 9 | === License 10 | The project is licensed under the BSD license, see the file COPYING for exact 11 | wording. 12 | 13 | === Description 14 | PyHSM aims to be a reference implementation implementing all the functions 15 | available in the YubiHSM. 16 | 17 | PyHSM also includes the regression test suite for the YubiHSM. Instructions on 18 | running this test suite can be found in test/README.adoc. 19 | 20 | In general, see the files in `examples/` to get an idea of how to use this 21 | code. 22 | 23 | In addition to the YubiHSM communication library, PyHSM also contains some 24 | applications utilizing the YubiHSM: 25 | 26 | * yhsm-val: a simple validation server supporting validation of YubiKey 27 | OTPs, OATH codes and password hashes. 28 | 29 | * yubikey-ksm: ykval YubiKey OTP decryption backend using the YubiHSM. 30 | 31 | Some smaller scripts are also available: 32 | 33 | * yhsm-linux-add-entropy: Feed Linux kernel with random entropy from 34 | the TRNG on the YubiHSM. 35 | 36 | * yhsm-keystore-unlock: Unlock the key storage in the YubiHSM with your HSM 37 | password. Use with incorrect password to lock it again. 38 | 39 | * yhsm-daemon: Talk to the YubiHSM directly over TCP. 40 | 41 | And some more in `examples/`: 42 | 43 | * yhsm-sysinfo.py: Print basic system information about the connected YubiHSM. 44 | 45 | * yhsm-monitor-exit.py: Get a YubiHSM *in debug mode* to enter configuration 46 | mode again, without having to press the little button while inserting it into 47 | the USB port. 48 | 49 | * yhsm-password-auth.py: Example of how to turn passwords (or hashes of 50 | passwords if you like PBKDF2) into AEADs that can be used to verify the 51 | password later on. 52 | 53 | === Installation 54 | PyHSM is known to work with Python 2.6 and 2.7, and is primarily tested using 55 | Debian/Ubuntu, but is of course meant to work on as many platforms as possible. 56 | 57 | NOTE: If you want to use any of the daemons (yhsm-validation-server, 58 | yhsm-yubikey-ksm) you will want to use Python 2.7 or later. `SocketServer.py` 59 | lacks critical timeout handling in Python 2.6. 60 | 61 | The http://pyserial.sourceforge.net[pyserial] package is needed. 62 | 63 | Debian: apt-get install python-serial 64 | 65 | You will also need http://www.pycrypto.org[pycrypto]. 66 | 67 | Debian: apt-get install python-crypto 68 | 69 | Please note that the pycrypto version has to be 2.1 or higher -- it is 70 | known that RHEL6 has a lower version. 71 | 72 | For database support http://www.sqlalchemy.org[SQLAlchemy] is needed. 73 | 74 | Debian: apt-get install python-sqlalchemy 75 | 76 | PyHSM easily installed via pip, which will also take care of the dependencies 77 | automatically, though some of these require other build dependencies to 78 | correctly install (note the added optional dependencies needed for yubikey-ksm, 79 | yhsm-val and tools): 80 | 81 | $ pip install pyhsm[db,daemon] 82 | 83 | or, on Debian based systems (dependencies will also be handled automatically): 84 | 85 | $ sudo apt-get install python-pyhsm 86 | 87 | additional Debian packages available are: 88 | 89 | yhsm-tools yhsm-yubikey-ksm yhsm-validation-server yhsm-daemon 90 | 91 | === Working with the source code repository 92 | To work with the source code repository, if you wish to build your own release 93 | or contribute pull requests, follow these steps to set up your environment. If 94 | you just wish to install the application use the source release packages. This 95 | project is developed on a Debian based system, other OSes may not be supported 96 | for development. 97 | 98 | ==== Check out the code 99 | Run these commands to check out the source code: 100 | 101 | git clone https://github.com/Yubico/python-pyhsm.git 102 | cd python-pyhsm 103 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | README -------------------------------------------------------------------------------- /doc/Database_Usage.adoc: -------------------------------------------------------------------------------- 1 | == Database Usage 2 | 3 | IMPORT / EXPORT AEADs, to/from a database using yhsm-db-import and yhsm-db-export 4 | 5 | === INSTALLATION 6 | 7 | On Debian/Ubuntu install 8 | [source, sh] 9 | ~$ sudo apt-get install python-sqlalchemy (or any other method illustrated at: http://docs.sqlalchemy.org/en/rel_0_8/intro.html#installation) 10 | 11 | 12 | 13 | On other systems: 14 | Install SQLAlchemy from http://docs.sqlalchemy.org/en/rel_0_8/intro.html#installation 15 | 16 | 17 | A database schema is provided to configure the database table for the 18 | import/export tools. Create your favourite database and use te schema db_schema 19 | provided. 20 | 21 | [source, sh] 22 | ---- 23 | ~$ cat doc/db_schema 24 | CREATE TABLE aead_table ( 25 | public_id varchar(16) NOT NULL, 26 | keyhandle INT NOT NULL, 27 | aead BLOB NOT NULL, 28 | PRIMARY KEY (public_id, keyhandle) 29 | ); 30 | ---- 31 | 32 | 33 | === USAGE 34 | 35 | 36 | IMPORT: yhsm-db-import aeads_source_folder database_url 37 | 38 | EXPORT: yhsm-db-export aeads_destination_folder database_url 39 | 40 | [CAUTION] 41 | You need to be extra careful when providing sensitive data, such as database 42 | credentials, as command line arguments. In some cases these may be logged or 43 | visible to other parts of the system. If this is a concern then it is advised 44 | to change the database password after importing or exporting data with these 45 | scripts. 46 | 47 | 48 | ==== IMPORT 49 | [source, sh] 50 | ~$ python yhsm-db-import /root/aeads/ mysql://localhost/database_name 51 | 52 | OR 53 | 54 | [source, sh] 55 | ~$ python yhsm-db-import /root/aeads/ mysql://root:password@localhost:3306/database_name 56 | 57 | 58 | ==== EXPORT 59 | [source, sh] 60 | ~$ python yhsm-db-export /root/aeads/ mysql://localhost/database_name 61 | 62 | OR 63 | 64 | [source, sh] 65 | ~$ python yhsm-db-export /root/aeads/ mysql://root:password@localhost:3306/database_name 66 | 67 | -------------------------------------------------------------------------------- /doc/Intro.adoc: -------------------------------------------------------------------------------- 1 | == Installation 2 | 3 | === Quick Ubuntu guide 4 | 5 | [source, sh] 6 | ---- 7 | $ sudo add-apt-repository ppa:yubico/stable 8 | $ sudo apt-get update 9 | $ sudo apt-get install python-pyhsm 10 | $ pydoc pyhsm 11 | $ pydoc pyhsm.base 12 | ---- 13 | 14 | === Windows 15 | 16 | python-pyhsm has been tested on Windows with Python 2.6 (2.7 will probably also 17 | work) from http://www.python.org/. 18 | 19 | You need to install pySerial (tested with pyserial-2.5.win32.exe) from 20 | http://pypi.python.org/pypi/pyserial. 21 | 22 | When you first insert the YubiHSM in your computer, Windows will indicate it 23 | cannot install the hardware. Download the .INF file from 24 | http://www.yubico.com/yubihsm/ and select that in the Windows dialog. 25 | 26 | Now, you should be able to communicate with the YubiHSM from Python like this 27 | (for this simple test, I put a copy of the pyhsm source tree in Z:\tmp\pyhsm): 28 | 29 | [source, py] 30 | ---- 31 | Z:\tmp\pyhsm>c:\Python26\python.exe 32 | Python 2.6.6 (r266:84297, Aug 24 2010, 18:46:32) [MSC v.1500 32 bit (Intel)] on win32 33 | Type "help", "copyright", "credits" or "license" for more information. 34 | >>> import sys 35 | >>> sys.path.append("Z:\\tmp\\pyhsm") 36 | >>> import pyhsm 37 | >>> hsm = pyhsm.YHSM(device = "COM3") 38 | >>> hsm.info() 39 | 40 | >>> 41 | ---- 42 | 43 | Here is how to for example get five bytes of random binary data from the YubiHSM: 44 | 45 | [source, py] 46 | ---- 47 | >>> hsm.random(5) 48 | '\x9c\x0fu\xcfP' 49 | >>> hsm.random(5).encode("hex") 50 | 'b562456719' 51 | >>> 52 | ---- 53 | -------------------------------------------------------------------------------- /doc/README: -------------------------------------------------------------------------------- 1 | This is the documentation directory of pyhsm. 2 | 3 | ---- 4 | API documentation 5 | ---- 6 | You can generate HTML API documentation from the source code by using epydoc. 7 | See the epydoc documentation for instructions on doing so. 8 | 9 | 10 | ---- 11 | Low level documentation 12 | ---- 13 | The reference manual for the YubiHSM is available from 14 | 15 | https://www.yubico.com/yubihsm/ 16 | 17 | The file doc/YubiHSM_if.h details all the available commands of the YubiHSM. 18 | -------------------------------------------------------------------------------- /doc/Running_Hardware_Tests.adoc: -------------------------------------------------------------------------------- 1 | == Tests 2 | Running these tests requires a YubiHSM. The user running the tests also needs 3 | to have permission to use the device. On Ubuntu this can be achieved by adding 4 | the user to the `dialout` group. 5 | 6 | === Setup the device 7 | Initialize the YubiHSM by holding down the button on it while it is inserted 8 | into a USB port. The YubiHSM should blink slowly to indicate configuration 9 | mode. Connect to the serial interface of the device and enter the following 10 | commands: 11 | 12 | HSM (keys not decrypted)> zap 13 | Confirm current config being erased (type yes) yes - wait - done 14 | NO_CFG> hsm ffffffff 15 | Enabled flags ffffffff = YSM_AEAD_GENERATE,YSM_BUFFER_AEAD_GENERATE,YSM_RANDOM_AEAD_GENERATE,YSM_AEAD_DECRYPT_CMP,YSM_DG 16 | Enter cfg password (g to generate) 17 | Enter admin Yubikey public id 001/008 (enter when done) 18 | Enter master key (g to generate) 19 | Confirm current config being erased (type yes) yes - wait - done 20 | HSM (keys changed)> keycommit - Done 21 | HSM> exit 22 | 23 | You should now be ready to run the tests. 24 | 25 | === Running the tests 26 | To run the tests, have a device configured as described above, then run: 27 | 28 | $ YHSM_ZAP=1 python setup.py test 29 | 30 | Note that subsequent testruns can omit `YHSM_ZAP=1` to skip the initial HSM 31 | device configuration, however each testrun that doesn't reset the device 32 | configuration will take longer and longer to complete, until the next 33 | configuration reset. This is due to the admin YubiKey OTP counters being 34 | incremented on each run, and not stored anywhere, requiring looping to find the 35 | correct value. It is also possible to just reset the configuration, without 36 | running the tests: 37 | 38 | $ python -m test.configure_hsm 39 | -------------------------------------------------------------------------------- /doc/YubiKey_KSM.adoc: -------------------------------------------------------------------------------- 1 | == YubiKey Key Storage Module using the YubiHSM 2 | 3 | === Introduction 4 | 5 | The YubiCloud architecture separates the online validation servers where 6 | queries are sent from the place where the actual secret YubiKey AES keys 7 | are stored. The KSM decrypts the YubiKey OTP using the AES key identified 8 | by the "public id" part of the OTP, and return the counter values of the 9 | OTP to the querying validation server, which decides if the OTP is valid 10 | or not. 11 | 12 | The application "yhsm-yubikey-ksm" bundled with pyhsm is a KSM backend using 13 | the YubiHSM to further protect the AES keys. 14 | 15 | The interface exposed to the validation servers is a simple REST web 16 | interface, currently implemented using the BaseHTTPServer. It is inter- 17 | changeable with the original PHP based KSM found at 18 | https://developers.yubico.com/yubikey-ksm/ 19 | 20 | [WARNING] 21 | The server is currently single threaded, and uses a short timeout 22 | (5 seconds by default) to prevent blocking on bad requests (typically 23 | network problems between a validation server and a KSM). 24 | 25 | [NOTE] 26 | .Key handle 27 | You need to configure the YubiHSM connected to the KSM server with at least 28 | one key handle that has the YSM_AEAD_OTP_DECODE permission flag set. 29 | 30 | It is preferable to use a separate server (and YubiHSM) to create AEAD:s, 31 | in which case the other YubiHSM will have the same key handle with the 32 | same secret key, but with different permission flags (the appropriate 33 | YSM AEAD generate flags for the type of keys you use). 34 | 35 | === Installation on Debian/Ubuntu 36 | 37 | You can install the yhsm-yubikey-ksm from the package repositories : 38 | 39 | [source, sh] 40 | ---- 41 | $ sudo apt-get install yhsm-yubikey-ksm 42 | $ sudo $EDITOR /etc/default/yhsm-yubikey-ksm 43 | ---- 44 | 45 | === Non-Debian/Ubuntu installation instructions 46 | 47 | . Install pyhsm (see README in top level of pyhsm) 48 | . Install yhsm-yubikey-ksm into a suitable directory such as /usr/local/sbin/ 49 | . Run yhsm-yubikey-ksm --help to see what options are available. You will need to 50 | supply *--key-handles*, and possibly other parameters (verify --output-dir for 51 | example). 52 | 53 | === The AEAD files 54 | 55 | yhsm-yubikey-ksm is supposed to work with at least a couple of million YubiKeys 56 | (although the use of BaseHTTPServer might prove to be a bit too simplistic for 57 | a validation service of that magnitude, since it is not threaded). 58 | 59 | To keep things (such as adding new keys to a non-stop service) simple, and thereby 60 | hopefully robust, the initial scheme is to use the filesystem as database. We store 61 | the secrets of every YubiKey (22 bytes) in a separate file. 62 | 63 | The secrets are randomized and encrypted by a YubiHSM (preferably a separate one at 64 | a key provisioning site) so that they are protected from attackers even if they were 65 | to gain administrative access to the server/servers with YubiHSM's attached to them. 66 | 67 | The YubiHSM adds a MAC of 8 bytes to the 22 bytes secret data of the YubiKey, 68 | resulting in an AEAD of 30 bytes. This data is opaque by nature to everyone but the 69 | YubiHSM, since it is the only one with the key to decrypt the AEAD. 70 | 71 | Most filesystems start under-performing if you put a million files in a single 72 | directory. To avoid that, the AEAD file is hashed into a directory for each octet 73 | but the last one in the public ID. This ensures there will never be more than 256 74 | AEAD files in each directory : 75 | 76 | .... 77 | public id key handle AEAD file 78 | ccbbddeeffcc 0x20 /var/cache/yubikey-ksm/aeads/0x20/cc/bb/dd/ee/ff/ccbbddeeffcc 79 | .... 80 | 81 | An important note for storing large numbers of AEAD files in a filesystem is that it 82 | will use up large numbers of inodes. Consideration for this should be taken into 83 | account when setting up the filesystem. 84 | 85 | === Importing YubiKey secrets 86 | 87 | If you had a YK-KSM server before getting a YubiHSM, use the export utility to 88 | export all the secrets into a text file of this format : 89 | 90 | .... 91 | # ykksm 1 92 | 123456,ftftftcccccc,534543524554,fcacd309a20ce1809c2db257f0e8d6ea,000000000000,,, 93 | ... 94 | .... 95 | 96 | and then use the import utility /usr/sbin/yhsm-import-keys to create AEAD's for 97 | all YubiKey's listed in the text file. 98 | 99 | === Running without a hardware YubiHSM 100 | 101 | _Available in pyhsm 1.1.0 and later._ 102 | 103 | It's also possible to run the yubikey-ksm server without a physical YubiHSM. 104 | This can be useful for testing or staging purposes, or other situations where 105 | is it inpractical or infeasable to use a real YubiHSM. 106 | 107 | NOTE: When running completely in software the AES keys will be stored in a file 108 | on disk, and can be compromised in the event of an attack. 109 | 110 | To run without a physical YubiHSM, specify a properly formatted JSON file as 111 | the device when invoking the yhsm-yubikey-ksm executable (this also works for 112 | yhsm-import-keys as well as yhsm-generate-keys). The file should contain the 113 | numeric key-handles associated with the corresponding AES keys, as follows: 114 | 115 | .... 116 | { 117 | "1": "0000000100020003000400050006000700080009000a000b000c000d000e000f", 118 | "2": "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff" 119 | } 120 | .... 121 | 122 | Keys must be strings that parse into integers corresponding with the numeric 123 | key handles used in the YubiHSM, and values must be the hex representation of 124 | the AES key for each key handle. 125 | 126 | For example: 127 | [source, sh] 128 | ---- 129 | $ yhsm-yubikey-ksm -D /path/to/my/secrets.json [other args here...] 130 | ---- 131 | -------------------------------------------------------------------------------- /doc/db_schema: -------------------------------------------------------------------------------- 1 | CREATE TABLE aead_table ( 2 | public_id varchar(16) NOT NULL, 3 | keyhandle INT NOT NULL, 4 | nonce BLOB(6) NOT NULL, 5 | aead BLOB(32) NOT NULL, 6 | PRIMARY KEY (public_id, keyhandle) 7 | ); 8 | -------------------------------------------------------------------------------- /examples/yhsm-monitor-exit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2011 Yubico AB 4 | # See the file COPYING for licence statement. 5 | # 6 | # Utility to send a MONITOR_EXIT command to a YubiHSM. 7 | # 8 | # MONITOR_EXIT only works if the YubiHSM is in debug mode. It would 9 | # be a security problem to allow remote reconfiguration of a production 10 | # YubiHSM. 11 | # 12 | # If your YubiHSM is not in debug mode, enter configuration mode by 13 | # pressing the small button while inserting the YubiHSM in the USB port. 14 | # 15 | 16 | import sys 17 | import pyhsm 18 | 19 | device = "/dev/ttyACM0" 20 | 21 | # simplified arguments parsing 22 | d_argv = dict.fromkeys(sys.argv) 23 | debug = d_argv.has_key('-v') 24 | raw = d_argv.has_key('-v') 25 | 26 | if d_argv.has_key('-h'): 27 | sys.stderr.write("Syntax: %s [-v] [-R]\n" % (sys.argv[0])) 28 | sys.stderr.write("\nOptions :\n") 29 | sys.stderr.write(" -v verbose\n") 30 | sys.stderr.write(" -R raw MONITOR_EXIT command\n") 31 | sys.exit(0) 32 | 33 | res = 0 34 | try: 35 | s = pyhsm.base.YHSM(device=device, debug = debug) 36 | 37 | if raw: 38 | # No initialization 39 | s.write('\x7f\xef\xbe\xad\xba\x10\x41\x52\x45') 40 | else: 41 | print "Version: %s" % s.info() 42 | s.monitor_exit() 43 | 44 | print "Exited monitor-mode (maybe)" 45 | 46 | if raw: 47 | print "s.stick == %s" % s.stick 48 | print "s.stick.ser == %s" % s.stick.ser 49 | 50 | for _ in xrange(3): 51 | s.stick.ser.write("\n") 52 | line = s.stick.ser.readline() 53 | print "%s" % (line) 54 | except pyhsm.exception.YHSM_Error, e: 55 | print "ERROR: %s" % e 56 | res = 1 57 | 58 | sys.exit(res) 59 | -------------------------------------------------------------------------------- /examples/yhsm-password-auth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Utility to generate an AEAD (encrypted block) from a password, 4 | # that can later on be validated securely. 5 | # 6 | # Copyright (c) 2011 Yubico AB 7 | # See the file COPYING for licence statement. 8 | # 9 | # If you only have a single YubiHSM, you have to have one key handle 10 | # that can both ENCRYPT and COMPARE AES ECB blocks. 11 | # 12 | # If you have two (or more) YubiHSM's, you can have a key handle in 13 | # YubiHSM 1 that can only ENCRYPT, and the same key (!) with the same 14 | # key handle (!) in YubiHSM 2 that can only COMPARE. This might add 15 | # to the overall security in certain applications. 16 | # 17 | # Hashing the password before sending it to the YubiHSM (e.g. using PBKDF2) 18 | # in order to further enhance security is left as an exercise to the reader. 19 | # 20 | # Example usage : 21 | # 22 | # First, set password (create AEAD) : 23 | # 24 | # $ yhsm-password-auth.py --set --key-handle 8192 --nonce 010203040506 --verbose 25 | # Enter password to encrypt : 26 | # Success! Remember the nonce and use this AEAD to validate the password later : 27 | # 28 | # AEAD: 2b70c81e3f84db190f772d8e8dbfe05ebded5db881e9574939a52257 NONCE: 010203040506 29 | # $ 30 | # 31 | # Then, later on, validate the password using the AEAD and NONCE from above : 32 | # 33 | # $ yhsm-password-auth.py --key-handle 8192 --nonce 010203040506 --verbose \ 34 | # --validate 2b70c81e3f84db190f772d8e8dbfe05ebded5db881e9574939a52257 35 | # Enter password to validate : 36 | # OK! Password validated. 37 | # $ 38 | # 39 | 40 | import sys 41 | import pyhsm 42 | import argparse 43 | import getpass 44 | 45 | default_device = "/dev/ttyACM0" 46 | 47 | def parse_args(): 48 | """ 49 | Parse the command line arguments 50 | """ 51 | global default_device 52 | 53 | parser = argparse.ArgumentParser(description = "Generate password AEAD using YubiHSM", 54 | add_help=True 55 | ) 56 | parser.add_argument('-D', '--device', 57 | dest='device', 58 | default=default_device, 59 | required=False, 60 | help='YubiHSM device (default : "%s").' % default_device 61 | ) 62 | parser.add_argument('-v', '--verbose', 63 | dest='verbose', 64 | action='store_true', default=False, 65 | help='Enable verbose operation.' 66 | ) 67 | parser.add_argument('--debug', 68 | dest='debug', 69 | action='store_true', default=False, 70 | help='Enable debug operation.' 71 | ) 72 | parser.add_argument('--key-handle', 73 | type=int, dest='key_handle', 74 | required=True, 75 | help='Key handle to use. Must have YHSM_ECB_BLOCK_ENCRYPT and/or YHSM_ECB_BLOCK_DECRYPT_CMP flag set.' 76 | ) 77 | parser.add_argument('-N', '--nonce', 78 | dest='nonce', 79 | required=True, 80 | help='Nonce to use. 6 bytes encoded as 12 chars hex.' 81 | ) 82 | parser.add_argument('--set', 83 | dest='set', 84 | action='store_true', default=False, 85 | help='Set password mode.' 86 | ) 87 | parser.add_argument('--validate', 88 | dest='validate', 89 | help='AEAD to validate.' 90 | ) 91 | parser.add_argument('--min_length', 92 | type=int, dest='min_len', 93 | required=False, default=20, 94 | help='Minimum length to pad passwords to (default: 20).' 95 | ) 96 | 97 | args = parser.parse_args() 98 | 99 | if args.set and args.validate: 100 | sys.stderr.write("Arguments --set and --validate are mutually exclusive.\n") 101 | sys.exit(1) 102 | if not args.set and not args.validate: 103 | sys.stderr.write("Either --set or --validate must be specified.\n") 104 | sys.exit(1) 105 | 106 | return args 107 | 108 | def generate_aead(hsm, args, password): 109 | """ 110 | Generate an AEAD using the YubiHSM. 111 | """ 112 | try: 113 | pw = password.ljust(args.min_len, chr(0x0)) 114 | return hsm.generate_aead_simple(args.nonce.decode('hex'), args.key_handle, pw) 115 | except pyhsm.exception.YHSM_CommandFailed, e: 116 | if e.status_str == 'YHSM_FUNCTION_DISABLED': 117 | print "ERROR: The key handle %s is not permitted to YSM_AEAD_GENERATE." % (args.key_handle) 118 | return None 119 | else: 120 | print "ERROR: %s" % (e.reason) 121 | 122 | def validate_aead(hsm, args, password): 123 | """ 124 | Validate a previously generated AEAD using the YubiHSM. 125 | """ 126 | try: 127 | pw = password.ljust(args.min_len, chr(0x0)) 128 | return hsm.validate_aead(args.nonce.decode('hex'), args.key_handle, args.validate.decode('hex'), pw) 129 | except pyhsm.exception.YHSM_CommandFailed, e: 130 | if e.status_str == 'YHSM_FUNCTION_DISABLED': 131 | print "ERROR: The key handle %s is not permitted to do AES ECB compare." % (args.key_handle) 132 | return None 133 | else: 134 | print "ERROR: %s" % (e.reason) 135 | 136 | def main(): 137 | args = parse_args() 138 | 139 | what="encrypt" 140 | if args.validate: 141 | what="validate" 142 | user_input = getpass.getpass('Enter password to %s : ' % (what)) 143 | if not user_input: 144 | print "\nAborted.\n" 145 | return 0 146 | 147 | try: 148 | hsm = pyhsm.base.YHSM(device=args.device, debug=args.debug) 149 | except pyhsm.exception.YHSM_Error, e: 150 | print "ERROR: %s" % e 151 | return 1 152 | 153 | if args.set: 154 | # 155 | # SET password 156 | # 157 | aead = generate_aead(hsm, args, user_input) 158 | if not aead: 159 | return 1 160 | 161 | if args.verbose: 162 | print "Success! Remember the nonce and use this AEAD to validate the password later :\n" 163 | print "AEAD: %s NONCE: %s" % (aead.data.encode('hex'), args.nonce) 164 | else: 165 | # 166 | # VALIDATE password 167 | # 168 | if not validate_aead(hsm, args, user_input): 169 | if args.verbose: 170 | print "FAIL! Password does not match the generated AEAD." 171 | return 1 172 | if args.verbose: 173 | print "OK! Password validated." 174 | 175 | return 0 176 | 177 | if __name__ == '__main__': 178 | res = main() 179 | sys.exit(res) 180 | -------------------------------------------------------------------------------- /examples/yhsm-sysinfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2011 Yubico AB 4 | # See the file COPYING for licence statement. 5 | # 6 | # Utility to show system information of a YubiHSM. 7 | # 8 | 9 | import sys 10 | import pyhsm 11 | 12 | device = "/dev/ttyACM0" 13 | 14 | # simplified arguments parsing 15 | d_argv = dict.fromkeys(sys.argv) 16 | debug = d_argv.has_key('-v') 17 | 18 | if d_argv.has_key('-h'): 19 | sys.stderr.write("Syntax: %s [-v]\n" % (sys.argv[0])) 20 | sys.exit(0) 21 | 22 | res = 0 23 | try: 24 | s = pyhsm.base.YHSM(device=device, debug=debug) 25 | 26 | print "Version : %s" % (s.info()) 27 | 28 | nonce = s.get_nonce() 29 | print "Power-up count : %i" % (nonce.pu_count) 30 | 31 | except pyhsm.exception.YHSM_Error, e: 32 | print "ERROR: %s" % e 33 | res = 1 34 | 35 | sys.exit(res) 36 | -------------------------------------------------------------------------------- /man/yhsm-daemon.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2013-2014 Yubico AB 2 | .\" See the file COPYING for license statement. 3 | .\" 4 | .de URL 5 | \\$2 \(laURL: \\$1 \(ra\\$3 6 | .. 7 | .if \n[.g] .mso www.tmac 8 | .TH yhsm-daemon "1" "May 2013" "python-pyhsm" 9 | 10 | .SH NAME 11 | yhsm-daemon \(hy Allow multiple users of a YubiHSM. 12 | 13 | .SH SYNOPSIS 14 | .B yhsm\-daemon [\fIoptions\fR] [device] [port] 15 | 16 | .SH DESCRIPTION 17 | This is a daemon to allow multiple users of a YubiHSM without requiring 18 | permission to use the device. The daemon listens on a TCP port on localhost 19 | and allows multiple connections to share a YubiHSM. Access the YubiHSM via 20 | the daemon by specifying a device string following the yhsm://: 21 | syntax: 22 | 23 | hsm = YHSM('yhsm://localhost:5348') 24 | 25 | Note that the daemon and clients need to share the same version number to be 26 | compatible. 27 | 28 | .SH OPTIONS 29 | .PP 30 | .TP 31 | \fB\-h\fR, \fB\-\-help\fR 32 | Shows this help message and exit 33 | .TP 34 | \fB\-D\fR, \fB\-\-device\fR 35 | YubiHSM device name 36 | .TP 37 | \fB\-I\fR, \fB\-\-interface\fR 38 | Network interface to bind to 39 | .TP 40 | \fB\-P\fR, \fB\-\-port\fR 41 | TCP port to bind to 42 | 43 | .SH BUGS 44 | Report python-pyhsm/yhsm-daemon bugs in 45 | .URL "https://github.com/Yubico/python-pyhsm/issues/" "the issue tracker" 46 | 47 | .SH "SEE ALSO" 48 | The 49 | .URL "https://developers.yubico.com/python-pyhsm/" "home page" 50 | .PP 51 | YubiHSMs and YubiKeys can be obtained from 52 | .URL "http://www.yubico.com/" "Yubico" "." 53 | -------------------------------------------------------------------------------- /man/yhsm-db-export.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2014 Yubico AB 2 | .\" See the file COPYING for license statement. 3 | .\" 4 | .de URL 5 | \\$2 \(laURL: \\$1 \(ra\\$3 6 | .. 7 | .if \n[.g] .mso www.tmac 8 | .TH yhsm-db-export "1" "September 2014" "python-pyhsm" 9 | 10 | .SH NAME 11 | yhsm-db-export \(hy Export AEADs from a database to a filesystem structure 12 | 13 | .SH SYNOPSIS 14 | .B yhsm-db-export 15 | [\fIoptions\fR] 16 | 17 | .SH DESCRIPTION 18 | Use this program to export AEADs from a sql database to a filesystem structure. 19 | 20 | .SH OPTIONS 21 | .PP 22 | .TP 23 | \fB\-\-path\fR 24 | filesystem path of where to put AEADs 25 | .TP 26 | \fB\-\-dburl\fR 27 | connection URL for the database 28 | 29 | .SH BUGS 30 | Report python-pyhsm/yhsm-db-export bugs in 31 | .URL "https://github.com/Yubico/python-pyhsm/issues/" "the issue tracker" 32 | 33 | .SH "SEE ALSO" 34 | The 35 | .URL "https://developers.yubico.com/python-pyhsm/" "home page" 36 | .PP 37 | YubiHSMs can be obtained from 38 | .URL "http://www.yubico.com/" "Yubico" "." 39 | -------------------------------------------------------------------------------- /man/yhsm-db-import.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2014 Yubico AB 2 | .\" See the file COPYING for license statement. 3 | .\" 4 | .de URL 5 | \\$2 \(laURL: \\$1 \(ra\\$3 6 | .. 7 | .if \n[.g] .mso www.tmac 8 | .TH yhsm-db-import "1" "September 2014" "python-pyhsm" 9 | 10 | .SH NAME 11 | yhsm-db-import \(hy Import AEADs from filesystem to database 12 | 13 | .SH SYNOPSIS 14 | .B yhsm-db-import 15 | [\fIoptions\fR] 16 | 17 | .SH DESCRIPTION 18 | Use this program to import AEADs from a filesystem structure into a sql database. 19 | 20 | .SH OPTIONS 21 | .PP 22 | .TP 23 | \fB\-\-path\fR 24 | filesystem path of where to find AEADs 25 | .TP 26 | \fB\-\-dburl\fR 27 | connection URL for the database 28 | 29 | .SH BUGS 30 | Report python-pyhsm/yhsm-db-import bugs in 31 | .URL "https://github.com/Yubico/python-pyhsm/issues/" "the issue tracker" 32 | 33 | .SH "SEE ALSO" 34 | The 35 | .URL "https://developers.yubico.com/python-pyhsm/" "home page" 36 | .PP 37 | YubiHSMs can be obtained from 38 | .URL "http://www.yubico.com/" "Yubico" "." 39 | -------------------------------------------------------------------------------- /man/yhsm-decrypt-aead.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2012-2014 Yubico AB 2 | .\" See the file COPYING for license statement. 3 | .\" 4 | .de URL 5 | \\$2 \(laURL: \\$1 \(ra\\$3 6 | .. 7 | .if \n[.g] .mso www.tmac 8 | .TH yhsm-decrypt-aead "1" "June 2012" "python-pyhsm" 9 | 10 | .SH NAME 11 | yhsm-decrypt-aead \(hy Decrypt AEADs (with secrets for YubiKeys) 12 | 13 | .SH SYNOPSIS 14 | .B yhsm\-decrypt\-aead [\fIoptions\fR] \-\-aes\-key KEY file-or-dir [...] 15 | 16 | .SH DESCRIPTION 17 | Decrypt AEADs generated using a YubiHSM. 18 | \fBNOTE\fR that this requires knowledge of the AES key used in the YubiHSM. 19 | 20 | After a number of YubiKey secrets have been generated using 21 | .BR yhsm-generate-keys (1) 22 | , this tool can decrypt them and produce a CSV file usable to personalize 23 | corresponding YubiKeys. 24 | 25 | .SH OPTIONS 26 | .PP 27 | .TP 28 | \fB\-v\fR, \fB\-\-verbose\fR 29 | Enable verbose operation. 30 | .TP 31 | \fB\-\-debug\fR 32 | Enable debug printout. 33 | .TP 34 | \fB\-\-format str\fR 35 | Select output format (raw or yubikey-csv). 36 | .TP 37 | \fB\-\-print\-filename\fR 38 | Prefix any output with the input filename. 39 | .TP 40 | \fB\-\-key\-handle kh\fR 41 | Key handle used when generated AEADs, if not stored in the AEAD file (AEAD generated with python-pyhsm 1.0.3 or lower). 42 | .TP 43 | \fB\-\-aes\-key hexstr\fR 44 | AES key used to generate the AEADs. 45 | 46 | .SH "EXIT STATUS" 47 | .IX Header "EXIT STATUS" 48 | .IP "\fB0\fR" 4 49 | .IX Item "0" 50 | OK 51 | .IP "\fB1\fR" 4 52 | .IX Item "1" 53 | Fail 54 | 55 | .SH BUGS 56 | Report python-pyhsm/yhsm-decrypt-aead bugs in 57 | .URL "https://github.com/Yubico/python-pyhsm/issues/" "the issue tracker" 58 | 59 | .SH "SEE ALSO" 60 | The 61 | .URL "https://developers.yubico.com/python-pyhsm/" "home page" 62 | .PP 63 | YubiHSMs and YubiKeys can be obtained from 64 | .URL "http://www.yubico.com/" "Yubico" "." 65 | -------------------------------------------------------------------------------- /man/yhsm-generate-keys.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2012-2014 Yubico AB 2 | .\" See the file COPYING for license statement. 3 | .\" 4 | .de URL 5 | \\$2 \(laURL: \\$1 \(ra\\$3 6 | .. 7 | .if \n[.g] .mso www.tmac 8 | .TH yhsm-generate-keys "1" "June 2012" "python-pyhsm" 9 | 10 | .SH NAME 11 | yhsm-generate-keys \(hy Generate AEADs with secrets for YubiKeys using a YubiHSM 12 | 13 | .SH SYNOPSIS 14 | .B yhsm\-generate\-keys \-\-key\-handles KEY_HANDLES \-\-start\-public\-id START_ID 15 | [\fIoptions\fR] 16 | 17 | .SH DESCRIPTION 18 | With this tool, a YubiHSM can generate random secrets (using it's internal true 19 | random number generator), and these secrets protected in AEAD files can be stored 20 | on the host computer. 21 | 22 | The AEADs will be ready to be used by for example 23 | .BR yhsm-yubikey-ksm (1) 24 | ), 25 | as a part of a YubiKey OTP validation service. 26 | 27 | To program YubiKeys with the generated secrets, it is possible to decrypt the 28 | AEADs (knowledge of the AES key used inside the YubiHSM is required) using 29 | .BR yhsm-decrypt-aead (1) 30 | . 31 | 32 | .SH OPTIONS 33 | .PP 34 | .TP 35 | \fB\-D\fR, \fB\-\-device\fR 36 | Device file name (default: /dev/ttyACM0). 37 | .TP 38 | \fB\-v\fR, \fB\-\-verbose\fR 39 | Enable verbose operation. 40 | .TP 41 | \fB\-\-debug\fR 42 | Enable debug printout, including all data sent to/from YubiHSM. 43 | .TP 44 | \fB\-O dir\fR 45 | Base output directory (default: /var/cache/yubikey-ksm/aeads). 46 | .TP 47 | \fB\-c integer\fR 48 | Number of AEADs to generate. 49 | .TP 50 | \fB\-\-public\-id\-chars integer\fR 51 | Number of chars in generated public ids (default: 12). Changing this might not work well. 52 | .TP 53 | \fB\-\-key\-handles kh [kh ...]\fR 54 | Key handles to encrypt the generated secrets with. Examples : "1", "0xabcd". 55 | .TP 56 | \fB\-\-start\-public\-id id\fR 57 | Public id of the first generated secret, in modhex. 58 | .TP 59 | \fB\-\-random\-nonce\fR 60 | Use random nonce generated from YubiHSM. 61 | 62 | .SH "EXIT STATUS" 63 | .IX Header "EXIT STATUS" 64 | .IP "\fB0\fR" 4 65 | .IX Item "0" 66 | Secrets generated successfully. 67 | .IP "\fB1\fR" 4 68 | .IX Item "1" 69 | Failed to generate secrets. 70 | 71 | .SH BUGS 72 | Report python-pyhsm/yhsm-generate-keys bugs in 73 | .URL "https://github.com/Yubico/python-pyhsm/issues/" "the issue tracker" 74 | 75 | .SH "SEE ALSO" 76 | The 77 | .URL "https://developers.yubico.com/python-pyhsm/" "home page" 78 | .PP 79 | YubiHSMs and YubiKeys can be obtained from 80 | .URL "http://www.yubico.com/" "Yubico" "." 81 | -------------------------------------------------------------------------------- /man/yhsm-import-keys.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2011-2014 Yubico AB 2 | .\" See the file COPYING for license statement. 3 | .\" 4 | .de URL 5 | \\$2 \(laURL: \\$1 \(ra\\$3 6 | .. 7 | .if \n[.g] .mso www.tmac 8 | .TH yhsm-import-keys "1" "December 2011" "python-pyhsm" 9 | 10 | .SH NAME 11 | yhsm-import-keys \(hy import YubiKey secrets to YubiHSM 12 | 13 | .SH SYNOPSIS 14 | .B yhsm-import-keys \fI--key-handles\fR ... 15 | [\fIoptions\fR] 16 | 17 | .SH DESCRIPTION 18 | Read YubiKey token data from standard input, and store it in files or 19 | in the YubiHSM internal database. 20 | 21 | The default mode is to turn each YubiKey secret into an AEAD 22 | (Authenticated Encryption with Associated Data) block that is stored 23 | in a file on the host computer (one file per YubiKey). This enables 24 | validation of virtually unlimited numbers of YubiKey's OTPs. 25 | 26 | If \-\-internal-db is used, the YubiKey secret will be stored inside 27 | the YubiHSM, and complete validation (including counter management) 28 | will be done inside the YubiHSM. The internal database is currently 29 | limited to 1024 entries. 30 | 31 | .SH OPTIONS 32 | .PP 33 | .TP 34 | \fB\-D\fR, \fB\-\-device\fR 35 | device file name (default: /dev/ttyACM0) 36 | .TP 37 | \fB\-v\fR, \fB\-\-verbose\fR 38 | enable verbose operation 39 | .TP 40 | \fB\-\-debug\fR 41 | enable debug printout, including all data sent to/from YubiHSM 42 | .TP 43 | \fB\-\-public_id_chars\fR num 44 | number of chars to pad public id to (default: 12) 45 | .TP 46 | \fB\-\-key-handles\fR kh 47 | key handles to use for decoding OTPs. Examples : "1", "0xabcd" 48 | .TP 49 | \fB\-\-output-dir\fR dir, \fB\-\-aead-dir\fR dir, \fB\-O\fR dir 50 | base directory for AEADs (default: /var/cache/yubikey-ksm/aeads) 51 | .TP 52 | \fB\-\-internal-db\fR 53 | add entries to YubiHSM internal database, rather than creating AEAD files 54 | .TP 55 | \fB\-\-random-nonce\fR 56 | use random nonce generated from YubiHSM 57 | 58 | .SH "INPUT FORMAT" 59 | 60 | The format of the input matches the export format of various Yubico tools, 61 | notably the PHP based 62 | .URL "http://code.google.com/p/yubikey-ksm/" "soft KSM" "." 63 | 64 | An example file, with a single entry for id 4711 would be : 65 | .in +4n 66 | .nf 67 | 68 | # ykksm 1 69 | 123456,ftftftcccc,534543524554,fcacd309a20ce1809c2db257f0e8d6ea,000000000000,,, 70 | .fi 71 | .in 72 | 73 | (seqno, public id, private uid, AES key, dunno,,,) 74 | 75 | The #ykksm 1 is a file marker, and has to be on the very first line of input. 76 | 77 | .SH "BUGS" 78 | Report python-pyhsm/yhsm-import-keys bugs in 79 | .URL "https://github.com/Yubico/python-pyhsm/issues/" "the issue tracker" 80 | 81 | .SH "SEE ALSO" 82 | The 83 | .URL "https://developers.yubico.com/python-pyhsm/" "home page" 84 | .PP 85 | YubiHSMs can be obtained from 86 | .URL "http://www.yubico.com/" "Yubico" "." 87 | -------------------------------------------------------------------------------- /man/yhsm-init-oath-token.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2011-2014 Yubico AB 2 | .\" See the file COPYING for license statement. 3 | .\" 4 | .de URL 5 | \\$2 \(laURL: \\$1 \(ra\\$3 6 | .. 7 | .if \n[.g] .mso www.tmac 8 | .TH yhsm-init-oath-token "1" "December 2011" "python-pyhsm" 9 | 10 | .SH NAME 11 | yhsm-init-oath-token \(hy Tool to add an OATH token to the \fIyhsm-validation-server\fR\|(1) database. 12 | 13 | .SH SYNOPSIS 14 | .B yhsm-init-oath-token \fI--key-handle kh\fR \fI--uid name\fR 15 | [\fIoptions\fR] 16 | 17 | .SH DESCRIPTION 18 | Use this tool to add OATH token entries to the \fIyhsm-validation-server\fR\|(1) database. 19 | 20 | .SH OPTIONS 21 | .PP 22 | .TP 23 | \fB\-D\fR, \fB\-\-device\fR 24 | device file name (default: /dev/ttyACM0) 25 | .TP 26 | \fB\-v\fR, \fB\-\-verbose\fR 27 | enable verbose operation 28 | .TP 29 | \fB\-\-debug\fR 30 | enable debug printout, including all data sent to/from YubiHSM 31 | .TP 32 | \fB\-\-force\fR 33 | overwrite any present entry 34 | .TP 35 | \fB\-\-key-handle\fR kh 36 | key handle to create AEAD. Examples : "1", "0xabcd". 37 | .TP 38 | \fB\-\-uid\fR name 39 | user id (lookup key in token database) 40 | .TP 41 | \fB\-\-oath-c\fR num 42 | initial OATH counter value (integer) 43 | .TP 44 | \fB\-\-test-oath-window\fR num 45 | number of codes to search with \-\-test-code 46 | .TP 47 | \fB\-\-test-code\fR digits 48 | optional OTP from token for verification 49 | .TP 50 | \fB\-\-oath-k\fR str 51 | secret HMAC-SHA-1 key of the token, hex encoded 52 | .TP 53 | \fB\-\-db-file\fR fn 54 | db file for storing AEADs for later use by the \fIyhsm-validation-server\fR\|(1) (default: /var/yubico/yhsm-validation-server.db) 55 | 56 | 57 | .SH "EXIT STATUS" 58 | .IX Header "EXIT STATUS" 59 | .IP "\fB0\fR" 4 60 | .IX Item "0" 61 | YubiHSM keystore successfully unlocked 62 | .IP "\fB1\fR" 4 63 | .IX Item "1" 64 | Failed to unlock keystore 65 | .IP "\fB255\fR" 4 66 | .IX Item "255" 67 | Client ID not found in internal database 68 | 69 | .SH BUGS 70 | Report python-pyhsm/yhsm-init-oath-token bugs in 71 | .URL "https://github.com/Yubico/python-pyhsm/issues/" "the issue tracker" 72 | 73 | .SH "SEE ALSO" 74 | The 75 | .URL "https://developers.yubico.com/python-pyhsm/" "home page" 76 | .PP 77 | YubiHSMs can be obtained from 78 | .URL "http://www.yubico.com/" "Yubico" "." 79 | -------------------------------------------------------------------------------- /man/yhsm-keystore-unlock.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2011-2016 Yubico AB 2 | .\" See the file COPYING for license statement. 3 | .\" 4 | .de URL 5 | \\$2 \(laURL: \\$1 \(ra\\$3 6 | .. 7 | .if \n[.g] .mso www.tmac 8 | .TH yhsm-keystore-unlock "1" "December 2011" "python-pyhsm" 9 | 10 | .SH NAME 11 | yhsm-keystore-unlock \(hy Unlock the keystore in a YubiHSM 12 | 13 | .SH SYNOPSIS 14 | .B yhsm-keystore-unlock 15 | [\fIoptions\fR] 16 | 17 | .SH DESCRIPTION 18 | In versions of the YubiHSM before 1.0, the YubiHSM could be protected 19 | using a 'HSM password'. The YubiHSM would unlock it's cryptographic functions 20 | if the correct password was given, but it was a simple comparison test. 21 | 22 | In YubiHSM 1.0, the password was changed into an actual key that was used to 23 | decrypt the contents of the YubiHSM internal key store, which was then AES-256 24 | encrypted using the new 'Master key' when stored in the device. 25 | 26 | In YubiHSM 1.0, the option to also require an YubiKey OTP to unlock the 27 | keystore was also added. One or more 'Admin YubiKeys' can be configured 28 | in the YubiHSM, and an OTP from one of these must also be provided before the 29 | YubiHSM will enable it's cryptographic functions. 30 | 31 | The OTP is simply validated against the non-encrypted internal database 32 | (not key store) in the YubiHSM though, but together with a 'Master key' not 33 | stored on the server with the YubiHSM, it provides enhanced security by being 34 | a second factor that an attacker can't just intercept even if the server is 35 | compromised. 36 | 37 | .SH OPTIONS 38 | .PP 39 | .TP 40 | \fB\-D\fR, \fB\-\-device\fR 41 | device file name (default: /dev/ttyACM0). 42 | .TP 43 | \fB\-v\fR, \fB\-\-verbose\fR 44 | enable verbose operation. 45 | .TP 46 | \fB\-\-debug\fR 47 | enable debug printout, including all data sent to/from YubiHSM. 48 | .TP 49 | \fB\-\-no-otp\fR 50 | skip the prompt for an OTP. For use by scripts where no OTP 51 | is required and the Master Key is stored on the server with the YubiHSM. 52 | .TP 53 | \fB\-\-stdin\fR 54 | read password and/or OTP from stdin rather than prompting for them. 55 | Python prompts does not accept piped input, so this option have to be 56 | used to unlock the YubiHSM from a script for example. 57 | 58 | .SH "EXIT STATUS" 59 | .IX Header "EXIT STATUS" 60 | .IP "\fB0\fR" 4 61 | .IX Item "0" 62 | YubiHSM keystore successfully unlocked. 63 | .IP "\fB1\fR" 4 64 | .IX Item "1" 65 | Failed to unlock keystore. 66 | 67 | .SH BUGS 68 | Report python-pyhsm/yhsm-keystore-unlock bugs in 69 | .URL "https://github.com/Yubico/python-pyhsm/issues/" "the issue tracker" 70 | 71 | .SH "SEE ALSO" 72 | The 73 | .URL "https://developers.yubico.com/python-pyhsm/" "home page" 74 | .PP 75 | YubiHSMs can be obtained from 76 | .URL "http://www.yubico.com/" "Yubico" "." 77 | -------------------------------------------------------------------------------- /man/yhsm-linux-add-entropy.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2011-2014 Yubico AB 2 | .\" See the file COPYING for license statement. 3 | .\" 4 | .de URL 5 | \\$2 \(laURL: \\$1 \(ra\\$3 6 | .. 7 | .if \n[.g] .mso www.tmac 8 | .TH yhsm-linux-add-entropy "1" "December 2011" "python-pyhsm" 9 | 10 | .SH NAME 11 | yhsm-linux-add-entropy \(hy Seed the Linux entropy pool with data from YubiHSM TRNG 12 | 13 | .SH SYNOPSIS 14 | .B yhsm-linux-add-entropy 15 | [\fIoptions\fR] 16 | 17 | .SH DESCRIPTION 18 | The YubiHSM uses "Avalanche Noise" TRNG together with USB SOF jitter sampling 19 | to feed a DRBG_CTR algorithm (NIST publication SP800-90). The result has been 20 | verified as being random data of good quality by at least one third party 21 | cryptographer. 22 | .URL "http://sartryck.idg.se/Art/Yubihsm_1_TW072011.html" 23 | 24 | Use this program to add random data from the YubiHSM to the entropy pool of 25 | your Linux operating system. This is useful whenever lots of random data is needed, 26 | such as when generating chryptographic keys (GPG-keys), on a server terminating SSL 27 | sessions etc. 28 | 29 | You may run this script from cron, or in a while-loop. Make sure it does not run 30 | at the same time as something else accessing the YubiHSM though, or the two tasks 31 | may interrupt each other \(hy probably making both fail. 32 | 33 | .SH OPTIONS 34 | .PP 35 | .TP 36 | \fB\-D\fR, \fB\-\-device\fR 37 | device file name (default: /dev/ttyACM0). 38 | .TP 39 | \fB\-v\fR, \fB\-\-verbose\fR 40 | enable verbose operation. 41 | .TP 42 | \fB\-c\fR, \fB\-\-count\fR 43 | number of iterations to run (default: 100). 44 | .TP 45 | \fB\-r\fR, \fB\-\-ratio\fR 46 | bits per byte read to use. 8 is probably fine, but as a conservative default 2 is used. 47 | .TP 48 | \fB\-\-debug\fR 49 | enable debug printout, including all data sent to/from YubiHSM. 50 | 51 | .SH "EXIT STATUS" 52 | .IX Header "EXIT STATUS" 53 | .IP "\fB0\fR" 4 54 | .IX Item "0" 55 | Entropy added successfully 56 | .IP "\fB1\fR" 4 57 | .IX Item "1" 58 | Failure 59 | 60 | .SH BUGS 61 | Report python-pyhsm/yhsm-linux-add-entropy bugs in 62 | .URL "https://github.com/Yubico/python-pyhsm/issues/" "the issue tracker" 63 | 64 | .SH "SEE ALSO" 65 | The 66 | .URL "https://developers.yubico.com/python-pyhsm/" "home page" 67 | .PP 68 | YubiHSMs can be obtained from 69 | .URL "http://www.yubico.com/" "Yubico" "." 70 | -------------------------------------------------------------------------------- /man/yhsm-validate-otp.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2011-2014 Yubico AB 2 | .\" See the file COPYING for license statement. 3 | .\" 4 | .de URL 5 | \\$2 \(laURL: \\$1 \(ra\\$3 6 | .. 7 | .if \n[.g] .mso www.tmac 8 | .TH yhsm-validate-otp "1" "December 2011" "python-pyhsm" 9 | 10 | .SH NAME 11 | yhsm-validate-otp \(hy Validate an OTP using a YubiHSM. 12 | 13 | .SH SYNOPSIS 14 | .B yhsm-validate-otp 15 | \fImode\fR 16 | [\fIoptions\fR] 17 | 18 | .SH DESCRIPTION 19 | This tool allows simple validation of YubiKey OTP from shell scripts. 20 | 21 | .SH OPTIONS 22 | .PP 23 | .TP 24 | \fB\-D\fR, \fB\-\-device\fR 25 | device file name (default: /dev/ttyACM0). 26 | .TP 27 | \fB\-v\fR, \fB\-\-verbose\fR 28 | enable verbose operation. 29 | .TP 30 | \fB\-\-debug\fR 31 | enable debug printout, including all data sent to/from YubiHSM. 32 | 33 | .SH MODES 34 | \fB\-\-otp\fR 35 | Validate YubiKey OTP against entry in the YubiHSM internal database. 36 | 37 | .\"\fB\-\-oath\fR 38 | .\"\fBNot implemented yet.\fR 39 | .\"Validate an OATH code using HMAC-SHA-1 in the YubiHSM. The OATH counter 40 | .\"database must be initialized using \fIyhsm-init-oath-token\fR\|(1) first. 41 | 42 | 43 | 44 | .SH "EXIT STATUS" 45 | .IX Header "EXIT STATUS" 46 | .IP "\fB0\fR" 4 47 | .IX Item "0" 48 | YubiHSM keystore successfully unlocked 49 | .IP "\fB1\fR" 4 50 | .IX Item "1" 51 | Failed to unlock keystore 52 | .IP "\fB255\fR" 4 53 | .IX Item "255" 54 | Client ID not found in internal database 55 | 56 | .SH BUGS 57 | Report python-pyhsm/yhsm-validate-otp bugs in 58 | .URL "https://github.com/Yubico/python-pyhsm/issues/" "the issue tracker" 59 | 60 | .SH "SEE ALSO" 61 | The 62 | .URL "https://developers.yubico.com/python-pyhsm/" "home page" 63 | .PP 64 | YubiHSMs can be obtained from 65 | .URL "http://www.yubico.com/" "Yubico" "." 66 | -------------------------------------------------------------------------------- /man/yhsm-validation-server.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2011-2014 Yubico AB 2 | .\" See the file COPYING for license statement. 3 | .\" 4 | .de URL 5 | \\$2 \(laURL: \\$1 \(ra\\$3 6 | .. 7 | .if \n[.g] .mso www.tmac 8 | .TH yhsm-validation-server "1" "December 2011" "python-pyhsm" 9 | 10 | .SH NAME 11 | yhsm-validation-server \(hy Credential validation server utilizing YubiHSM 12 | 13 | .SH SYNOPSIS 14 | .B yhsm-validation-server 15 | [\fImode\fR] 16 | 17 | .SH DESCRIPTION 18 | This is a validation server using the YubiHSM for cryptographic operations. 19 | 20 | It is primarily built to validate YubiKey OTPs (\fInot\fR stored in the YubiHSM 21 | internal database), but it can also validate OATH token codes and legacy passwords. 22 | 23 | .SH OPTIONS 24 | .PP 25 | .TP 26 | \fB\-D\fR, \fB\-\-device\fR 27 | device file name (default: /dev/ttyACM0) 28 | .TP 29 | \fB\-v\fR, \fB\-\-verbose\fR 30 | enable verbose operation 31 | .TP 32 | \fB\-\-debug\fR 33 | enable debug printout, including all data sent to/from YubiHSM 34 | .TP 35 | \fB\-\-U\fR, \fB\-\-serve-url\fR base 36 | base of URL for validation web service (default: /yhsm/validate?) 37 | .TP 38 | \fB\-\-port\fR num 39 | port to listen on (default: 8003) 40 | .TP 41 | \fB\-\-addr\fR addr 42 | address to bind to (default: 127.0.0.1) 43 | .TP 44 | \fB\-\-hmac-kh\fR kh 45 | key handle to use for HMAC\(hySHA\(hy1. Examples : "1", "0xabcd". 46 | .TP 47 | \fB\-\-hotp-window\fR num 48 | number of OATH counter values to try (default: 5) 49 | .TP 50 | \fB\-\-totp-interval\fR num 51 | interval for TOTP time-step to use in seconds (default: 30) 52 | .TP 53 | \fB\-\-totp-tolerance\fR num 54 | number of time-steps on either side of now to allow TOTP codes for (default: 1) 55 | .TP 56 | \fB\-\-db-file\fR fn 57 | db file holding AEADs (see \fIyhsm-init-oath-token\fR\|(1)) (default: /var/yubico/yhsm-validation-server.db) 58 | .TP 59 | \fB\-\-clients-file\fR fn 60 | text file with mode OTP validation client shared secrets (see \fIyhsm-init-oath-token\fR\|(1)) (default: /var/yubico/yhsm-validation-server.db) 61 | .TP 62 | \fB\-\-pid-file\fR fn 63 | write process id of server to this file 64 | 65 | .SH "MODES" 66 | .TP 67 | \fB\-\-otp\fR 68 | Validate YubiKey OTP against entry in the YubiHSM internal database. 69 | Response should be compatible with those of 70 | .URL "http://code.google.com/p/yubikey-val-server-php/" "yubikey-val-server-php" "." 71 | .TP 72 | \fB\-\-short-otp\fR 73 | Validate YubiKey OTP against entry in the YubiHSM internal database. 74 | Returns a single line with the decrypted information from the OTP, compatible with 75 | .URL "http://code.google.com/p/yubikey-ksm/" "yubikey-ksm" "." 76 | .TP 77 | \fB\-\-hotp\fR 78 | Validate codes using the OATH HOTP algorithm, performing the HMAC\(hySHA\(hy1 inside the YubiHSM. 79 | .TP 80 | \fB\-\-totp\fR 81 | Validate codes using the OATH TOTP algorithm, performing the HMAC\(hySHA\(hy1 inside the YubiHSM. 82 | .TP 83 | \fB\-\-pwhash\fR 84 | Validate that a string (a PBKDF2 hash of a password for example) matches the one in an AEAD. 85 | Can be used to protect legacy passwords within an AEAD only readable to a YubiHSM, but 86 | still recoverable if you know the AEAD key (since you put it in the YubiHSM). 87 | 88 | 89 | .\"\fB\-\-oath\fR 90 | .\"\fBNot implemented yet.\fR 91 | .\"Validate an OATH code using HMAC\(hySHA\(hy1 in the YubiHSM. The OATH counter 92 | .\"database must be initialized using \fIyhsm-init-oath-token\fR\|(1) first. 93 | 94 | 95 | .SH "CLIENTS FILE" 96 | 97 | This file holds HMAC\(hySHA\(hy1 secrets shared between the validation client and server. 98 | 99 | An example file, with a single entry for id 4711 would be : 100 | .in +4n 101 | .nf 102 | 103 | # hash-style comments and blank lines are ignored 104 | 4711,grF5BERXEXPPpww1/TBvFg== 105 | 106 | # end 107 | .fi 108 | .in 109 | 110 | .SH "EXIT STATUS" 111 | .IX Header "EXIT STATUS" 112 | .IP "\fB0\fR" 4 113 | .IX Item "0" 114 | YubiHSM keystore successfully unlocked 115 | .IP "\fB1\fR" 4 116 | .IX Item "1" 117 | Failed to unlock keystore 118 | .IP "\fB255\fR" 4 119 | .IX Item "255" 120 | Client ID not found in internal database 121 | 122 | .SH "BUGS" 123 | Report python-pyhsm/yhsm-validation-server bugs in 124 | .URL "https://github.com/Yubico/python-pyhsm/issues/" "the issue tracker" 125 | 126 | .SH "SEE ALSO" 127 | The 128 | .URL "https://developers.yubico.com/python-pyhsm/" "home page" 129 | .PP 130 | YubiHSMs can be obtained from 131 | .URL "http://www.yubico.com/" "Yubico" "." 132 | -------------------------------------------------------------------------------- /man/yhsm-yubikey-ksm.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2011-2014 Yubico AB 2 | .\" See the file COPYING for license statement. 3 | .\" 4 | .de URL 5 | \\$2 \(laURL: \\$1 \(ra\\$3 6 | .. 7 | .if \n[.g] .mso www.tmac 8 | .TH yhsm-yubikey-ksm "1" "December 2011" "python-pyhsm" 9 | 10 | .SH NAME 11 | yhsm-yubikey-ksm \(hy Decrypt YubiKey OTPs using an attached YubiHSM 12 | 13 | .SH SYNOPSIS 14 | .B yhsm-yubikey-ksm \fI--key-handles\fR ... 15 | [\fIoptions\fR] 16 | 17 | .SH DESCRIPTION 18 | This is a small network server with a REST-like API that decodes YubiKey OTPs. 19 | 20 | It can be used as a decryption backend (Key Storage Module) to a validation service 21 | such as the YubiCloud. 22 | 23 | The AES keys of the YubiKeys must be present as AEAD files decryptable 24 | to the attached YubiHSM. Such AEADs can for example be created using \fIyhsm-import-keys\fR\|(1). 25 | 26 | Note that this daemon is single threaded \(hy it will only handle a single request at once. 27 | A request timeout is therefor most important. 28 | 29 | .SH OPTIONS 30 | .PP 31 | .TP 32 | \fB\-D\fR, \fB\-\-device\fR 33 | device file name (default: /dev/ttyACM0) 34 | .TP 35 | \fB\-v\fR, \fB\-\-verbose\fR 36 | enable verbose operation 37 | .TP 38 | \fB\-\-debug\fR 39 | enable debug printout, including all data sent to/from YubiHSM 40 | .TP 41 | \fB\-U\fR, \fB\-\-serve\-url\fR base 42 | base of URL for decrypt web service (default: /wsapi/decrypt?otp=) 43 | .TP 44 | \fB\-S\fR, \fB\-\-stats\-url\fR url 45 | URL where some basic statistics about operations since start can be collected 46 | .TP 47 | \fB\-\-port\fR num 48 | port to listen on (default: 8002) 49 | .TP 50 | \fB\-\-addr\fR addr 51 | address to bind to (default: 127.0.0.1) 52 | .TP 53 | \fB\-\-key-handles\fR kh, \fB\-\-key-handle\fR kh 54 | key handles to use for decoding OTPs. Examples : "1", "0xabcd". 55 | .TP 56 | \fB\-\-aead-dir\fR dir, \fB\-B\fR dir 57 | base directory for AEADs (default: /var/cache/yubikey-ksm/aeads) 58 | .TP 59 | \fB\-\-reqtimeout\fR num 60 | number of seconds before a request times out (default: 5) 61 | .TP 62 | \fB\-\-pid-file\fR fn 63 | write process id of server to this file 64 | .TP 65 | \fB\-\-db-url\fR url 66 | read AEAD data from this database 67 | .TP 68 | \fB\-\-proxies\fR ip, \fB\-\-proxy\fR ip 69 | log X-Forwarded-For header for requests from these IPs. 70 | 71 | .SH "BUGS" 72 | Report python-pyhsm/yhsm-yubikey-ksm bugs in 73 | .URL "https://github.com/Yubico/python-pyhsm/issues/" "the issue tracker" 74 | 75 | .SH "SEE ALSO" 76 | The 77 | .URL "https://developers.yubico.com/python-pyhsm/" "home page" 78 | .PP 79 | YubiHSMs can be obtained from 80 | .URL "http://www.yubico.com/" "Yubico" "." 81 | -------------------------------------------------------------------------------- /pyhsm/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011-2015 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | # 28 | """ 29 | the pyhsm package 30 | 31 | Basic usage :: 32 | 33 | import pyhsm 34 | 35 | try: 36 | hsm = pyhsm.base.YHSM(device="/dev/ttyACM0", debug=False) 37 | print "Version : %s" % (hsm.info()) 38 | except pyhsm.exception.YHSM_Error, e: 39 | print "ERROR: %s" % e 40 | 41 | See help(pyhsm.base) (L{pyhsm.base.YHSM}) for more information. 42 | """ 43 | 44 | __version__ = '1.2.2-dev0' 45 | __copyright__ = 'Yubico AB' 46 | __organization__ = 'Yubico' 47 | __license__ = 'BSD' 48 | __authors__ = ['Fredrik Thulin', 'Dain Nilsson'] 49 | 50 | __all__ = ["base", 51 | "cmd", 52 | "defines", 53 | "exception", 54 | "stick", 55 | "util", 56 | "version", 57 | "yubikey", 58 | "soft_hsm", 59 | # 60 | "aead_cmd", 61 | "aes_ecb_cmd", 62 | "basic_cmd", 63 | "buffer_cmd", 64 | "db_cmd", 65 | "debug_cmd", 66 | "hmac_cmd", 67 | "oath_hotp", 68 | "oath_totp", 69 | "validate_cmd", 70 | ] 71 | 72 | from pyhsm.base import YHSM 73 | -------------------------------------------------------------------------------- /pyhsm/aes_ecb_cmd.py: -------------------------------------------------------------------------------- 1 | """ 2 | implementations of AES ECB block cipher commands to execute on a YubiHSM 3 | """ 4 | 5 | # Copyright (c) 2011 Yubico AB 6 | # See the file COPYING for licence statement. 7 | 8 | import struct 9 | 10 | __all__ = [ 11 | # constants 12 | # functions 13 | # classes 14 | #'YHSM_Cmd_AES_ECB', 15 | 'YHSM_Cmd_AES_ECB_Encrypt', 16 | 'YHSM_Cmd_AES_ECB_Decrypt', 17 | 'YHSM_Cmd_AES_ECB_Compare', 18 | ] 19 | 20 | import pyhsm.defines 21 | import pyhsm.exception 22 | from pyhsm.cmd import YHSM_Cmd 23 | 24 | class YHSM_Cmd_AES_ECB(YHSM_Cmd): 25 | """ Common code for command classes in this module. """ 26 | status = None 27 | key_handle = 0x00 28 | 29 | def __init__(self, stick, command, payload): 30 | YHSM_Cmd.__init__(self, stick, command, payload) 31 | 32 | def __repr__(self): 33 | return '<%s instance at %s: key_handle=0x%x>' % ( 34 | self.__class__.__name__, 35 | hex(id(self)), 36 | self.key_handle 37 | ) 38 | 39 | def parse_result(self, data): 40 | # typedef struct { 41 | # uint32_t keyHandle; // Key handle 42 | # uint8_t ciphertext[YSM_BLOCK_SIZE]; // Ciphertext block 43 | # YHSM_STATUS status; // Encryption status 44 | # } YHSM_ECB_BLOCK_ENCRYPT_RESP; 45 | 46 | # OR 47 | 48 | # typedef struct { 49 | # uint32_t keyHandle; // Key handle 50 | # uint8_t plaintext[YSM_BLOCK_SIZE]; // Plaintext block 51 | # YHSM_STATUS status; // Decryption status 52 | # } YHSM_ECB_BLOCK_DECRYPT_RESP; 53 | 54 | fmt = "< I %is B" % (pyhsm.defines.YSM_BLOCK_SIZE) 55 | 56 | key_handle, \ 57 | result, \ 58 | self.status = struct.unpack(fmt, data) 59 | 60 | # check that returned key_handle matches the one in the request 61 | pyhsm.util.validate_cmd_response_hex('key_handle', key_handle, self.key_handle) 62 | 63 | if self.status == pyhsm.defines.YSM_STATUS_OK: 64 | return result 65 | else: 66 | raise pyhsm.exception.YHSM_CommandFailed(pyhsm.defines.cmd2str(self.command), self.status) 67 | 68 | 69 | class YHSM_Cmd_AES_ECB_Encrypt(YHSM_Cmd_AES_ECB): 70 | """ 71 | Have the YubiHSM AES ECB encrypt something using the key of a key handle. 72 | """ 73 | def __init__(self, stick, key_handle, plaintext): 74 | pyhsm.util.input_validate_str(plaintext, name='plaintext', max_len = pyhsm.defines.YSM_BLOCK_SIZE) 75 | self.key_handle = pyhsm.util.input_validate_key_handle(key_handle) 76 | # typedef struct { 77 | # uint32_t keyHandle; // Key handle 78 | # uint8_t plaintext[YHSM_BLOCK_SIZE]; // Plaintext block 79 | # } YHSM_ECB_BLOCK_ENCRYPT_REQ; 80 | payload = struct.pack(' .*', this): 137 | raise pyhsm.exception.YHSM_Error('YubiHSM is in configuration mode') 138 | raise pyhsm.exception.YHSM_Error('Unknown response from serial device %s : "%s"' \ 139 | % (self.stick.device, res.encode('hex'))) 140 | 141 | def parse_result(self, data): 142 | """ 143 | This function is intended to be overridden by sub-classes that 144 | implements commands that should not just return the data read from 145 | the YubiHSM. 146 | """ 147 | return data 148 | 149 | def reset(stick): 150 | """ 151 | Send a bunch of zero-bytes to the YubiHSM, and flush the input buffer. 152 | """ 153 | nulls = (pyhsm.defines.YSM_MAX_PKT_SIZE - 1) * '\x00' 154 | res = YHSM_Cmd(stick, pyhsm.defines.YSM_NULL, payload = nulls).execute(read_response = False) 155 | unlock = stick.acquire() 156 | try: 157 | stick.drain() 158 | stick.flush() 159 | finally: 160 | unlock() 161 | return res == 0 162 | -------------------------------------------------------------------------------- /pyhsm/db_cmd.py: -------------------------------------------------------------------------------- 1 | """ 2 | implementations of internal DB commands for YubiHSM 3 | """ 4 | 5 | # Copyright (c) 2011 Yubico AB 6 | # See the file COPYING for licence statement. 7 | 8 | import struct 9 | 10 | __all__ = [ 11 | # constants 12 | # functions 13 | # classes 14 | 'YHSM_Cmd_DB_YubiKey_Store', 15 | 'YHSM_Cmd_DB_Validate_OTP', 16 | ] 17 | 18 | import pyhsm.defines 19 | import pyhsm.exception 20 | import pyhsm.aead_cmd 21 | import pyhsm.validate_cmd 22 | from pyhsm.cmd import YHSM_Cmd 23 | 24 | class YHSM_Cmd_DB_YubiKey_Store(YHSM_Cmd): 25 | """ 26 | Ask YubiHSM to store data about a YubiKey in the internal database (not buffer). 27 | 28 | The input is an AEAD, perhaps previously created using generate_aead(). 29 | 30 | If the nonce for the AEAD is not the same as the public_id, specify it with the nonce keyword argument. 31 | This requires a YubiHSM >= 1.0.4. 32 | """ 33 | 34 | status = None 35 | 36 | def __init__(self, stick, public_id, key_handle, aead, nonce = None): 37 | self.key_handle = pyhsm.util.input_validate_key_handle(key_handle) 38 | self.public_id = pyhsm.util.input_validate_nonce(public_id, pad = True) 39 | aead = pyhsm.util.input_validate_aead(aead, expected_len = pyhsm.defines.YSM_YUBIKEY_AEAD_SIZE) 40 | if nonce is None: 41 | # typedef struct { 42 | # uint8_t publicId[YSM_PUBLIC_ID_SIZE]; // Public id (nonce) 43 | # uint32_t keyHandle; // Key handle 44 | # uint8_t aead[YSM_YUBIKEY_AEAD_SIZE]; // AEAD block 45 | # } YSM_DB_YUBIKEY_AEAD_STORE_REQ; 46 | fmt = "< %is I %is" % (pyhsm.defines.YSM_PUBLIC_ID_SIZE, \ 47 | pyhsm.defines.YSM_YUBIKEY_AEAD_SIZE) 48 | packed = struct.pack(fmt, self.public_id, self.key_handle, aead) 49 | YHSM_Cmd.__init__(self, stick, pyhsm.defines.YSM_DB_YUBIKEY_AEAD_STORE, packed) 50 | else: 51 | nonce = pyhsm.util.input_validate_nonce(nonce) 52 | # typedef struct { 53 | # uint8_t publicId[YSM_PUBLIC_ID_SIZE]; // Public id 54 | # uint32_t keyHandle; // Key handle 55 | # uint8_t aead[YSM_YUBIKEY_AEAD_SIZE]; // AEAD block 56 | # uint8_t nonce[YSM_AEAD_NONCE_SIZE]; // Nonce 57 | # } YSM_DB_YUBIKEY_AEAD_STORE2_REQ; 58 | fmt = "< %is I %is %is" % (pyhsm.defines.YSM_PUBLIC_ID_SIZE, \ 59 | pyhsm.defines.YSM_YUBIKEY_AEAD_SIZE, \ 60 | pyhsm.defines.YSM_AEAD_NONCE_SIZE) 61 | packed = struct.pack(fmt, self.public_id, self.key_handle, aead, nonce) 62 | YHSM_Cmd.__init__(self, stick, pyhsm.defines.YSM_DB_YUBIKEY_AEAD_STORE2, packed) 63 | 64 | def parse_result(self, data): 65 | """ Return True if the AEAD was stored sucessfully. """ 66 | # typedef struct { 67 | # uint8_t publicId[YSM_PUBLIC_ID_SIZE]; // Public id (nonce) 68 | # uint32_t keyHandle; // Key handle 69 | # YSM_STATUS status; // Validation status 70 | # } YSM_DB_YUBIKEY_AEAD_STORE_RESP; 71 | public_id, \ 72 | key_handle, \ 73 | self.status = struct.unpack("< %is I B" % (pyhsm.defines.YSM_AEAD_NONCE_SIZE), data) 74 | 75 | pyhsm.util.validate_cmd_response_str('public_id', public_id, self.public_id) 76 | pyhsm.util.validate_cmd_response_hex('key_handle', key_handle, self.key_handle) 77 | 78 | if self.status == pyhsm.defines.YSM_STATUS_OK: 79 | return True 80 | else: 81 | raise pyhsm.exception.YHSM_CommandFailed(pyhsm.defines.cmd2str(self.command), self.status) 82 | 83 | class YHSM_Cmd_DB_Validate_OTP(YHSM_Cmd): 84 | """ 85 | Request the YubiHSM to validate an OTP for a YubiKey stored 86 | in the internal database. 87 | """ 88 | 89 | response = None 90 | status = None 91 | 92 | def __init__(self, stick, public_id, otp): 93 | self.public_id = pyhsm.util.input_validate_nonce(public_id, pad = True) 94 | self.otp = pyhsm.util.input_validate_str(otp, 'otp', exact_len = pyhsm.defines.YSM_OTP_SIZE) 95 | # typedef struct { 96 | # uint8_t publicId[YSM_PUBLIC_ID_SIZE]; // Public id 97 | # uint8_t otp[YSM_OTP_SIZE]; // OTP 98 | # } YSM_DB_OTP_VALIDATE_REQ; 99 | fmt = "%is %is" % (pyhsm.defines.YSM_AEAD_NONCE_SIZE, pyhsm.defines.YSM_OTP_SIZE) 100 | packed = struct.pack(fmt, self.public_id, self.otp) 101 | YHSM_Cmd.__init__(self, stick, pyhsm.defines.YSM_DB_OTP_VALIDATE, packed) 102 | 103 | def __repr__(self): 104 | if self.executed: 105 | return '<%s instance at %s: public_id=%s, status=0x%x>' % ( 106 | self.__class__.__name__, 107 | hex(id(self)), 108 | self.public_id.encode('hex'), 109 | self.status 110 | ) 111 | else: 112 | return '<%s instance at %s (not executed)>' % ( 113 | self.__class__.__name__, 114 | hex(id(self)) 115 | ) 116 | 117 | def parse_result(self, data): 118 | # typedef struct { 119 | # uint8_t public_id[YSM_PUBLIC_ID_SIZE]; // Public id 120 | # uint16_t use_ctr; // Use counter 121 | # uint8_t session_ctr; // Session counter 122 | # uint8_t tstph; // Timestamp (high part) 123 | # uint16_t tstpl; // Timestamp (low part) 124 | # YHSM_STATUS status; // Validation status 125 | # } YHSM_AEAD_OTP_DECODED_RESP; 126 | fmt = "%is H B B H B" % (pyhsm.defines.YSM_PUBLIC_ID_SIZE) 127 | public_id, \ 128 | use_ctr, \ 129 | session_ctr, \ 130 | ts_high, \ 131 | ts_low, \ 132 | self.status = struct.unpack(fmt, data) 133 | 134 | pyhsm.util.validate_cmd_response_str('public_id', public_id, self.public_id) 135 | 136 | if self.status == pyhsm.defines.YSM_STATUS_OK: 137 | self.response = pyhsm.validate_cmd.YHSM_ValidationResult( \ 138 | public_id, use_ctr, session_ctr, ts_high, ts_low) 139 | return self.response 140 | else: 141 | raise pyhsm.exception.YHSM_CommandFailed(pyhsm.defines.cmd2str(self.command), self.status) 142 | -------------------------------------------------------------------------------- /pyhsm/debug_cmd.py: -------------------------------------------------------------------------------- 1 | """ 2 | implementations of debugging commands to execute on a YubiHSM 3 | 4 | """ 5 | 6 | # Copyright (c) 2011 Yubico AB 7 | # See the file COPYING for licence statement. 8 | 9 | import struct 10 | 11 | __all__ = [ 12 | # constants 13 | # functions 14 | # classes 15 | 'YHSM_Cmd_Monitor_Exit', 16 | ] 17 | 18 | import pyhsm.defines 19 | from pyhsm.cmd import YHSM_Cmd 20 | 21 | class YHSM_Cmd_Monitor_Exit(YHSM_Cmd): 22 | """ 23 | Send magics to YubiHSM in debug mode, and get it to exit to configuration mode again. 24 | """ 25 | def __init__(self, stick, payload=''): 26 | #define YHSM_MONITOR_EXIT 0x7f // Exit to monitor (no response sent) 27 | #define YHSM_MONITOR_EXIT_MAGIC 0xbaadbeef 28 | # typedef struct { 29 | # uint32_t magic; // Magic number for trigger 30 | # uint32_t magicInv; // 1st complement of magic 31 | # } YHSM_MONITOR_EXIT_REQ; 32 | 33 | packed = struct.pack('' % ( 38 | self.__class__.__name__, 39 | hex(id(self)), 40 | self.reason 41 | ) 42 | 43 | class YHSM_WrongInputSize(YHSM_Error): 44 | """ 45 | Exception raised for errors in the size of an argument to some function. 46 | """ 47 | def __init__(self, name, expected, size): 48 | reason = "Bad size of argument '%s', expected %i got %i" % (name, expected, size) 49 | YHSM_Error.__init__(self, reason) 50 | 51 | class YHSM_InputTooShort(YHSM_Error): 52 | """ 53 | Exception raised for too short input to some function. 54 | """ 55 | def __init__(self, name, expected, size): 56 | reason = "Argument '%s' too short, expected min %i got %i" % (name, expected, size) 57 | YHSM_Error.__init__(self, reason) 58 | 59 | class YHSM_InputTooLong(YHSM_Error): 60 | """ 61 | Exception raised for too long input to some function. 62 | """ 63 | def __init__(self, name, expected, size): 64 | reason = "Argument '%s' too long, expected max %i got %i" % (name, expected, size) 65 | YHSM_Error.__init__(self, reason) 66 | 67 | class YHSM_WrongInputType(YHSM_Error): 68 | """ 69 | Exception raised for errors in the type of an argument to some function. 70 | """ 71 | def __init__(self, name, expected, name_type): 72 | reason = "Bad type of argument '%s', expected %s got %s" % (name, expected, name_type) 73 | YHSM_Error.__init__(self, reason) 74 | 75 | class YHSM_CommandFailed(YHSM_Error): 76 | """ 77 | Exception raised when a command sent to the YubiHSM returned an error. 78 | """ 79 | def __init__(self, name, status): 80 | self.status = status 81 | self.status_str = pyhsm.defines.status2str(status) 82 | reason = "Command %s failed: %s" % (name, self.status_str) 83 | YHSM_Error.__init__(self, reason) 84 | -------------------------------------------------------------------------------- /pyhsm/hmac_cmd.py: -------------------------------------------------------------------------------- 1 | """ 2 | implementations of HMAC commands to execute on a YubiHSM 3 | """ 4 | 5 | # Copyright (c) 2011 Yubico AB 6 | # See the file COPYING for licence statement. 7 | 8 | import struct 9 | 10 | __all__ = [ 11 | # constants 12 | # functions 13 | # classes 14 | 'YHSM_Cmd_HMAC_SHA1_Write', 15 | 'YHSM_GeneratedHMACSHA1', 16 | ] 17 | 18 | import pyhsm.exception 19 | import pyhsm.defines 20 | from pyhsm.cmd import YHSM_Cmd 21 | 22 | class YHSM_Cmd_HMAC_SHA1_Write(YHSM_Cmd): 23 | """ 24 | Calculate HMAC SHA1 using a key_handle in the YubiHSM. 25 | 26 | Set final=False to not get a hash generated for the initial request. 27 | 28 | Set to_buffer=True to get the SHA1 stored into the internal buffer, for 29 | use in some other cryptographic operation. 30 | """ 31 | 32 | status = None 33 | result = None 34 | 35 | def __init__(self, stick, key_handle, data, flags = None, final = True, to_buffer = False): 36 | data = pyhsm.util.input_validate_str(data, 'data', max_len = pyhsm.defines.YSM_MAX_PKT_SIZE - 6) 37 | self.key_handle = pyhsm.util.input_validate_key_handle(key_handle) 38 | 39 | if flags != None: 40 | flags = pyhsm.util.input_validate_int(flags, 'flags', max_value=0xff) 41 | else: 42 | flags = pyhsm.defines.YSM_HMAC_SHA1_RESET 43 | if final: 44 | flags |= pyhsm.defines.YSM_HMAC_SHA1_FINAL 45 | if to_buffer: 46 | flags |= pyhsm.defines.YSM_HMAC_SHA1_TO_BUFFER 47 | 48 | self.final = final 49 | self.flags = flags 50 | packed = _raw_pack(self.key_handle, self.flags, data) 51 | YHSM_Cmd.__init__(self, stick, pyhsm.defines.YSM_HMAC_SHA1_GENERATE, packed) 52 | 53 | def next(self, data, final = False, to_buffer = False): 54 | """ 55 | Add more input to the HMAC SHA1. 56 | """ 57 | if final: 58 | self.flags = pyhsm.defines.YSM_HMAC_SHA1_FINAL 59 | else: 60 | self.flags = 0x0 61 | if to_buffer: 62 | self.flags |= pyhsm.defines.YSM_HMAC_SHA1_TO_BUFFER 63 | self.payload = _raw_pack(self.key_handle, self.flags, data) 64 | self.final = final 65 | return self 66 | 67 | def get_hash(self): 68 | """ 69 | Get the HMAC-SHA1 that has been calculated this far. 70 | """ 71 | if not self.executed: 72 | raise pyhsm.exception.YHSM_Error("HMAC-SHA1 hash not available, before execute().") 73 | return self.result.hash_result 74 | 75 | def __repr__(self): 76 | if self.executed: 77 | return '<%s instance at %s: key_handle=0x%x, flags=0x%x, executed=%s>' % ( 78 | self.__class__.__name__, 79 | hex(id(self)), 80 | self.key_handle, 81 | self.flags, 82 | self.executed, 83 | ) 84 | 85 | def parse_result(self, data): 86 | # typedef struct { 87 | # uint32_t keyHandle; // Key handle 88 | # YHSM_STATUS status; // Status 89 | # uint8_t numBytes; // Number of bytes in hash output 90 | # uint8_t hash[YSM_SHA1_HASH_SIZE]; // Hash output (if applicable) 91 | # } YHSM_HMAC_SHA1_GENERATE_RESP; 92 | key_handle, \ 93 | self.status, \ 94 | num_bytes = struct.unpack_from('' % ( 133 | self.__class__.__name__, 134 | hex(id(self)), 135 | self.key_handle, 136 | self.hash_result[:4].encode('hex'), 137 | self.final, 138 | ) 139 | -------------------------------------------------------------------------------- /pyhsm/ksm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yubico/python-pyhsm/9d5a802e1d27338caa8f87179ad23ac18a4a7e24/pyhsm/ksm/__init__.py -------------------------------------------------------------------------------- /pyhsm/ksm/db_export.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2013-2014 Yubico AB 3 | # See the file COPYING for licence statement. 4 | # 5 | """ 6 | Export AEAD from database. 7 | """ 8 | 9 | import os 10 | import sys 11 | import errno 12 | import argparse 13 | import sqlalchemy 14 | 15 | 16 | import pyhsm.aead_cmd 17 | 18 | 19 | def insert_slash(string, every=2): 20 | """insert_slash insert / every 2 char""" 21 | return os.path.join(string[i:i+every] for i in xrange(0, len(string), every)) 22 | 23 | 24 | def mkdir_p(path): 25 | """mkdir -p: creates path like mkdir -p""" 26 | try: 27 | os.makedirs(path) 28 | except OSError as exc: 29 | if exc.errno == errno.EEXIST and os.path.isdir(path): 30 | pass 31 | else: raise 32 | 33 | 34 | def main(): 35 | parser = argparse.ArgumentParser(description='Import AEADs into the database') 36 | 37 | parser.add_argument('path', help='filesystem path of where to put AEADs') 38 | parser.add_argument('dburl', help='connection URL for the database') 39 | args = parser.parse_args() 40 | 41 | 42 | #set the path 43 | path = args.path 44 | if not os.path.isdir(path): 45 | print("\nInvalid path, make sure it exists.\n") 46 | return 2 47 | 48 | #mysql url 49 | databaseUrl = args.dburl 50 | 51 | try: 52 | #check database connection 53 | engine = sqlalchemy.create_engine(databaseUrl) 54 | 55 | #SQLAlchemy voodoo 56 | metadata = sqlalchemy.MetaData() 57 | aeadobj = sqlalchemy.Table('aead_table', metadata, autoload=True, autoload_with=engine) 58 | connection = engine.connect() 59 | 60 | except: 61 | print("FATAL: Database connect failure") 62 | return 1 63 | 64 | aead = None 65 | nonce = None 66 | key_handle = None 67 | 68 | aead = pyhsm.aead_cmd.YHSM_GeneratedAEAD(nonce, key_handle, aead) 69 | 70 | #get data from the database 71 | result = connection.execute("SELECT * from aead_table") 72 | 73 | #cycle through resutls 74 | for row in result: 75 | #read values row by row 76 | aead.data = row['aead'] 77 | publicId = row['public_id'] 78 | aead.key_handle = row['keyhandle'] 79 | aead.nonce = row['nonce'] 80 | 81 | aead_dir = os.path.join(path, str(hex(aead.key_handle)).rstrip('L'), insert_slash(publicId)) 82 | #sanitize path 83 | aead_dir = os.path.normpath(aead_dir) 84 | #create path 85 | mkdir_p(aead_dir) 86 | 87 | #write the file in the path 88 | pyhsm.aead_cmd.YHSM_GeneratedAEAD.save(aead, os.path.join(aead_dir, publicId)) 89 | 90 | #close connection 91 | connection.close() 92 | 93 | 94 | if __name__ == '__main__': 95 | sys.exit(main()) 96 | -------------------------------------------------------------------------------- /pyhsm/ksm/db_import.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2013-2014 Yubico AB 3 | # See the file COPYING for licence statement. 4 | # 5 | """ 6 | Import AEADs to database. 7 | """ 8 | 9 | import os 10 | import re 11 | import sys 12 | import argparse 13 | import sqlalchemy 14 | 15 | from pyhsm.util import key_handle_to_int 16 | import pyhsm.aead_cmd 17 | 18 | 19 | def extract_keyhandle(path, filepath): 20 | """extract keyhandle value from the path""" 21 | 22 | keyhandle = filepath.lstrip(path) 23 | keyhandle = keyhandle.split("/") 24 | return keyhandle[0] 25 | 26 | 27 | def insert_query(connection, publicId, aead, keyhandle, aeadobj): 28 | """this functions read the response fields and creates sql query. then 29 | inserts everything inside the database""" 30 | 31 | # turn the keyhandle into an integer 32 | keyhandle = key_handle_to_int(keyhandle) 33 | if not keyhandle == aead.key_handle: 34 | print("WARNING: keyhandle does not match aead.key_handle") 35 | return None 36 | 37 | # creates the query object 38 | try: 39 | sql = aeadobj.insert().values(public_id=publicId, keyhandle=aead.key_handle, nonce=aead.nonce, aead=aead.data) 40 | # insert the query 41 | result = connection.execute(sql) 42 | return result 43 | except sqlalchemy.exc.IntegrityError: 44 | pass 45 | return None 46 | 47 | 48 | def main(): 49 | parser = argparse.ArgumentParser(description='Import AEADs into the database') 50 | 51 | parser.add_argument('path', help='filesystem path of where to find AEADs') 52 | parser.add_argument('dburl', help='connection URL for the database') 53 | 54 | args = parser.parse_args() 55 | 56 | path = args.path 57 | databaseUrl = args.dburl 58 | 59 | if not os.path.isdir(path): 60 | print("\nInvalid path, check your spelling.\n") 61 | return 2 62 | 63 | try: 64 | engine = sqlalchemy.create_engine(databaseUrl) 65 | 66 | #SQLAlchemy voodoo 67 | metadata = sqlalchemy.MetaData() 68 | aeadobj = sqlalchemy.Table('aead_table', metadata, autoload=True, autoload_with=engine) 69 | connection = engine.connect() 70 | except: 71 | print("FATAL: Database connect failure") 72 | return 1 73 | 74 | for root, subFolders, files in os.walk(path): 75 | if files: 76 | if not re.match(r'^[cbdefghijklnrtuv]+$', files[0]): 77 | continue 78 | 79 | #build file path 80 | filepath = os.path.join(root, files[0]) 81 | 82 | #extract the key handle from the path 83 | keyhandle = extract_keyhandle(path, filepath) 84 | kh_int = pyhsm.util.key_handle_to_int(keyhandle) 85 | 86 | #instantiate a new aead object 87 | aead = pyhsm.aead_cmd.YHSM_GeneratedAEAD(None, kh_int, '') 88 | aead.load(filepath) 89 | 90 | #set the public_id 91 | public_id = str(files[0]) 92 | 93 | #check it is old format aead 94 | if not aead.nonce: 95 | #configure values for oldformat 96 | aead.nonce = pyhsm.yubikey.modhex_decode(public_id).decode('hex') 97 | aead.key_handle = key_handle_to_int(keyhandle) 98 | 99 | if not insert_query(connection, public_id, aead, keyhandle, aeadobj): 100 | print("WARNING: could not insert %s" % public_id) 101 | 102 | #close sqlalchemy 103 | connection.close() 104 | 105 | 106 | if __name__ == '__main__': 107 | sys.exit(main()) 108 | -------------------------------------------------------------------------------- /pyhsm/oath_hotp.py: -------------------------------------------------------------------------------- 1 | """ 2 | helper functions to work with OATH HOTP (RFC4226) OTP's and YubiHSM 3 | """ 4 | 5 | # Copyright (c) 2011 Yubico AB 6 | # See the file COPYING for licence statement. 7 | 8 | import struct 9 | import pyhsm.exception 10 | import pyhsm.aead_cmd 11 | 12 | 13 | __all__ = [ 14 | # constants 15 | # functions 16 | 'search_for_oath_code', 17 | # classes 18 | ] 19 | 20 | def search_for_oath_code(hsm, key_handle, nonce, aead, counter, user_code, look_ahead=1): 21 | """ 22 | Try to validate an OATH HOTP OTP generated by a token whose secret key is 23 | available to the YubiHSM through the AEAD. 24 | 25 | The parameter `aead' is either a string, or an instance of YHSM_GeneratedAEAD. 26 | 27 | Returns next counter value on successful auth, and None otherwise. 28 | """ 29 | key_handle = pyhsm.util.input_validate_key_handle(key_handle) 30 | nonce = pyhsm.util.input_validate_nonce(nonce, pad = False) 31 | aead = pyhsm.util.input_validate_aead(aead) 32 | counter = pyhsm.util.input_validate_int(counter, 'counter') 33 | user_code = pyhsm.util.input_validate_int(user_code, 'user_code') 34 | hsm.load_temp_key(nonce, key_handle, aead) 35 | # User might have produced codes never sent to us, so we support trying look_ahead 36 | # codes to see if we find the user's current code. 37 | for j in xrange(look_ahead): 38 | this_counter = counter + j 39 | secret = struct.pack("> Q", this_counter) 40 | hmac_result = hsm.hmac_sha1(pyhsm.defines.YSM_TEMP_KEY_HANDLE, secret).get_hash() 41 | this_code = truncate(hmac_result) 42 | if this_code == user_code: 43 | return this_counter + 1 44 | return None 45 | 46 | def truncate(hmac_result, length=6): 47 | """ Perform the truncating. """ 48 | assert(len(hmac_result) == 20) 49 | offset = ord(hmac_result[19]) & 0xf 50 | bin_code = (ord(hmac_result[offset]) & 0x7f) << 24 \ 51 | | (ord(hmac_result[offset+1]) & 0xff) << 16 \ 52 | | (ord(hmac_result[offset+2]) & 0xff) << 8 \ 53 | | (ord(hmac_result[offset+3]) & 0xff) 54 | return bin_code % (10 ** length) 55 | -------------------------------------------------------------------------------- /pyhsm/oath_totp.py: -------------------------------------------------------------------------------- 1 | """ 2 | helper functions to work with OATH TOTP (RFC6238) OTP's and YubiHSM 3 | """ 4 | 5 | # Copyright (c) 2016 Storedsafe AB 6 | # Copyright (c) 2011 Yubico AB 7 | # See the file COPYING for licence statement. 8 | 9 | import datetime 10 | import time 11 | from pyhsm.oath_hotp import search_for_oath_code as search_hotp 12 | 13 | __all__ = [ 14 | # constants 15 | # functions 16 | 'search_for_oath_code', 17 | # classes 18 | ] 19 | 20 | 21 | def search_for_oath_code(hsm, key_handle, nonce, aead, user_code, interval=30, 22 | tolerance=0): 23 | """ 24 | Try to validate an OATH TOTP OTP generated by a token whose secret key is 25 | available to the YubiHSM through the AEAD. 26 | 27 | The parameter `aead' is either a string, or an instance of YHSM_GeneratedAEAD. 28 | 29 | Returns timecounter value on successful auth, and None otherwise. 30 | """ 31 | 32 | # timecounter is the lowest acceptable value based on tolerance 33 | timecounter = timecode(datetime.datetime.now(), interval) - tolerance 34 | 35 | return search_hotp( 36 | hsm, key_handle, nonce, aead, timecounter, user_code, 1 + 2*tolerance) 37 | 38 | 39 | def timecode(time_now, interval): 40 | """ make integer and divide by time interval of valid OTP """ 41 | i = time.mktime(time_now.timetuple()) 42 | return int(i / interval) 43 | -------------------------------------------------------------------------------- /pyhsm/stick.py: -------------------------------------------------------------------------------- 1 | """ 2 | module for actually talking to the YubiHSM 3 | """ 4 | 5 | # Copyright (c) 2011 Yubico AB 6 | # See the file COPYING for licence statement. 7 | 8 | __all__ = [ 9 | # constants 10 | # functions 11 | 'read', 12 | 'write', 13 | 'flush', 14 | # classes 15 | 'YHSM_Stick', 16 | ] 17 | 18 | import sys 19 | import serial 20 | 21 | import pyhsm.util 22 | import pyhsm.exception 23 | 24 | class YHSM_Stick(): 25 | """ 26 | The current YHSM is a USB device using serial communication. 27 | 28 | This class exposes the basic functions read, write and flush (input). 29 | """ 30 | def __init__(self, device, timeout=1, debug=False): 31 | """ 32 | Open YHSM device. 33 | """ 34 | self.debug = debug 35 | self.device = device 36 | self.num_read_bytes = 0 37 | self.num_write_bytes = 0 38 | self.ser = None # to not bomb in destructor on open fail 39 | self.ser = serial.serial_for_url(device) 40 | self.ser.baudrate = 115200 41 | self.ser.timeout = timeout 42 | if self.debug: 43 | sys.stderr.write("%s: OPEN %s\n" %( 44 | self.__class__.__name__, 45 | self.ser 46 | )) 47 | return None 48 | 49 | def acquire(self): 50 | """ 51 | Do nothing 52 | """ 53 | return self.acquire 54 | 55 | def write(self, data, debug_info=None): 56 | """ 57 | Write data to YHSM device. 58 | """ 59 | self.num_write_bytes += len(data) 60 | if self.debug: 61 | if not debug_info: 62 | debug_info = str(len(data)) 63 | sys.stderr.write("%s: WRITE %s:\n%s\n" %( 64 | self.__class__.__name__, 65 | debug_info, 66 | pyhsm.util.hexdump(data) 67 | )) 68 | return self.ser.write(data) 69 | 70 | def read(self, num_bytes, debug_info=None): 71 | """ 72 | Read a number of bytes from YubiHSM device. 73 | """ 74 | if self.debug: 75 | if not debug_info: 76 | debug_info = str(num_bytes) 77 | sys.stderr.write("%s: READING %s\n" %( 78 | self.__class__.__name__, 79 | debug_info 80 | )) 81 | res = self.ser.read(num_bytes) 82 | if self.debug: 83 | sys.stderr.write("%s: READ %i:\n%s\n" %( 84 | self.__class__.__name__, 85 | len(res), 86 | pyhsm.util.hexdump(res) 87 | )) 88 | self.num_read_bytes += len(res) 89 | return res 90 | 91 | def flush(self): 92 | """ 93 | Flush input buffers. 94 | """ 95 | if self.debug: 96 | sys.stderr.write("%s: FLUSH INPUT (%i bytes waiting)\n" %( 97 | self.__class__.__name__, 98 | self.ser.inWaiting() 99 | )) 100 | self.ser.flushInput() 101 | 102 | def drain(self): 103 | """ Drain input. """ 104 | if self.debug: 105 | sys.stderr.write("%s: DRAIN INPUT (%i bytes waiting)\n" %( 106 | self.__class__.__name__, 107 | self.ser.inWaiting() 108 | )) 109 | old_timeout = self.ser.timeout 110 | self.ser.timeout = 0.1 111 | data = self.ser.read(1) 112 | while len(data): 113 | if self.debug: 114 | sys.stderr.write("%s: DRAINED 0x%x (%c)\n" %(self.__class__.__name__, ord(data[0]), data[0])) 115 | data = self.ser.read(1) 116 | self.ser.timeout = old_timeout 117 | return True 118 | 119 | def raw_device(self): 120 | """ Get raw serial device. Only intended for test code/debugging! """ 121 | return self.ser 122 | 123 | def set_debug(self, new): 124 | """ 125 | Set debug mode (boolean). 126 | 127 | Returns old setting. 128 | """ 129 | if type(new) is not bool: 130 | raise pyhsm.exception.YHSM_WrongInputType( 131 | 'new', bool, type(new)) 132 | old = self.debug 133 | self.debug = new 134 | return old 135 | 136 | def __repr__(self): 137 | return '<%s instance at %s: %s - r:%i w:%i>' % ( 138 | self.__class__.__name__, 139 | hex(id(self)), 140 | self.device, 141 | self.num_read_bytes, 142 | self.num_write_bytes 143 | ) 144 | 145 | def __del__(self): 146 | """ 147 | Close device when YHSM instance is destroyed. 148 | """ 149 | if self.debug: 150 | sys.stderr.write("%s: CLOSE %s\n" %( 151 | self.__class__.__name__, 152 | self.ser 153 | )) 154 | if self.ser: 155 | self.ser.close() 156 | -------------------------------------------------------------------------------- /pyhsm/stick_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | module for talking to the YubiHSM over a socket. 3 | """ 4 | 5 | # Copyright (c) 2011 Yubico AB 6 | # See the file COPYING for licence statement. 7 | 8 | __all__ = [ 9 | # constants 10 | # functions 11 | # classes 12 | 'YHSM_Stick_Client', 13 | ] 14 | 15 | import sys 16 | import re 17 | import socket 18 | import json 19 | 20 | import pyhsm.util 21 | import pyhsm.exception 22 | 23 | CMD_WRITE = 0 24 | CMD_READ = 1 25 | CMD_FLUSH = 2 26 | CMD_DRAIN = 3 27 | CMD_LOCK = 4 28 | CMD_UNLOCK = 5 29 | 30 | 31 | DEVICE_PATTERN = re.compile(r'yhsm://(?P[^:]+)(:(?P\d+))?/?') 32 | DEFAULT_PORT = 5348 33 | 34 | 35 | def pack_data(data): 36 | if isinstance(data, basestring): 37 | return data.encode('base64') 38 | return data 39 | 40 | 41 | def unpack_data(data): 42 | if isinstance(data, basestring): 43 | return data.decode('base64') 44 | elif isinstance(data, dict) and 'error' in data: 45 | return pyhsm.exception.YHSM_Error(data['error']) 46 | return data 47 | 48 | 49 | def read_sock(sf): 50 | line = sf.readline() 51 | return unpack_data(json.loads(line)) 52 | 53 | 54 | def write_sock(sf, cmd, *args): 55 | json.dump([cmd] + map(pack_data, args), sf) 56 | sf.write("\n") 57 | sf.flush() 58 | 59 | 60 | class YHSM_Stick_Client(): 61 | """ 62 | The current YHSM is a USB device using serial communication. 63 | 64 | This class exposes the basic functions read, write and flush (input). 65 | """ 66 | def __init__(self, device, timeout=1, debug=False): 67 | """ 68 | Open YHSM device. 69 | """ 70 | self.debug = debug 71 | self.device = device 72 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 73 | self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 74 | match = DEVICE_PATTERN.match(device) 75 | host = match.group('host') 76 | port = match.group('port') or DEFAULT_PORT 77 | self.socket.connect((host, int(port))) 78 | self.socket_file = self.socket.makefile('wb') 79 | 80 | self.num_read_bytes = 0 81 | self.num_write_bytes = 0 82 | if self.debug: 83 | sys.stderr.write("%s: OPEN %s\n" % ( 84 | self.__class__.__name__, 85 | self.socket 86 | )) 87 | return None 88 | 89 | def acquire(self): 90 | write_sock(self.socket_file, CMD_LOCK) 91 | return self.release 92 | 93 | def release(self): 94 | write_sock(self.socket_file, CMD_UNLOCK) 95 | 96 | def write(self, data, debug_info=None): 97 | """ 98 | Write data to YHSM device. 99 | """ 100 | self.num_write_bytes += len(data) 101 | if self.debug: 102 | if not debug_info: 103 | debug_info = str(len(data)) 104 | sys.stderr.write("%s: WRITE %s:\n%s\n" % ( 105 | self.__class__.__name__, 106 | debug_info, 107 | pyhsm.util.hexdump(data) 108 | )) 109 | write_sock(self.socket_file, CMD_WRITE, data) 110 | return read_sock(self.socket_file) 111 | 112 | def read(self, num_bytes, debug_info=None): 113 | """ 114 | Read a number of bytes from YubiHSM device. 115 | """ 116 | if self.debug: 117 | if not debug_info: 118 | debug_info = str(num_bytes) 119 | sys.stderr.write("%s: READING %s\n" % ( 120 | self.__class__.__name__, 121 | debug_info 122 | )) 123 | write_sock(self.socket_file, CMD_READ, num_bytes) 124 | res = read_sock(self.socket_file) 125 | if isinstance(res, Exception): 126 | raise res 127 | 128 | if self.debug: 129 | sys.stderr.write("%s: READ %i:\n%s\n" % ( 130 | self.__class__.__name__, 131 | len(res), 132 | pyhsm.util.hexdump(res) 133 | )) 134 | self.num_read_bytes += len(res) 135 | return res 136 | 137 | def flush(self): 138 | """ 139 | Flush input buffers. 140 | """ 141 | write_sock(self.socket_file, CMD_FLUSH) 142 | return read_sock(self.socket_file) 143 | 144 | def drain(self): 145 | """ Drain input. """ 146 | write_sock(self.socket_file, CMD_DRAIN) 147 | return read_sock(self.socket_file) 148 | 149 | def raw_device(self): 150 | """ Get the socket address. Only intended for test code/debugging! """ 151 | return self.socket 152 | 153 | def set_debug(self, new): 154 | """ 155 | Set debug mode (boolean). 156 | 157 | Returns old setting. 158 | """ 159 | if type(new) is not bool: 160 | raise pyhsm.exception.YHSM_WrongInputType( 161 | 'new', bool, type(new)) 162 | old = self.debug 163 | self.debug = new 164 | return old 165 | 166 | def __repr__(self): 167 | return '<%s instance at %s: %s - r:%i w:%i>' % ( 168 | self.__class__.__name__, 169 | hex(id(self)), 170 | self.device, 171 | self.num_read_bytes, 172 | self.num_write_bytes 173 | ) 174 | 175 | def __del__(self): 176 | """ 177 | Close device when YHSM instance is destroyed. 178 | """ 179 | if self.debug: 180 | sys.stderr.write("%s: CLOSE %s\n" % ( 181 | self.__class__.__name__, 182 | self.device 183 | )) 184 | try: 185 | self.socket_file.close() 186 | self.socket.close() 187 | except Exception: 188 | pass 189 | -------------------------------------------------------------------------------- /pyhsm/stick_daemon.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2013 Yubico AB. All rights reserved. 3 | # 4 | """ 5 | This is a daemon to allow multiple users of a YubiHSM without requiring 6 | permission to use the device. The daemon listens on a TCP port on localhost 7 | and allows multiple connections to share a YubiHSM. Access the YubiHSM via 8 | the daemon by specifying a device string following the yhsm://: 9 | syntax: 10 | 11 | hsm = YHSM('yhsm://localhost:5348') 12 | 13 | Note that the daemon and clients need to share the same version number to be 14 | compatible. 15 | """ 16 | 17 | import sys 18 | import socket 19 | import json 20 | import threading 21 | import argparse 22 | import pyhsm.stick 23 | import daemon 24 | import os 25 | 26 | 27 | CMD_WRITE = 0 28 | CMD_READ = 1 29 | CMD_FLUSH = 2 30 | CMD_DRAIN = 3 31 | CMD_LOCK = 4 32 | CMD_UNLOCK = 5 33 | 34 | COMMANDS = { 35 | CMD_WRITE: 'write', 36 | CMD_READ: 'read', 37 | CMD_FLUSH: 'flush', 38 | CMD_DRAIN: 'drain' 39 | } 40 | 41 | context = daemon.DaemonContext() 42 | 43 | def pack_data(data): 44 | if isinstance(data, basestring): 45 | return data.encode('base64') 46 | return data 47 | 48 | 49 | def unpack_data(data): 50 | if isinstance(data, basestring): 51 | return data.decode('base64') 52 | return data 53 | 54 | def write_pid_file(fn): 55 | """ Create a file with our PID. """ 56 | if not fn: 57 | return None 58 | if fn == '' or fn == "''": 59 | # work around argument passings in init-scripts 60 | return None 61 | f = open(fn, "w") 62 | f.write("%s\n" % (os.getpid())) 63 | f.close() 64 | 65 | class YHSM_Stick_Server(): 66 | def __init__(self, device, addr): 67 | self.device = device 68 | self._stick = None 69 | self.pidfile = None 70 | 71 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 72 | self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 73 | self.socket.bind(addr) 74 | self.lock = threading.Lock() 75 | 76 | def serve(self): 77 | write_pid_file(self.pidfile) 78 | 79 | self.socket.listen(20) 80 | 81 | try: 82 | while True: 83 | cs, address = self.socket.accept() 84 | thread = threading.Thread(target=self.client_handler, 85 | args=(cs,)) 86 | thread.start() 87 | except Exception as e: 88 | print e 89 | sys.exit(1) 90 | 91 | def invoke(self, cmd, *args): 92 | try: 93 | if not self._stick: 94 | self._stick = pyhsm.stick.YHSM_Stick(self.device) 95 | res = getattr(self._stick, COMMANDS[cmd])(*args) 96 | except Exception as e: 97 | res = e 98 | print e 99 | self._stick = None 100 | return res 101 | 102 | def client_handler(self, socket): 103 | socket_file = socket.makefile('wb') 104 | has_lock = False 105 | 106 | try: 107 | while True: 108 | data = json.loads(socket_file.readline()) 109 | cmd = data[0] 110 | args = map(unpack_data, data[1:]) 111 | if cmd == CMD_LOCK: 112 | if not has_lock: 113 | self.lock.acquire() 114 | has_lock = True 115 | elif has_lock: 116 | if cmd == CMD_UNLOCK: 117 | self.lock.release() 118 | has_lock = False 119 | else: 120 | res = self.invoke(cmd, *args) 121 | json.dump(pack_data(res), socket_file) 122 | socket_file.write("\n") 123 | socket_file.flush() 124 | else: 125 | err = 'Command run without holding lock!' 126 | print err 127 | json.dump({'error': err}, socket_file) 128 | socket_file.write("\n") 129 | socket_file.flush() 130 | break 131 | except Exception: 132 | # Client disconnected, ignore. 133 | pass 134 | finally: 135 | if has_lock: 136 | self.lock.release() 137 | socket_file.close() 138 | socket.close() 139 | 140 | 141 | def main(): 142 | parser = argparse.ArgumentParser( 143 | description='YubiHSM server daemon', 144 | add_help=True, 145 | formatter_class=argparse.ArgumentDefaultsHelpFormatter 146 | ) 147 | parser.add_argument('-D', '--device', nargs='?', default='/dev/ttyACM0', 148 | help='device name') 149 | parser.add_argument('-d', '--daemon', default=False, 150 | action='store_true', help='run as daemon') 151 | parser.add_argument('-I', '--interface', nargs='?', default='localhost', 152 | help='network interface to bind to') 153 | parser.add_argument('-P', '--port', nargs='?', type=int, default=5348, 154 | help='TCP port to bind to') 155 | parser.add_argument('--pid-file', 156 | dest='pid_file', 157 | default=None, 158 | required=False, 159 | help='PID file', 160 | metavar='FILENAME') 161 | 162 | args = parser.parse_args() 163 | 164 | print 'Starting YubiHSM daemon for device: %s, listening on: %s:%d' % \ 165 | (args.device, args.interface, args.port) 166 | 167 | server = YHSM_Stick_Server(args.device, (args.interface, args.port)) 168 | print 'You can connect to the server using the following device string:' 169 | print 'yhsm://127.0.0.1:%d' % args.port 170 | 171 | server.pidfile = args.pid_file 172 | context.files_preserve = [server.socket] 173 | if args.daemon: 174 | with context: 175 | server.serve() 176 | else: 177 | server.serve() 178 | -------------------------------------------------------------------------------- /pyhsm/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yubico/python-pyhsm/9d5a802e1d27338caa8f87179ad23ac18a4a7e24/pyhsm/tools/__init__.py -------------------------------------------------------------------------------- /pyhsm/tools/keystore_unlock.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2011 Yubico AB 3 | # See the file COPYING for licence statement. 4 | # 5 | """ 6 | Utility to unlock the key store of a YubiHSM, 7 | using the 'HSM password'/'master key'. 8 | """ 9 | 10 | import sys 11 | import pyhsm 12 | import argparse 13 | import getpass 14 | 15 | default_device = "/dev/ttyACM0" 16 | 17 | def parse_args(): 18 | """ 19 | Parse the command line arguments 20 | """ 21 | parser = argparse.ArgumentParser(description = "Unlock key store of YubiHSM", 22 | add_help = True, 23 | formatter_class = argparse.ArgumentDefaultsHelpFormatter, 24 | ) 25 | parser.add_argument('-D', '--device', 26 | dest='device', 27 | default=default_device, 28 | required=False, 29 | help='YubiHSM device', 30 | ) 31 | parser.add_argument('-v', '--verbose', 32 | dest='verbose', 33 | action='store_true', default=False, 34 | help='Enable verbose operation' 35 | ) 36 | parser.add_argument('--debug', 37 | dest='debug', 38 | action='store_true', default=False, 39 | help='Enable debug operation' 40 | ) 41 | parser.add_argument('--no-otp', 42 | dest='no_otp', 43 | action='store_true', default=False, 44 | help='Don\'t ask for OTP, even if YubiHSM supports it', 45 | ) 46 | parser.add_argument('--stdin', 47 | dest='stdin', 48 | action='store_true', default=False, 49 | help='Read data from stdin instead of prompting', 50 | ) 51 | 52 | args = parser.parse_args() 53 | 54 | return args 55 | 56 | def get_password(hsm, args): 57 | """ Get password of correct length for this YubiHSM version. """ 58 | expected_len = 32 59 | name = 'HSM password' 60 | if hsm.version.have_key_store_decrypt(): 61 | expected_len = 64 62 | name = 'master key' 63 | 64 | if args.stdin: 65 | password = sys.stdin.readline() 66 | while password and password[-1] == '\n': 67 | password = password[:-1] 68 | else: 69 | if args.debug: 70 | password = raw_input('Enter %s (press enter to skip) (will be echoed) : ' % (name)) 71 | else: 72 | password = getpass.getpass('Enter %s (press enter to skip) : ' % (name)) 73 | 74 | if len(password) <= expected_len: 75 | password = password.decode('hex') 76 | if not password: 77 | return None 78 | return password 79 | else: 80 | sys.stderr.write("ERROR: Invalid HSM password (expected max %i chars, got %i)\n" % \ 81 | (expected_len, len(password))) 82 | return 1 83 | 84 | def get_otp(hsm, args): 85 | """ Get OTP from YubiKey. """ 86 | if args.no_otp: 87 | return None 88 | if hsm.version.have_unlock(): 89 | if args.stdin: 90 | otp = sys.stdin.readline() 91 | while otp and otp[-1] == '\n': 92 | otp = otp[:-1] 93 | else: 94 | otp = raw_input('Enter admin YubiKey OTP (press enter to skip) : ') 95 | if len(otp) == 44: 96 | # YubiHSM admin OTP's always have a public_id length of 6 bytes 97 | return otp 98 | if otp: 99 | sys.stderr.write("ERROR: Invalid YubiKey OTP\n") 100 | return None 101 | 102 | def main(): 103 | """ 104 | What will be executed when running as a stand alone program. 105 | """ 106 | args = parse_args() 107 | 108 | try: 109 | hsm = pyhsm.base.YHSM(device=args.device, debug=args.debug) 110 | 111 | if args.debug or args.verbose: 112 | print "Device : %s" % (args.device) 113 | print "Version : %s" % (hsm.info()) 114 | print "" 115 | 116 | password = get_password(hsm, args) 117 | otp = get_otp(hsm, args) 118 | if not password and not otp: 119 | print "\nAborted\n" 120 | return 1 121 | else: 122 | if args.debug or args.verbose: 123 | print "" 124 | if hsm.unlock(password = password, otp = otp): 125 | if args.debug or args.verbose: 126 | print "OK\n" 127 | except pyhsm.exception.YHSM_Error, e: 128 | sys.stderr.write("ERROR: %s\n" % (e.reason)) 129 | if e.reason == "YubiHSM did not respond to command YSM_SYSTEM_INFO_QUERY": 130 | sys.stderr.write("Please check whether your YubiHSM is really at " + args.device + ", you can specify an alternate device using the option -D") 131 | return 1 132 | 133 | return 0 134 | -------------------------------------------------------------------------------- /pyhsm/tools/linux_add_entropy.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2011 Yubico AB 3 | # See the file COPYING for licence statement. 4 | # 5 | """ 6 | Get random data from TRNG on YubiHSM and insert it into host 7 | entropy pool. Probably only works on Linux since the ioctl() 8 | request value RNDADDENTROPY seems Linux specific. 9 | """ 10 | 11 | import os 12 | import sys 13 | import fcntl 14 | import struct 15 | import argparse 16 | import pyhsm 17 | 18 | default_device = "/dev/ttyACM0" 19 | default_iterations = 100 20 | default_entropy_ratio = 2 # number of bits of entropy per byte of random data 21 | 22 | RNDADDENTROPY = 1074287107 # from /usr/include/linux/random.h 23 | 24 | def parse_args(): 25 | """ 26 | Parse the command line arguments 27 | """ 28 | parser = argparse.ArgumentParser(description = "Add random data from YubiHSM to Linux entropy", 29 | add_help = True, 30 | formatter_class = argparse.ArgumentDefaultsHelpFormatter, 31 | ) 32 | parser.add_argument('-D', '--device', 33 | dest='device', 34 | default=default_device, 35 | required=False, 36 | help='YubiHSM device', 37 | ) 38 | parser.add_argument('-v', '--verbose', 39 | dest='verbose', 40 | action='store_true', default=False, 41 | help='Enable verbose operation' 42 | ) 43 | parser.add_argument('--debug', 44 | dest='debug', 45 | action='store_true', default=False, 46 | help='Enable debug operation' 47 | ) 48 | parser.add_argument('-r', '--ratio', 49 | dest='ratio', 50 | type=int, default=default_entropy_ratio, 51 | help='Bits per byte of data read to use as entropy', 52 | ) 53 | parser.add_argument('-c', '--count', 54 | dest='iterations', 55 | type=int, default=default_iterations, 56 | help='Number of iterations to run', 57 | ) 58 | 59 | args = parser.parse_args() 60 | 61 | return args 62 | 63 | 64 | def get_entropy(hsm, iterations, entropy_ratio): 65 | """ 66 | Read entropy from YubiHSM and feed it to Linux as entropy using ioctl() syscall. 67 | """ 68 | fd = os.open("/dev/random", os.O_WRONLY) 69 | # struct rand_pool_info { 70 | # int entropy_count; 71 | # int buf_size; 72 | # __u32 buf[0]; 73 | # }; 74 | fmt = 'ii%is' % (pyhsm.defines.YSM_MAX_PKT_SIZE - 1) 75 | for _ in xrange(iterations): 76 | rnd = hsm.random(pyhsm.defines.YSM_MAX_PKT_SIZE - 1) 77 | this = struct.pack(fmt, entropy_ratio * len(rnd), len(rnd), rnd) 78 | fcntl.ioctl(fd, RNDADDENTROPY, this) 79 | os.close(fd) 80 | 81 | def main(): 82 | """ 83 | What will be executed when running as a stand alone program. 84 | """ 85 | args = parse_args() 86 | 87 | try: 88 | s = pyhsm.base.YHSM(device=args.device, debug=args.debug) 89 | get_entropy(s, args.iterations, args.ratio) 90 | return 0 91 | except pyhsm.exception.YHSM_Error as e: 92 | sys.stderr.write("ERROR: %s" % (e.reason)) 93 | return 1 94 | -------------------------------------------------------------------------------- /pyhsm/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | collection of utility functions 3 | """ 4 | 5 | # Copyright (c) 2011 Yubico AB 6 | # See the file COPYING for licence statement. 7 | 8 | import struct 9 | 10 | __all__ = [ 11 | # constants 12 | # functions 13 | 'hexdump', 14 | 'group', 15 | 'key_handle_to_int', 16 | # classes 17 | ] 18 | 19 | import pyhsm.exception 20 | 21 | def hexdump(src, length=8): 22 | """ Produce a string hexdump of src, for debug output.""" 23 | if not src: 24 | return str(src) 25 | src = input_validate_str(src, 'src') 26 | offset = 0 27 | result = '' 28 | for this in group(src, length): 29 | hex_s = ' '.join(["%02x" % ord(x) for x in this]) 30 | result += "%04X %s\n" % (offset, hex_s) 31 | offset += length 32 | return result 33 | 34 | def group(data, num): 35 | """ Split data into chunks of num chars each """ 36 | return [data[i:i+num] for i in xrange(0, len(data), num)] 37 | 38 | def key_handle_to_int(this): 39 | """ 40 | Turn "123" into 123 and "KSM1" into 827151179 41 | (0x314d534b, 'K' = 0x4b, S = '0x53', M = 0x4d). 42 | 43 | YHSM is little endian, so this makes the bytes KSM1 appear 44 | in the most human readable form in packet traces. 45 | """ 46 | try: 47 | num = int(this) 48 | return num 49 | except ValueError: 50 | if this[:2] == "0x": 51 | return int(this, 16) 52 | if (len(this) == 4): 53 | num = struct.unpack(' max_len: 62 | raise pyhsm.exception.YHSM_InputTooLong(name, max_len, len(string)) 63 | if exact_len != None and len(string) != exact_len: 64 | raise pyhsm.exception.YHSM_WrongInputSize(name, exact_len, len(string)) 65 | return string 66 | 67 | def input_validate_int(value, name, max_value=None): 68 | """ Input validation for integers. """ 69 | if type(value) is not int: 70 | raise pyhsm.exception.YHSM_WrongInputType(name, int, type(value)) 71 | if max_value != None and value > max_value: 72 | raise pyhsm.exception.YHSM_WrongInputSize(name, max_value, value) 73 | return value 74 | 75 | def input_validate_nonce(nonce, name='nonce', pad = False): 76 | """ Input validation for nonces. """ 77 | if type(nonce) is not str: 78 | raise pyhsm.exception.YHSM_WrongInputType( \ 79 | name, str, type(nonce)) 80 | if len(nonce) > pyhsm.defines.YSM_AEAD_NONCE_SIZE: 81 | raise pyhsm.exception.YHSM_InputTooLong( 82 | name, pyhsm.defines.YSM_AEAD_NONCE_SIZE, len(nonce)) 83 | if pad: 84 | return nonce.ljust(pyhsm.defines.YSM_AEAD_NONCE_SIZE, chr(0x0)) 85 | else: 86 | return nonce 87 | 88 | def input_validate_key_handle(key_handle, name='key_handle'): 89 | """ Input validation for key_handles. """ 90 | if type(key_handle) is not int: 91 | try: 92 | return key_handle_to_int(key_handle) 93 | except pyhsm.exception.YHSM_Error: 94 | raise pyhsm.exception.YHSM_WrongInputType(name, int, type(key_handle)) 95 | return key_handle 96 | 97 | def input_validate_yubikey_secret(data, name='data'): 98 | """ Input validation for YHSM_YubiKeySecret or string. """ 99 | if isinstance(data, pyhsm.aead_cmd.YHSM_YubiKeySecret): 100 | data = data.pack() 101 | return input_validate_str(data, name) 102 | 103 | def input_validate_aead(aead, name='aead', expected_len=None, max_aead_len = pyhsm.defines.YSM_AEAD_MAX_SIZE): 104 | """ Input validation for YHSM_GeneratedAEAD or string. """ 105 | if isinstance(aead, pyhsm.aead_cmd.YHSM_GeneratedAEAD): 106 | aead = aead.data 107 | if expected_len != None: 108 | return input_validate_str(aead, name, exact_len = expected_len) 109 | else: 110 | return input_validate_str(aead, name, max_len=max_aead_len) 111 | 112 | 113 | 114 | def validate_cmd_response_int(name, got, expected): 115 | """ 116 | Check that some value returned in the response to a command matches what 117 | we put in the request (the command). 118 | """ 119 | if got != expected: 120 | raise(pyhsm.exception.YHSM_Error("Bad %s in response (got %i, expected %i)" \ 121 | % (name, got, expected))) 122 | return got 123 | 124 | 125 | def validate_cmd_response_hex(name, got, expected): 126 | """ 127 | Check that some value returned in the response to a command matches what 128 | we put in the request (the command). 129 | """ 130 | if got != expected: 131 | raise(pyhsm.exception.YHSM_Error("Bad %s in response (got 0x%x, expected 0x%x)" \ 132 | % (name, got, expected))) 133 | return got 134 | 135 | 136 | def validate_cmd_response_str(name, got, expected, hex_encode=True): 137 | """ 138 | Check that some value returned in the response to a command matches what 139 | we put in the request (the command). 140 | """ 141 | if got != expected: 142 | if hex_encode: 143 | got_s = got.encode('hex') 144 | exp_s = expected.encode('hex') 145 | else: 146 | got_s = got 147 | exp_s = expected 148 | raise(pyhsm.exception.YHSM_Error("Bad %s in response (got %s, expected %s)" \ 149 | % (name, got_s, exp_s))) 150 | return got 151 | 152 | def validate_cmd_response_nonce(got, used): 153 | """ 154 | Check that the returned nonce matches nonce used in request. 155 | 156 | A request nonce of 000000000000 means the HSM should generate a nonce internally though, 157 | so if 'used' is all zeros we actually check that 'got' does NOT match 'used'. 158 | """ 159 | if used == '000000000000'.decode('hex'): 160 | if got == used: 161 | raise(pyhsm.exception.YHSM_Error("Bad nonce in response (got %s, expected HSM generated nonce)" \ 162 | % (got.encode('hex')))) 163 | return got 164 | return validate_cmd_response_str('nonce', got, used) 165 | -------------------------------------------------------------------------------- /pyhsm/val/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yubico/python-pyhsm/9d5a802e1d27338caa8f87179ad23ac18a4a7e24/pyhsm/val/__init__.py -------------------------------------------------------------------------------- /pyhsm/val/validate_otp.py: -------------------------------------------------------------------------------- 1 | # 2 | # Tool to validate a YubiKey OTP using the YubiHSM internal database. 3 | # 4 | # This requires that you have imported the secret AES key of the YubiKey 5 | # into the database with `../yubikey-ksm/yhsm-import-keys --internal-db' 6 | # or otherwise. 7 | # 8 | # Copyright (c) 2011 Yubico AB 9 | # See the file COPYING for licence statement. 10 | # 11 | 12 | import os 13 | import re 14 | import sys 15 | import struct 16 | import argparse 17 | import pyhsm 18 | import pyhsm.yubikey 19 | 20 | default_device = "/dev/ttyACM0" 21 | 22 | def parse_args(): 23 | """ 24 | Parse the command line arguments 25 | """ 26 | global default_device 27 | 28 | parser = argparse.ArgumentParser(description = "Validate YubiKey OTP's using YubiHSM", 29 | add_help=True 30 | ) 31 | parser.add_argument('-D', '--device', 32 | dest='device', 33 | default=default_device, 34 | required=False, 35 | help='YubiHSM device (default : %s).' % default_device 36 | ) 37 | parser.add_argument('-v', '--verbose', 38 | dest='verbose', 39 | action='store_true', default=False, 40 | help='Enable verbose operation.' 41 | ) 42 | parser.add_argument('--debug', 43 | dest='debug', 44 | action='store_true', default=False, 45 | help='Enable debug operation.' 46 | ) 47 | group = parser.add_argument_group('Modes', 'What you want to validate') 48 | mode_group = group.add_mutually_exclusive_group(required = True) 49 | mode_group.add_argument('--otp', 50 | dest='otp', 51 | help='The output from your YubiKey.' 52 | ) 53 | mode_group.add_argument('--oath', 54 | dest='oath', 55 | help='The output from your OATH-HOTP token.' 56 | ) 57 | 58 | args = parser.parse_args() 59 | return args 60 | 61 | def validate_otp(hsm, args): 62 | """ 63 | Validate an OTP. 64 | """ 65 | try: 66 | res = pyhsm.yubikey.validate_otp(hsm, args.otp) 67 | if args.verbose: 68 | print "OK counter=%04x low=%04x high=%02x use=%02x" % \ 69 | (res.use_ctr, res.ts_low, res.ts_high, res.session_ctr) 70 | return 0 71 | except pyhsm.exception.YHSM_CommandFailed, e: 72 | if args.verbose: 73 | print "%s" % (pyhsm.defines.status2str(e.status)) 74 | # figure out numerical response code 75 | for r in [pyhsm.defines.YSM_OTP_INVALID, \ 76 | pyhsm.defines.YSM_OTP_REPLAY, \ 77 | pyhsm.defines.YSM_ID_NOT_FOUND]: 78 | if e.status == r: 79 | return r - pyhsm.defines.YSM_RESPONSE 80 | # not found 81 | return 0xff 82 | 83 | def validate_oath(hsm, args): 84 | """ 85 | Validate an OATH OTP. 86 | """ 87 | print "ERROR: Not implemented, try 'yhsm-validation-server'." 88 | return 0 89 | 90 | 91 | def main(): 92 | args = parse_args() 93 | 94 | if args.debug: 95 | print "YHSM device : %s" % (args.device) 96 | print "" 97 | 98 | hsm = pyhsm.YHSM(device = args.device, debug=args.debug) 99 | 100 | status = 1 101 | if args.otp: 102 | status = validate_otp(hsm, args) 103 | elif args.oath: 104 | status = validate_oath(hsm, args) 105 | 106 | return status 107 | 108 | 109 | if __name__ == '__main__': 110 | sys.exit(main()) 111 | -------------------------------------------------------------------------------- /pyhsm/validate_cmd.py: -------------------------------------------------------------------------------- 1 | """ 2 | implementations of validation commands for YubiHSM 3 | """ 4 | 5 | # Copyright (c) 2011 Yubico AB 6 | # See the file COPYING for licence statement. 7 | 8 | import struct 9 | 10 | __all__ = [ 11 | # constants 12 | # functions 13 | # classes 14 | 'YHSM_Cmd_AEAD_Validate_OTP', 15 | 'YHSM_ValidationResult', 16 | ] 17 | 18 | import pyhsm.defines 19 | import pyhsm.exception 20 | from pyhsm.aead_cmd import YHSM_AEAD_Cmd 21 | 22 | class YHSM_Cmd_AEAD_Validate_OTP(YHSM_AEAD_Cmd): 23 | """ 24 | Request the YubiHSM to validate an OTP using an externally stored AEAD. 25 | """ 26 | 27 | response = None 28 | status = None 29 | 30 | def __init__(self, stick, public_id, otp, key_handle, aead): 31 | self.public_id = pyhsm.util.input_validate_nonce(public_id, pad = True) 32 | self.otp = pyhsm.util.input_validate_str(otp, 'otp', exact_len = pyhsm.defines.YSM_OTP_SIZE) 33 | self.key_handle = pyhsm.util.input_validate_key_handle(key_handle) 34 | aead = pyhsm.util.input_validate_aead(aead, expected_len = pyhsm.defines.YSM_YUBIKEY_AEAD_SIZE) 35 | # typedef struct { 36 | # uint8_t publicId[YSM_PUBLIC_ID_SIZE]; // Public id (nonce) 37 | # uint32_t keyHandle; // Key handle 38 | # uint8_t otp[YSM_OTP_SIZE]; // OTP 39 | # uint8_t aead[YSM_YUBIKEY_AEAD_SIZE]; // AEAD block 40 | # } YSM_AEAD_YUBIKEY_OTP_DECODE_REQ; 41 | fmt = "< %is I %is %is" % (pyhsm.defines.YSM_AEAD_NONCE_SIZE, \ 42 | pyhsm.defines.YSM_OTP_SIZE, \ 43 | pyhsm.defines.YSM_YUBIKEY_AEAD_SIZE) 44 | packed = struct.pack(fmt, self.public_id, \ 45 | self.key_handle, \ 46 | self.otp, \ 47 | aead) 48 | YHSM_AEAD_Cmd.__init__(self, stick, pyhsm.defines.YSM_AEAD_YUBIKEY_OTP_DECODE, packed) 49 | 50 | def parse_result(self, data): 51 | # typedef struct { 52 | # uint8_t public_id[YSM_PUBLIC_ID_SIZE]; // Public id 53 | # uint32_t keyHandle; // Key handle 54 | # uint16_t use_ctr; // Use counter 55 | # uint8_t session_ctr; // Session counter 56 | # uint8_t tstph; // Timestamp (high part) 57 | # uint16_t tstpl; // Timestamp (low part) 58 | # YHSM_STATUS status; // Validation status 59 | # } YHSM_AEAD_OTP_DECODED_RESP; 60 | fmt = "< %is I H B B H B" % (pyhsm.defines.YSM_PUBLIC_ID_SIZE) 61 | public_id, \ 62 | key_handle, \ 63 | use_ctr, \ 64 | session_ctr, \ 65 | ts_high, \ 66 | ts_low, \ 67 | self.status = struct.unpack(fmt, data) 68 | 69 | pyhsm.util.validate_cmd_response_str('public_id', public_id, self.public_id) 70 | pyhsm.util.validate_cmd_response_hex('key_handle', key_handle, self.key_handle) 71 | 72 | if self.status == pyhsm.defines.YSM_STATUS_OK: 73 | self.response = YHSM_ValidationResult(self.public_id, use_ctr, session_ctr, ts_high, ts_low) 74 | return self.response 75 | else: 76 | raise pyhsm.exception.YHSM_CommandFailed(pyhsm.defines.cmd2str(self.command), self.status) 77 | 78 | 79 | class YHSM_ValidationResult(): 80 | """ 81 | The result of a Validate operation. 82 | 83 | Contains the counters and timestamps decrypted from the OTP. 84 | 85 | @ivar public_id: The six bytes public ID of the YubiKey that produced the OTP 86 | @ivar use_ctr: The 16-bit power-on non-volatile counter of the YubiKey 87 | @ivar session_ctr: The 8-bit volatile session counter of the YubiKey 88 | @ivar ts_high: The high 8 bits of the 24-bit 8 hz timer since power-on of the YubiKey 89 | @ivar ts_low: The low 16 bits of the 24-bit 8 hz timer since power-on of the YubiKey 90 | @type public_id: string 91 | @type use_ctr: integer 92 | @type session_ctr: integer 93 | @type ts_high: integer 94 | @type ts_low: integer 95 | """ 96 | 97 | public_id = use_ctr = session_ctr = ts_high = ts_low = None 98 | 99 | def __init__(self, public_id, use_ctr, session_ctr, ts_high, ts_low): 100 | self.public_id = public_id 101 | self.use_ctr = use_ctr 102 | self.session_ctr = session_ctr 103 | self.ts_high = ts_high 104 | self.ts_low = ts_low 105 | 106 | def __repr__(self): 107 | return '<%s instance at %s: public_id=%s, use_ctr=%i, session_ctr=%i, ts=%i/%i>' % ( 108 | self.__class__.__name__, 109 | hex(id(self)), 110 | self.public_id.encode('hex'), 111 | self.use_ctr, 112 | self.session_ctr, 113 | self.ts_high, 114 | self.ts_low 115 | ) 116 | -------------------------------------------------------------------------------- /pyhsm/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | module for keeping track of different capabilities in different versions 3 | """ 4 | 5 | # Copyright (c) 2011 Yubico AB 6 | # See the file COPYING for licence statement. 7 | 8 | __all__ = [ 9 | # constants 10 | # functions 11 | # classes 12 | 'YHSM_Version' 13 | ] 14 | 15 | class YHSM_Version(): 16 | """ Keeps the YubiHSM's version number and can tell what capabilities it has. 17 | 18 | @ivar sysinfo: Sysinfo when YubiHSM was initialized. 19 | @type sysinfo: L{YHSM_Cmd_System_Info} 20 | """ 21 | 22 | def __init__(self, sysinfo): 23 | """ 24 | @param sysinfo: YubiHSM sysinfo. 25 | @type sysinfo: L{YHSM_Cmd_System_Info} 26 | """ 27 | self.sysinfo = sysinfo 28 | self.ver = (sysinfo.version_major, sysinfo.version_minor, sysinfo.version_build,) 29 | 30 | def have_key_storage_unlock(self): 31 | """ 32 | YSM_KEY_STORAGE_UNLOCK was removed in 1.0. 33 | 34 | The basic concept of a passphrase to unlock the YubiHSM is now provided 35 | with the more secure YSM_KEY_STORE_DECRYPT. 36 | """ 37 | return self.ver < (1, 0,) 38 | 39 | def have_key_store_decrypt(self): 40 | """ YSM_KEY_STORE_DECRYPT was introduced in 1.0, replacing YSM_KEY_STORAGE_UNLOCK. """ 41 | return self.ver >= (1, 0, 0) 42 | 43 | def have_unlock(self): 44 | """ 45 | YSM_HSM_UNLOCK, featuring YubiKey OTP unlocking of operations, 46 | was introduced in 1.0. 47 | """ 48 | return self.ver >= (1, 0, 0) 49 | 50 | def have_keycommit(self): 51 | """ 52 | YubiHSM have the 'keycommit' command in configuration mode. 53 | 54 | 'keycommit' was introduced in 1.0. 55 | """ 56 | return self.ver >= (1, 0, 0) 57 | 58 | def have_keydisable(self): 59 | """ 60 | YubiHSM have the 'keydis'(able) command in configuration mode. 61 | 62 | 'keydis' was introduced in 1.0. 63 | """ 64 | return self.ver >= (1, 0, 1) 65 | 66 | def have_YSM_BUFFER_LOAD(self): 67 | """ 68 | This is a key handle permission flag that was introduced in 0.9.9. 69 | """ 70 | return self.ver >= (0, 9, 9,) 71 | 72 | def have_YSM_DB_YUBIKEY_AEAD_STORE2(self): 73 | """ 74 | The 2nd generation store command (with public id != nonce) was introduced in 1.0.4. 75 | """ 76 | return self.ver >= (1, 0, 4) 77 | -------------------------------------------------------------------------------- /pyhsm/yubikey.py: -------------------------------------------------------------------------------- 1 | """ 2 | helper functions to work with Yubikeys and YubiHSM 3 | """ 4 | 5 | # Copyright (c) 2011 Yubico AB 6 | # See the file COPYING for licence statement. 7 | 8 | import string 9 | 10 | __all__ = [ 11 | # constants 12 | # functions 13 | 'validate_otp', 14 | 'validate_yubikey_with_aead', 15 | 'modhex_encode', 16 | 'modhex_decode', 17 | 'split_id_otp', 18 | # classes 19 | ] 20 | 21 | import pyhsm.exception 22 | import pyhsm.aead_cmd 23 | 24 | def validate_otp(hsm, from_key): 25 | """ 26 | Try to validate an OTP from a YubiKey using the internal database 27 | on the YubiHSM. 28 | 29 | `from_key' is the modhex encoded string emitted when you press the 30 | button on your YubiKey. 31 | 32 | Will only return on succesfull validation. All failures will result 33 | in an L{pyhsm.exception.YHSM_CommandFailed}. 34 | 35 | @param hsm: The YHSM instance 36 | @param from_key: The OTP from a YubiKey (in modhex) 37 | @type hsm: L{pyhsm.YHSM} 38 | @type from_key: string 39 | 40 | @returns: validation response, if successful 41 | @rtype: L{YHSM_ValidationResult} 42 | 43 | @see: L{pyhsm.db_cmd.YHSM_Cmd_DB_Validate_OTP.parse_result} 44 | """ 45 | public_id, otp = split_id_otp(from_key) 46 | return hsm.db_validate_yubikey_otp(modhex_decode(public_id).decode('hex'), 47 | modhex_decode(otp).decode('hex') 48 | ) 49 | 50 | def validate_yubikey_with_aead(hsm, from_key, aead, key_handle): 51 | """ 52 | Try to validate an OTP from a YubiKey using the AEAD that can decrypt this YubiKey's 53 | internal secret, using the key_handle for the AEAD. 54 | 55 | `from_key' is the modhex encoded string emitted when you press the 56 | button on your YubiKey. 57 | 58 | Will only return on succesfull validation. All failures will result 59 | in an L{pyhsm.exception.YHSM_CommandFailed}. 60 | 61 | @param hsm: The YHSM instance 62 | @param from_key: The OTP from a YubiKey (in modhex) 63 | @param aead: AEAD containing the cryptographic key and permission flags 64 | @param key_handle: The key handle that can decrypt the AEAD 65 | @type hsm: L{pyhsm.YHSM} 66 | @type from_key: string 67 | @type aead: L{YHSM_GeneratedAEAD} or string 68 | @type key_handle: integer or string 69 | 70 | @returns: validation response 71 | @rtype: L{YHSM_ValidationResult} 72 | 73 | @see: L{pyhsm.validate_cmd.YHSM_Cmd_AEAD_Validate_OTP.parse_result} 74 | """ 75 | 76 | from_key = pyhsm.util.input_validate_str(from_key, 'from_key', max_len = 48) 77 | nonce = aead.nonce 78 | aead = pyhsm.util.input_validate_aead(aead) 79 | key_handle = pyhsm.util.input_validate_key_handle(key_handle) 80 | 81 | public_id, otp = split_id_otp(from_key) 82 | 83 | public_id = modhex_decode(public_id) 84 | otp = modhex_decode(otp) 85 | 86 | if not nonce: 87 | nonce = public_id.decode('hex') 88 | 89 | return hsm.validate_aead_otp(nonce, otp.decode('hex'), 90 | key_handle, aead) 91 | 92 | def modhex_decode(data): 93 | """ 94 | Convert a modhex string to ordinary hex. 95 | 96 | @param data: Modhex input 97 | @type data: string 98 | 99 | @returns: Hex 100 | @rtype: string 101 | """ 102 | t_map = string.maketrans("cbdefghijklnrtuv", "0123456789abcdef") 103 | return data.translate(t_map) 104 | 105 | def modhex_encode(data): 106 | """ 107 | Convert an ordinary hex string to modhex. 108 | 109 | @param data: Hex input 110 | @type data: string 111 | 112 | @returns: Modhex 113 | @rtype: string 114 | """ 115 | t_map = string.maketrans("0123456789abcdef", "cbdefghijklnrtuv") 116 | return data.translate(t_map) 117 | 118 | def split_id_otp(from_key): 119 | """ 120 | Separate public id from OTP given a YubiKey OTP as input. 121 | 122 | @param from_key: The OTP from a YubiKey (in modhex) 123 | @type from_key: string 124 | 125 | @returns: public_id and OTP 126 | @rtype: tuple of string 127 | """ 128 | if len(from_key) > 32: 129 | public_id, otp = from_key[:-32], from_key[-32:] 130 | elif len(from_key) == 32: 131 | public_id = '' 132 | otp = from_key 133 | else: 134 | raise pyhsm.exception.YHSM_Error("Bad from_key length %i < 32 : %s" \ 135 | % (len(from_key), from_key)) 136 | return public_id, otp 137 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | 28 | 29 | from setuptools import setup, find_packages 30 | import re 31 | 32 | VERSION_PATTERN = re.compile(r"(?m)^__version__\s*=\s*['\"](.+)['\"]$") 33 | 34 | 35 | def get_version(): 36 | """Return the current version as defined by yubico/yubico_version.py.""" 37 | 38 | with open('pyhsm/__init__.py', 'r') as f: 39 | match = VERSION_PATTERN.search(f.read()) 40 | return match.group(1) 41 | 42 | setup( 43 | name='pyhsm', 44 | version=get_version(), 45 | description='Python code for talking to a YubiHSM', 46 | author='Dain Nilsson', 47 | author_email='dain@yubico.com', 48 | url='https://github.com/Yubico/python-pyhsm', 49 | license='BSD 2 clause', 50 | packages=find_packages(exclude=['test']), 51 | entry_points={ 52 | 'console_scripts': [ 53 | # tools 54 | 'yhsm-daemon = pyhsm.stick_daemon:main [daemon]', 55 | 'yhsm-decrypt-aead = pyhsm.tools.decrypt_aead:main', 56 | 'yhsm-generate-keys = pyhsm.tools.generate_keys:main', 57 | 'yhsm-keystore-unlock = pyhsm.tools.keystore_unlock:main', 58 | 'yhsm-linux-add-entropy = pyhsm.tools.linux_add_entropy:main', 59 | # ksm 60 | 'yhsm-yubikey-ksm = pyhsm.ksm.yubikey_ksm:main [db,daemon]', 61 | 'yhsm-import-keys = pyhsm.ksm.import_keys:main', 62 | 'yhsm-db-export = pyhsm.ksm.db_export:main [db]', 63 | 'yhsm-db-import = pyhsm.ksm.db_import:main [db]', 64 | # validation server 65 | 'yhsm-validation-server = pyhsm.val.validation_server:main', 66 | 'yhsm-validate-otp = pyhsm.val.validate_otp:main', 67 | 'yhsm-init-oath-token = pyhsm.val.init_oath_token:main' 68 | ] 69 | }, 70 | test_suite='test.test_init', 71 | tests_require=[], 72 | install_requires=[ 73 | 'pyserial >= 2.3', 74 | 'pycrypto >= 2.1' 75 | ], 76 | extras_require={ 77 | 'db': ['sqlalchemy'], 78 | 'daemon': ['python-daemon'] 79 | }, 80 | classifiers=[ 81 | 'License :: OSI Approved :: BSD License', 82 | 'Operating System :: OS Independent', 83 | 'Programming Language :: Python', 84 | 'Development Status :: 4 - Beta', 85 | 'Environment :: Web Environment', 86 | 'Intended Audience :: Developers', 87 | 'Intended Audience :: System Administrators', 88 | 'Topic :: Internet :: WWW/HTTP', 89 | 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', 90 | ] 91 | ) 92 | -------------------------------------------------------------------------------- /test/README.adoc: -------------------------------------------------------------------------------- 1 | ../doc/Running_Hardware_Tests.adoc -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Yubico AB 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or 5 | # without modification, are permitted provided that the following 6 | # conditions are met: 7 | # 8 | # 1. Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # 2. Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following 12 | # disclaimer in the documentation and/or other materials provided 13 | # with the distribution. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | # POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /test/configure_hsm.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Yubico AB 2 | # See the file COPYING for licence statement. 3 | 4 | import re 5 | import sys 6 | import os 7 | import time 8 | import unittest 9 | import pyhsm 10 | import pyhsm.util 11 | 12 | import test_common 13 | 14 | from StringIO import StringIO 15 | from test_common import CfgPassphrase, AdminYubiKeys, HsmPassphrase, PrimaryAdminYubiKey 16 | 17 | 18 | class ConfigureYubiHSMforTest(test_common.YHSM_TestCase): 19 | 20 | def test_aaa_echo(self): 21 | """ Test echo before reconfiguration. """ 22 | self.assertTrue(self.hsm.echo('test')) 23 | 24 | def test_configure_YHSM(self): 25 | """ 26 | Reconfiguring YubiHSM for tests. 27 | """ 28 | self.ser = self.hsm.get_raw_device() 29 | 30 | # get the YubiHSM to exit to configuration mode. 31 | #self.assertTrue(self.hsm.monitor_exit()) 32 | self.hsm.monitor_exit() 33 | 34 | # get the first prompt without sending anything 35 | self.config_do("", add_cr = False) 36 | 37 | self.config_do("sysinfo") 38 | 39 | self.config_do("help") 40 | 41 | # clear memory and configure as HSM - has a few prompts we have to get past 42 | # 43 | if not self.hsm.version.have_key_store_decrypt(): 44 | self.config_do ("hsm ffffffff\r%s\r%s\ryes" % (CfgPassphrase, HsmPassphrase)) 45 | else: 46 | # HSM> < hsm ffffffff 47 | # Enabled flags ffffffff = ... 48 | # Enter cfg password (g to generate) 49 | # Enter admin Yubikey public id (enter when done) 50 | # Enter master key (g to generate) yes 51 | # Confirm current config being erased (type yes) 52 | AdminYubiKeysStr = '\r'.join(AdminYubiKeys) 53 | AdminYubiKeysStr += '\r' 54 | self.config_do ("hsm ffffffff\r%s\r%s\r%s\ryes" % (CfgPassphrase, AdminYubiKeysStr, HsmPassphrase)) 55 | 56 | self.hsm.drain() 57 | self.add_keys(xrange(31)) 58 | self.hsm.drain() 59 | 60 | self.config_do("keylist") 61 | 62 | if self.hsm.version.have_key_store_decrypt(): 63 | self.config_do("keycommit") 64 | 65 | # load a YubiKey (the first Admin YubiKey) into the internal database 66 | escape_char = chr(27) 67 | self.config_do("dbload\r00001,%s,%s,%s,\r" % (PrimaryAdminYubiKey) + escape_char, add_cr = False) 68 | 69 | self.config_do("dblist") 70 | 71 | # get back into HSM mode 72 | sys.stderr.write("exit") 73 | self.ser.write("exit\r") 74 | 75 | self.hsm.drain() 76 | 77 | self.hsm.reset() 78 | 79 | def test_zzz_unlock(self): 80 | """ Test unlock of keystore after reconfiguration. """ 81 | if self.hsm.version.have_unlock(): 82 | Params = PrimaryAdminYubiKey 83 | YK = test_common.FakeYubiKey(pyhsm.yubikey.modhex_decode(Params[0]).decode('hex'), 84 | Params[1].decode('hex'), Params[2].decode('hex') 85 | ) 86 | # After reconfigure, we know the counter values for PrimaryAdminYubiKey is zero 87 | # in the internal db. However, the test suite initialization will unlock the keystore 88 | # (in test_common.YHSM_TestCase.setUp) so a value of 0/1 should result in a replayed OTP. 89 | YK.use_ctr = 0 90 | YK.session_ctr = 1 91 | # first verify counters 1/0 gives the expected YSM_OTP_REPLAY 92 | try: 93 | self.hsm.unlock(otp = YK.from_key()) 94 | except pyhsm.exception.YHSM_CommandFailed, e: 95 | if e.status != pyhsm.defines.YSM_OTP_REPLAY: 96 | raise 97 | # now do real unlock with values 2/1 (there is an extra unlock done somewhere...) 98 | YK.use_ctr = 2 99 | self.assertTrue(self.hsm.unlock(password = HsmPassphrase.decode("hex"), otp = YK.from_key())) 100 | else: 101 | self.assertTrue(self.hsm.unlock(password = HsmPassphrase.decode("hex"))) 102 | 103 | def test_zzz_echo(self): 104 | """ Test echo after reconfiguration. """ 105 | self.assertTrue(self.hsm.echo('test')) 106 | 107 | def config_do(self, cmd, add_cr = True): 108 | # Don't have to output command - it is echoed 109 | #sys.__stderr__.write("> " + cmd + "\n") 110 | if add_cr: 111 | self.ser.write(cmd + "\r") 112 | else: 113 | self.ser.write(cmd) 114 | #time.sleep(0.5) 115 | recv = '' 116 | fail_count = 0 117 | sys.stderr.write("< ") 118 | while True: 119 | b = self.ser.read(1) 120 | if not b: 121 | fail_count += 1 122 | if fail_count == 5: 123 | raise Exception("Did not get the next prompt", recv) 124 | sys.stderr.write(b) 125 | 126 | recv += b 127 | lines = recv.split('\n') 128 | if re.match('^(NO_CFG|WSAPI|HSM).*> .*', lines[-1]): 129 | break 130 | return recv 131 | 132 | def add_keys(self, iterator): 133 | # Set up one key for every available flag 134 | for num in iterator: 135 | flags = 1 << num 136 | key = ("%02x" % (num + 1)) * 32 137 | self.add_key(flags, num + 1, key) 138 | 139 | # Set up some extra keys with the same key as the flag-keys, but other flags 140 | 141 | # flags YHSM_OTP_BLOB_VALIDATE (0x200) matching key 0x06 (with flags 0x20, YHSM_BLOB_GENERATE) 142 | flags = 0x200 143 | key = "06" * 32 144 | self.add_key(flags, 0x1000, key) 145 | 146 | # Key with full AES ECB capabilities 147 | # Enabled flags 0000e000 = YHSM_ECB_BLOCK_ENCRYPT,YHSM_ECB_BLOCK_DECRYPT,YHSM_ECB_BLOCK_DECRYPT_CMP 148 | flags = 0xe000 149 | key = "1001" * 16 150 | self.add_key(flags, 0x1001, key) 151 | 152 | # Key allowed to generate AEAD from known data (loaded into buffer), with user specified noncey 153 | flags = 0x4 | 0x40000000 | 0x20000000 154 | key = "1002" * 16 155 | self.add_key(flags, 0x1002, key) 156 | 157 | # Key with everything enabled at once 158 | flags = 0xffffffff 159 | key = "2000" * 16 160 | self.add_key(flags, 0x2000, key) 161 | 162 | # Key with everything enabled at once, and then revoked 163 | flags = 0xffffffff 164 | key = "2001" * 16 165 | self.add_key(flags, 0x2001, key) 166 | self.config_do("keydis 2001") 167 | 168 | # Key with NIST test vector for HMAC SHA1 169 | # Enabled flags 00010000 = YSM_HMAC_SHA1_GENERATE 170 | flags = 0x10000 171 | key = "303132333435363738393a3b3c3d3e3f40414243".ljust(64, '0') 172 | self.add_key(flags, 0x3031, key) 173 | 174 | # Key permitting AEAD generate with user specified nonce 175 | flags = 0x20000002 176 | key = "20000002" * 8 177 | self.add_key(flags, 0x20000002, key) 178 | 179 | # Key permitting random AEAD generate with user specified nonce 180 | flags = 0x20000008 181 | key = "20000008" * 8 182 | self.add_key(flags, 0x20000008, key) 183 | 184 | def add_key(self, flags, num, key): 185 | keyline = "%08x,%s\r" % (num, key) 186 | self.config_do("flags %04x" % (flags)) 187 | escape_char = chr(27) 188 | self.config_do("keyload\r" + keyline + escape_char, add_cr = False) 189 | 190 | 191 | if __name__ == '__main__': 192 | unittest.main() 193 | -------------------------------------------------------------------------------- /test/test_aes_ecb.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Yubico AB 2 | # See the file COPYING for licence statement. 3 | 4 | import sys 5 | import unittest 6 | import pyhsm 7 | 8 | import test_common 9 | 10 | class TestOtpValidate(test_common.YHSM_TestCase): 11 | 12 | def setUp(self): 13 | test_common.YHSM_TestCase.setUp(self) 14 | # Enabled flags 0000e000 = YHSM_ECB_BLOCK_ENCRYPT,YHSM_ECB_BLOCK_DECRYPT,YHSM_ECB_BLOCK_DECRYPT_CMP 15 | self.kh_encrypt = 0x1001 16 | self.kh_decrypt = 0x1001 17 | self.kh_compare = 0x1001 18 | 19 | def test_aes_ecb_cmd_class(self): 20 | """ Test YHSM_Cmd_AES_ECB class. """ 21 | this = pyhsm.aes_ecb_cmd.YHSM_Cmd_AES_ECB(None, None, '') 22 | # test repr method 23 | self.assertEquals(str, type(str(this))) 24 | this.executed = True 25 | self.assertEquals(str, type(str(this))) 26 | 27 | def test_encrypt_decrypt(self): 28 | """ Test to AES ECB decrypt something encrypted. """ 29 | plaintext = 'Fjaellen 2011'.ljust(pyhsm.defines.YSM_BLOCK_SIZE) # pad for compare after decrypt 30 | 31 | ciphertext = self.hsm.aes_ecb_encrypt(self.kh_encrypt, plaintext) 32 | 33 | self.assertNotEqual(plaintext, ciphertext) 34 | 35 | decrypted = self.hsm.aes_ecb_decrypt(self.kh_decrypt, ciphertext) 36 | 37 | self.assertEqual(plaintext, decrypted) 38 | 39 | def test_compare(self): 40 | """ Test to AES ECB decrypt and then compare something. """ 41 | plaintext = 'Maverick'.ljust(pyhsm.defines.YSM_BLOCK_SIZE) 42 | 43 | ciphertext = self.hsm.aes_ecb_encrypt(self.kh_encrypt, plaintext) 44 | 45 | self.assertTrue(self.hsm.aes_ecb_compare(self.kh_compare, ciphertext, plaintext)) 46 | self.assertFalse(self.hsm.aes_ecb_compare(self.kh_compare, ciphertext, plaintext[:-1] + 'x')) 47 | 48 | def test_compare_bad(self): 49 | """ Test AES decrypt compare with incorrect plaintext. """ 50 | plaintext = 'Maverick'.ljust(pyhsm.defines.YSM_BLOCK_SIZE) 51 | 52 | ciphertext = self.hsm.aes_ecb_encrypt(self.kh_encrypt, plaintext) 53 | 54 | self.assertFalse(self.hsm.aes_ecb_compare(self.kh_compare, ciphertext, plaintext[:-1] + 'x')) 55 | 56 | def test_who_can_encrypt(self): 57 | """ Test what key handles can encrypt AES ECB encrypted blocks. """ 58 | # Enabled flags 00002000 = YSM_AES_ECB_BLOCK_ENCRYPT 59 | # 0000000e - stored ok 60 | kh_enc = 0x0e 61 | 62 | plaintext = 'sommar' 63 | 64 | this = lambda kh: self.hsm.aes_ecb_encrypt(kh, plaintext) 65 | self.who_can(this, expected = [kh_enc]) 66 | 67 | def test_who_can_decrypt(self): 68 | """ Test what key handles can decrypt AES ECB encrypted blocks. """ 69 | # Enabled flags 00002000 = YSM_AES_ECB_BLOCK_ENCRYPT 70 | # 0000000e - stored ok 71 | kh_enc = 0x0e 72 | 73 | # Enabled flags 00004000 = YSM_AES_ECB_BLOCK_DECRYPT 74 | # 0000000f - stored ok 75 | kh_dec = 0x0f 76 | 77 | plaintext = 'sommar' 78 | ciphertext = self.hsm.aes_ecb_encrypt(kh_enc, plaintext) 79 | 80 | this = lambda kh: self.hsm.aes_ecb_decrypt(kh, ciphertext) 81 | self.who_can(this, expected = [kh_dec]) 82 | 83 | def test_who_can_compare(self): 84 | """ Test what key handles can decrypt_compare AES ECB encrypted blocks. """ 85 | # Enabled flags 00002000 = YSM_AES_ECB_BLOCK_ENCRYPT 86 | # 0000000e - stored ok 87 | kh_enc = 0x0e 88 | 89 | # Enabled flags 00008000 = YSM_AES_ECB_BLOCK_DECRYPT_CMP 90 | # 00000010 - stored ok 91 | kh_cmp = 0x10 92 | 93 | # Decrypt implies decrypt_cmp 94 | # 95 | # Enabled flags 00004000 = YSM_AES_ECB_BLOCK_DECRYPT 96 | # 0000000f - stored ok 97 | kh_dec = 0x0f 98 | 99 | plaintext = 'sommar' 100 | ciphertext = self.hsm.aes_ecb_encrypt(kh_enc, plaintext) 101 | 102 | this = lambda kh: self.hsm.aes_ecb_decrypt(kh, ciphertext) 103 | self.who_can(this, expected = [kh_cmp, kh_dec]) 104 | 105 | def test_aes_with_keystore_locked(self): 106 | """ Test AES with locking and then unlocking keystore. """ 107 | if self.hsm.version.ver <= (0, 9, 8,): 108 | print ("Test for known bug in 0.9.8 disabled.") 109 | return None 110 | cleartext = "reference" 111 | res_before = self.hsm.aes_ecb_encrypt(0x2000, cleartext) 112 | # lock key store 113 | try: 114 | res = self.hsm.key_storage_unlock("A" * 8) 115 | self.fail("Expected YSM_MISMATCH/YSM_KEY_STORAGE_LOCKED, got %s" % (res)) 116 | except pyhsm.exception.YHSM_CommandFailed, e: 117 | if self.hsm.version.have_key_store_decrypt(): 118 | self.assertEquals(e.status, pyhsm.defines.YSM_MISMATCH) 119 | else: 120 | self.assertEquals(e.status, pyhsm.defines.YSM_KEY_STORAGE_LOCKED) 121 | # make sure we can't AES encrypt when keystore is locked 122 | try: 123 | res = self.hsm.aes_ecb_encrypt(0x2000, cleartext) 124 | self.fail("Expected YSM_KEY_STORAGE_LOCKED, got %s (before lock: %s)" \ 125 | % (res.encode("hex"), res_before.encode("hex"))) 126 | except pyhsm.exception.YHSM_CommandFailed, e: 127 | self.assertEquals(e.status, pyhsm.defines.YSM_KEY_STORAGE_LOCKED) 128 | # unlock key store with correct passphrase 129 | self.assertTrue(self.hsm.key_storage_unlock(test_common.HsmPassphrase.decode("hex"))) 130 | # make sure it is properly unlocked 131 | res_after = self.hsm.aes_ecb_encrypt(0x2000, cleartext) 132 | self.assertEquals(res_before, res_after) 133 | -------------------------------------------------------------------------------- /test/test_basics.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Yubico AB 2 | # See the file COPYING for licence statement. 3 | 4 | import sys 5 | import unittest 6 | import pyhsm 7 | import serial 8 | import struct 9 | 10 | import test_common 11 | 12 | class TestBasics(test_common.YHSM_TestCase): 13 | 14 | def setUp(self): 15 | test_common.YHSM_TestCase.setUp(self) 16 | 17 | def test_echo(self): 18 | """ Test echo command. """ 19 | self.assertTrue(self.hsm.echo('test')) 20 | 21 | def test_random(self): 22 | """ Test random number generator . """ 23 | r1 = self.hsm.random(10) 24 | r2 = self.hsm.random(10) 25 | self.assertNotEqual(r1, r2) 26 | self.assertEqual(len(r1), 10) 27 | 28 | def test_util_key_handle_to_int(self): 29 | """ Test util.key_handle_to_int. """ 30 | self.assertEqual(1, pyhsm.util.key_handle_to_int("1")) 31 | self.assertEqual(1, pyhsm.util.key_handle_to_int("0x1")) 32 | self.assertEqual(0xffffffee, pyhsm.util.key_handle_to_int("0xffffffee")) 33 | self.assertEqual(1413895238, pyhsm.util.key_handle_to_int("FTFT")) 34 | 35 | def test_nonce(self): 36 | """ Test nonce retreival. """ 37 | n1 = self.hsm.get_nonce() 38 | n2 = self.hsm.get_nonce() 39 | self.assertEqual(n1.nonce_int + 1, n2.nonce_int) 40 | n3 = self.hsm.get_nonce(9) 41 | # YubiHSM returns nonce _before_ adding increment, so the increment 42 | # is still only 1 between n2 and n3 43 | self.assertEqual(n2.nonce_int + 1, n3.nonce_int) 44 | n4 = self.hsm.get_nonce(1) 45 | # and now we see the 9 increment 46 | self.assertEqual(n3.nonce_int + 9, n4.nonce_int) 47 | 48 | def test_nonce_class(self): 49 | """ Test nonce class. """ 50 | # test repr method 51 | self.assertEquals(str, type(str(self.hsm.get_nonce(0)))) 52 | 53 | def test_random_reseed(self): 54 | """ 55 | Tets random reseed. 56 | """ 57 | # Unsure if we can test anything except the status returned is OK 58 | self.assertTrue(self.hsm.random_reseed('A' * 32)) 59 | # at least test we didn't disable the RNG 60 | r1 = self.hsm.random(10) 61 | r2 = self.hsm.random(10) 62 | self.assertNotEqual(r1, r2) 63 | 64 | def test_load_temp_key(self): 65 | """ Test load_temp_key. """ 66 | key = "A" * 16 67 | uid = '\x4d\x01\x4d\x02' 68 | nonce = 'f1f2f3f4f5f6'.decode('hex') 69 | # key 0x2000 has all flags set 70 | key_handle = 0x2000 71 | 72 | my_flags = struct.pack("< I", 0xffffffff) # full permissions when loaded into phantom key handle 73 | my_key = 'C' * pyhsm.defines.YSM_MAX_KEY_SIZE 74 | self.hsm.load_secret(my_key + my_flags) 75 | 76 | aead = self.hsm.generate_aead(nonce, key_handle) 77 | 78 | self.assertTrue(isinstance(aead, pyhsm.aead_cmd.YHSM_GeneratedAEAD)) 79 | 80 | # Load the AEAD into the phantom key handle 0xffffffff. 81 | self.assertTrue(self.hsm.load_temp_key(nonce, key_handle, aead)) 82 | 83 | # Encrypt something with the phantom key 84 | plaintext = 'Testing'.ljust(pyhsm.defines.YSM_BLOCK_SIZE) # pad for compare after decrypt 85 | ciphertext = self.hsm.aes_ecb_encrypt(pyhsm.defines.YSM_TEMP_KEY_HANDLE, plaintext) 86 | self.assertNotEqual(plaintext, ciphertext) 87 | 88 | # Now decrypt it again and verify result 89 | decrypted = self.hsm.aes_ecb_decrypt(pyhsm.defines.YSM_TEMP_KEY_HANDLE, ciphertext) 90 | self.assertEqual(plaintext, decrypted) 91 | 92 | def test_yhsm_class(self): 93 | """ Test YHSM class. """ 94 | # test repr method 95 | self.assertEquals(str, type(str(self.hsm))) 96 | 97 | def test_yhsm_stick_class(self): 98 | """ Test YHSM_Stick class. """ 99 | # test repr method 100 | self.assertEquals(str, type(str(self.hsm.stick))) 101 | 102 | def test_set_debug(self): 103 | """ Test set_debug on YHSM. """ 104 | old = self.hsm.set_debug(True) 105 | if old: 106 | self.hsm.set_debug(False) 107 | self.hsm.set_debug(old) 108 | try: 109 | self.hsm.set_debug('Test') 110 | self.fail("Expected non-bool exception.") 111 | except pyhsm.exception.YHSM_WrongInputType: 112 | pass 113 | 114 | def test_sysinfo_cmd_class(self): 115 | """ Test YHSM_Cmd_System_Info class. """ 116 | this = pyhsm.basic_cmd.YHSM_Cmd_System_Info(None) 117 | # test repr method 118 | self.assertEquals(str, type(str(this))) 119 | 120 | def test_sysinfo(self): 121 | """ Test sysinfo. """ 122 | info = self.hsm.info() 123 | self.assertTrue(info.version_major > 0 or info.version_minor > 0) 124 | self.assertEqual(12, len(info.system_uid)) 125 | self.assertEquals(str, type(str(info))) 126 | 127 | def test_drain(self): 128 | """ Test YubiHSM drain. """ 129 | self.hsm.drain() 130 | 131 | def test_raw_device(self): 132 | """ Test YubiHSM raw device fetch. """ 133 | self.assertNotEqual(False, self.hsm.get_raw_device()) 134 | 135 | def test_unknown_defines(self): 136 | """ Test command/response to string. """ 137 | self.assertEqual("YSM_NULL", pyhsm.defines.cmd2str(0)) 138 | self.assertEqual("0xff", pyhsm.defines.cmd2str(0xff)) 139 | self.assertEqual("YSM_STATUS_OK", pyhsm.defines.status2str(0x80)) 140 | self.assertEqual("0x00", pyhsm.defines.status2str(0)) 141 | -------------------------------------------------------------------------------- /test/test_buffer.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Yubico AB 2 | # See the file COPYING for licence statement. 3 | 4 | import sys 5 | import unittest 6 | import pyhsm 7 | 8 | import test_common 9 | 10 | class TestBuffer(test_common.YHSM_TestCase): 11 | 12 | def setUp(self): 13 | test_common.YHSM_TestCase.setUp(self) 14 | 15 | def test_load_random(self): 16 | """ Test load_random. """ 17 | nonce = "abc123" 18 | # key 0x2000 has all flags set 19 | key_handle = 0x2000 20 | self.hsm.load_random(16) 21 | aead1 = self.hsm.generate_aead(nonce, key_handle) 22 | # nonce should NEVER be re-used for the same key_handle, but 23 | # we do it to test that the random-function actually changes 24 | # the buffer. 25 | self.hsm.load_random(16) 26 | aead2 = self.hsm.generate_aead(nonce, key_handle) 27 | 28 | self.assertNotEqual(aead1.data, aead2.data) 29 | 30 | def test_would_overflow_buffer(self): 31 | """ Test overflow of buffer. """ 32 | nonce = "abc123" 33 | # key 0x2000 has all flags set 34 | key_handle = 0x2000 35 | 36 | self.assertEqual(64, self.hsm.load_random(16, offset = pyhsm.defines.YSM_DATA_BUF_SIZE - 8)) 37 | self.assertEqual(16, self.hsm.load_random(16, offset = 0)) # offset = 0 clears buffer 38 | self.assertEqual(17, self.hsm.load_random(1, offset = 16)) 39 | self.assertEqual(17, self.hsm.load_random(7, offset = 10)) 40 | self.assertEqual(63, self.hsm.load_random(1, offset = 62)) 41 | self.assertEqual(64, self.hsm.load_random(63, offset = 62)) 42 | 43 | def test_load_data(self): 44 | """ Test loading data into buffer. """ 45 | c1 = self.hsm.load_data('Samp', offset = 0) 46 | self.assertEqual(c1, 4) 47 | c2 = self.hsm.load_data('123', offset = 3) 48 | self.assertEqual(c2, 6) 49 | c3 = self.hsm.load_data('ple #2', offset = 3) 50 | self.assertEqual(c3, 9) 51 | nonce = "abc123" 52 | # key 0x2000 has all flags set 53 | key_handle = 0x2000 54 | aead = self.hsm.generate_aead(nonce, key_handle) 55 | self.assertEqual(aead.data.encode('hex'), '18a88fbd7bd2275ba0a722bf80423ffab7') 56 | -------------------------------------------------------------------------------- /test/test_common.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Yubico AB 2 | # See the file COPYING for licence statement. 3 | 4 | import os 5 | import sys 6 | import unittest 7 | import pyhsm 8 | import struct 9 | from pyhsm.soft_hsm import crc16 10 | 11 | # configuration parameters 12 | CfgPassphrase = "" 13 | HsmPassphrase = "bada" * 2 14 | PrimaryAdminYubiKey = ('ftftftfteeee', 'f0f1f2f3f4f5', '4d' * 16,) 15 | AdminYubiKeys = [PrimaryAdminYubiKey[0]] 16 | 17 | class YHSM_TestCase(unittest.TestCase): 18 | 19 | hsm = None 20 | 21 | def setUp(self, device = os.getenv('YHSM_DEVICE', '/dev/ttyACM0'), debug = False): 22 | """ 23 | Common initialization class for our tests. Initializes a 24 | YubiHSM in self.hsm. 25 | """ 26 | self.hsm = pyhsm.base.YHSM(device = device, debug = debug) 27 | # unlock keystore if our test configuration contains a passphrase 28 | if HsmPassphrase is not None and HsmPassphrase != "": 29 | try: 30 | self.hsm.unlock(password = HsmPassphrase.decode("hex")) 31 | self.otp_unlock() 32 | except pyhsm.exception.YHSM_CommandFailed, e: 33 | # ignore errors from the unlock function, in case our test configuration 34 | # hasn't been loaded into the YubiHSM yet 35 | pass 36 | 37 | def tearDown(self): 38 | # get destructor called properly 39 | self.hsm = None 40 | 41 | def who_can(self, what, expected = [], extra_khs = []): 42 | """ 43 | Try the lambda what() with all key handles between 1 and 32, except the expected one. 44 | Fail on anything but YSM_FUNCTION_DISABLED. 45 | """ 46 | for kh in list(xrange(1, 32)) + extra_khs: 47 | if kh in expected: 48 | continue 49 | res = None 50 | try: 51 | res = what(kh) 52 | self.fail("Expected YSM_FUNCTION_DISABLED for key handle 0x%0x, got '%s'" % (kh, res)) 53 | except pyhsm.exception.YHSM_CommandFailed, e: 54 | if e.status != pyhsm.defines.YSM_FUNCTION_DISABLED: 55 | self.fail("Expected YSM_FUNCTION_DISABLED for key handle 0x%0x, got %s" \ 56 | % (kh, e.status_str)) 57 | 58 | def otp_unlock(self): 59 | """ 60 | Do OTP unlock of the YubiHSM keystore. 61 | 62 | Since we don't always reprogram the YubiHSM, we might need to hunt for an unused OTP. 63 | """ 64 | if not self.hsm.version.have_unlock(): 65 | return None 66 | Params = PrimaryAdminYubiKey 67 | YK = FakeYubiKey(pyhsm.yubikey.modhex_decode(Params[0]).decode('hex'), 68 | Params[1].decode('hex'), Params[2].decode('hex') 69 | ) 70 | YK.session_ctr = 0 71 | use_ctr = 1 # the 16 bit power-up counter of the YubiKey 72 | while use_ctr < 0xffff: 73 | YK.use_ctr = use_ctr 74 | otp = YK.from_key() 75 | try: 76 | res = self.hsm.unlock(otp = otp) 77 | self.assertTrue(res) 78 | # OK - if we got here we've got a successful response for this OTP 79 | break 80 | except pyhsm.exception.YHSM_CommandFailed, e: 81 | if e.status != pyhsm.defines.YSM_OTP_REPLAY: 82 | raise 83 | # don't bother with the session_ctr - test run 5 would mean we first have to 84 | # exhaust 4 * 256 session_ctr increases before the YubiHSM would pass our OTP 85 | use_ctr += 1 86 | 87 | class YubiKeyEmu(): 88 | """ 89 | Emulate the internal memory of a YubiKey. 90 | """ 91 | 92 | def __init__(self, user_id, use_ctr, timestamp, session_ctr): 93 | if len(user_id) != pyhsm.defines.UID_SIZE: 94 | raise pyhsm.exception.YHSM_WrongInputSize( 95 | 'user_id', pyhsm.defines.UID_SIZE, len(user_id)) 96 | 97 | self.user_id = user_id 98 | self.use_ctr = use_ctr 99 | self.timestamp = timestamp 100 | self.session_ctr = session_ctr 101 | self.rnd = struct.unpack('H', os.urandom(2))[0] 102 | 103 | def pack(self): 104 | """ 105 | Return contents packed. Only add AES ECB encryption and modhex to 106 | get your own YubiKey OTP (see function 'from_key'). 107 | """ 108 | 109 | #define UID_SIZE 6 110 | #typedef struct { 111 | # uint8_t userId[UID_SIZE]; 112 | # uint16_t sessionCtr; # NOTE: this is use_ctr 113 | # uint24_t timestamp; 114 | # uint8_t sessionUse; # NOTE: this is session_ctr 115 | # uint16_t rnd; 116 | # uint16_t crc; 117 | #} TICKET; 118 | fmt = "< %is H HB B H" % (pyhsm.defines.UID_SIZE) 119 | 120 | ts_high = (self.timestamp & 0x00ff0000) >> 16 121 | ts_low = self.timestamp & 0x0000ffff 122 | 123 | res = struct.pack(fmt, self.user_id, \ 124 | self.use_ctr, \ 125 | ts_low, ts_high, \ 126 | self.session_ctr, \ 127 | self.rnd) 128 | crc = 0xffff - crc16(res) 129 | 130 | return res + struct.pack(' < keyload - Load key data now using flags ffffffff. Press ESC to quit 20 | # 00002001 - stored ok 21 | # HSM> < keydis 2001 22 | try: 23 | res = self.hsm.aes_ecb_encrypt(0x2001, "klartext") 24 | self.fail("Expected YSM_FUNCTION_DISABLED, got %s" % (res)) 25 | except pyhsm.exception.YHSM_CommandFailed, e: 26 | self.assertEquals(e.status, pyhsm.defines.YSM_FUNCTION_DISABLED) 27 | 28 | def test_keystore_unlock(self): 29 | """ Test locking and then unlocking keystore. """ 30 | if self.hsm.version.ver <= (0, 9, 8,): 31 | print ("Test for known bug in 0.9.8 disabled.") 32 | return None 33 | cleartext = "reference" 34 | nonce = '010203040506'.decode('hex') 35 | res_before = self.hsm.generate_aead_simple(nonce, 0x2000, cleartext) 36 | # lock key store 37 | try: 38 | res = self.hsm.key_storage_unlock("A" * 8) 39 | self.fail("Expected YSM_MISMATCH/YSM_KEY_STORAGE_LOCKED, got %s" % (res)) 40 | except pyhsm.exception.YHSM_CommandFailed, e: 41 | if self.hsm.version.have_key_store_decrypt(): 42 | self.assertEquals(e.status, pyhsm.defines.YSM_MISMATCH) 43 | else: 44 | self.assertEquals(e.status, pyhsm.defines.YSM_KEY_STORAGE_LOCKED) 45 | # make sure we can't generate AEADs when keystore is locked 46 | try: 47 | res = self.hsm.generate_aead_simple(nonce, 0x2000, cleartext) 48 | self.fail("Expected YSM_KEY_STORAGE_LOCKED, got %s (before lock: %s)" \ 49 | % (res.data.encode('hex'), res_before.data.encode('hex'))) 50 | except pyhsm.exception.YHSM_CommandFailed, e: 51 | self.assertEquals(e.status, pyhsm.defines.YSM_KEY_STORAGE_LOCKED) 52 | # unlock key store with correct passphrase 53 | self.assertTrue(self.hsm.key_storage_unlock(test_common.HsmPassphrase.decode("hex"))) 54 | # make sure it is properly unlocked 55 | res_after = self.hsm.generate_aead_simple(nonce, 0x2000, cleartext) 56 | self.assertEquals(res_before.data, res_after.data) 57 | -------------------------------------------------------------------------------- /test/test_oath.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Yubico AB 2 | # See the file COPYING for licence statement. 3 | 4 | import struct 5 | import unittest 6 | import pyhsm 7 | import pyhsm.oath_hotp 8 | 9 | import test_common 10 | 11 | class TestOath(test_common.YHSM_TestCase): 12 | 13 | def setUp(self): 14 | test_common.YHSM_TestCase.setUp(self) 15 | 16 | key = "3132333435363738393031323334353637383930".decode('hex') 17 | # Enabled flags 00010000 = YSM_HMAC_SHA1_GENERATE 18 | flags = struct.pack("< I", 0x10000) 19 | self.nonce = 'f1f2f3f4f5f6'.decode('hex') 20 | # key 0x2000 has all flags set 21 | self.key_handle = 0x2000 22 | self.phantom = pyhsm.defines.YSM_TEMP_KEY_HANDLE 23 | 24 | self.hsm.load_secret(key + flags) 25 | self.aead = self.hsm.generate_aead(self.nonce, self.key_handle) 26 | 27 | self.assertTrue(isinstance(self.aead, pyhsm.aead_cmd.YHSM_GeneratedAEAD)) 28 | 29 | # Load the AEAD into the phantom key handle 0xffffffff. 30 | self.assertTrue(self.hsm.load_temp_key(self.nonce, self.key_handle, self.aead)) 31 | 32 | def test_OATH_HOTP_values(self): 33 | """ Test OATH HOTP known results. """ 34 | test_vectors = [(0, "cc93cf18508d94934c64b65d8ba7667fb7cde4b0", 755224,), 35 | (1, "75a48a19d4cbe100644e8ac1397eea747a2d33ab", 287082,), 36 | (2, "0bacb7fa082fef30782211938bc1c5e70416ff44", 359152,), 37 | (3, "66c28227d03a2d5529262ff016a1e6ef76557ece", 969429,), 38 | (4, "a904c900a64b35909874b33e61c5938a8e15ed1c", 338314,), 39 | (5, "a37e783d7b7233c083d4f62926c7a25f238d0316", 254676,), 40 | (6, "bc9cd28561042c83f219324d3c607256c03272ae", 287922,), 41 | (7, "a4fb960c0bc06e1eabb804e5b397cdc4b45596fa", 162583,), 42 | (8, "1b3c89f65e6c9e883012052823443f048b4332db", 399871,), 43 | (9, "1637409809a679dc698207310c8c7fc07290d9e5", 520489,), 44 | (30, "543c61f8f9aeb35f6dbc3a6847c3fe288cc0ee4c", 26920,), 45 | ] 46 | 47 | for c, expected, code in test_vectors: 48 | hmac_result = self.hsm.hmac_sha1(self.phantom, struct.pack("> Q", c)).get_hash() 49 | self.assertEqual(expected, hmac_result.encode('hex')) 50 | self.assertEqual(code, pyhsm.oath_hotp.truncate(hmac_result, length=6)) 51 | 52 | def test_OATH_HOTP_validation(self): 53 | """ Test complete OATH HOTP code validation. """ 54 | 55 | oath = lambda counter, user_code, look_ahead: \ 56 | pyhsm.oath_hotp.search_for_oath_code(self.hsm, self.key_handle, self.nonce, self.aead, \ 57 | counter, user_code, look_ahead) 58 | 59 | self.assertEqual(1, oath(0, 755224, 1)) 60 | self.assertEqual(4, oath(0, 969429, 4)) 61 | self.assertEqual(None, oath(0, 969429, 3)) 62 | self.assertEqual(10, oath(9, 520489, 3)) 63 | self.assertEqual(31, oath(30, 26920, 1)) 64 | -------------------------------------------------------------------------------- /test/test_otp_validate.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Yubico AB 2 | # See the file COPYING for licence statement. 3 | 4 | import sys 5 | import unittest 6 | import pyhsm 7 | 8 | import test_common 9 | 10 | class TestOtpValidate(test_common.YHSM_TestCase): 11 | 12 | def setUp(self): 13 | test_common.YHSM_TestCase.setUp(self) 14 | 15 | def test_load_secret_wrong_key(self): 16 | """ Test load_secret with key that should not be allowed to. """ 17 | key = "A" * 16 18 | uid = '\x4d\x4d\x4d\x4d\x4d\x4d' 19 | public_id = 'f0f1f2f3f4f5'.decode('hex') 20 | # Enabled flags 00000100 = YHSM_AEAD_STORE 21 | # HSM> < keyload - Load key data now using flags 00000100. Press ESC to quit 22 | # 00000009 - stored ok 23 | key_handle = 9 # Enabled flags 00000020 = YHSM_AEAD_GENERATE 24 | 25 | secret = pyhsm.aead_cmd.YHSM_YubiKeySecret(key, uid) 26 | self.hsm.load_secret(secret) 27 | 28 | try: 29 | res = self.hsm.generate_aead(public_id, key_handle) 30 | self.fail("Expected YSM_FUNCTION_DISABLED, got %s" % (res)) 31 | except pyhsm.exception.YHSM_CommandFailed, e: 32 | self.assertEquals(e.status, pyhsm.defines.YSM_FUNCTION_DISABLED) 33 | 34 | def test_load_secret(self): 35 | """ Test load_secret. """ 36 | key = "A" * 16 37 | uid = '\x4d\x01\x4d\x02' 38 | public_id = 'f1f2f3f4f5f6'.decode('hex') 39 | if self.hsm.version.have_YSM_BUFFER_LOAD(): 40 | # Enabled flags 60000004 = YSM_BUFFER_AEAD_GENERATE,YSM_USER_NONCE,YSM_BUFFER_LOAD 41 | # HSM (keys changed)> < keyload - Load key data now using flags 60000004. Press ESC to quit 42 | # 00001002 - stored ok 43 | key_handle = 0x1002 44 | else: 45 | # Enabled flags 00000004 = YSM_BUFFER_AEAD_GENERATE 46 | # HSM> < keyload - Load key data now using flags 00000004. Press ESC to quit 47 | # 00000003 - stored ok 48 | key_handle = 3 49 | 50 | secret = pyhsm.aead_cmd.YHSM_YubiKeySecret(key, uid) 51 | self.hsm.load_secret(secret) 52 | 53 | aead = self.hsm.generate_aead(public_id, key_handle) 54 | 55 | self.assertTrue(isinstance(aead, pyhsm.aead_cmd.YHSM_GeneratedAEAD)) 56 | 57 | self.assertEqual(aead.nonce, public_id) 58 | self.assertEqual(aead.key_handle, key_handle) 59 | 60 | def test_yubikey_secrets(self): 61 | """ Test the class representing the YUBIKEY_SECRETS struct. """ 62 | aes_128_key = 'a' * 16 63 | first = pyhsm.aead_cmd.YHSM_YubiKeySecret(aes_128_key, 'b') 64 | self.assertEqual(len(first.pack()), pyhsm.defines.KEY_SIZE + pyhsm.defines.UID_SIZE) 65 | -------------------------------------------------------------------------------- /test/test_soft_hsm.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 Yubico AB 2 | # See the file COPYING for licence statement. 3 | 4 | import sys 5 | import unittest 6 | import pyhsm 7 | 8 | import test_common 9 | 10 | class TestSoftHSM(test_common.YHSM_TestCase): 11 | 12 | def setUp(self): 13 | test_common.YHSM_TestCase.setUp(self) 14 | self.nonce = "4d4d4d4d4d4d".decode('hex') 15 | self.key = "A" * 16 16 | 17 | def test_aes_CCM_encrypt_decrypt(self): 18 | """ Test decrypting encrypted data. """ 19 | key = chr(0x09) * 16 20 | key_handle = 1 21 | plaintext = "foo".ljust(16, chr(0x0)) 22 | ct = pyhsm.soft_hsm.aesCCM(key, key_handle, self.nonce, plaintext, decrypt = False) 23 | pt = pyhsm.soft_hsm.aesCCM(key, key_handle, self.nonce, ct, decrypt = True) 24 | self.assertEquals(plaintext, pt) 25 | 26 | def test_aes_CCM_wrong_key(self): 27 | """ Test decrypting encrypted data with wrong key. """ 28 | key = chr(0x09) * 16 29 | key_handle = 1 30 | plaintext = "foo".ljust(16, chr(0x0)) 31 | ct = pyhsm.soft_hsm.aesCCM(key, key_handle, self.nonce, plaintext, decrypt = False) 32 | key = chr(0x08) * 16 33 | self.assertRaises(pyhsm.exception.YHSM_Error, pyhsm.soft_hsm.aesCCM, 34 | key, key_handle, self.nonce, ct, decrypt = True) 35 | 36 | def test_aes_CCM_wrong_key_handle(self): 37 | """ Test decrypting encrypted data with wrong key_handle. """ 38 | key = chr(0x09) * 16 39 | key_handle = 1 40 | plaintext = "foo".ljust(16, chr(0x0)) 41 | ct = pyhsm.soft_hsm.aesCCM(key, key_handle, self.nonce, plaintext, decrypt = False) 42 | key_handle = 2 43 | self.assertRaises(pyhsm.exception.YHSM_Error, pyhsm.soft_hsm.aesCCM, 44 | key, key_handle, self.nonce, ct, decrypt = True) 45 | 46 | def test_soft_simple_aead_generation(self): 47 | """ Test soft_hsm simple AEAD generation. """ 48 | key_handle = 0x2000 49 | plaintext = 'foo'.ljust(16, chr(0x0)) 50 | key = str("2000" * 16).decode('hex') 51 | # generate soft AEAD 52 | ct = pyhsm.soft_hsm.aesCCM(key, key_handle, self.nonce, plaintext, decrypt = False) 53 | # generate hard AEAD 54 | aead = self.hsm.generate_aead_simple(self.nonce, key_handle, plaintext) 55 | 56 | ct = pyhsm.soft_hsm.aesCCM(key, key_handle, self.nonce, plaintext, decrypt = False) 57 | self.assertEquals(aead.data, ct) 58 | 59 | # decrypt the AEAD again 60 | pt = pyhsm.soft_hsm.aesCCM(key, key_handle, self.nonce, ct, decrypt = True) 61 | self.assertEquals(plaintext, pt) 62 | 63 | def test_soft_generate_long_aead(self): 64 | """ Test soft_hsm generation of long AEAD. """ 65 | key_handle = 0x2000 66 | plaintext = 'A' * 64 67 | key = str("2000" * 16).decode('hex') 68 | # generate soft AEAD 69 | ct = pyhsm.soft_hsm.aesCCM(key, key_handle, self.nonce, plaintext, decrypt = False) 70 | # generate hard AEAD 71 | aead = self.hsm.generate_aead_simple(self.nonce, key_handle, plaintext) 72 | 73 | self.assertEquals(aead.data, ct) 74 | 75 | # decrypt the AEAD again 76 | pt = pyhsm.soft_hsm.aesCCM(key, key_handle, self.nonce, ct, decrypt = True) 77 | self.assertEquals(plaintext, pt) 78 | 79 | def test_soft_generate_yubikey_secrets_aead(self): 80 | """ Test soft_hsm generation of YubiKey secrets AEAD. """ 81 | key_handle = 0x2000 82 | plaintext = 'A' * 22 83 | key = str("2000" * 16).decode('hex') 84 | # generate soft AEAD 85 | ct = pyhsm.soft_hsm.aesCCM(key, key_handle, self.nonce, plaintext, decrypt = False) 86 | # generate hard AEAD 87 | aead = self.hsm.generate_aead_simple(self.nonce, key_handle, plaintext) 88 | 89 | self.assertEquals(aead.data, ct) 90 | 91 | # decrypt the AEAD again 92 | pt = pyhsm.soft_hsm.aesCCM(key, key_handle, self.nonce, ct, decrypt = True) 93 | self.assertEquals(plaintext, pt) 94 | -------------------------------------------------------------------------------- /test/test_stick.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Yubico AB 2 | # See the file COPYING for licence statement. 3 | 4 | import sys 5 | import unittest 6 | import pyhsm 7 | 8 | import test_common 9 | 10 | class TestUtil(test_common.YHSM_TestCase): 11 | 12 | def setUp(self): 13 | self.saved_stderr = sys.stderr 14 | # Discard everything written to stderr from these tests (i.e. debug output 15 | # from YubiHSM communication routines with debugging enabled). 16 | sys.stderr = DiscardOutput() 17 | DontChange = True # we test debug output from YubiHSM communication here 18 | test_common.YHSM_TestCase.setUp(self, debug = DontChange) 19 | 20 | def test_debug_output(self): 21 | """ Test debug output of YubiHSM communication. """ 22 | self.assertTrue(self.hsm.echo('testing')) 23 | self.assertTrue(self.hsm.drain()) 24 | 25 | def tearDown(self): 26 | # Close YubiHSM interface before restoring stderr, to avoid output 27 | # when it is closed. 28 | self.hsm = None 29 | sys.stderr = self.saved_stderr 30 | 31 | class DiscardOutput(object): 32 | def write(self, text): 33 | pass 34 | -------------------------------------------------------------------------------- /test/test_util.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Yubico AB 2 | # See the file COPYING for licence statement. 3 | 4 | import sys 5 | import unittest 6 | import pyhsm 7 | 8 | import test_common 9 | 10 | class TestUtil(test_common.YHSM_TestCase): 11 | 12 | def setUp(self): 13 | test_common.YHSM_TestCase.setUp(self) 14 | 15 | def test_hexdump(self): 16 | """ Test hexdump function. """ 17 | data1 = ''.join([chr(x) for x in xrange(8)]) 18 | self.assertEquals('0000 00 01 02 03 04 05 06 07\n', pyhsm.util.hexdump(data1)) 19 | data2 = ''.join([chr(x) for x in xrange(64)]) 20 | self.assertEquals(248, len(pyhsm.util.hexdump(data2))) 21 | self.assertEquals('', pyhsm.util.hexdump('')) 22 | 23 | def test_response_validation(self): 24 | """ Test response validation functions. """ 25 | self.assertRaises(pyhsm.exception.YHSM_Error, pyhsm.util.validate_cmd_response_str, \ 26 | 'test', 'abc', 'def', hex_encode=True) 27 | 28 | self.assertRaises(pyhsm.exception.YHSM_Error, pyhsm.util.validate_cmd_response_str, \ 29 | 'test', 'abc', 'def', hex_encode=False) 30 | 31 | def test_input_validate_str(self): 32 | """ Test string input validation. """ 33 | self.assertRaises(pyhsm.exception.YHSM_WrongInputType, pyhsm.util.input_validate_str, \ 34 | 0, 'foo', exact_len = 5) 35 | 36 | self.assertRaises(pyhsm.exception.YHSM_InputTooLong, pyhsm.util.input_validate_str, \ 37 | '1234', 'foo', max_len = 3) 38 | self.assertEquals('1234', pyhsm.util.input_validate_str('1234', 'foo', max_len = 4)) 39 | self.assertEquals('1234', pyhsm.util.input_validate_str('1234', 'foo', max_len = 14)) 40 | 41 | self.assertRaises(pyhsm.exception.YHSM_WrongInputSize, pyhsm.util.input_validate_str, \ 42 | '1234', 'foo', exact_len = 5) 43 | self.assertEquals('1234', pyhsm.util.input_validate_str('1234', 'foo', exact_len = 4)) 44 | -------------------------------------------------------------------------------- /test/test_yubikey_validate.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Yubico AB 2 | # See the file COPYING for licence statement. 3 | 4 | import sys 5 | import string 6 | import unittest 7 | import pyhsm 8 | 9 | import test_common 10 | from test_common import YubiKeyEmu, YubiKeyRnd 11 | 12 | class TestYubikeyValidate(test_common.YHSM_TestCase): 13 | 14 | def setUp(self): 15 | test_common.YHSM_TestCase.setUp(self) 16 | 17 | self.yk_key = 'F' * 16 # 128 bit AES key 18 | self.yk_uid = '\x4d\x01\x4d\x02\x4d\x4d' 19 | self.yk_rnd = YubiKeyRnd(self.yk_uid) 20 | self.yk_public_id = '4d4d4d4d4d4d'.decode('hex') 21 | 22 | secret = pyhsm.aead_cmd.YHSM_YubiKeySecret(self.yk_key, self.yk_uid) 23 | self.hsm.load_secret(secret) 24 | 25 | # YubiHSM includes key handle id in AES-CCM of aeads, so we must use same 26 | # key to generate and validate. Key 0x2000 has all flags. 27 | self.kh_generate = 0x2000 28 | self.kh_validate = 0x2000 29 | 30 | self.aead = self.hsm.generate_aead(self.yk_public_id, self.kh_generate) 31 | 32 | def test_validate_aead_cmp(self): 33 | """ Test that the AEAD generated contains our secrets. """ 34 | secret = pyhsm.aead_cmd.YHSM_YubiKeySecret(self.yk_key, self.yk_uid) 35 | cleartext = secret.pack() 36 | self.assertTrue(self.hsm.validate_aead(self.yk_public_id, self.kh_validate, self.aead, cleartext)) 37 | wrong_cleartext = 'X' + cleartext[1:] 38 | self.assertFalse(self.hsm.validate_aead(self.yk_public_id, self.kh_validate, self.aead, wrong_cleartext)) 39 | 40 | def test_validate_aead_cmp_long(self): 41 | """ Test validating a long AEAD """ 42 | cleartext = 'C' * 36 43 | key_handle = 0x2000 # key 0x2000 has all flags set 44 | nonce = '123456' 45 | aead = self.hsm.generate_aead_simple(nonce, key_handle, cleartext) 46 | self.assertTrue(self.hsm.validate_aead(nonce, key_handle, aead, cleartext)) 47 | wrong_cleartext = 'X' + cleartext[1:] 48 | self.assertFalse(self.hsm.validate_aead(nonce, key_handle, aead, wrong_cleartext)) 49 | 50 | def test_validate_yubikey(self): 51 | """ Test validate YubiKey OTP. """ 52 | from_key = self.yk_rnd.from_key(self.yk_public_id, self.yk_key) 53 | self.assertTrue(pyhsm.yubikey.validate_yubikey_with_aead( \ 54 | self.hsm, from_key, self.aead, self.kh_validate)) 55 | 56 | def test_modhex_encode_decode(self): 57 | """ Test modhex encoding/decoding. """ 58 | h = '4d014d024d4ddd5382b11195144da07d' 59 | self.assertEquals(h, pyhsm.yubikey.modhex_decode( pyhsm.yubikey.modhex_encode(h) ) ) 60 | 61 | def test_split_id_otp(self): 62 | """ Test public_id + OTP split function. """ 63 | public_id, otp, = pyhsm.yubikey.split_id_otp("ft" * 16) 64 | self.assertEqual(public_id, '') 65 | self.assertEqual(otp, "ft" * 16) 66 | 67 | public_id, otp, = pyhsm.yubikey.split_id_otp("cc" + "ft" * 16) 68 | self.assertEqual(public_id, 'cc') 69 | self.assertEqual(otp, "ft" * 16) 70 | --------------------------------------------------------------------------------