Compare commits

...

767 Commits

Author SHA1 Message Date
Roman Zeyde 71f357c1bf
Add 'hidapi' dependency 6 years ago
Eli Boyarski 8f1d008eb2
fixed typo + missing word 6 years ago
Roman Zeyde 7a351acf15
Merge remote-tracking branch 'matejcik/master' 6 years ago
Roman Zeyde 7f9aa2b147
Bump version: 0.11.3 → 0.12.0 6 years ago
Roman Zeyde eed168341c
Don't inheric from 'object' (after deprecating Python 2.x support) 6 years ago
matejcik 8b85090fba trezor: usage for TREZOR_PATH variable
This is not a great place, as the variable will work anywhere,
but I couldn't find a better place to put it.

Also fixes a typo in the service definition.
6 years ago
matejcik 8708b1e16d trezor: use TREZOR_PATH environment variable to specify device path 6 years ago
Roman Zeyde 03e7fc48e9
Improve Git-related documentation 6 years ago
Roman Zeyde 4968ca7ff3
Merge branch 'master' into neopg-wip 6 years ago
Roman Zeyde 6b6d9f5d20
Add a link to neopg-trezor wrapper at documentation 6 years ago
Roman Zeyde c22109df24
Document argv[0] hack for NeoPG 6 years ago
Roman Zeyde 47ce035e79
Remove unused import 6 years ago
Roman Zeyde 36cbba6c57
Fix a few lint issues 6 years ago
Roman Zeyde 6afe20350b
Simplify GPG command generation 6 years ago
Roman Zeyde fa171e8923
Add short example for NeoPG usage 6 years ago
Roman Zeyde f0bda9a3e6
Allow using $PATH when looking for GPG binary
It's needed for running neopg (instead of gnupg).
6 years ago
Roman Zeyde 71b56e15d7
Add NeoPG commandline wrapper for TREZOR-based agent
It invokes `trezor-gpg-agent` instead of `neopg agent`, by putting
its own path at argv[0].
6 years ago
Roman Zeyde 3b9c00e02a
Default to $GNUPGHOME when not specified on commandline 6 years ago
Roman Zeyde dcee59a19e
Assume NeoPG binary runs GnuPG functionality 6 years ago
Roman Zeyde a274de30b8
Parse NeoPG development versions
e.g. v0.0.5-37-g1fe5046-dirty
6 years ago
Roman Zeyde 4fe9e437ad
Simplify GPG homedir setting 6 years ago
Roman Zeyde d04527a8ed
Replace GPG version assertion by an error log
since NeoPG uses different versioning
6 years ago
Roman Zeyde 3329c29cb4
Use gpg_command() for identity generation 6 years ago
Roman Zeyde df2cb52f8d
fixup! Reply with an ERR to `SCD SERIALNO openpgp` ASSUAN command 6 years ago
Roman Zeyde f36ef4ffe0
Allow running NeoPG binary (instead of GnuPG) 6 years ago
Roman Zeyde f74de828fc
Reply with an ERR to `SCD SERIALNO openpgp` ASSUAN command
(for NeoPG)
6 years ago
Roman Zeyde 912b1cde7a
Add support for file-descriptor-based socket server
(for NeoPG)
6 years ago
Roman Zeyde b7a8c42893
Merge pull request #153 from romanz/drop-py2
setup: deprecate Python2 support
6 years ago
Roman Zeyde 1e6c4e6930
Add links to SSH/GPG usage examples 6 years ago
Roman Zeyde a8f19e4150
Comment about SSH argument separation 6 years ago
Roman Zeyde 6a9fdf75e2
Bump version: 0.11.2 → 0.11.3 6 years ago
Roman Zeyde 6bc5b6af5e
Add small example for IdentityOnly use-case 6 years ago
Roman Zeyde 8672a6901a
Document IdentitiesOnly support 6 years ago
Roman Zeyde 672af98ad7
Explicitly use IdentityFile option when connecting to specific host 6 years ago
Roman Zeyde ed531cfff8
Remove trailing whitespace
git ls-files | xargs -n1 sed -e's/[[:space:]]*$//' -i
6 years ago
Bram bd1ae0f091
Update INSTALL.md
I've sorted out the Formula for Homebrew and it's been merged.
6 years ago
Roman Zeyde 0c762e8998
Use `pinentry` homebrew formula on macOS 6 years ago
Roman Zeyde bd0df4f801
trezor: update setup.py for latest libagent and trezorlib 6 years ago
Roman Zeyde 3d1639d271
gpg: require symmetric passphrase re-entry 6 years ago
Roman Zeyde bea899d1ef
gpg: allow symmetric encryption with a passphrase 6 years ago
Roman Zeyde ccc2174775
gpg: allow more verbose output during GnuPG pubkey import 6 years ago
Roman Zeyde afa3fdb89c
gpg: allow setting passphrase cache expriration duration 6 years ago
Roman Zeyde 2ca3941cfa
ssh: allow setting passphrase cache expriration duration 6 years ago
Roman Zeyde b1bd6cb690
gpg: refactor GETINFO handling into a separate method 6 years ago
Roman Zeyde 766536d2c4
trezor: allow expiring cached passphrase 6 years ago
Roman Zeyde 91f70e7a96
Merge pull request #238 from pruflyos/patch-1
Update INSTALL.md
6 years ago
Roman Zeyde cf5bfd960a
Merge pull request #237 from menteb/patch-2
Update to Install.md reflecting Homebrew formula
6 years ago
pruflyos 4bd769f138
Update INSTALL.md
On Fedora `python3-tk` is called `python3-tkinter`
6 years ago
Bram 91b850f184
Update to Install.md reflecting Homebrew formula 6 years ago
Roman Zeyde c6bb090dfc
Merge pull request #235 from timthelion/git-email-readme
Document the configuration of the git email setting and errors
6 years ago
Timothy Hobbs fef4fd06c9 Document the configuration of the git email setting and errors
Signed-off-by: Timothy Hobbs <timothyhobbs@seznam.cz>
6 years ago
Roman Zeyde bc691ae795
gpg: fix method's caching 6 years ago
Roman Zeyde 61e516e200
Add link to Ledger Nano S guide 6 years ago
Roman Zeyde 543ff7021d
doc: explain how to reset cached passphrase 6 years ago
Roman Zeyde 2e0cfc8088
gpg: fail if new identity is missing 6 years ago
Roman Zeyde 18f33f8a08
README: document PIN entry depedencies 6 years ago
Roman Zeyde 2973413995
Merge pull request #227 from kvbik/patch-1
mention brew install libusb on macOS
6 years ago
Jakub Vysoký 2360693dc5
mention brew install libusb on macOS 6 years ago
Roman Zeyde 7443fc6512
Pass 'state' during TREZOR initialization 6 years ago
Roman Zeyde 5efb752979
doc: update Fedora installation instructions 6 years ago
Roman Zeyde 4546cd674b
Bump version: 0.11.1 → 0.11.2 6 years ago
Roman Zeyde 5dba12f144
gpg: don't clear options on RESET assuan command 6 years ago
Roman Zeyde 887561de9f
pylint: skip 'fixme' warnings 6 years ago
Roman Zeyde 6d730e0a5b
ui: subprocess.Popen doesn't have 'args' attribute in Python 2 6 years ago
Roman Zeyde d0732d16e8
ui: don't log passphrases (since the log may be persisted) 6 years ago
Roman Zeyde dafb80ad7a
trezor: don't retry on PIN/passphrase entry cancellation 6 years ago
Roman Zeyde df6249b071
Merge remote-tracking branch 'rendaw/pinentry-docs' 6 years ago
rendaw 942f01418b Also set DISPLAY in SSH unit 6 years ago
rendaw 93b548b737 Add docs to show using the gpg agent with systemd; set PATH for ssh unit 6 years ago
rendaw 329f07249a Small reword 6 years ago
rendaw a1f7088d33 Remove pin entry instructions from INSTALL, didn't seem that relevant 6 years ago
rendaw 25f066e113 Document --pin-entry-binary with usage guide 6 years ago
Roman Zeyde 0699273d49
util: move ASSUAN serialization to break circular import 6 years ago
Roman Zeyde 92c352e860
Bump version: 0.11.0 → 0.11.1 6 years ago
Roman Zeyde 34c03a462c
ui: merge into a single module 6 years ago
Roman Zeyde 51dbecd4c2
Bump version: 0.10.0 → 0.11.0 6 years ago
Roman Zeyde ceae65aa5a
ui: use {} as default config 6 years ago
Roman Zeyde d0497b0137
pinentry: specify device name at PIN/passphrase entry UI 6 years ago
Roman Zeyde 870152a7af
gpg: allow specifying custom homedir during init 6 years ago
Roman Zeyde cbdc52c0a4
trezor: handle passphrase on-device entry (for Model T) 6 years ago
Roman Zeyde 0c9fc33757
gpg: replace gpg-agent.conf by run-agent.sh 6 years ago
Roman Zeyde 17ea941add
gpg: use pinentry UI for initialization and agent 6 years ago
Roman Zeyde 64064b5ecc
ssh: use pinentry UI 6 years ago
Roman Zeyde 601a2b1336
device: refactor PIN/passphrase UI into a separate class
This would allow easier customization.
6 years ago
Roman Zeyde 2e688ccac9
setup: deprecate Python2 support 6 years ago
Roman Zeyde b6181bb5b5
trezor: replace tk-based pinentry with GnuPG pinentry 6 years ago
Roman Zeyde b6da299cb0
pinentry: add simple wrapper for PIN/passphrase entry 6 years ago
Roman Zeyde 04627f0899
gpg: collect OPTIONs from agent 6 years ago
Roman Zeyde 54ce6f2cec
trezor: limit passphrase length 6 years ago
Roman Zeyde a1047ba7b1
Bump version: 0.9.8 → 0.10.0 6 years ago
Roman Zeyde e90bd0cd81
trezor: refactor transport enumeration a bit 6 years ago
slush 66e3e60370 trezor: Use composite transport for device detection. 6 years ago
slush 3f1604d609 Use Python3 by default 6 years ago
slush d0f4cccfd2 trezor: Both Trezor One and Model T are supported. 6 years ago
Roman Zeyde 08d81c992c
trezor: split pinentry tool into a separate file 6 years ago
Roman Zeyde 55a899f929
trezor: initialize cached_passphrase_ack with None (instead of 0) 6 years ago
Roman Zeyde e7604dff68
ssh: fix small commandline documentation nits 6 years ago
rendaw 8849545700 Clarify a couple sentences 6 years ago
rendaw d109cd73b5 Adjust ssh systemd instructions; cleanup 6 years ago
rendaw 95e98d6eda Merge remote-tracking branch 'upstream/master' into doc-enhancements2 6 years ago
rendaw 9e78d52721 SSH clarification 6 years ago
Roman Zeyde 2a76ef6819
gpg: notice encryption of gpg-agent logs (for privacy) 6 years ago
Roman Zeyde 654a3c465a
Merge remote-tracking branch 'rendaw/systemd-instructions' 6 years ago
Roman Zeyde 2168115b06
ssh: fixup small refactoring bug 6 years ago
Roman Zeyde 4a9140c42d
Merge branch 'serge' 6 years ago
Roman Zeyde b20d98bf57
gpg: move socket path generation into a helper function 6 years ago
Roman Zeyde 199fb299c3
gpg: use 'sys.exit' and log homedir 6 years ago
rendaw 06e169f141 Add instructions for using SSH agent as systemd unit 6 years ago
rendaw 131111bc0e Add additional information to docs; collect and reorganize sections 6 years ago
Roman Zeyde f4208009e0
trezor: init_device before failing PIN/passphrase entry 6 years ago
Roman Zeyde 73d60dbec0
gpg: cache the passphrase for better UX 6 years ago
Roman Zeyde 34ea224290
gpg: the scripts should be only user-readable 6 years ago
Roman Zeyde 7803026f61
gpg: allow setting passphrase from environment variable
as done by TREZOR's client library
6 years ago
Roman Zeyde 34ce1005fd
build: add simple script for PyPI release 6 years ago
Roman Zeyde 8677c8ebaa
trezor_agent: fix broken PyPI package 6 years ago
Serge Pokhodyaev 6363eb0d4a add -f/--foreground option to run as systemd service 6 years ago
Serge Pokhodyaev a32bfc749b don't overwrite homedir 6 years ago
Roman Zeyde 75d117ad0d
Bump version: 0.9.7 → 0.9.8 6 years ago
Roman Zeyde cefc5f180a
ssh: add --sock-path flag to explicitly specify SSH agent's UNIX socket 6 years ago
Roman Zeyde 0f5c71b748
ssh: add --log-file flag 6 years ago
Roman Zeyde d5f97b7efa
Update README title 6 years ago
Roman Zeyde 4a12bfa0b7
Allow SSH agent to daemonize when invoked with `-d` flag
This change adds the support for "eval `trezor-agent -d`" invocation.
6 years ago
Roman Zeyde cac889ff7d
Update trezorlib dependency for trezor_agent 6 years ago
Roman Zeyde 92c6e680ed
doc: add python-tk dependency
Following #194
6 years ago
Roman Zeyde bf294beb56
gpg: decode stdout as UTF-8 6 years ago
Roman Zeyde 713345918e
ssh: document sub-shell mode 7 years ago
Roman Zeyde eb60c2f475
fix more pylint issues 7 years ago
Roman Zeyde 6d8d43db9b
fix pylint issues 7 years ago
Roman Zeyde 3e67bc9f0e
gpg: log GnuPG commands' output 7 years ago
Roman Zeyde 38b50485de
ssh: remove old demo from README 7 years ago
Roman Zeyde 9cba27b31a
Merge pull request #188 from eli-b/patch-2
README-SSH.md: spelling
7 years ago
Eli Boyarski 00a65a9820
README-SSH.md: spelling 7 years ago
Roman Zeyde 52ad601e66
Merge pull request #187 from eli-b/patch-1
INSTALL.md: update the Ledger Nano S udev link
7 years ago
Eli Boyarski d96a2820ff
INSTALL.md: update the Ledger Nano S udev link 7 years ago
Roman Zeyde 29aaf777ad
Bump version: 0.9.6 → 0.9.7 7 years ago
Roman Zeyde 385fc9457b
Support multiple devices 7 years ago
Jonathan Roelofs 9cf73f677a Show libagent version too 7 years ago
Jonathan Roelofs ec97cd0c44 Implement #182 7 years ago
Jonathan Roelofs 4cd7dc02eb Fix an argparse nuance
https://bugs.python.org/issue16308
7 years ago
Roman Zeyde 8fe9460ed6
trezor: allow UDP connection (for emulator) 7 years ago
Roman Zeyde db16aa3d1c
trezor: update to latest trezorlib version 7 years ago
Roman Zeyde 41ccd2f332
fix new pylint warning 7 years ago
Roman Zeyde cb14d1e00b
Bump version: 0.9.5 → 0.9.6 7 years ago
Roman Zeyde cc6ee31deb
add .bumpversion.cfg 7 years ago
Roman Zeyde b1f302151b
tests: fix test_server.py 7 years ago
Roman Zeyde fde50f04ab
Merge branch 'config' 7 years ago
Roman Zeyde 7e42e455a1
gpg: add an example for adding new user ID 7 years ago
Roman Zeyde 13cd6be2d1
travis: pep8 -> pycodestyle 7 years ago
Roman Zeyde 40469c4100
tox: pep8 -> pycodestyle 7 years ago
Roman Zeyde 0d059587a7
ssh: allow configuration from a file 7 years ago
Roman Zeyde 283cb3d7e8
setup: add ConfigArgParse 7 years ago
Roman Zeyde 51cc716e3f
Merge pull request #169 from dirkx/master
Some background and designn rationale for the use of derived keys
7 years ago
Roman Zeyde 8b4850b0ce Explain rationale better, several typos fixed, include warning about keepkey not yet supporting encryption/decryption. 7 years ago
Roman Zeyde f22c07e970
trezor: retry in case of invalid PIN 7 years ago
Roman Zeyde 29c7234ef4
trezor: make sure scrambled PIN is valid 7 years ago
Roman Zeyde 1942e3999b
ssh: fix exception type for missing device 7 years ago
Roman Zeyde f2e52a88be
` -> ' 7 years ago
Roman Zeyde b26a4cc7b0
A few small fixes 7 years ago
Roman Zeyde c4dfca04f2
trezor: use UI-based passphrase entry
Now TREZOR_PASSPHRASE environment variable is ignored.
7 years ago
Roman Zeyde a1ecbf447e
gpg: return correct keygrip for KEYINFO assuan command 7 years ago
Roman Zeyde 1f9d457e92
gpg: no need to remove the UNIX socket
Our agent should be invoked and re-used when running 'gpg --import'.
7 years ago
Roman Zeyde cb3477fc69
Merge branch 'which-fix' 7 years ago
Roman Zeyde 9bbc66cc16
util: add backport for shutil.which() 7 years ago
Roman Zeyde 06afc971db
Merge pull request #166 from aitorpazos/master
doc: Include python3 support in OpenSUSE instructions
7 years ago
Dirk-Willem van Gulik 2b51a85c26 Rework order of paragraphs for clarity II 7 years ago
Dirk-Willem van Gulik 1906e6d9b0 Rework order of paragraphs for clarity. 7 years ago
Dirk-Willem van Gulik b3f6e39b48 First cut at a design rationale 7 years ago
Aitor Pazos 8b03b649d5
doc: Include python3 support in OpenSUSE instructions 7 years ago
Roman Zeyde 90cbc41b17
gpg: refactor messagebox UI from PIN entry 7 years ago
Roman Zeyde 4926d4f4d3
gpg: set PATH explicitly for $DEVICE-gpg-agent 7 years ago
Roman Zeyde d52f295326
gpg: use shutil.which() for Python 3 7 years ago
Max Pixel 47a8a53247
Add OpenSUSE-specific installation instructions
The packages required are slightly different on OpenSUSE.
7 years ago
Roman Zeyde 9530c4d7db
gpg: use gpgconf for getting gpg binary path 7 years ago
Roman Zeyde a2d0c1067d
gpg: don't hardcode Python binary 7 years ago
Roman Zeyde 3d5717dca1
gpg: use a separate process for PIN entrering UI 7 years ago
Roman Zeyde 08fef24e39
gpg: use pymsgbox for PIN entrering UI 7 years ago
Roman Zeyde bab46dae5c
gpg-agent: use correct GNUPGHOME when getting public keys 7 years ago
Roman Zeyde e2625cc521
gpg: fail if no public keys are found 7 years ago
Roman Zeyde 7ed76fe472
gpg: use correct GNUPGHOME for gpgconf 7 years ago
Roman Zeyde a5929eed62
gpg: create config files first 7 years ago
Roman Zeyde 5f722f8ae1
logging: add more DEBUG information 7 years ago
Roman Zeyde 7212b2fa37 Merge pull request #156 from codeHatcher/feature/155
add documentation related to #155 to help other macOS/OSX users who are
7 years ago
Avishaan 55e1c614a7 add documentation related to #155 to help other macOS/OSX users who are
still using system python
7 years ago
Roman Zeyde 8cf1f0463a
device: release HID handle before failing 7 years ago
Roman Zeyde f177b0b55a
bump version 7 years ago
Roman Zeyde b2450d448c
Merge branch 'gpg-init' 7 years ago
Roman Zeyde 93e5f0cd8b
gpg: update README for latest CLI 7 years ago
Roman Zeyde 9998456fe0
ledger: add DEBUG logging 7 years ago
Roman Zeyde 0f85ae6e2c
Rewrite gpg-init Bash script in Python 7 years ago
Roman Zeyde 44cdeed024
Merge branch 'fix-gpg-prefs' 7 years ago
Roman Zeyde 867e2cfd1b
gpg: add MDC support 7 years ago
Roman Zeyde df6ddab2cf
gpg: add compression and stronger digests 7 years ago
Roman Zeyde 5b9f03d198
gpg: show warnings while importing new pubkey 7 years ago
Roman Zeyde 06ea890095
gpg: add note regarding Pinentry 7 years ago
Roman Zeyde 0999a85529
gpg: add documentation for subkey generation 7 years ago
Roman Zeyde 835f283ccf
gpg: support multiple keygrips for HAVEKEY command 7 years ago
Roman Zeyde f57dbb553f
gpg: allow setting trezor-gpg arguments via gpg-init script 7 years ago
Roman Zeyde a890dcc085
gpg: add MDC support 7 years ago
Roman Zeyde c8ed4a223a
gpg: add compression and stronger digests 7 years ago
Roman Zeyde 1ef96bed03
gpg: handle NOP assuan command 7 years ago
Roman Zeyde e4fdca08e5
ssh: fix identity stringification 7 years ago
Roman Zeyde 51b297e93b
doc: add Enigmail tutorial 7 years ago
Roman Zeyde c22c959cf9
doc: move READMEs to separate directory 7 years ago
Roman Zeyde 3199cb964a
Merge branch 'timestamplookup' 7 years ago
Roman Zeyde c5f245957d
README: add spaces around '|' operators 7 years ago
Chris Cowan fbb3059a0b README: Add a note about how to fetch a key's timestamp. 7 years ago
Roman Zeyde e922f45871
bump version 7 years ago
Roman Zeyde 377af1466c
gpg: detect installed GnuPG binary 7 years ago
Roman Zeyde b7743e12a5
gpg: support GnuPG 2.2.x default installation 7 years ago
Roman Zeyde 48d5630561
gpg: fix identity stringification 7 years ago
Roman Zeyde b88dff8430
ssh: fix identity stringification 7 years ago
Roman Zeyde 2af1086ed8
bump version 7 years ago
Roman Zeyde 7e95179128
ssh: fix unicode identity handling for Python 2 7 years ago
Roman Zeyde ac8898a434
gpg: allow using 'gpg' instead of 'gpg2' 7 years ago
Roman Zeyde 0b829636e1
ssh: close stdin when running subshell 7 years ago
Roman Zeyde 7598f6cdbf
Merge branch 'travis' 7 years ago
Roman Zeyde c8bf57cbcc
travis: fix setuptools issue 7 years ago
Roman Zeyde 62af49236c Merge pull request #132 from romanzolotarev/patch-1
Fix link in README-GPG.md
7 years ago
Roman Zolotarev af3f669780 Update README-GPG.md 7 years ago
Roman Zeyde 1520dbd8b9
gpg: add a screencast for identity re-generation 7 years ago
Roman Zeyde 4f05d51e9b
bump version 7 years ago
Roman Zeyde 9d38c26a0f
gpg: add 'fake' device for GPG-integration testing 7 years ago
Roman Zeyde 3a9330b995
gpg: return SCD version from agent 7 years ago
Roman Zeyde f904aac92e
Allow unicode in identity string for SSH and GPG 7 years ago
Roman Zeyde ca67923fe8
INSTALL: add instructions for Fedora/RedHat 7 years ago
Roman Zeyde ce90e61eb2
README: add troubleshooting section for GPG 7 years ago
Roman Zeyde 90dc124e8d
install: note '~/.local/bin' issue with pip 7 years ago
Roman Zeyde 442bf725ef
gpg: fail SCD-related requests 7 years ago
Roman Zeyde 5820480052
GPG: set default user ID for signing during gpg-init 7 years ago
Roman Zeyde ae2a84e168
INSTALL: comment about Ledger's 'SSH/PGP Agent' app requirement 7 years ago
Roman Zeyde f6911a0016
pin: use PyQt only when running with no TTY 7 years ago
Roman Zeyde 69c54eb425
device: allow Qt-based PIN entry for Trezor/Keepkey 7 years ago
Roman Zeyde 931573f32b
gpg: echo during identity initialization 7 years ago
Roman Zeyde 1e8363d4fc
gpg: refactor client usage at agent module
This allows caching public keys (as done in the SSH agent).
7 years ago
Roman Zeyde b7113083b4
README: add 'git config' for enabling commit signing 7 years ago
Roman Zeyde b5c4eca0d2
README: elaborate trezor/ledger usage for GPG 7 years ago
Roman Zeyde 8aa08d0862
INSTALL: remove duplicate line 7 years ago
Roman Zeyde b452b49f4c
README: note qtpass for password management 7 years ago
Roman Zeyde 639c4efb6d
README: add links to products 7 years ago
Roman Zeyde 6f1686c614
README: remove extra parentheses 7 years ago
Roman Zeyde 300d9a7140
README: update GPG screencasts and other examples 7 years ago
Roman Zeyde b143bafc70
README: remove PyPI badges 7 years ago
Roman Zeyde f2c6b6b9c1
README: move installation to a separate file 7 years ago
Roman Zeyde e0507b1508
tests: cover file-based logging case 7 years ago
Roman Zeyde 85274d8374
bump version 7 years ago
Roman Zeyde f358ca29d4
Allow loading previously exported SSH public keys from a file 7 years ago
Roman Zeyde 53d43cba29
gpg: update README after re-packaging 7 years ago
Roman Zeyde 214b556f83
ssh: simplify argparse creation 7 years ago
Roman Zeyde 051a3fd4ab
ssh: remove unused git-related code 7 years ago
Roman Zeyde 91050ee64a
ssh: remove unused git wrapper 7 years ago
Roman Zeyde 257992d04c
ssh: move related code to a separate subdirectory 7 years ago
Roman Zeyde 6c2273387d
util: extend logging.basicConfig() 7 years ago
Roman Zeyde b6ad8207ba
setup: add Python 3.6 support 7 years ago
Roman Zeyde 3a93fc859d
setup: fix indentation for lists 7 years ago
Roman Zeyde 7d9b3ff1d0
README: update device-related info 7 years ago
Roman Zeyde 4af881b3cb
Split the package into a shared library and separate per-device packages 7 years ago
Roman Zeyde eb525e1b62
gpg: simplify Python entry point and refactor Bash scripts a bit
Now there is a single 'trezor-gpg' tool, with various subcommands.
7 years ago
Roman Zeyde 02c8e729b7
ssh: retrieve all keys using a single device session 7 years ago
Roman Zeyde 12359938ad
keepkey: fix transport import 7 years ago
Roman Zeyde 93cd3e688b
travis: add Python 3.6 7 years ago
Tomás Rojas 26d7dd3124
Cache public keys for the duration of the agent
This saves a lot of time when connecting to multiple hosts
simultaneously (e.g., during a deploy) as every time we are asked to sign a
challenge, all public keys are iterated to find the correct one. This
can become especially slow when using the Bridge transport and/or many
identities are defined.
7 years ago
Tomás Rojas 0d5c3a9ca7
Allow using TREZOR bridge (instead of HID transport) 7 years ago
Roman Zeyde 97ec6b2719
travis: fix dependency 7 years ago
Roman Zeyde 8ba9be1780
fix pylint warnings 7 years ago
Roman Zeyde b2bc87c0c7
fix pydocstyle warnings 7 years ago
Timothy Hobbs d522d148ef Connection error is confusing (#105)
Hi,

I ran into this connection error:

````
> trezor-agent  timothy@localhost
2017-04-10 00:22:01,818 ERROR        Connection error: open failed                                                                        [__main__.py:130]
````

I didn't know what was going on, whether there was a problem connecting to localhost or what. I eventually logged into trezor wallet and found that there too, my device was not recognized (probably because I did not unplug it/replug it after updating the HID settings.) Unplugging it and replugging it fixed everything.
7 years ago
Roman Zeyde c796a3b01d
README: password manager usage example 7 years ago
Roman Zeyde a3362bbf3e
bump version 7 years ago
Nubis 9a271d115b
Allow contents in buffer when using _legacy_pubs 7 years ago
Roman Zeyde 6a7165298f
decode: skip invalid pubkeys (instead of crashing) 7 years ago
Roman Zeyde c4f3fa6e04
agent: reply correctly to HAVEKEY requests 7 years ago
Roman Zeyde 8a77fa519f
decode: raise an error when keygrip is missing 7 years ago
Roman Zeyde 59560ec0b0
util: add simple memoization decorator 7 years ago
Roman Zeyde 7a91196dd5
agent: add link to HAVEKEY implementation 7 years ago
Roman Zeyde 43c424a402
ssh: allow "just-in-time" connection for agent-like behaviour
This would allow launching trezor-agent into the background
during the system startup, and the connecting the device
when the cryptographic operations are required.
8 years ago
Roman Zeyde 6672ea9bc4
device: set passphrase from environment 8 years ago
Roman Zeyde 002dc2a0e0
tox: order imports 8 years ago
Roman Zeyde 61ced2808f
device: allow non-empty passphrases 8 years ago
Roman Zeyde 71a8930021
bump version 8 years ago
Roman Zeyde 74e8f21a22
gpg: export secret subkey 8 years ago
Roman Zeyde 897236d556
gpg: allow decoding secret keys 8 years ago
Roman Zeyde 5bec0e8382
README: upgrade also pip 8 years ago
Roman Zeyde 3cb7f6fd21
gpg: export secret primary key 8 years ago
Roman Zeyde cad2ec1239
device: import device-specific `defs` module lazily
It may fail on unsupported platforms (e.g. keepkeylib does not supoprt Python 3)
8 years ago
Roman Zeyde 604b2b7e99
gpg: allow GPG 2.1.11+ (to support Ubuntu 16.04 & Mint 18) 8 years ago
Roman Zeyde 159bd79b5f
gpg: list fingerpints explicitly during init 8 years ago
Roman Zeyde dde0b60e83 Merge pull request #87 from aceat64/bugfix/mosh
Mosh doesn't support "-l" for user, only user@host for args
8 years ago
Andrew LeCody 109bb3b47f Mosh doesn't support "-l" for user, only user@host for args 8 years ago
Roman Zeyde 0f20bfa239
README: add a note about udev 8 years ago
Roman Zeyde 798597c436
bump version 8 years ago
Roman Zeyde a13b1103f7
setup: temporary pin dependency for keepkey compatibilty 8 years ago
Roman Zeyde 9fe1a235c1
gpg: check that the configuration is in place 8 years ago
Roman Zeyde f86aae9a40
gpg: check that GnuPG v2 is installed 8 years ago
Roman Zeyde fc070e3ca0
GPG: add OS X installation link to README 8 years ago
Roman Zeyde 05fac995eb
README: add 'setup.py' installation 8 years ago
Roman Zeyde 188b74b327
gpg: use explicit '--subkey' flag for adding a subkey to an existing GPG key 8 years ago
Roman Zeyde fc31847f8e
decode: add test for custom markers 8 years ago
Roman Zeyde 0faf21a102
README: easier tag signature verification 8 years ago
Roman Zeyde 6b82f8b9b7
keyring: add test for get_agent_sock_path() 8 years ago
Roman Zeyde fabfcaaae2
keyring: fix test case for iterlines() 8 years ago
Roman Zeyde f0f89310ac
main: add '--mosh' for better SSH client 8 years ago
Roman Zeyde 47ff7c5cb3
gpg: add usage example for GPA 8 years ago
Roman Zeyde 0440025083
gpg: use explicit function to check for custom subpacket marker 8 years ago
Roman Zeyde c49fe97f63
gpg: remove unused parser for literal packets 8 years ago
Roman Zeyde 7f8abcb5c5
client: remove unused code 8 years ago
Roman Zeyde e13039e52d
gpg: remove property method and unused member variable from PublicKey 8 years ago
Roman Zeyde c420571eb8
gpg: import test coverage for protocol 8 years ago
Roman Zeyde 827119a18d
gpg: handle KILLAGENT command
so `gpg-connect-agent KILLAGENT` should stop the running agent
8 years ago
Roman Zeyde 9be6504658
util: import test coverage 8 years ago
Roman Zeyde 07cbe65875
formats: improve test coverage 8 years ago
Roman Zeyde 180120e787
README: link to trezor-ssh-agent for Windows support 8 years ago
Roman Zeyde f4ce81fa94
README: add 1.4.0-related post 8 years ago
Roman Zeyde 176bf4ef7c
README: add Hg demo 8 years ago
Roman Zeyde d22cd7512d
gpg: launch agent before starting the shell 8 years ago
Roman Zeyde 83f17704cb
server: remove 'SSH_AUTH_SOCK=' from logging 8 years ago
Roman Zeyde 92f6751ccb
README: multiple SSH identities from config file 8 years ago
Roman Zeyde abe80533eb
README: trim whitespace 8 years ago
Roman Zeyde de51665c71
bump version 8 years ago
Roman Zeyde c30e5f5a67
gpg: add optional arguments to `gpg-shell` 8 years ago
Roman Zeyde 2eab2a152c
device: verify keepkey constraints 8 years ago
Roman Zeyde 5e93d97be3
Merge branch 'ssh-ids' 8 years ago
Roman Zeyde 4c8fcd6714
ssh: use special UNIX socket name 8 years ago
Roman Zeyde ee593bc66e
gpg: show user ID on a single line 8 years ago
Roman Zeyde dbed773e54
fix pylint and tests 8 years ago
Roman Zeyde ac4a86d312
ssh: remove git utility 8 years ago
Roman Zeyde 021831073e
ssh: simple support for multiple public keys loading 8 years ago
Roman Zeyde 6a5acba0b0
gpg: decouple identity from device 8 years ago
Roman Zeyde 9123cef810
ssh: decouple identity from device 8 years ago
Roman Zeyde 6f6e7c0bcc
device: allow loading identities from a file (instead of argument) 8 years ago
Roman Zeyde 47ff081525
bump version 8 years ago
Roman Zeyde 6d53baafe2
setup: add 'device' package 8 years ago
Roman Zeyde 317b672add
setup: support Python 3.5 8 years ago
Roman Zeyde 9964c200ff
travis: install keepkey from GitHub 8 years ago
Roman Zeyde 75405b4944
gpg: allow PIN entry before starting GPG shell 8 years ago
Roman Zeyde e74b9c77af
gpg: rename gpg.device into gpg.client 8 years ago
Roman Zeyde c2158947c8
Merge branch 'refactor-device' 8 years ago
Roman Zeyde e39d5025d5 travis: use pip to install trezor-agent 8 years ago
Roman Zeyde efdb9fcfb5 gpg: fix bytes/str issue with GPG user ID 8 years ago
Roman Zeyde a20b1ed2a8 factory: remove obsolete code 8 years ago
Roman Zeyde ca507126d6 gpg: use new device package (instead of factory) 8 years ago
Roman Zeyde 0f79b5ff2e ssh: use new device package (instead of factory) 8 years ago
Roman Zeyde 946ab633d4 device: move device-related code into a separate package 8 years ago
Roman Zeyde 4108c9287f
bump version 8 years ago
Roman Zeyde d9cb75e95d
gpg: use `which gpg2` for 'gpg.program' git configuration
This enables git-cola support TREZOR-based commit signatues,
together with "cola.signcommits=true" setting.
8 years ago
Roman Zeyde 2cecd2ed08
pylint: enable "fixme" 8 years ago
Roman Zeyde 05f40085b2
gpg: remove spurious space 8 years ago
Roman Zeyde c7346d621d
gpg: use policy URI subpacket for marking our public keys
keybase.io does not support experimental/private subpacket IDs
8 years ago
Roman Zeyde 0342b39465
gpg: allow setting key creation timestamp 8 years ago
Roman Zeyde fa6d8564b9
gpg: use SHA-512 as default hash for signatures 8 years ago
Roman Zeyde e09712c793 gpg: add OS X support for gpg-init 8 years ago
Roman Zeyde 0cbb3bb9fa Merge pull request #67 from romanz/concurrent-handler
Concurrent SSH handler
8 years ago
Roman Zeyde d7a6641ffa gpg: update screencasts 8 years ago
Roman Zeyde 6fe89241c4 Merge branch 'auto-spawn-agent' 8 years ago
Roman Zeyde c5262d075b gpg: use 'gpg-agent.conf' to configure trezor-gpg-agent
currently support logfile and logging verbosity
8 years ago
Roman Zeyde 683d24f4eb gpg: use gpg.conf to automatically spawn trezor-gpg-agent 8 years ago
Roman Zeyde 921e2954c1 gpg: support more digests (with larger output than 256 bits)
NIST256 signs the prefix of a longer digest.
Ed25519 signs the whole one.
8 years ago
Roman Zeyde 3f784289d8 gpg: allow setting CURVE from environment 8 years ago
Roman Zeyde 04d790767d gpg: don't fail on non-zero shell exit code 8 years ago
Roman Zeyde 97efdf4a45 ssh: handle connections concurrently 8 years ago
Roman Zeyde ee2f6b75dc server: log SSH version for debugging 8 years ago
Roman Zeyde a26f0ea034 README: make tag example clearer 8 years ago
Roman Zeyde a68f1e5c26 gpg: update README for easier usage 8 years ago
Roman Zeyde 93e3c66a15 gpg: notify the user for confirmation 8 years ago
Roman Zeyde 44eaaa6b9c gpg: don't spawn gpg-shell automatically 8 years ago
Roman Zeyde b83d4960e7 gpg: run gpg-shell in verbose mode 8 years ago
Roman Zeyde 75fe7b4e05 gpg: improve shell helper scripts
- explicit trust configuration
- less debug prints
8 years ago
Roman Zeyde 742136b22d gpg: add helper scripts 8 years ago
Roman Zeyde 513e99dd57 server: refactor server_thread() to decouple it from handle_connection() 8 years ago
Roman Zeyde 1bd6775c35 gpg: replace -s flag by implicit adding to existing GPG key 8 years ago
Roman Zeyde aaade1737f gpg: comment about digest size 8 years ago
Roman Zeyde fe185c190e ledger: move factory-related code to a separate file 8 years ago
Roman Zeyde 1bc0165368 setup: update trezorlib dependency 8 years ago
Roman Zeyde 0f841ffbc4 factory: add Python 3 support for Ledger 8 years ago
Roman Zeyde b2942035a3 gpg: skip "progress" status messages 8 years ago
Roman Zeyde 215b64f253 gpg: fix comment 8 years ago
Roman Zeyde 79e68b29c2 bump version 8 years ago
Roman Zeyde 8265515641 gpg: fix small Python2/3 issue 8 years ago
Roman Zeyde 749799845d bump version 8 years ago
Roman Zeyde eaea35003e gpg: remove unused function (_time_format) 8 years ago
Roman Zeyde eefb38ce83 gpg: remove unused function (_verify_keygrip) 8 years ago
Roman Zeyde 0730eb7223 gpg: use same logging configuration as in SSH 8 years ago
Roman Zeyde 5b61702205 gpg: don't crash gpg-agent on error 8 years ago
Roman Zeyde 0ad0ca3b9a README: add a note about SSH incompatible options 8 years ago
Roman Zeyde 2843cdcf41 ssh: pretty-print user name 8 years ago
Roman Zeyde c7bc78ebe7 Merge pull request #58 from romanz/keygrip-agent
gpg: replace TREZOR_GPG_USER_ID usage in gpg-agent mode
8 years ago
Roman Zeyde a6d9edcb0b README: update for new user ID specification for GPG 8 years ago
Roman Zeyde bc64205a85 gpg: replace TREZOR_GPG_USER_ID usage in gpg-agent mode
Use the keygrip to find the correct public key instead.
8 years ago
Roman Zeyde 34dc803856 README: add "user@" for SSH example usage
This should help when local username and remote username are different.
8 years ago
Roman Zeyde f7ebb02799 isort: fix imports 8 years ago
Roman Zeyde 0ba33a5bc4 gpg: document agent responses 8 years ago
Roman Zeyde 13752ddcd5 gpg: require latest GPG version 8 years ago
Roman Zeyde 487a8e56c4 gpg: add keygrip logic into decoding 8 years ago
Roman Zeyde ef56ee4602 gpg: remove verifying logic from decoding 8 years ago
Roman Zeyde ae381a38e5 gpg: export keygrips from protocol 8 years ago
Roman Zeyde 446ec99bf4 gpg: remove complex pubkey parsing code 8 years ago
Roman Zeyde 80c6f10533 README: correct pip commands order 8 years ago
Roman Zeyde ff984c60e4 README: link to PIN entering instructions 8 years ago
Roman Zeyde c9bc079dc9 gpg: add file:line to logging format 8 years ago
Roman Zeyde 65d2c04478 gpg: fix agent module to work with Python 3 8 years ago
Roman Zeyde 2d57bf4453 gpg: beter logging while search for GPG key 8 years ago
Roman Zeyde 79b6d31dfe gpg: raise proper exception when keygrip mismatch is detected 8 years ago
Roman Zeyde 7de88a3980 gpg: add comment for stopping current gpg-agent 8 years ago
Roman Zeyde 6f8d0df116 bump version 8 years ago
Roman Zeyde b4a382d22e Merge pull request #51 from romanz/curve25519
Curve25519
8 years ago
Roman Zeyde d236f4667e gpg: allow Curve25519 for ECDH 8 years ago
Roman Zeyde 42813ddbb4 gpg: parse curve OID from public key to select curve name 8 years ago
Roman Zeyde 8f19690943 gpg: support Curve25519 for creating encryption subkeys 8 years ago
Roman Zeyde 5047805385 gpg: move HardwareSigner to device module 8 years ago
Roman Zeyde 915b326da7 gpg: simplify AgentSigner and move to keyring module 8 years ago
Roman Zeyde e7b8379a97 factory: explicitly only the first interface 8 years ago
Roman Zeyde 26435130d7 factory: emit warning (instead of exception) when an import fails 8 years ago
Roman Zeyde dfde6dbee4 bump version 8 years ago
Cédric Félizard 085a3e81c7 Add explanation about entering PIN (#47)
[ci skip]
8 years ago
Cédric Félizard 3082d61deb Fix typo (#48) 8 years ago
Roman Zeyde e3286a4510 gpg: don't clear the session after PIN is entered
This would allow single PIN entry when running multiple GPG commands.
8 years ago
Roman Zeyde fcd5671626 Handle keyinfo request (#44)
gpg: handle KEYINFO request

See https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=agent/command.c;h=9522f898997e95207d59122d056f0f0be03ccecb;hb=6bee88dd067e03e7767ceacf6a849d9ba38cc11d#l1027 for more details.
8 years ago
Roman Zeyde 1454d2f4d7 Merge pull request #43 from romanz/fix-gpg-manual
GPG: fix installation instructions
8 years ago
Roman Zeyde 9b395363a3 GPG: fix installation instructions 8 years ago
Roman Zeyde 5bb9dd7770 Merge pull request #42 from romanz/ssh-git
README: add an example for remote git repository
8 years ago
Roman Zeyde 51df023a23 README: add an example for remote git repository 8 years ago
Roman Zeyde d74f375637 Merge pull request #41 from romanz/protobuf-note
README: add note about protobuf permissions issue
8 years ago
Roman Zeyde 1fd0659051 README: add note about protobuf permissions issue 8 years ago
Roman Zeyde 18be290bd6 Merge branch 'fix_agent' of https://github.com/Solution4Future/trezor-agent into Solution4Future-fix_agent 8 years ago
Roman Zeyde a1ab496bf4 Merge branch 'ledger' 8 years ago
Roman Zeyde 784e14647a Merge branch 'master' into HEAD
Conflicts:
	trezor_agent/factory.py
8 years ago
Dominik Kozaczko 7d2c649e83
don't stop polling for more devices as having more than one inserted raises more problems and we need to keep the check 8 years ago
Dominik Kozaczko cf27b345f6
better handling of keepkey dependency; fixes #36 8 years ago
Dominik Kozaczko 386ed5a81f Merge pull request #1 from romanz/master
pull changes from upstream
8 years ago
Roman Zeyde 5a64954324 Merge pull request #37 from Solution4Future/fix_agent
removed .decode('ascii') and added missing bytestrings and also some missing deps
8 years ago
Dominik Kozaczko 3aebd137b0
removed .decode('ascii') and added missing bytestrings 8 years ago
Nicolas Pouillard 1fa35e7f1a Fix the URL for the TREZOR firmware 8 years ago
Roman Zeyde aeda85275d bump version 8 years ago
Roman Zeyde e41206b350 setup: update trezorlib version 8 years ago
Roman Zeyde 03650550dd Use latest protobuf library (for native Python 3 support) 8 years ago
Roman Zeyde f7b07070da README: update setuptools to the latest version 8 years ago
Roman Zeyde 96eede9c83 Merge branch 'np-encode-subpackets' 8 years ago
Roman Zeyde 91146303a3 Follow GPG implementation for subpacket prefix encoding.
Conflicts:
	trezor_agent/gpg/protocol.py
8 years ago
Roman Zeyde bf598435fb client: keep the session open (doesn't forget PIN) 8 years ago
Roman Zeyde 459b882b89 ledger: don't use debug=True 8 years ago
Roman Zeyde 998c9ee958 README: update usage section 8 years ago
Roman Zeyde d408a592aa README: get only the first lines of 'trezorctl get_features' 8 years ago
Roman Zeyde 282e91ace3 update README about protobuf issueOF 8 years ago
Roman Zeyde 23c37cf1e3 README: update TREZOR's required version 8 years ago
Roman Zeyde 5c5c6f9cbb bump version 8 years ago
Roman Zeyde 17c8bd0e92 gpg: add experimental warning 8 years ago
Nicolas Pouillard 016e864503
Attempt at fixing issue #32 8 years ago
Roman Zeyde 57e09248db Merge pull request #31 from romanz/master
Update ledger branch with the latest changes from master branch
8 years ago
Roman Zeyde 0c4e67c837 Merge pull request #30 from np/decode-subpackets
gpg/decode/parse_subpackets: parse subpacket length according to RFC
8 years ago
Nicolas Pouillard adcbe6e7b2
gpg/decode/parse_subpackets: parse subpacket length according to RFC 8 years ago
Roman Zeyde 73bdf417e4 factory: require TREZOR firmware v1.4.0+ for GPG signatures and decryption 8 years ago
Roman Zeyde ee347252b4 README: update gitter badge position 8 years ago
Roman Zeyde d63f048b78 gpg: update trezor-agent installation instruction (using pip) 8 years ago
Roman Zeyde 05fada91d2 gpg: use gpgconf to get correct GPG agent UNIX socket path 8 years ago
Roman Zeyde 27a3fddfa2 gpg: add a note about restoring GPG keys with --time command-line flag 8 years ago
Roman Zeyde 030ae4c3f6 gpg: include unsupport hash algorithm ID in exception message 8 years ago
Roman Zeyde 4897b70888 factory: fix pylint import-error warnings 8 years ago
Roman Zeyde f4ecd47ed6 factory: fix pep8 and pylint warnings 8 years ago
Roman Zeyde c4bbac0e77 util: move BIP32 address related functions 8 years ago
BTChip 5d0b0f65d3 Merge branch 'ledger' of https://github.com/btchip/trezor-agent into ledger 8 years ago
BTChip 33747592ca Fix eddsa, SSH optimization with signature + key, cleanup 8 years ago
BTChip adb09cd8ca Ledger integration 8 years ago
Roman Zeyde 45f6f1a3d8 gpg: allow setting GPG home directory via $GNUPGHOME 8 years ago
Roman Zeyde c4c56b9faf gpg: no support for empty user_id 8 years ago
BTChip bc1d7a5448 Fix eddsa, SSH optimization with signature + key, cleanup 8 years ago
BTChip 8fe16d24c2 Ledger integration 8 years ago
Roman Zeyde 1704ae7683 gpg: add '--user' flag to pip install command 8 years ago
Roman Zeyde a7190223fd gpg: note WIP status in README 8 years ago
Roman Zeyde 220735c6ad gpg: note WIP status in README 8 years ago
Roman Zeyde 82e08d073b gpg: rename proto -> protocol 8 years ago
Roman Zeyde 8ab0908388 proto: don't hardcode name length 8 years ago
Roman Zeyde fd3183d71c gitignore: sublime text project files 8 years ago
Roman Zeyde 295d52ef10 gpg: move 'iterlines' to keyring 8 years ago
Roman Zeyde 8a51099488 gpg: remove unused "sign_message" 8 years ago
Roman Zeyde f4dd1eacdd gpg: allow parsing multiple keys 8 years ago
Roman Zeyde 024b5f131f README: reformat links 8 years ago
Roman Zeyde b9b7b8dafd gpg: re-structure public key packets for easier parsing 8 years ago
Roman Zeyde 744696fdee gpg: decode user_attribute packets 8 years ago
Roman Zeyde ccdbc7abfc gpg: parse_packets() should get file-like stream
and wrap it with util.Reader()
8 years ago
Roman Zeyde e70f0ec681 gpg: refactor hash algorithm handling 8 years ago
Roman Zeyde aeaf978d8e gpg: add mulitple GPG public keys as test vectors 8 years ago
Roman Zeyde d60fff202a gpg: don't validate non-ECDSA signatures 8 years ago
Roman Zeyde 9171dd08c8 README: update posts 8 years ago
Roman Zeyde 4c5004d838 Merge pull request #16 from jhoenicke/master
More robust gpg key parsing
8 years ago
Jochen Hoenicke a2e46048a1
Use TREZOR_GPG_USER_ID in agent 8 years ago
Jochen Hoenicke e66b0f47ed
More robust gpg key parsing
Handle new packet format.
Ignore unknown packets.
Handle packets that are not immediately followed by signature.
Handle other hash algorithms.
8 years ago
Roman Zeyde db874ad98f README: add GPG part 8 years ago
Roman Zeyde ed2d71cc08 README: split into main and SSH parts 8 years ago
Roman Zeyde 59b39ce81f Merge branch 'gpg-agent' 8 years ago
Roman Zeyde 75f879edbb gpg: update README.md 8 years ago
Roman Zeyde 45a85a317b gpg: allow setting UNIX socket from command-line 8 years ago
Roman Zeyde 7b3874e6f7 gpg: fixup logging during key creation 8 years ago
Roman Zeyde 6c96cc37b9 gpg: add support for adding subkeys to EdDSA primary GPG keys 8 years ago
Roman Zeyde c98cb22ba4 gpg: use separate derivations for GPG keys 8 years ago
Roman Zeyde d9fbfccd35 gpg: load correct key if ECDH is requested 8 years ago
Roman Zeyde fe4d9ed3c8 gpg: add SLIP-0017 support for ECDH session key generation 8 years ago
Roman Zeyde 092445af71 agent: handle connection errors 8 years ago
Roman Zeyde 602e867c7d gpg: add test for keygrip 8 years ago
Roman Zeyde 16de8cdabc agent: refactor signature and ECDH 8 years ago
Roman Zeyde 7bbf11b631 gpg: refactor key creation 8 years ago
Roman Zeyde 3e41fddcef gpg: add test for ECDH pubkey generation 8 years ago
Roman Zeyde 8108e5400d gpg: support TREZOR-based primary key 8 years ago
Roman Zeyde a1659e0f0d gpg: add preferred symmetric algo 8 years ago
Roman Zeyde 3b139314b6 gpg: refactor sign_message method 8 years ago
Roman Zeyde a05cff5079 gpg: use "gpg2" for 'git config --local gpg.program' 8 years ago
Roman Zeyde 694cee17ac gpg: refactor create_* methods 8 years ago
Roman Zeyde bc281d4411 gpg: use local version 8 years ago
Roman Zeyde 04af6b737b gpg: remove extra param from Factory.from_public_key() 8 years ago
Roman Zeyde 171c746c7e gpg: move agent main code to __main__ 8 years ago
Roman Zeyde 8b5ac14150 gpg: add docstrings 8 years ago
Roman Zeyde 16090cebed pylint: ignore TODOs 8 years ago
Roman Zeyde d2167cd4ff gpg: check keygrip on ECDH 8 years ago
Roman Zeyde 10cbe67c9a gpg: add TODO 8 years ago
Roman Zeyde 29a984eebb gpg: improve flags selection 8 years ago
Roman Zeyde a6660fd5c5 gpg: handle BYE command 8 years ago
Roman Zeyde 2acd0bf3b7 gpg: fix keygrip computation 8 years ago
Roman Zeyde e9f7894d62 ecdh: fixup pubkey ID 8 years ago
Roman Zeyde 56e9d7c776 gpg: allow graceful exit via Ctrl+C 8 years ago
Roman Zeyde e7bacf829c gpg: refactor ecdh case 8 years ago
Roman Zeyde c1c679b541 HACK: support ECDH in agent - note keygrip and ID errors. 8 years ago
Roman Zeyde 49c343df94 HACK: create subkey with ECDH support 8 years ago
Roman Zeyde 7da7f5c256 HACK: fixup tests 8 years ago
Roman Zeyde 39cb5565bf HACK: better line iteration 8 years ago
Roman Zeyde f89c5bb125 HACK: better logging 8 years ago
Roman Zeyde 92649b290f HACK: add preliminary gpg support 8 years ago
Roman Zeyde d9b07e2ac6 gpg: hack agent prototype 8 years ago
Roman Zeyde 6975671cc1 setup: fix protobuf dependency to allow 2.6.1+
protobuf 3.0+ is needed for Python 3 support.
8 years ago
Roman Zeyde f0ea568bb8 gpg: add more UTs for decode 8 years ago
Roman Zeyde 34c614db6e gpg: add more UTs for decode 8 years ago
Roman Zeyde 2bbd335f7e pep8: use tox.ini for configuration 8 years ago
Roman Zeyde af8ad99c7a gpg: add UTs for decode 8 years ago
Roman Zeyde 313271ac06 gpg: move signer.py to __main__.py 8 years ago
Roman Zeyde 969e08140b gpg: add more tests for keyring 8 years ago
Roman Zeyde 39f00af65d gpg: add help for sign arguments 8 years ago
Roman Zeyde 272759e907 gpg: allow dependency injection for subprocess module 8 years ago
Roman Zeyde 4be55156ed gpg: refactor pubkeys' parsing code 8 years ago
Roman Zeyde 80a5ea0f2a gpg: add UTs for keyring 8 years ago
Roman Zeyde 87e50449e5 travis: add Python 3.5 8 years ago
Roman Zeyde dcf35c4267 decode: split _remove_armor() from verify() 8 years ago
Roman Zeyde 7570861765 gpg: fixup signer docstring 8 years ago
Roman Zeyde 339f61c071 gpg: better __repr__ and logging for public keys 8 years ago
Roman Zeyde 3c4fb7a17b gpg: allow pinentry UI via "display=" option 8 years ago
Roman Zeyde a6a0c05f57 keyring: fix more Python 2/3 issues 8 years ago
Roman Zeyde 4c036d2ce7 gpg: fixup str/bytes issues 8 years ago
Roman Zeyde eaa91cfdbd gpg: add tests for basic protocol utils 8 years ago
Roman Zeyde fd61941d0f gpg: fixup subcommand for Python 3
http://bugs.python.org/issue9253#msg186387
8 years ago
Roman Zeyde decd3ddf75 gpg: fixup str/bytes issues 8 years ago
Roman Zeyde 4c07b360cd gpg: fix pep8/pylint warning 8 years ago
Roman Zeyde 0b0f60dd89 gpg: rename load_from_gpg -> get_public_key 8 years ago
Roman Zeyde db6903eab7 gpg: rename agent -> keyring 8 years ago
Roman Zeyde 171a0c2f6a gpg: remove agent's main 8 years ago
Roman Zeyde a535b31a1b gpg: fixup lint/pep8 8 years ago
Roman Zeyde ee4bcddd22 gpg: rename main API 8 years ago
Roman Zeyde f626d34e21 gpg: using closing() context handler 8 years ago
Roman Zeyde 2cf081420f gpg: move armor to proto 8 years ago
Roman Zeyde 0e72e3b7ff gpg: move PublicKey to proto 8 years ago
Roman Zeyde ce61c8b2ae gpg: move timeformat from util 8 years ago
Roman Zeyde 3192e570ed gpg: initial support for ElGamal and DSA
Doesn't verify anything (yet).
8 years ago
Roman Zeyde bf8f516ef4 gpg: no visual challenge 8 years ago
Roman Zeyde 51f7d6120b client: not visual challength for SSH 8 years ago
Roman Zeyde 0cb7cf0746 Merge branch 'python3' 8 years ago
Roman Zeyde b4ff31f816 gpg: handle ECDH keys 8 years ago
Roman Zeyde 6e9d6d6430 gpg: add URLs for subpackets 8 years ago
Roman Zeyde fa9391ede6 gpg: update required firmware version 8 years ago
Roman Zeyde ad8eafe6f8 Merge branch 'master' into python3
Conflicts:
	setup.py
8 years ago
Roman Zeyde 695079e4b9 agent: raise explicit error when signature fails 8 years ago
Roman Zeyde 9888ef971a gpg: add installation command to README 8 years ago
Roman Zeyde 04a878374f setup: add gpg subpackage 8 years ago
Roman Zeyde 4270d8464f gpg: add screencasts 8 years ago
Roman Zeyde 25a427081c gpg: add more output examples 8 years ago
Roman Zeyde 939fdbe829 gpg: add output examples 8 years ago
Roman Zeyde 1f126f3002 gpg: better logging 8 years ago
Roman Zeyde 78526d1379 gpg: install gpg-git wrapper script 8 years ago
Roman Zeyde 7e3c3b4f77 gpg: fixup README 8 years ago
Roman Zeyde 513c19bf1f gpg: remove unused files 8 years ago
Roman Zeyde f1e75783c4 gpg: use environment variable for user_id 8 years ago
Roman Zeyde 68637525ea Merge branch 'master' into python3 8 years ago
Roman Zeyde fce45832c2 gpg: fix small typo 8 years ago
Roman Zeyde df001c4100 gpg: rename README 8 years ago
Roman Zeyde 1a228a1af6 gpg: refactor cli 8 years ago
Roman Zeyde 7a7c9efc47 pylint: disable unbalanced-tuple-unpacking warning 8 years ago
Roman Zeyde 859cee9757 travis: use latest tools 8 years ago
Roman Zeyde 2846c0bf1a util: add tests for gpg-related code 8 years ago
Roman Zeyde b2147a8418 formats: curve name should be a string 8 years ago
Roman Zeyde 4cbf8a9f0a setup: WiP for Python 3 support 8 years ago
Roman Zeyde d9c4e930f3 main: fixup str/bytes issue for curve_name 8 years ago
Roman Zeyde 6fd6fe6520 handle missing imports 8 years ago
Roman Zeyde 4a7fef3011 gpg: fix logging and arguments in demo 8 years ago
Roman Zeyde a0e476ea19 gpg: remove unused code 8 years ago
Roman Zeyde 683aae7aa4 gpg: add logging for digest 8 years ago
Roman Zeyde d369638c7b gpg: add a script for faster commit verification 8 years ago
Roman Zeyde 07c4100618 gpg: fixup logging and make sure it works with git 8 years ago
Roman Zeyde b9f139b74a gpg: refactor subkey as pubkey 8 years ago
Roman Zeyde 3bf926620b gpg: handle multiple packets 8 years ago
Roman Zeyde ab192619f4 gpg: move protocol utils to proto.py 8 years ago
Roman Zeyde f982d785bd gpg: add marker to our pubkey signature packets 8 years ago
Roman Zeyde 38c1acf4db Merge branch 'subkey' 8 years ago
Roman Zeyde 31c3686fa4 gpg: small fixes 8 years ago
Roman Zeyde 87ca33c104 gpg: fixup encoding for large packets 8 years ago
Roman Zeyde c3d23ea7f5 gpg: allow longer packets 8 years ago
Roman Zeyde 5c04d17c43 gpg: demo with ed25519 TREZOR-based keys 8 years ago
Roman Zeyde 2d2d6efa93 gpg: small refactoring 8 years ago
Roman Zeyde 131c30acca gpg: use explicit public key algo_id 8 years ago
Roman Zeyde a7ef263954 gpg: generalize RSA/ECDSA signatures 8 years ago
Roman Zeyde d486c1ee7b gpg: refactor agent rsa/ecdsa signature parsing 8 years ago
Roman Zeyde f35b5be3ac gpg: 1st try for RSA primary key support 8 years ago
Roman Zeyde 9ed9781496 gpg: support RSA decode and verify 8 years ago
Roman Zeyde 5d007260e1 gpg: add docstrings 8 years ago
Roman Zeyde 7dfa3ab255 gpg: replace PublicKey.curve_name attribute 8 years ago
Roman Zeyde b8eba72d0b gpg: fixup subkey/export handling 8 years ago
Roman Zeyde 492285de1b gpg: rename pubkey methods 8 years ago
Roman Zeyde cc326b1f7d gpg: pubkey is not needed for make_signature 8 years ago
Roman Zeyde 169ff39b1a gpg: remove visual keyword for now 8 years ago
Roman Zeyde dcc7ef2600 minor fixes 8 years ago
Roman Zeyde ac2d12b354 It works again! 8 years ago
Roman Zeyde f3b49ff553 gpg: use strict bash mode for demo 8 years ago
Roman Zeyde 12d640c66b fixup pep8 8 years ago
Roman Zeyde 32984d2d3f agent: add support for gpg passphrase entry 8 years ago
Roman Zeyde a45c6c1300 horrible hack - but IT WORKS!!! 8 years ago
Roman Zeyde 1d3ba7e9b7 subkey: add backsig 8 years ago
Roman Zeyde 673b1df648 1st try 8 years ago
Roman Zeyde e63f03354e gpg: refactor signing providers from actual Signer class 8 years ago
Roman Zeyde 3c9c1b4e14 gpg: export verifying_key from parsing 8 years ago
Roman Zeyde 5caf4728ee gpg: fixup comment 8 years ago
Roman Zeyde dde6dcdaeb gpg: fix unpacking for subkey-case 8 years ago
Roman Zeyde 1f3c989884 gpg: 'dump' -> 'serialize' 8 years ago
Roman Zeyde 55dea41959 gpg: make sure gpg-agent is running before connecting 8 years ago
Roman Zeyde ed01c00d0c gpg: add agent-signing tool 8 years ago
Roman Zeyde e09571151c gpg: remove length type logging 8 years ago
Roman Zeyde 340aae4fb8 gpg: refactor decode to functional style 8 years ago
Roman Zeyde 9875c9927e gpg: demo for subkeys decoding 8 years ago
Roman Zeyde d9862ae0e1 gpg: debug logging for ECDSA verification 8 years ago
Roman Zeyde 5fb8b0e047 decode: parse GPG subkeys 8 years ago
Roman Zeyde 324fc21a5c decode: refactor digest calculation 8 years ago
Roman Zeyde e2f5ccafdf signer: allow importing to local keyring (using "-o" flag) 8 years ago
Roman Zeyde a0b4776374 gpg: fixup exception message 8 years ago
Roman Zeyde 5abc3dc41b gpg: fix check script -v option 8 years ago
Roman Zeyde 3c2eb64e0d gpg: fixup demo script 8 years ago
Roman Zeyde 67d58a5ae0 Merge pull request #10 from romanz/gpg
GPG v2.1 support
8 years ago
Roman Zeyde 9a435ae23e gpg: minor renames and code refactoring 8 years ago
Roman Zeyde d7913a84d5 gpg: pydocstyle fixes 8 years ago
Roman Zeyde a114242243 gpg: small fixes before merging to master 8 years ago
Roman Zeyde b6dbc4aa81 gpg: small fixes before merging to master 8 years ago
Roman Zeyde 6cc3a629a8 gpg: export git-gpg wrapper
should be used as 'gpg.program' in .git/config
8 years ago
Roman Zeyde 0c94363595 gpg: export command-line tool 8 years ago
Roman Zeyde 40377fc66b gpg: add __init__.py 8 years ago
Roman Zeyde 489c8fe357 gpg: rename git wrapper 8 years ago
Roman Zeyde 6f4f33bfa5 gpg: verify signature after signing 8 years ago
Roman Zeyde 76ce25fab1 gpg: fixup imports 8 years ago
Roman Zeyde 5506310239 gpg: move under trezor_agent 8 years ago
Roman Zeyde 9dc955aae8 gpg: fix signer logging 8 years ago
Roman Zeyde 80f29469d0 gpg: deduce curve name from existing pubkey information 8 years ago
Roman Zeyde fb368d24eb gpg: use subprocess.call() 8 years ago
Roman Zeyde 8c0848b459 gpg: debug during check 8 years ago
Roman Zeyde 276dec5728 gpg: support ed25519 public keys and signatures 8 years ago
Roman Zeyde 74f7ebf228 gpg: support ed25519 decoding 8 years ago
Roman Zeyde 7ef0958c33 gpg: minor fixes 8 years ago
Roman Zeyde 1402918bb3 gpg: use user name instead of key id 8 years ago
Roman Zeyde b6cfa0c03f main: show better error when no SSH remote is found 8 years ago
Roman Zeyde 33ff9ba667 signer: update required patch link 8 years ago
Roman Zeyde ab64505cdb gpg: refactor hexlification of key_id 8 years ago
Roman Zeyde 5651452c0d gpg: rename GPG public key file 8 years ago
Roman Zeyde af6d0caf33 Add GPG-wrapper script for Git 8 years ago
Roman Zeyde 96592269b6 signer: refactor a bit 8 years ago
Roman Zeyde b2d078eec6 simplify signer usage
and make less INFO loggin
8 years ago
Roman Zeyde 01dafb0ebd signer: show key ID on TREZOR screen 8 years ago
Roman Zeyde 447faf973c signer should export public key or sign a file 8 years ago
Roman Zeyde add90e3c51 signer: support armoring public keys 8 years ago
Roman Zeyde 34670c601d Fix PEP8 warnings 8 years ago
Roman Zeyde b9ba4a3082 split decoding functionality 8 years ago
Roman Zeyde 4335740abe Add experimental support for GPG signing via TREZOR
In order to use this feature, GPG "modern" (v2.1) is required [1].
Also, since TREZOR protocol does not support arbitrary long fields,
TREZOR firmware needs to be adapted  with the following patch [2],
to support signing fixed-size digests of GPG messages of arbitrary size.

[1] https://gist.github.com/vt0r/a2f8c0bcb1400131ff51
[2] https://gist.github.com/romanz/b66f5df1ca8ef15641df8ea5bb09fd47
8 years ago
Roman Zeyde 861401e89a client: make get_address() public 8 years ago
Roman Zeyde 335d050212 formats: fixup comment 8 years ago
Roman Zeyde 6e1b08c27a README: fix links 8 years ago
Roman Zeyde b3a6c76631 bump version 8 years ago
Roman Zeyde f056f1fac5 fixup lint errors 8 years ago
Roman Zeyde 716dc82312 bump version 8 years ago
Roman Zeyde 0e2a19f7ce client: fixup UT 8 years ago
Roman Zeyde 2cdbc89d28 protocol: fixup UT 8 years ago
Roman Zeyde 1022e54d6a protocol: fail gracefully on cancellation 8 years ago
Roman Zeyde ea88f425f5 protocol: fail on unsupported commands 8 years ago
Roman Zeyde 000860feaf main: add --test flag for verifying SSH configuration
https://help.github.com/articles/testing-your-ssh-connection/
8 years ago
Roman Zeyde 2a5196003e tests: update for CallException handling 8 years ago
Roman Zeyde e10b42bbb5 client: catch CallException for cancellation handling 8 years ago
Roman Zeyde b07d7e6535 server: handle IOError gracefully 8 years ago
Roman Zeyde 4838030be5 factory: add CallException type 8 years ago
Roman Zeyde c9f341a42b main: handle 'pushurl' and 'url' remote settings 8 years ago
Roman Zeyde bdd2568b2c main: log pubkey fingerprint on INFO level 8 years ago
Roman Zeyde ae20ae4a04 bump version 8 years ago
Roman Zeyde f15c2c7236 README: add trezor-git screencast 8 years ago
Roman Zeyde e6ccc324a0 main: ignore path from git remote URL
It's much easier to use single keypair per user@host
8 years ago
Roman Zeyde 7c102e435e setup: add more classifiers 8 years ago
Roman Zeyde 7f6bb12b24 bump version 8 years ago
Roman Zeyde 98e875562e main: add `trezor-git` entry point 8 years ago
Roman Zeyde 4384b93c19 main: remove unneeded use_shell parameter 8 years ago
Roman Zeyde 8a90a8cd84 main: split git from ssh 8 years ago
Roman Zeyde 1e86983782 main: split argument parser 8 years ago
Roman Zeyde c63201c90c client: show visual challenge 8 years ago
Roman Zeyde 19b00dc427 client: add logging for challenge sizes 8 years ago
Roman Zeyde aa35981980 README: add 'apt-get' to installation section 8 years ago
Roman Zeyde 8909b38107 main: use command-line for git interaction 8 years ago
Roman Zeyde 6d9aa9cb8a README: license badge is broken most of the time 8 years ago
Roman Zeyde d6532311b9 fix PEP8 & docstrings 8 years ago
Roman Zeyde 41b30b42b5 main: add git identity via "origin" remote 8 years ago
Roman Zeyde 5b0e56697f travis: add pydocstyle 8 years ago
Roman Zeyde 0e6d998b4c tox: add pydocstyle 8 years ago
Roman Zeyde 2c7fabfa35 tests: add docstrings 8 years ago
Roman Zeyde 1adccdbfe6 __init__: add docstrings 8 years ago
Roman Zeyde 04f4bbf2ac main: add docstrings 8 years ago
Roman Zeyde bbe963d0ff util: rename UTs 8 years ago
Roman Zeyde c49514754b util: add docstrings 8 years ago
Roman Zeyde 2ebefff909 server: add docstrings 8 years ago
Roman Zeyde 21e89014c9 protocol: add docstrings and replace custom exceptions 8 years ago
Roman Zeyde 566e4310e1 formats: add docstrings 8 years ago
Roman Zeyde e1441518d4 factory: add docstrings 8 years ago
Roman Zeyde 5cb12a43de client: add docstrings 8 years ago
Roman Zeyde df607f3665 pylint: add 'no-member' check 8 years ago
Roman Zeyde d712509a4e client: show current time instead of identity.path 8 years ago
Roman Zeyde 40e2d9fb2c fixup imports order
isort -rc trezor_agent
8 years ago
Roman Zeyde cd4cc059d6 main: remove git-config parsing code 8 years ago
Roman Zeyde 2b047f0525 main: refactor shell flag 8 years ago
Roman Zeyde 64776fd294 rename client test 8 years ago
Roman Zeyde 231995bd1a remove trezor module 8 years ago
Roman Zeyde ff76f17c02 client: elaborate SSH blob parsing 8 years ago
Roman Zeyde 963e80b49b client: move logging from parsing code 9 years ago
Roman Zeyde dee13b75ea client: remove unneeded 'if' 9 years ago
Roman Zeyde be86507e00 client: pass index as default argument 9 years ago
Roman Zeyde 2f2663ef94 client: set identity index explicitly 9 years ago
Roman Zeyde cafa218e19 server: pass handler and add debug option 9 years ago
Roman Zeyde 50b627ed45 protocol: allow debugging SSH message handler 9 years ago
Roman Zeyde 7f36097c15 tests: refactor mocks and fakes 9 years ago
Roman Zeyde a4b905cd6f bump version 9 years ago
Roman Zeyde 2eff21f96c factory: refactor for easier testing 9 years ago
Roman Zeyde 9afd07e867 server: make sure accepted UNIX sockets are blocking
It was a problem on Mac OS X, where sometimes we got EAGAIN
errors from calling socket.recv() on them.
9 years ago
Roman Zeyde b101281a5b main: add command-line argument for setting UNIX socket timeout 9 years ago
Roman Zeyde 8c6ac43cf4 Merge Trezor and KeepKey functionality 9 years ago
Kenneth Heutmaker 5932a89dc5 Make it work with KeepKey 9 years ago
Roman Zeyde 2009160ff2 Revert "travis: test with tox"
This reverts commit 3d8072522c.
9 years ago
Roman Zeyde 3d8072522c travis: test with tox 9 years ago
Roman Zeyde 0c63aef719 sort imports using isort tool 9 years ago
Roman Zeyde c454114c4e README: add gitter chat 9 years ago
Roman Zeyde f9133f7e05 README: fixup license link 9 years ago
Roman Zeyde 33a6951a96 server: don't crash after single exception 9 years ago
Roman Zeyde fb0d0a5f61 server: stop the server via a threading.Event
It seems that Mac OS does not support calling socket.shutdown(socket.SHUT_RD)
on a listening socket (see https://github.com/romanz/trezor-agent/issues/6).
The following implementation will set the accept() timeout to 0.1s and stop
the server if a threading.Event (named "quit_event") is set by the main thread.
9 years ago
Roman Zeyde 7ea20c7009 test_trezor: verify serialized signature 9 years ago
Roman Zeyde 4247558166 README: add subshell demo 9 years ago
Roman Zeyde fe1e1d2bb9 server: log command with INFO level 9 years ago
Roman Zeyde 1a5b8118ad setup.py: support for Python 3.4 9 years ago
Roman Zeyde 3a806c6d77 beta release 9 years ago
Roman Zeyde 3b61f86c25 README: fixup license to match the repository 9 years ago
Roman Zeyde 06d84c387c bump version 9 years ago
Roman Zeyde 8347142a99 setup.py: fixup license to match the repository 9 years ago
Roman Zeyde 7dabe2c555 test_protocol: fix bytes->str 9 years ago
Roman Zeyde d6ee3d8995 tox: add py34 9 years ago
Roman Zeyde c3fa79e450 Fix a few pylint issues 9 years ago
Roman Zeyde 15b10c9a7e bump version 9 years ago
Roman Zeyde e19d76398e formats: verify public key according to requested ECDSA curve 9 years ago
Roman Zeyde 535b4d50c7 Fix SSH connection arguments handling 9 years ago
Roman Zeyde 461f38d599 travis: fix up dependency 9 years ago
Roman Zeyde 60571e65dd trezor: add support for Ed25519 SSH keys 9 years ago
Roman Zeyde 34cecb276a README: fix URL 9 years ago
Roman Zeyde 903ba919b3 README: fix whitespace 9 years ago
Roman Zeyde 3184d34440 README: update badges and blog post 9 years ago
Roman Zeyde d7099cb863 bump version 9 years ago
Roman Zeyde e3f04f3389 Merge pull request #2 from romanz/pr
trezor: don't ask for passphrase (always use empty one)
9 years ago
Roman Zeyde e59404737d trezor: fix PEP8 9 years ago
Pavol Rusnak ca30707789 don't ask for passphrase (always use empty one similarly to TREZOR Connect) 9 years ago
Roman Zeyde 5449411d09 README: update trezorlib version 9 years ago

@ -0,0 +1,7 @@
[bumpversion]
commit = True
tag = True
current_version = 0.12.0
[bumpversion:file:setup.py]

3
.gitignore vendored

@ -55,3 +55,6 @@ docs/_build/
# PyBuilder
target/
# Sublime Text
*.sublime-*

@ -1,2 +1,5 @@
[MESSAGES CONTROL]
disable=invalid-name, missing-docstring, locally-disabled
disable=invalid-name, missing-docstring, locally-disabled, unbalanced-tuple-unpacking,no-else-return,fixme,duplicate-code
[SIMILARITIES]
min-similarity-lines=5

@ -1,17 +1,26 @@
sudo: false
language: python
python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
cache:
directories:
- $HOME/.cache/pip
before_install:
- pip install -U pip wheel
- pip install -U setuptools
- pip install -U pylint coverage pycodestyle pydocstyle
install:
- pip install ecdsa # test without trezorlib for now
- pip install pylint coverage pep8
- pip install -U -e .
script:
- pep8 trezor_agent
- pylint --report=no --rcfile .pylintrc trezor_agent
- coverage run --source trezor_agent/ -m py.test -v
- pycodestyle libagent
- pylint --reports=no --rcfile .pylintrc libagent
- pydocstyle libagent
- coverage run --source libagent/ -m py.test -v
after_success:
- coverage report

@ -1,62 +1,27 @@
# Using Trezor as a hardware SSH agent
# Hardware-based SSH/GPG agent
[![Build Status](https://travis-ci.org/romanz/trezor-agent.svg?branch=master)](https://travis-ci.org/romanz/trezor-agent)
[![Chat](https://badges.gitter.im/romanz/trezor-agent.svg)](https://gitter.im/romanz/trezor-agent)
## Screencast demo usage
This project allows you to use various hardware security devices to operate GPG and SSH. Instead of keeping your key on your computer and decrypting it with a passphrase when you want to use it, the key is generated and stored on the device and never reaches your computer. Read more about the design [here](doc/DESIGN.md).
[![Demo](https://asciinema.org/a/22959.png)](https://asciinema.org/a/22959)
You can do things like sign your emails, git commits, and software packages, manage your passwords (with [pass](https://www.passwordstore.org/) and [gopass](https://www.justwatch.com/gopass/), among others), authenticate web tunnels and file transfers, and more.
## Installation
See the following blog posts about this tool:
First, make sure that the latest `trezorlib` Python package
is installed correctly:
- [TREZOR Firmware 1.3.4 enables SSH login](https://medium.com/@satoshilabs/trezor-firmware-1-3-4-enables-ssh-login-86a622d7e609)
- [TREZOR Firmware 1.3.6GPG Signing, SSH Login Updates and Advanced Transaction Features for Segwit](https://medium.com/@satoshilabs/trezor-firmware-1-3-6-20a7df6e692)
- [TREZOR Firmware 1.4.0GPG decryption support](https://www.reddit.com/r/TREZOR/comments/50h8r9/new_trezor_firmware_fidou2f_and_initial_ethereum/d7420q7/)
- [A Step by Step Guide to Securing your SSH Keys with the Ledger Nano S](https://thoughts.t37.net/a-step-by-step-guide-to-securing-your-ssh-keys-with-the-ledger-nano-s-92e58c64a005)
$ pip install Cython trezor
Currently [TREZOR One](https://trezor.io/), [TREZOR Model T](https://trezor.io/), [Keepkey](https://www.keepkey.com/), and [Ledger Nano S](https://www.ledgerwallet.com/products/ledger-nano-s) are supported.
Then, install the latest `trezor_agent` package:
## Documentation
$ pip install trezor_agent
* **Installation** instructions are [here](doc/INSTALL.md)
* **SSH** instructions and common use cases are [here](doc/README-SSH.md)
Finally, verify that you are running the latest TREZOR firmware version (at least v1.3.4):
Note: If you're using Windows, see [trezor-ssh-agent](https://github.com/martin-lizner/trezor-ssh-agent) by Martin Lízner.
$ trezorctl get_features
vendor: "bitcointrezor.com"
major_version: 1
minor_version: 3
patch_version: 4
...
## Public key generation
Run:
/tmp $ trezor-agent ssh.hostname.com -v > hostname.pub
2015-09-02 15:03:18,929 INFO getting "ssh://ssh.hostname.com" public key from Trezor...
2015-09-02 15:03:23,342 INFO disconnected from Trezor
/tmp $ cat hostname.pub
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGSevcDwmT+QaZPUEWUUjTeZRBICChxMKuJ7dRpBSF8+qt+8S1GBK5Zj8Xicc8SHG/SE/EXKUL2UU3kcUzE7ADQ= ssh://ssh.hostname.com
Append `hostname.pub` contents to `~/.ssh/authorized_keys`
configuration file at `ssh.hostname.com`, so the remote server
would allow you to login using the corresponding private key signature.
## Usage
Run:
/tmp $ trezor-agent ssh.hostname.com -v -c
2015-09-02 15:09:39,782 INFO getting "ssh://ssh.hostname.com" public key from Trezor...
2015-09-02 15:09:44,430 INFO please confirm user "roman" login to "ssh://ssh.hostname.com" using Trezor...
2015-09-02 15:09:46,152 INFO signature status: OK
Linux lmde 3.16.0-4-amd64 #1 SMP Debian 3.16.7-ckt11-1+deb8u3 (2015-08-04) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Tue Sep 1 15:57:05 2015 from localhost
~ $
Make sure to confirm SSH signature on the Trezor device when requested.
* **GPG** instructions and common use cases are [here](doc/README-GPG.md)
* Instructions to configure a Trezor-style **PIN entry** program are [here](doc/README-PINENTRY.md)

@ -0,0 +1,7 @@
import libagent.gpg
import libagent.ssh
from libagent.device.fake_device import FakeDevice as DeviceType
ssh_agent = lambda: libagent.ssh.main(DeviceType)
gpg_tool = lambda: libagent.gpg.main(DeviceType)
gpg_agent = lambda: libagent.gpg.run_agent(DeviceType)

@ -0,0 +1,42 @@
#!/usr/bin/env python
from setuptools import setup
print('NEVER USE THIS CODE FOR REAL-LIFE USE-CASES!!!')
print('ONLY FOR DEBUGGING AND TESTING!!!')
setup(
name='fake_device_agent',
version='0.9.0',
description='Testing trezor_agent with a fake device - NOT SAFE!!!',
author='Roman Zeyde',
author_email='roman.zeyde@gmail.com',
url='http://github.com/romanz/trezor-agent',
scripts=['fake_device_agent.py'],
install_requires=[
'libagent>=0.9.0',
],
platforms=['POSIX'],
classifiers=[
'Environment :: Console',
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',
'Operating System :: POSIX',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Networking',
'Topic :: Communications',
'Topic :: Security',
'Topic :: Utilities',
],
entry_points={'console_scripts': [
'fake-device-agent = fake_device_agent:ssh_agent',
'fake-device-gpg = fake_device_agent:gpg_tool',
'fake-device-gpg-agent = fake_device_agent:gpg_agent',
]},
)

@ -0,0 +1,5 @@
import libagent.gpg
import libagent.ssh
from libagent.device import keepkey
ssh_agent = lambda: libagent.ssh.main(keepkey.KeepKey)

@ -0,0 +1,38 @@
#!/usr/bin/env python
from setuptools import setup
setup(
name='keepkey_agent',
version='0.9.0',
description='Using KeepKey as hardware SSH/GPG agent',
author='Roman Zeyde',
author_email='roman.zeyde@gmail.com',
url='http://github.com/romanz/trezor-agent',
scripts=['keepkey_agent.py'],
install_requires=[
'libagent>=0.9.0',
'keepkey>=0.7.3'
],
platforms=['POSIX'],
classifiers=[
'Environment :: Console',
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',
'Operating System :: POSIX',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Networking',
'Topic :: Communications',
'Topic :: Security',
'Topic :: Utilities',
],
entry_points={'console_scripts': [
'keepkey-agent = keepkey_agent:ssh_agent',
]},
)

@ -0,0 +1,7 @@
import libagent.gpg
import libagent.ssh
from libagent.device.ledger import LedgerNanoS as DeviceType
ssh_agent = lambda: libagent.ssh.main(DeviceType)
gpg_tool = lambda: libagent.gpg.main(DeviceType)
gpg_agent = lambda: libagent.gpg.run_agent(DeviceType)

@ -0,0 +1,40 @@
#!/usr/bin/env python
from setuptools import setup
setup(
name='ledger_agent',
version='0.9.0',
description='Using Ledger as hardware SSH/GPG agent',
author='Roman Zeyde',
author_email='roman.zeyde@gmail.com',
url='http://github.com/romanz/trezor-agent',
scripts=['ledger_agent.py'],
install_requires=[
'libagent>=0.9.0',
'ledgerblue>=0.1.8'
],
platforms=['POSIX'],
classifiers=[
'Environment :: Console',
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',
'Operating System :: POSIX',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Networking',
'Topic :: Communications',
'Topic :: Security',
'Topic :: Utilities',
],
entry_points={'console_scripts': [
'ledger-agent = ledger_agent:ssh_agent',
'ledger-gpg = ledger_agent:gpg_tool',
'ledger-gpg-agent = ledger_agent:gpg_agent',
]},
)

@ -0,0 +1,40 @@
#!/usr/bin/env python
from setuptools import setup
setup(
name='trezor_agent',
version='0.9.3',
description='Using Trezor as hardware SSH/GPG agent',
author='Roman Zeyde',
author_email='roman.zeyde@gmail.com',
url='http://github.com/romanz/trezor-agent',
scripts=['trezor_agent.py'],
install_requires=[
'libagent>=0.11.2',
'trezor[hidapi]>=0.9.0'
],
platforms=['POSIX'],
classifiers=[
'Environment :: Console',
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',
'Operating System :: POSIX',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Networking',
'Topic :: Communications',
'Topic :: Security',
'Topic :: Utilities',
],
entry_points={'console_scripts': [
'trezor-agent = trezor_agent:ssh_agent',
'trezor-gpg = trezor_agent:gpg_tool',
'trezor-gpg-agent = trezor_agent:gpg_agent',
]},
)

@ -0,0 +1,7 @@
import libagent.gpg
import libagent.ssh
from libagent.device.trezor import Trezor as DeviceType
ssh_agent = lambda: libagent.ssh.main(DeviceType)
gpg_tool = lambda: libagent.gpg.main(DeviceType)
gpg_agent = lambda: libagent.gpg.run_agent(DeviceType)

@ -0,0 +1,15 @@
#!/usr/bin/env python3
import os
import sys
agent = 'trezor-gpg-agent'
binary = 'neopg'
if sys.argv[1:2] == ['agent']:
os.execvp(agent, [agent, '-vv'] + sys.argv[2:])
else:
# HACK: pass this script's path as argv[0], so it will be invoked again
# when NeoPG tries to run its own agent:
# https://github.com/das-labor/neopg/blob/1fe50460abe01febb118641e37aa50bc429a1786/src/neopg.cpp#L114
# https://github.com/das-labor/neopg/blob/1fe50460abe01febb118641e37aa50bc429a1786/legacy/gnupg/common/asshelp.cpp#L217
os.execvp(binary, [__file__, 'gpg2'] + sys.argv[1:])

@ -0,0 +1,51 @@
# Design
Most cryptographic tools (such as gpg, ssh and openssl) allow the offloading of some key cryptographic steps to *engines* or *agents*. This is to allow sensitive operations, such as asking for a password or doing the actual encryption step, to be kept separate from the larger body of code. This makes it easier to secure those steps, move them onto hardware or easier to audit.
SSH and GPG do this by means of a simple interprocess communication protocol (usually a unix domain socket) and an agent (`ssh-agent`) or GPG key daemon (`gpg-agent`). The `trezor-agent` mimics these two protocols.
These two agents make the connection between the front end (e.g. a `gpg --sign` command, or an `ssh user@fqdn`). And then they wait for a request from the 'front end', and then do the actual asking for a password and subsequent using the private key to sign or decrypt something.
The various hardware wallets (Trezor, KeepKey and Ledger) each have the ability (as of Firmware 1.3.4) to use the NIST P-256 elliptic curve to sign, encrypt or decrypt. This curve can be used with S/MIME, GPG and SSH.
So when you `ssh` to a machine - rather than consult the normal ssh-agent (which in turn will use your private SSH key in files such as `~/.ssh/id_rsa`) -- the trezor-agent will aks your hardware wallet to use its private key to sign the challenge.
## Key Naming
`trezor-agent` goes to great length to avoid using the valuable parent key.
The rationale behind this is that `trezor-agent` is to some extent condemned to *blindly* signing any NONCE given to it (e.g. as part of a challenge respone, or as the hash/hmac of someting to sign).
And doing so with the master private key is risky - as rogue (ssh) server could possibly provide a doctored NONCE that happens to be tied to a transaction or something else.
It therefore uses only derived child keys pairs instead (according to the [BIP-0032: Hierarchical Deterministic Wallets][1] system) - and ones on different leafs. So the parent key is only used within the device for creating the child keys - and not exposed in any way to `trezor-agent`.
### SSH
It is common for SSH users to use one (or a few) private keys with SSH on all servers they log into. The `trezor-agent` is slightly more cautious and derives a child key that is *unique* to the server and username you are logging into from your master private key on the device.
So taking a commmand such as:
$ trezor-agent -c user@fqdn.com
The `trezor-agent` will take the `user`@`fqdn.com`; canonicalise it (e.g. to add the ssh default port number if none was specified) and then apply some simple hashing (See [SLIP-0013 : Authentication using deterministic hierarchy][2]). The resulting 128bit hash is then used to construct a lead 'HD node' that contains an extened public private *child* key.
This way they keypair is specific to the server/hostname/port and protocol combination used. And it is this private key that is used to sign the nonce passed by the SSH server (as opposed to the master key).
The `trezor-agent` then instructs SSH to connect to the server. It will then engage in the normal challenge response process, ask the hardware wallet to blindly sign any nonce flashed by the server with the derived child private key and return this to the server. It then hands over to normal SSH for the rest of the logged in session.
### GPG
GPG uses much the same approach as SSH, except in this case it relies on [SLIP-0017 : ECDH using deterministic hierarchy][3] for the mapping to an ECDH key and it maps these to the normal GPG child key infrastructure.
Note: Keepkey does not support en-/de-cryption at this time.
### Index
The canonicalisation process ([SLIP-0013][2] and [SLIP-0017][3]) of an email address or ssh address allows for the mixing in of an extra 'index' - a unsigned 32 bit number. This allows one to have multiple, different keys, for the same address.
This feature is currently not used -- it is set to '0'. This may change in the future.
[1]: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
[2]: https://github.com/satoshilabs/slips/blob/master/slip-0013.md
[3]: https://github.com/satoshilabs/slips/blob/master/slip-0017.md

@ -0,0 +1,141 @@
# Installation
## 1. Prerequisites
Install the following packages (depending on your distribution):
### OS dependencies
This software needs Python, libusb, and libudev along with development files.
You can install them on these distributions as follows:
##### Debian
$ apt-get install python3-pip python3-dev python3-tk libusb-1.0-0-dev libudev-dev
##### RedHat
$ yum install python3-pip python3-devel python3-tk libusb-devel libudev-devel \
gcc redhat-rpm-config
##### Fedora
$ dnf install python3-pip python3-devel python3-tkinter libusb-devel libudev-devel \
gcc redhat-rpm-config
##### OpenSUSE
$ zypper install python-pip python-devel python-tk libusb-1_0-devel libudev-devel
If you are using python3 or your system `pip` command points to `pip3.x`
(`/etc/alternatives/pip -> /usr/bin/pip3.6`) you will need to install these
dependencies instead:
$ zypper install python3-pip python3-devel python3-tk libusb-1_0-devel libudev-devel
##### macOS
There are many different options to install python environment on macOS ([official](https://www.python.org/downloads/mac-osx/), [anaconda](https://conda.io/docs/user-guide/install/macos.html), ..). Most importantly you need `libusb`. Probably the easiest way is via [homebrew](https://brew.sh/)
$ brew install libusb
### GPG
If you intend to use GPG make sure you have GPG installed and up to date. This software requires a GPG version >= 2.1.11.
You can verify your installed version by running:
```
$ gpg2 --version | head -n1
gpg (GnuPG) 2.1.15
```
* Follow this installation guide for [Debian](https://gist.github.com/vt0r/a2f8c0bcb1400131ff51)
* Install GPG for [macOS](https://sourceforge.net/p/gpgosx/docu/Download/)
* Install packages for Ubuntu 16.04 [here](https://launchpad.net/ubuntu/+source/gnupg2)
* Install packages for Linux Mint 18 [here](https://community.linuxmint.com/software/view/gnupg2)
# 2. Install the TREZOR agent
1. Make sure you are running the latest firmware version on your Trezor:
* [TREZOR firmware releases](https://wallet.trezor.io/data/firmware/releases.json): `1.4.2+`
2. Make sure that your `udev` rules are configured [correctly](https://doc.satoshilabs.com/trezor-user/settingupchromeonlinux.html#manual-configuration-of-udev-rules).
3. Then, install the latest [trezor_agent](https://pypi.python.org/pypi/trezor_agent) package:
```
$ pip3 install Cython hidapi
$ pip3 install trezor_agent
```
Or, directly from the latest source code:
```
$ git clone https://github.com/romanz/trezor-agent
$ pip3 install --user -e trezor-agent/agents/trezor
```
Or, through Homebrew on macOS:
```
$ brew install trezor-agent
```
# 3. Install the KeepKey agent
1. Make sure you are running the latest firmware version on your KeepKey:
* [KeepKey firmware releases](https://github.com/keepkey/keepkey-firmware/releases): `3.0.17+`
2. Make sure that your `udev` rules are configured [correctly](https://support.keepkey.com/support/solutions/articles/6000037796-keepkey-wallet-is-not-being-recognized-by-linux).
Then, install the latest [keepkey_agent](https://pypi.python.org/pypi/keepkey_agent) package:
```
$ pip3 install keepkey_agent
```
Or, on Mac using Homebrew:
```
$ homebrew install keepkey-agent
```
Or, directly from the latest source code:
```
$ git clone https://github.com/romanz/trezor-agent
$ pip3 install --user -e trezor-agent/agents/keepkey
```
# 4. Install the Ledger Nano S agent
1. Make sure you are running the latest firmware version on your Ledger Nano S:
* [Ledger Nano S firmware releases](https://github.com/LedgerHQ/blue-app-ssh-agent): `0.0.3+` (install [SSH/PGP Agent](https://www.ledgerwallet.com/images/apps/chrome-mngr-apps.png) app)
2. Make sure that your `udev` rules are configured [correctly](https://ledger.zendesk.com/hc/en-us/articles/115005165269-What-if-Ledger-Wallet-is-not-recognized-on-Linux-).
3. Then, install the latest [ledger_agent](https://pypi.python.org/pypi/ledger_agent) package:
```
$ pip3 install ledger_agent
```
Or, directly from the latest source code:
```
$ git clone https://github.com/romanz/trezor-agent
$ pip3 install --user -e trezor-agent/agents/ledger
```
# 5. Installation Troubleshooting
If there is an import problem with the installed `protobuf` package,
see [this issue](https://github.com/romanz/trezor-agent/issues/28) for fixing it.
If you can't find the command-line utilities (after running `pip install --user`),
please make sure that `~/.local/bin` is on your `PATH` variable
(see a [relevant](https://github.com/pypa/pip/issues/3813) issue).
If you can't find command-line utilities and are on macOS/OSX check `~/Library/Python/2.7/bin` and add to `PATH` if necessary (see a [relevant](https://github.com/romanz/trezor-agent/issues/155) issue).

@ -0,0 +1,251 @@
# GPG Agent
Note: the GPG-related code is still under development, so please try the current implementation
and please let me [know](https://github.com/romanz/trezor-agent/issues/new) if something doesn't
work well for you. If possible:
* record the session (e.g. using [asciinema](https://asciinema.org))
* attach the GPG agent log from `~/.gnupg/{trezor,ledger}/gpg-agent.log` (can be [encrypted](https://keybase.io/romanz))
Thanks!
## 1. Configuration
1. Initialize the agent GPG directory.
[![asciicast](https://asciinema.org/a/3iNw2L9QWB8R3EVdYdAxMOLK8.png)](https://asciinema.org/a/3iNw2L9QWB8R3EVdYdAxMOLK8)
Run
```
$ (trezor|keepkey|ledger)-gpg init "Roman Zeyde <roman.zeyde@gmail.com>"
```
Follow the instructions provided to complete the setup. Keep note of the timestamp value which you'll need if you want to regenerate the key later.
If you'd like a Trezor-style PIN entry program, follow [these instructions](README-PINENTRY.md).
2. Add `export GNUPGHOME=~/.gnupg/(trezor|keepkey|ledger)` to your `.bashrc` or other environment file.
This `GNUPGHOME` contains your hardware keyring and agent settings. This agent software assumes all keys are backed by hardware devices so you can't use standard GPG keys in `GNUPGHOME` (if you do mix keys you'll receive an error when you attempt to use them).
If you wish to switch back to your software keys unset `GNUPGHOME`.
3. Log out and back into your session to ensure your environment is updated everywhere.
## 2. Usage
You can use any GPG commands or software that uses GPG as usual and will be prompted to interact with your hardware device as necessary. The agent is automatically started if it isn't running when you run any `gpg` command.
##### Restarting the agent
If you change settings or need to restart the agent for some other reason, simply kill it. It will restart the next time GPG is invoked.
## 3. Common Use Cases
### Sign and decrypt files
[![asciicast](https://asciinema.org/a/120441.png)](https://asciinema.org/a/120441)
### Inspect GPG keys
You can use GNU Privacy Assistant (GPA) in order to inspect the created keys and perform signature and decryption operations as usual:
```
$ sudo apt install gpa
$ gpa
```
[![GPA](https://cloud.githubusercontent.com/assets/9900/20224804/053d7474-a849-11e6-87f3-ab07dc536158.png)](https://www.gnupg.org/related_software/swlist.html#gpa)
### Sign Git commits and tags
Git can use GPG to sign and verify commits and tags (see [here](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work)):
```
$ git config --local commit.gpgsign 1
$ git config --local gpg.program $(which gpg2)
$ git commit --gpg-sign # create GPG-signed commit
$ git log --show-signature -1 # verify commit signature
$ git tag v1.2.3 --sign # create GPG-signed tag
$ git tag v1.2.3 --verify # verify tag signature
```
Note that your git email has to correlate to your gpg key email. If you use a different email for git, you'll need to either generate a new gpg key for that email or set your git email using the command:
````
$ git config user.email foo@example.com
````
If your git email is configured incorrectly, you will receive the error:
````
error: gpg failed to sign the data
fatal: failed to write commit object
````
when committing to git.
### Manage passwords
Password managers such as [pass](https://www.passwordstore.org/) and [gopass](https://www.justwatch.com/gopass/) rely on GPG for encryption so you can use your device with them too.
##### With `pass`:
First install `pass` from [passwordstore.org] and initialize it to use your TREZOR-based GPG identity:
```
$ pass init "Roman Zeyde <roman.zeyde@gmail.com>"
Password store initialized for Roman Zeyde <roman.zeyde@gmail.com>
```
Then, you can generate truly random passwords and save them encrypted using your public key (as separate `.gpg` files under `~/.password-store/`):
```
$ pass generate Dev/github 32
$ pass generate Social/hackernews 32
$ pass generate Social/twitter 32
$ pass generate VPS/linode 32
$ pass
Password Store
├── Dev
│   └── github
├── Social
│   ├── hackernews
│   └── twitter
└── VPS
└── linode
```
In order to paste them into the browser, you'd need to decrypt the password using your hardware device:
```
$ pass --clip VPS/linode
Copied VPS/linode to clipboard. Will clear in 45 seconds.
```
You can also use the following [Qt-based UI](https://qtpass.org/) for `pass`:
```
$ sudo apt install qtpass
```
### Re-generate a GPG identity
[![asciicast](https://asciinema.org/a/5tIQa5qt5bV134oeOqFyKEU29.png)](https://asciinema.org/a/5tIQa5qt5bV134oeOqFyKEU29)
If you've forgotten the timestamp value, but still have access to the public key, then you can
retrieve the timestamp with the following command (substitute "john@doe.bit" for the key's address or id):
```
$ gpg2 --export 'john@doe.bit' | gpg2 --list-packets | grep created | head -n1
```
### Add new UIDs to your identity
After your main identity is created, you can add new user IDs using the regular GnuPG commands:
```
$ trezor-gpg init "Foobar" -vv
$ export GNUPGHOME=${HOME}/.gnupg/trezor
$ gpg2 -K
------------------------------------------
sec nistp256/6275E7DA 2017-12-05 [SC]
uid [ultimate] Foobar
ssb nistp256/35F58F26 2017-12-05 [E]
$ gpg2 --edit Foobar
gpg> adduid
Real name: Xyzzy
Email address:
Comment:
You selected this USER-ID:
"Xyzzy"
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? o
gpg> save
$ gpg2 -K
------------------------------------------
sec nistp256/6275E7DA 2017-12-05 [SC]
uid [ultimate] Xyzzy
uid [ultimate] Foobar
ssb nistp256/35F58F26 2017-12-05 [E]
```
### Generate GnuPG subkeys
In order to add TREZOR-based subkey to an existing GnuPG identity, use the `--subkey` flag:
```
$ gpg2 -k foobar
pub rsa2048/90C4064B 2017-10-10 [SC]
uid [ultimate] foobar
sub rsa2048/4DD05FF0 2017-10-10 [E]
$ trezor-gpg init "foobar" --subkey
```
[![asciicast](https://asciinema.org/a/Ick5G724zrZRFsGY7ZUdFSnV1.png)](https://asciinema.org/a/Ick5G724zrZRFsGY7ZUdFSnV1)
In order to enter existing GPG passphrase, I recommend installing and using a graphical Pinentry:
```
$ sudo apt install pinentry-gnome3
$ sudo update-alternatives --config pinentry
There are 4 choices for the alternative pinentry (providing /usr/bin/pinentry).
Selection Path Priority Status
------------------------------------------------------------
* 0 /usr/bin/pinentry-gnome3 90 auto mode
1 /usr/bin/pinentry-curses 50 manual mode
2 /usr/bin/pinentry-gnome3 90 manual mode
3 /usr/bin/pinentry-qt 80 manual mode
4 /usr/bin/pinentry-tty 30 manual mode
Press <enter> to keep the current choice[*], or type selection number: 0
```
### Sign and decrypt email
Follow [these instructions](enigmail.md) to set up Enigmail in Thunderbird.
### Start the agent as a systemd unit
##### 1. Create these files in `~/.config/systemd/user`
Replace `trezor` with `keepkey` or `ledger` as required.
###### `trezor-gpg-agent.service`
````
[Unit]
Description=trezor-gpg-agent
Requires=trezor-gpg-agent.socket
[Service]
Type=Simple
Environment="GNUPGHOME=%h/.gnupg/trezor"
Environment="PATH=/bin:/usr/bin:/usr/local/bin:%h/.local/bin"
ExecStart=/usr/bin/trezor-gpg-agent -vv
````
If you've installed `trezor-agent` locally you may have to change the path in `ExecStart=`.
###### `trezor-gpg-agent.socket`
````
[Unit]
Description=trezor-gpg-agent socket
[Socket]
ListenStream=%t/gnupg/S.gpg-agent
FileDescriptorName=std
SocketMode=0600
DirectoryMode=0700
[Install]
WantedBy=sockets.target
````
##### 2. Stop trezor-gpg-agent if it's already running
```
killall trezor-gpg-agent
```
##### 3. Run
```
systemctl --user start trezor-gpg-agent.service trezor-gpg-agent.socket
systemctl --user enable trezor-gpg-agent.socket
```

@ -0,0 +1,31 @@
# NeoPG experimental support
1. Download build and install NeoPG from [source code](https://github.com/das-labor/neopg#installation).
2. Generate Ed25519-based identity (using a [special wrapper](https://github.com/romanz/trezor-agent/blob/c22109df24c6eb8263aa40183a016be3437b1a0c/contrib/neopg-trezor) to invoke TREZOR-based agent):
```bash
$ export NEOPG_BINARY=$PWD/contrib/neopg-trezor
$ $NEOPG_BINARY --help
$ export GNUPGHOME=/tmp/homedir
$ trezor-gpg init "FooBar" -e ed25519
sec ed25519 2018-07-01 [SC]
802AF7E2DCF4491FFBB2F032341E95EF57CD7D5E
uid [ultimate] FooBar
ssb cv25519 2018-07-01 [E]
```
3. Sign and verify signatures:
```
$ $NEOPG_BINARY -v --detach-sign FILE
neopg: starting agent '/home/roman/Code/trezor/trezor-agent/contrib/neopg-trezor'
neopg: using pgp trust model
neopg: writing to 'FILE.sig'
neopg: EDDSA/SHA256 signature from: "341E95EF57CD7D5E FooBar"
$ $NEOPG_BINARY --verify FILE.sig FILE
neopg: Signature made Sun Jul 1 11:52:51 2018 IDT
neopg: using EDDSA key 802AF7E2DCF4491FFBB2F032341E95EF57CD7D5E
neopg: Good signature from "FooBar" [ultimate]
```

@ -0,0 +1,69 @@
# Custom PIN entry
In order to use the default GPG pinentry program, install one of the following Linux packages:
```
$ apt install pinentry-{curses,gnome3,qt}
```
or (on macOS):
```
$ brew install pinentry
```
By default a standard GPG PIN entry program is used when entering your Trezor PIN, but it's difficult to use if you don't have a numeric keypad or want to use your mouse.
You can specify a custom PIN entry program such as [trezor-gpg-pinentry-tk](https://github.com/rendaw/trezor-gpg-pinentry-tk) (and separately, a passphrase entry program) to match your workflow.
The below examples use `trezor-gpg-pinentry-tk` but any GPG compatible PIN entry can be used.
##### 1. Install the PIN entry
Run
```
pip install trezor-gpg-pinentry-tk
```
##### 2. SSH
Add the flag `--pin-entry-binary trezor-gpg-pinentry-tk` to all calls to `trezor-agent`.
To automatically use this flag, add the line `pinentry=trezor-gpg-pinentry-tk` to `~/.ssh/agent.config`. **Note** this is currently broken due to [this dependency issue](https://github.com/bw2/ConfigArgParse/issues/114).
If you run the SSH agent with Systemd you'll need to add `--pin-entry-binary` to the `ExecStart` command. You may also need to add this line:
```
Environment="DISPLAY=:0"
```
to the `[Service]` section to tell the PIN entry program how to connect to the X11 server.
##### 3. GPG
If you haven't completed initialization yet, run:
```
$ (trezor|keepkey|ledger)-gpg init --pin-entry-binary trezor-gpg-pinentry-tk "Roman Zeyde <roman.zeyde@gmail.com>"
```
to configure the PIN entry at the same time.
Otherwise, open `$GNUPGHOME/trezor/run-agent.sh` and change the `--pin-entry-binary` option to `trezor-gpg-pinentry-tk` and run:
```
killall trezor-gpg-agent
```
##### 4. Troubleshooting
Any problems running the PIN entry program with GPG should appear in `$HOME/.gnupg/trezor/gpg-agent.log`.
You can get similar logs for SSH by specifying `--log-file` in the SSH command line.
The passphrase is cached by the agent (after its first entry), which needs to be restarted in order to reset the passphrase:
```
$ killall trezor-agent # (for SSH)
$ killall trezor-gpg-agent # (for GPG)
```

@ -0,0 +1,209 @@
# SSH Agent
## 1. Configuration
SSH requires no configuration, but you may put common command line options in `~/.ssh/agent.conf` to avoid repeating them in every invocation.
See `(trezor|keepkey|ledger)-agent -h` for details on supported options and the configuration file format.
If you'd like a Trezor-style PIN entry program, follow [these instructions](README-PINENTRY.md).
## 2. Usage
Use the `(trezor|keepkey|ledger)-agent` program to work with SSH. It has three main modes of operation:
##### 1. Export public keys
To get your public key so you can add it to `authorized_hosts` or allow
ssh access to a service that supports it, run:
```
(trezor|keepkey|ledger)-agent identity@myhost
```
The identity (ex: `identity@myhost`) is used to derive the public key and is added as a comment to the exported key string.
##### 2. Run a command with the agent's environment
Run
```
$ (trezor|keepkey|ledger)-agent identity@myhost -- COMMAND --WITH --ARGUMENTS
```
to start the agent in the background and execute the command with environment variables set up to use the SSH agent. The specified identity is used for all SSH connections. The agent will exit after the command completes.
Note the `--` separator, which is used to separate `trezor-agent`'s arguments from the SSH command arguments.
As a shortcut you can run
```
$ (trezor|keepkey|ledger)-agent identity@myhost -s
```
to start a shell with the proper environment.
##### 2. Connect to a server directly via `(trezor|keepkey|ledger)-agent`
If you just want to connect to a server this is the simplest way to do it:
```
$ (trezor|keepkey|ledger)-agent user@remotehost -c
```
The identity `user@remotehost` is used as both the destination user and host as well as for key derivation, so you must generate a separate key for each host you connect to.
## 3. Common Use Cases
### Start a single SSH session
[![Demo](https://asciinema.org/a/22959.png)](https://asciinema.org/a/22959)
### Start multiple SSH sessions from a sub-shell
This feature allows using regular SSH-related commands within a subprocess running user's shell.
`SSH_AUTH_SOCK` environment variable is defined for the subprocess (pointing to the SSH agent, running as a parent process).
This way the user can use SSH-related commands (e.g. `ssh`, `ssh-add`, `sshfs`, `git`, `hg`), while authenticating via the hardware device.
[![Subshell](https://asciinema.org/a/33240.png)](https://asciinema.org/a/33240)
### Load different SSH identities from configuration file
[![Config](https://asciinema.org/a/bdxxtgctk5syu56yfz8lcp7ny.png)](https://asciinema.org/a/bdxxtgctk5syu56yfz8lcp7ny)
### Implement passwordless login
Run:
/tmp $ trezor-agent user@ssh.hostname.com -v > hostname.pub
2015-09-02 15:03:18,929 INFO getting "ssh://user@ssh.hostname.com" public key from Trezor...
2015-09-02 15:03:23,342 INFO disconnected from Trezor
/tmp $ cat hostname.pub
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGSevcDwmT+QaZPUEWUUjTeZRBICChxMKuJ7dRpBSF8+qt+8S1GBK5Zj8Xicc8SHG/SE/EXKUL2UU3kcUzE7ADQ= ssh://user@ssh.hostname.com
Append `hostname.pub` contents to `/home/user/.ssh/authorized_keys`
configuration file at `ssh.hostname.com`, so the remote server
would allow you to login using the corresponding private key signature.
### Access remote Git/Mercurial repositories
Export your public key and register it in your repository web interface
(e.g. [GitHub](https://help.github.com/articles/adding-a-new-ssh-key-to-your-github-account/)):
$ trezor-agent -v -e ed25519 git@github.com > ~/.ssh/github.pub
Add the following configuration to your `~/.ssh/config` file:
Host github.com
IdentityFile ~/.ssh/github.pub
Use the following Bash alias for convenient Git operations:
$ alias ssh-shell='trezor-agent ~/.ssh/github.pub -v --shell'
Now, you can use regular Git commands under the "SSH-enabled" sub-shell:
$ ssh-shell
$ git push origin master
The same works for Mercurial (e.g. on [BitBucket](https://confluence.atlassian.com/bitbucket/set-up-ssh-for-mercurial-728138122.html)):
$ ssh-shell
$ hg push
### Start the agent as a systemd unit
##### 1. Create these files in `~/.config/systemd/user`
Replace `trezor` with `keepkey` or `ledger` as required.
###### `trezor-ssh-agent.service`
````
[Unit]
Description=trezor-agent SSH agent
Requires=trezor-ssh-agent.socket
[Service]
Type=simple
Environment="DISPLAY=:0"
Environment="PATH=/bin:/usr/bin:/usr/local/bin:%h/.local/bin"
ExecStart=/usr/bin/trezor-agent --foreground --sock-path %t/trezor-agent/S.ssh IDENTITY
````
If you've installed `trezor-agent` locally you may have to change the path in `ExecStart=`.
Replace `IDENTITY` with the identity you used when exporting the public key.
If you have multiple Trezors connected, you can select which one to use via a `TREZOR_PATH`
environment variable. Use `trezorctl list` to find the correct path. Then add it
to the agent with the following line:
````
Environment="TREZOR_PATH=<your path here>"
````
Note that USB paths depend on the _USB port_ which you use.
###### `trezor-ssh-agent.socket`
````
[Unit]
Description=trezor-agent SSH agent socket
[Socket]
ListenStream=%t/trezor-agent/S.ssh
FileDescriptorName=ssh
Service=trezor-ssh-agent.service
SocketMode=0600
DirectoryMode=0700
[Install]
WantedBy=sockets.target
````
##### 2. Run
```
systemctl --user start trezor-ssh-agent.service trezor-ssh-agent.socket
systemctl --user enable trezor-ssh-agent.socket
```
##### 3. Add this line to your `.bashrc` or equivalent file:
```bash
export SSH_AUTH_SOCK=$(systemctl show --user --property=Listen trezor-ssh-agent.socket | grep -o "/run.*")
```
##### 4. SSH will now automatically use your device key in all terminals.
## 4. Troubleshooting
If SSH connection fails to work, please open an [issue](https://github.com/romanz/trezor-agent/issues)
with a verbose log attached (by running `trezor-agent -vv`) .
##### `IdentitiesOnly` SSH option
Note that your local SSH configuration may ignore `trezor-agent`, if it has `IdentitiesOnly` option set to `yes`.
IdentitiesOnly
Specifies that ssh(1) should only use the authentication identity files configured in
the ssh_config files, even if ssh-agent(1) or a PKCS11Provider offers more identities.
The argument to this keyword must be “yes” or “no”.
This option is intended for situations where ssh-agent offers many different identities.
The default is “no”.
If you are failing to connect, save your public key using:
$ trezor-agent -vv foobar@hostname.com > ~/.ssh/hostname.pub
And add the following lines to `~/.ssh/config` (providing the public key explicitly to SSH):
Host hostname.com
User foobar
IdentityFile ~/.ssh/hostname.pub
Then, the following commands should successfully command to the remote host:
$ trezor-agent -v foobar@hostname.com -s
$ ssh foobar@hostname.com
or,
$ trezor-agent -v foobar@hostname.com -c

@ -0,0 +1,26 @@
# Tutorial
First, install [Thunderbird](https://www.mozilla.org/en-US/thunderbird/) and
the [Enigmail](https://www.enigmail.net/index.php/en/) add-on.
Make sure to use the correct GNUPGHOME path before starting Thunderbird:
```bash
$ export GNUPGHOME=${HOME}/.gnupg/trezor
$ thunderbird
```
Run the Enigmail's setup wizard and choose your GPG identity:
![01](https://user-images.githubusercontent.com/9900/31327339-47a5f69a-acd7-11e7-997c-7b5a286fe5bc.png)
![02](https://user-images.githubusercontent.com/9900/31327344-51dcd246-acd7-11e7-8cdc-dd305a512dbb.png)
![03](https://user-images.githubusercontent.com/9900/31327346-546862a0-acd7-11e7-8e00-b40994bd6f17.png)
Then, you can compose encrypted (and signed) messages using the regular UI:
NOTES:
- The email's title is **public** - only the body is encrypted.
- You will be asked to confirm the signature using the hardware device before sending the email.
![04](https://user-images.githubusercontent.com/9900/31327356-660d098e-acd7-11e7-9e43-762898f5b57e.png)
![05](https://user-images.githubusercontent.com/9900/31327365-76679dda-acd7-11e7-9403-6965f0c6d8fe.png)
After receiving the email, you will be asked to confirm the decryption the hardware device:
![06](https://user-images.githubusercontent.com/9900/31327371-7c1da4cc-acd7-11e7-9a5a-20accf621b49.png)

@ -0,0 +1 @@
"""SSH-agent implementation using hardware authentication devices."""

@ -0,0 +1,3 @@
"""Cryptographic hardware device management."""
from . import interface, ui

@ -0,0 +1,74 @@
"""Fake device - ONLY FOR TESTS!!! (NEVER USE WITH REAL DATA)."""
import hashlib
import logging
import ecdsa
from . import interface
from .. import formats
log = logging.getLogger(__name__)
def _verify_support(identity):
"""Make sure the device supports given configuration."""
if identity.curve_name not in {formats.CURVE_NIST256}:
raise NotImplementedError(
'Unsupported elliptic curve: {}'.format(identity.curve_name))
class FakeDevice(interface.Device):
"""Connection to TREZOR device."""
@classmethod
def package_name(cls):
"""Python package name."""
return 'fake-device-agent'
def connect(self):
"""Return "dummy" connection."""
log.critical('NEVER USE THIS CODE FOR REAL-LIFE USE-CASES!!!')
log.critical('ONLY FOR DEBUGGING AND TESTING!!!')
# The code below uses HARD-CODED secret key - and should be used ONLY
# for GnuPG integration tests (e.g. when no real device is available).
# pylint: disable=attribute-defined-outside-init
self.secexp = 1
self.sk = ecdsa.SigningKey.from_secret_exponent(
secexp=self.secexp, curve=ecdsa.curves.NIST256p, hashfunc=hashlib.sha256)
self.vk = self.sk.get_verifying_key()
return self
def close(self):
"""Close connection."""
self.conn = None
def pubkey(self, identity, ecdh=False):
"""Return public key."""
_verify_support(identity)
data = self.vk.to_string()
x, y = data[:32], data[32:]
prefix = bytearray([2 + (bytearray(y)[0] & 1)])
return bytes(prefix) + x
def sign(self, identity, blob):
"""Sign given blob and return the signature (as bytes)."""
if identity.identity_dict['proto'] in {'ssh'}:
digest = hashlib.sha256(blob).digest()
else:
digest = blob
return self.sk.sign_digest_deterministic(digest=digest,
hashfunc=hashlib.sha256)
def ecdh(self, identity, pubkey):
"""Get shared session key using Elliptic Curve Diffie-Hellman."""
assert pubkey[:1] == b'\x04'
peer = ecdsa.VerifyingKey.from_string(
pubkey[1:],
curve=ecdsa.curves.NIST256p,
hashfunc=hashlib.sha256)
shared = ecdsa.VerifyingKey.from_public_point(
point=(peer.pubkey.point * self.secexp),
curve=ecdsa.curves.NIST256p,
hashfunc=hashlib.sha256)
return shared.to_string()

@ -0,0 +1,143 @@
"""Device abstraction layer."""
import hashlib
import io
import logging
import re
import struct
import unidecode
from .. import formats, util
log = logging.getLogger(__name__)
_identity_regexp = re.compile(''.join([
'^'
r'(?:(?P<proto>.*)://)?',
r'(?:(?P<user>.*)@)?',
r'(?P<host>.*?)',
r'(?::(?P<port>\w*))?',
r'(?P<path>/.*)?',
'$'
]))
def string_to_identity(identity_str):
"""Parse string into Identity dictionary."""
m = _identity_regexp.match(identity_str)
result = m.groupdict()
log.debug('parsed identity: %s', result)
return {k: v for k, v in result.items() if v}
def identity_to_string(identity_dict):
"""Dump Identity dictionary into its string representation."""
result = []
if identity_dict.get('proto'):
result.append(identity_dict['proto'] + '://')
if identity_dict.get('user'):
result.append(identity_dict['user'] + '@')
result.append(identity_dict['host'])
if identity_dict.get('port'):
result.append(':' + identity_dict['port'])
if identity_dict.get('path'):
result.append(identity_dict['path'])
log.debug('identity parts: %s', result)
return ''.join(result)
class Error(Exception):
"""Device-related error."""
class NotFoundError(Error):
"""Device could not be found."""
class DeviceError(Error):
"""Error during device operation."""
class Identity:
"""Represent SLIP-0013 identity, together with a elliptic curve choice."""
def __init__(self, identity_str, curve_name):
"""Configure for specific identity and elliptic curve usage."""
self.identity_dict = string_to_identity(identity_str)
self.curve_name = curve_name
def items(self):
"""Return a copy of identity_dict items."""
return [(k, unidecode.unidecode(v))
for k, v in self.identity_dict.items()]
def to_bytes(self):
"""Transliterate Unicode into ASCII."""
s = identity_to_string(self.identity_dict)
return unidecode.unidecode(s).encode('ascii')
def to_string(self):
"""Return identity serialized to string."""
return u'<{}|{}>'.format(identity_to_string(self.identity_dict), self.curve_name)
def get_bip32_address(self, ecdh=False):
"""Compute BIP32 derivation address according to SLIP-0013/0017."""
index = struct.pack('<L', self.identity_dict.get('index', 0))
addr = index + self.to_bytes()
log.debug('bip32 address string: %r', addr)
digest = hashlib.sha256(addr).digest()
s = io.BytesIO(bytearray(digest))
hardened = 0x80000000
addr_0 = 17 if bool(ecdh) else 13
address_n = [addr_0] + list(util.recv(s, '<LLLL'))
return [(hardened | value) for value in address_n]
def get_curve_name(self, ecdh=False):
"""Return correct curve name for device operations."""
if ecdh:
return formats.get_ecdh_curve_name(self.curve_name)
else:
return self.curve_name
class Device:
"""Abstract cryptographic hardware device interface."""
def __init__(self):
"""C-tor."""
self.conn = None
def connect(self):
"""Connect to device, otherwise raise NotFoundError."""
raise NotImplementedError()
def __enter__(self):
"""Allow usage as context manager."""
self.conn = self.connect()
return self
def __exit__(self, *args):
"""Close and mark as disconnected."""
try:
self.conn.close()
except Exception as e: # pylint: disable=broad-except
log.exception('close failed: %s', e)
self.conn = None
def pubkey(self, identity, ecdh=False):
"""Get public key (as bytes)."""
raise NotImplementedError()
def sign(self, identity, blob):
"""Sign given blob and return the signature (as bytes)."""
raise NotImplementedError()
def ecdh(self, identity, pubkey):
"""Get shared session key using Elliptic Curve Diffie-Hellman."""
raise NotImplementedError()
def __str__(self):
"""Human-readable representation."""
return '{}'.format(self.__class__.__name__)

@ -0,0 +1,45 @@
"""KeepKey-related code (see https://www.keepkey.com/)."""
from . import trezor
from .. import formats
def _verify_support(identity, ecdh):
"""Make sure the device supports given configuration."""
protocol = identity.identity_dict['proto']
if protocol not in {'ssh'}:
raise NotImplementedError(
'Unsupported protocol: {}'.format(protocol))
if ecdh:
raise NotImplementedError('No support for ECDH')
if identity.curve_name not in {formats.CURVE_NIST256}:
raise NotImplementedError(
'Unsupported elliptic curve: {}'.format(identity.curve_name))
class KeepKey(trezor.Trezor):
"""Connection to KeepKey device."""
@classmethod
def package_name(cls):
"""Python package name (at PyPI)."""
return 'keepkey-agent'
@property
def _defs(self):
from . import keepkey_defs
return keepkey_defs
required_version = '>=1.0.4'
def _override_state_handler(self, _):
"""No support for `state` handling on Keepkey."""
def pubkey(self, identity, ecdh=False):
"""Return public key."""
_verify_support(identity, ecdh)
return trezor.Trezor.pubkey(self, identity=identity, ecdh=ecdh)
def ecdh(self, identity, pubkey):
"""No support for ECDH in KeepKey firmware."""
_verify_support(identity, ecdh=True)

@ -0,0 +1,14 @@
"""KeepKey-related definitions."""
# pylint: disable=unused-import,import-error
from keepkeylib.client import CallException, PinException
from keepkeylib.client import KeepKeyClient as Client
from keepkeylib.messages_pb2 import PassphraseAck, PinMatrixAck
from keepkeylib.transport_hid import HidTransport
from keepkeylib.types_pb2 import IdentityType
def find_device():
"""Returns first USB HID transport."""
return next(HidTransport(p) for p in HidTransport.enumerate())

@ -0,0 +1,122 @@
"""Ledger-related code (see https://www.ledgerwallet.com/)."""
import binascii
import logging
import struct
from ledgerblue import comm # pylint: disable=import-error
from . import interface
log = logging.getLogger(__name__)
def _expand_path(path):
"""Convert BIP32 path into bytes."""
return b''.join((struct.pack('>I', e) for e in path))
def _convert_public_key(ecdsa_curve_name, result):
"""Convert Ledger reply into PublicKey object."""
if ecdsa_curve_name == 'nist256p1':
if (result[64] & 1) != 0:
result = bytearray([0x03]) + result[1:33]
else:
result = bytearray([0x02]) + result[1:33]
else:
result = result[1:]
keyX = bytearray(result[0:32])
keyY = bytearray(result[32:][::-1])
if (keyX[31] & 1) != 0:
keyY[31] |= 0x80
result = b'\x00' + bytes(keyY)
return bytes(result)
class LedgerNanoS(interface.Device):
"""Connection to Ledger Nano S device."""
@classmethod
def package_name(cls):
"""Python package name (at PyPI)."""
return 'ledger-agent'
def connect(self):
"""Enumerate and connect to the first USB HID interface."""
try:
return comm.getDongle()
except comm.CommException as e:
raise interface.NotFoundError(
'{} not connected: "{}"'.format(self, e))
def pubkey(self, identity, ecdh=False):
"""Get PublicKey object for specified BIP32 address and elliptic curve."""
curve_name = identity.get_curve_name(ecdh)
path = _expand_path(identity.get_bip32_address(ecdh))
if curve_name == 'nist256p1':
p2 = '01'
else:
p2 = '02'
apdu = '800200' + p2
apdu = binascii.unhexlify(apdu)
apdu += bytearray([len(path) + 1, len(path) // 4])
apdu += path
log.debug('apdu: %r', apdu)
result = bytearray(self.conn.exchange(bytes(apdu)))
log.debug('result: %r', result)
return _convert_public_key(curve_name, result[1:])
def sign(self, identity, blob):
"""Sign given blob and return the signature (as bytes)."""
path = _expand_path(identity.get_bip32_address(ecdh=False))
if identity.identity_dict['proto'] == 'ssh':
ins = '04'
p1 = '00'
else:
ins = '08'
p1 = '00'
if identity.curve_name == 'nist256p1':
p2 = '81' if identity.identity_dict['proto'] == 'ssh' else '01'
else:
p2 = '82' if identity.identity_dict['proto'] == 'ssh' else '02'
apdu = '80' + ins + p1 + p2
apdu = binascii.unhexlify(apdu)
apdu += bytearray([len(blob) + len(path) + 1])
apdu += bytearray([len(path) // 4]) + path
apdu += blob
log.debug('apdu: %r', apdu)
result = bytearray(self.conn.exchange(bytes(apdu)))
log.debug('result: %r', result)
if identity.curve_name == 'nist256p1':
offset = 3
length = result[offset]
r = result[offset+1:offset+1+length]
if r[0] == 0:
r = r[1:]
offset = offset + 1 + length + 1
length = result[offset]
s = result[offset+1:offset+1+length]
if s[0] == 0:
s = s[1:]
offset = offset + 1 + length
return bytes(r) + bytes(s)
else:
return bytes(result[:64])
def ecdh(self, identity, pubkey):
"""Get shared session key using Elliptic Curve Diffie-Hellman."""
path = _expand_path(identity.get_bip32_address(ecdh=True))
if identity.curve_name == 'nist256p1':
p2 = '01'
else:
p2 = '02'
apdu = '800a00' + p2
apdu = binascii.unhexlify(apdu)
apdu += bytearray([len(pubkey) + len(path) + 1])
apdu += bytearray([len(path) // 4]) + path
apdu += pubkey
log.debug('apdu: %r', apdu)
result = bytearray(self.conn.exchange(bytes(apdu)))
log.debug('result: %r', result)
assert result[0] == 0x04
return bytes(result)

@ -0,0 +1,191 @@
"""TREZOR-related code (see http://bitcointrezor.com/)."""
import binascii
import logging
import mnemonic
import semver
from . import interface
from .. import util
log = logging.getLogger(__name__)
class Trezor(interface.Device):
"""Connection to TREZOR device."""
@classmethod
def package_name(cls):
"""Python package name (at PyPI)."""
return 'trezor-agent'
@property
def _defs(self):
from . import trezor_defs
return trezor_defs
required_version = '>=1.4.0'
ui = None # can be overridden by device's users
def _override_pin_handler(self, conn):
if self.ui is None:
return
def new_handler(_):
try:
scrambled_pin = self.ui.get_pin()
result = self._defs.PinMatrixAck(pin=scrambled_pin)
if not set(scrambled_pin).issubset('123456789'):
raise self._defs.PinException(
None, 'Invalid scrambled PIN: {!r}'.format(result.pin))
return result
except: # noqa
conn.init_device()
raise
conn.callback_PinMatrixRequest = new_handler
cached_passphrase_ack = util.ExpiringCache(seconds=float('inf'))
cached_state = None
def _override_passphrase_handler(self, conn):
if self.ui is None:
return
def new_handler(msg):
try:
if msg.on_device is True:
return self._defs.PassphraseAck()
ack = self.__class__.cached_passphrase_ack.get()
if ack:
log.debug('re-using cached %s passphrase', self)
return ack
passphrase = self.ui.get_passphrase()
passphrase = mnemonic.Mnemonic.normalize_string(passphrase)
ack = self._defs.PassphraseAck(passphrase=passphrase)
length = len(ack.passphrase)
if length > 50:
msg = 'Too long passphrase ({} chars)'.format(length)
raise ValueError(msg)
self.__class__.cached_passphrase_ack.set(ack)
return ack
except: # noqa
conn.init_device()
raise
conn.callback_PassphraseRequest = new_handler
def _override_state_handler(self, conn):
def callback_PassphraseStateRequest(msg):
log.debug('caching state from %r', msg)
self.__class__.cached_state = msg.state
return self._defs.PassphraseStateAck()
conn.callback_PassphraseStateRequest = callback_PassphraseStateRequest
def _verify_version(self, connection):
f = connection.features
log.debug('connected to %s %s', self, f.device_id)
log.debug('label : %s', f.label)
log.debug('vendor : %s', f.vendor)
current_version = '{}.{}.{}'.format(f.major_version,
f.minor_version,
f.patch_version)
log.debug('version : %s', current_version)
log.debug('revision : %s', binascii.hexlify(f.revision))
if not semver.match(current_version, self.required_version):
fmt = ('Please upgrade your {} firmware to {} version'
' (current: {})')
raise ValueError(fmt.format(self, self.required_version,
current_version))
def connect(self):
"""Enumerate and connect to the first available interface."""
transport = self._defs.find_device()
if not transport:
raise interface.NotFoundError('{} not connected'.format(self))
log.debug('using transport: %s', transport)
for _ in range(5): # Retry a few times in case of PIN failures
connection = self._defs.Client(transport=transport,
state=self.__class__.cached_state)
self._override_pin_handler(connection)
self._override_passphrase_handler(connection)
self._override_state_handler(connection)
self._verify_version(connection)
try:
connection.ping(msg='', pin_protection=True) # unlock PIN
return connection
except (self._defs.PinException, ValueError) as e:
log.error('Invalid PIN: %s, retrying...', e)
continue
except Exception as e:
log.exception('ping failed: %s', e)
connection.close() # so the next HID open() will succeed
raise
def close(self):
"""Close connection."""
self.conn.close()
def pubkey(self, identity, ecdh=False):
"""Return public key."""
curve_name = identity.get_curve_name(ecdh=ecdh)
log.debug('"%s" getting public key (%s) from %s',
identity.to_string(), curve_name, self)
addr = identity.get_bip32_address(ecdh=ecdh)
result = self.conn.get_public_node(
n=addr, ecdsa_curve_name=curve_name)
log.debug('result: %s', result)
return bytes(result.node.public_key)
def _identity_proto(self, identity):
result = self._defs.IdentityType()
for name, value in identity.items():
setattr(result, name, value)
return result
def sign(self, identity, blob):
"""Sign given blob and return the signature (as bytes)."""
curve_name = identity.get_curve_name(ecdh=False)
log.debug('"%s" signing %r (%s) on %s',
identity.to_string(), blob, curve_name, self)
try:
result = self.conn.sign_identity(
identity=self._identity_proto(identity),
challenge_hidden=blob,
challenge_visual='',
ecdsa_curve_name=curve_name)
log.debug('result: %s', result)
assert len(result.signature) == 65
assert result.signature[:1] == b'\x00'
return bytes(result.signature[1:])
except self._defs.CallException as e:
msg = '{} error: {}'.format(self, e)
log.debug(msg, exc_info=True)
raise interface.DeviceError(msg)
def ecdh(self, identity, pubkey):
"""Get shared session key using Elliptic Curve Diffie-Hellman."""
curve_name = identity.get_curve_name(ecdh=True)
log.debug('"%s" shared session key (%s) for %r from %s',
identity.to_string(), curve_name, pubkey, self)
try:
result = self.conn.get_ecdh_session_key(
identity=self._identity_proto(identity),
peer_public_key=pubkey,
ecdsa_curve_name=curve_name)
log.debug('result: %s', result)
assert len(result.session_key) in {65, 33} # NIST256 or Curve25519
assert result.session_key[:1] == b'\x04'
return bytes(result.session_key)
except self._defs.CallException as e:
msg = '{} error: {}'.format(self, e)
log.debug(msg, exc_info=True)
raise interface.DeviceError(msg)

@ -0,0 +1,28 @@
"""TREZOR-related definitions."""
# pylint: disable=unused-import,import-error
import os
import logging
from trezorlib.client import CallException, PinException
from trezorlib.client import TrezorClient as Client
from trezorlib.messages import IdentityType, PassphraseAck, PinMatrixAck, PassphraseStateAck
try:
from trezorlib.transport import get_transport
except ImportError:
from trezorlib.device import TrezorDevice
get_transport = TrezorDevice.find_by_path
log = logging.getLogger(__name__)
def find_device():
"""Selects a transport based on `TREZOR_PATH` environment variable.
If unset, picks first connected device.
"""
try:
return get_transport(os.environ.get("TREZOR_PATH"))
except Exception as e: # pylint: disable=broad-except
log.debug("Failed to find a Trezor device: %s", e)

@ -0,0 +1,129 @@
"""UIs for PIN/passphrase entry."""
import logging
import os
import subprocess
from .. import util
log = logging.getLogger(__name__)
class UI:
"""UI for PIN/passphrase entry (for TREZOR devices)."""
def __init__(self, device_type, config=None):
"""C-tor."""
default_pinentry = 'pinentry' # by default, use GnuPG pinentry tool
if config is None:
config = {}
self.pin_entry_binary = config.get('pin_entry_binary',
default_pinentry)
self.passphrase_entry_binary = config.get('passphrase_entry_binary',
default_pinentry)
self.options_getter = create_default_options_getter()
self.device_name = device_type.__name__
def get_pin(self, name=None):
"""Ask the user for (scrambled) PIN."""
description = (
'Use the numeric keypad to describe number positions.\n'
'The layout is:\n'
' 7 8 9\n'
' 4 5 6\n'
' 1 2 3')
return interact(
title='{} PIN'.format(name or self.device_name),
prompt='PIN:',
description=description,
binary=self.pin_entry_binary,
options=self.options_getter())
def get_passphrase(self, name=None):
"""Ask the user for passphrase."""
return interact(
title='{} passphrase'.format(name or self.device_name),
prompt='Passphrase:',
description=None,
binary=self.passphrase_entry_binary,
options=self.options_getter())
def create_default_options_getter():
"""Return current TTY and DISPLAY settings for GnuPG pinentry."""
options = []
try:
ttyname = subprocess.check_output(args=['tty']).strip()
options.append(b'ttyname=' + ttyname)
except subprocess.CalledProcessError as e:
log.warning('no TTY found: %s', e)
display = os.environ.get('DISPLAY')
if display is not None:
options.append('display={}'.format(display).encode('ascii'))
else:
log.warning('DISPLAY not defined')
log.info('using %s for pinentry options', options)
return lambda: options
def write(p, line):
"""Send and flush a single line to the subprocess' stdin."""
log.debug('%s <- %r', p.args, line)
p.stdin.write(line)
p.stdin.flush()
class UnexpectedError(Exception):
"""Unexpected response."""
def expect(p, prefixes, confidential=False):
"""Read a line and return it without required prefix."""
resp = p.stdout.readline()
log.debug('%s -> %r', p.args, resp if not confidential else '********')
for prefix in prefixes:
if resp.startswith(prefix):
return resp[len(prefix):]
raise UnexpectedError(resp)
def interact(title, description, prompt, binary, options):
"""Use GPG pinentry program to interact with the user."""
args = [binary]
p = subprocess.Popen(args=args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
env=os.environ)
p.args = args # TODO: remove after Python 2 deprecation.
expect(p, [b'OK'])
title = util.assuan_serialize(title.encode('ascii'))
write(p, b'SETTITLE ' + title + b'\n')
expect(p, [b'OK'])
if description:
description = util.assuan_serialize(description.encode('ascii'))
write(p, b'SETDESC ' + description + b'\n')
expect(p, [b'OK'])
if prompt:
prompt = util.assuan_serialize(prompt.encode('ascii'))
write(p, b'SETPROMPT ' + prompt + b'\n')
expect(p, [b'OK'])
log.debug('setting %d options', len(options))
for opt in options:
write(p, b'OPTION ' + opt + b'\n')
expect(p, [b'OK', b'ERR'])
write(p, b'GETPIN\n')
pin = expect(p, [b'OK', b'D '], confidential=True)
p.communicate() # close stdin and wait for the process to exit
exit_code = p.wait()
if exit_code:
raise subprocess.CalledProcessError(exit_code, binary)
return pin.decode('ascii').strip()

@ -0,0 +1,212 @@
"""SSH format parsing and formatting tools."""
import base64
import hashlib
import io
import logging
import ecdsa
import ed25519
from . import util
log = logging.getLogger(__name__)
# Supported ECDSA curves (for SSH and GPG)
CURVE_NIST256 = 'nist256p1'
CURVE_ED25519 = 'ed25519'
SUPPORTED_CURVES = {CURVE_NIST256, CURVE_ED25519}
# Supported ECDH curves (for GPG)
ECDH_NIST256 = 'nist256p1'
ECDH_CURVE25519 = 'curve25519'
# SSH key types
SSH_NIST256_DER_OCTET = b'\x04'
SSH_NIST256_KEY_PREFIX = b'ecdsa-sha2-'
SSH_NIST256_CURVE_NAME = b'nistp256'
SSH_NIST256_KEY_TYPE = SSH_NIST256_KEY_PREFIX + SSH_NIST256_CURVE_NAME
SSH_ED25519_KEY_TYPE = b'ssh-ed25519'
SUPPORTED_KEY_TYPES = {SSH_NIST256_KEY_TYPE, SSH_ED25519_KEY_TYPE}
hashfunc = hashlib.sha256
def fingerprint(blob):
"""
Compute SSH fingerprint for specified blob.
See https://en.wikipedia.org/wiki/Public_key_fingerprint for details.
"""
digest = hashlib.md5(blob).digest()
return ':'.join('{:02x}'.format(c) for c in bytearray(digest))
def parse_pubkey(blob):
"""
Parse SSH public key from given blob.
Construct a verifier for ECDSA signatures.
The verifier returns the signatures in the required SSH format.
Currently, NIST256P1 and ED25519 elliptic curves are supported.
"""
fp = fingerprint(blob)
s = io.BytesIO(blob)
key_type = util.read_frame(s)
log.debug('key type: %s', key_type)
assert key_type in SUPPORTED_KEY_TYPES, key_type
result = {'blob': blob, 'type': key_type, 'fingerprint': fp}
if key_type == SSH_NIST256_KEY_TYPE:
curve_name = util.read_frame(s)
log.debug('curve name: %s', curve_name)
point = util.read_frame(s)
assert s.read() == b''
_type, point = point[:1], point[1:]
assert _type == SSH_NIST256_DER_OCTET
size = len(point) // 2
assert len(point) == 2 * size
coords = (util.bytes2num(point[:size]), util.bytes2num(point[size:]))
curve = ecdsa.NIST256p
point = ecdsa.ellipticcurve.Point(curve.curve, *coords)
def ecdsa_verifier(sig, msg):
assert len(sig) == 2 * size
sig_decode = ecdsa.util.sigdecode_string
vk = ecdsa.VerifyingKey.from_public_point(point, curve, hashfunc)
vk.verify(signature=sig, data=msg, sigdecode=sig_decode)
parts = [sig[:size], sig[size:]]
return b''.join([util.frame(b'\x00' + p) for p in parts])
result.update(point=coords, curve=CURVE_NIST256,
verifier=ecdsa_verifier)
if key_type == SSH_ED25519_KEY_TYPE:
pubkey = util.read_frame(s)
assert s.read() == b''
def ed25519_verify(sig, msg):
assert len(sig) == 64
vk = ed25519.VerifyingKey(pubkey)
vk.verify(sig, msg)
return sig
result.update(curve=CURVE_ED25519, verifier=ed25519_verify)
return result
def _decompress_ed25519(pubkey):
"""Load public key from the serialized blob (stripping the prefix byte)."""
if pubkey[:1] == b'\x00':
# set by Trezor fsm_msgSignIdentity() and fsm_msgGetPublicKey()
return ed25519.VerifyingKey(pubkey[1:])
else:
return None
def _decompress_nist256(pubkey):
"""
Load public key from the serialized blob.
The leading byte least-significant bit is used to decide how to recreate
the y-coordinate from the specified x-coordinate. See bitcoin/main.py#L198
(from https://github.com/vbuterin/pybitcointools/) for details.
"""
if pubkey[:1] in {b'\x02', b'\x03'}: # set by ecdsa_get_public_key33()
curve = ecdsa.NIST256p
P = curve.curve.p()
A = curve.curve.a()
B = curve.curve.b()
x = util.bytes2num(pubkey[1:33])
beta = pow(int(x * x * x + A * x + B), int((P + 1) // 4), int(P))
p0 = util.bytes2num(pubkey[:1])
y = (P - beta) if ((beta + p0) % 2) else beta
point = ecdsa.ellipticcurve.Point(curve.curve, x, y)
return ecdsa.VerifyingKey.from_public_point(point, curve=curve,
hashfunc=hashfunc)
else:
return None
def decompress_pubkey(pubkey, curve_name):
"""
Load public key from the serialized blob.
Raise ValueError on parsing error.
"""
vk = None
if len(pubkey) == 33:
decompress = {
CURVE_NIST256: _decompress_nist256,
CURVE_ED25519: _decompress_ed25519,
ECDH_CURVE25519: _decompress_ed25519,
}[curve_name]
vk = decompress(pubkey)
if not vk:
msg = 'invalid {!s} public key: {!r}'.format(curve_name, pubkey)
raise ValueError(msg)
return vk
def serialize_verifying_key(vk):
"""
Serialize a public key into SSH format (for exporting to text format).
Currently, NIST256P1 and ED25519 elliptic curves are supported.
Raise TypeError on unsupported key format.
"""
if isinstance(vk, ed25519.keys.VerifyingKey):
pubkey = vk.to_bytes()
key_type = SSH_ED25519_KEY_TYPE
blob = util.frame(SSH_ED25519_KEY_TYPE) + util.frame(pubkey)
return key_type, blob
if isinstance(vk, ecdsa.keys.VerifyingKey):
curve_name = SSH_NIST256_CURVE_NAME
key_blob = SSH_NIST256_DER_OCTET + vk.to_string()
parts = [SSH_NIST256_KEY_TYPE, curve_name, key_blob]
key_type = SSH_NIST256_KEY_TYPE
blob = b''.join([util.frame(p) for p in parts])
return key_type, blob
raise TypeError('unsupported {!r}'.format(vk))
def export_public_key(vk, label):
"""
Export public key to text format.
The resulting string can be written into a .pub file or
appended to the ~/.ssh/authorized_keys file.
"""
key_type, blob = serialize_verifying_key(vk)
log.debug('fingerprint: %s', fingerprint(blob))
b64 = base64.b64encode(blob).decode('ascii')
return u'{} {} {}\n'.format(key_type.decode('ascii'), b64, label)
def import_public_key(line):
"""Parse public key textual format, as saved at a .pub file."""
log.debug('loading SSH public key: %r', line)
file_type, base64blob, name = line.split()
blob = base64.b64decode(base64blob)
result = parse_pubkey(blob)
result['name'] = name.encode('utf-8')
assert result['type'] == file_type.encode('ascii')
log.debug('loaded %s public key: %s', file_type, result['fingerprint'])
return result
def get_ecdh_curve_name(signature_curve_name):
"""Return appropriate curve for ECDH for specified signing curve."""
return {
CURVE_NIST256: ECDH_NIST256,
CURVE_ED25519: ECDH_CURVE25519,
ECDH_CURVE25519: ECDH_CURVE25519,
}[signature_curve_name]

@ -0,0 +1,324 @@
"""
TREZOR support for ECDSA GPG signatures.
See these links for more details:
- https://www.gnupg.org/faq/whats-new-in-2.1.html
- https://tools.ietf.org/html/rfc4880
- https://tools.ietf.org/html/rfc6637
- https://tools.ietf.org/html/draft-irtf-cfrg-eddsa-05
"""
import argparse
import contextlib
import functools
import logging
import os
import re
import subprocess
import sys
import time
import pkg_resources
import semver
from . import agent, client, encode, keyring, protocol
from .. import device, formats, server, util
log = logging.getLogger(__name__)
def export_public_key(device_type, args):
"""Generate a new pubkey for a new/existing GPG identity."""
log.warning('NOTE: in order to re-generate the exact same GPG key later, '
'run this command with "--time=%d" commandline flag (to set '
'the timestamp of the GPG key manually).', args.time)
c = client.Client(device=device_type())
identity = client.create_identity(user_id=args.user_id,
curve_name=args.ecdsa_curve)
verifying_key = c.pubkey(identity=identity, ecdh=False)
decryption_key = c.pubkey(identity=identity, ecdh=True)
signer_func = functools.partial(c.sign, identity=identity)
if args.subkey: # add as subkey
log.info('adding %s GPG subkey for "%s" to existing key',
args.ecdsa_curve, args.user_id)
# subkey for signing
signing_key = protocol.PublicKey(
curve_name=args.ecdsa_curve, created=args.time,
verifying_key=verifying_key, ecdh=False)
# subkey for encryption
encryption_key = protocol.PublicKey(
curve_name=formats.get_ecdh_curve_name(args.ecdsa_curve),
created=args.time, verifying_key=decryption_key, ecdh=True)
primary_bytes = keyring.export_public_key(args.user_id)
result = encode.create_subkey(primary_bytes=primary_bytes,
subkey=signing_key,
signer_func=signer_func)
result = encode.create_subkey(primary_bytes=result,
subkey=encryption_key,
signer_func=signer_func)
else: # add as primary
log.info('creating new %s GPG primary key for "%s"',
args.ecdsa_curve, args.user_id)
# primary key for signing
primary = protocol.PublicKey(
curve_name=args.ecdsa_curve, created=args.time,
verifying_key=verifying_key, ecdh=False)
# subkey for encryption
subkey = protocol.PublicKey(
curve_name=formats.get_ecdh_curve_name(args.ecdsa_curve),
created=args.time, verifying_key=decryption_key, ecdh=True)
result = encode.create_primary(user_id=args.user_id,
pubkey=primary,
signer_func=signer_func)
result = encode.create_subkey(primary_bytes=result,
subkey=subkey,
signer_func=signer_func)
return protocol.armor(result, 'PUBLIC KEY BLOCK')
def verify_gpg_version():
"""Make sure that the installed GnuPG is not too old."""
existing_gpg = keyring.gpg_version().decode('ascii')
required_gpg = '>=2.1.11'
msg = 'Existing GnuPG has version "{}" ({} required)'.format(existing_gpg,
required_gpg)
if not semver.match(existing_gpg, required_gpg):
log.error(msg)
def check_output(args):
"""Runs command and returns the output as string."""
log.debug('run: %s', args)
out = subprocess.check_output(args=args).decode('utf-8')
log.debug('out: %r', out)
return out
def check_call(args, stdin=None, env=None):
"""Runs command and verifies its success."""
log.debug('run: %s%s', args, ' {}'.format(env) if env else '')
subprocess.check_call(args=args, stdin=stdin, env=env)
def write_file(path, data):
"""Writes data to specified path."""
with open(path, 'w') as f:
log.debug('setting %s contents:\n%s', path, data)
f.write(data)
return f
def run_init(device_type, args):
"""Initialize hardware-based GnuPG identity."""
util.setup_logging(verbosity=args.verbose)
log.warning('This GPG tool is still in EXPERIMENTAL mode, '
'so please note that the API and features may '
'change without backwards compatibility!')
verify_gpg_version()
# Prepare new GPG home directory for hardware-based identity
device_name = os.path.basename(sys.argv[0]).rsplit('-', 1)[0]
log.info('device name: %s', device_name)
homedir = args.homedir
if not homedir:
homedir = os.path.expanduser('~/.gnupg/{}'.format(device_name))
log.info('GPG home directory: %s', homedir)
if os.path.exists(homedir):
log.error('GPG home directory %s exists, '
'remove it manually if required', homedir)
sys.exit(1)
check_call(['mkdir', '-p', homedir])
check_call(['chmod', '700', homedir])
agent_path = util.which('{}-gpg-agent'.format(device_name))
# Prepare GPG agent invocation script (to pass the PATH from environment).
with open(os.path.join(homedir, 'run-agent.sh'), 'w') as f:
f.write(r"""#!/bin/sh
export PATH={0}
{1} \
-vv \
--pin-entry-binary={pin_entry_binary} \
--passphrase-entry-binary={passphrase_entry_binary} \
--cache-expiry-seconds={cache_expiry_seconds} \
$*
""".format(os.environ['PATH'], agent_path, **vars(args)))
check_call(['chmod', '700', f.name])
run_agent_script = f.name
# Prepare GPG configuration file
with open(os.path.join(homedir, 'gpg.conf'), 'w') as f:
f.write("""# Hardware-based GPG configuration
agent-program {0}
personal-digest-preferences SHA512
default-key \"{1}\"
""".format(run_agent_script, args.user_id))
# Prepare a helper script for setting up the new identity
with open(os.path.join(homedir, 'env'), 'w') as f:
f.write("""#!/bin/bash
set -eu
export GNUPGHOME={0}
COMMAND=$*
if [ -z "${{COMMAND}}" ]
then
${{SHELL}}
else
${{COMMAND}}
fi
""".format(homedir))
check_call(['chmod', '700', f.name])
# Generate new GPG identity and import into GPG keyring
pubkey = write_file(os.path.join(homedir, 'pubkey.asc'),
export_public_key(device_type, args))
verbosity = ('-' + ('v' * args.verbose)) if args.verbose else '--quiet'
check_call(keyring.gpg_command(['--homedir', homedir, verbosity,
'--import', pubkey.name]))
# Make new GPG identity with "ultimate" trust (via its fingerprint)
out = check_output(keyring.gpg_command(['--homedir', homedir,
'--list-public-keys',
'--with-fingerprint',
'--with-colons']))
fpr = re.findall('fpr:::::::::([0-9A-F]+):', out)[0]
f = write_file(os.path.join(homedir, 'ownertrust.txt'), fpr + ':6\n')
check_call(keyring.gpg_command(['--homedir', homedir,
'--import-ownertrust', f.name]))
# Load agent and make sure it responds with the new identity
check_call(keyring.gpg_command(['--list-secret-keys', args.user_id,
'--homedir', homedir]))
def run_unlock(device_type, args):
"""Unlock hardware device (for future interaction)."""
util.setup_logging(verbosity=args.verbose)
with device_type() as d:
log.info('unlocked %s device', d)
def _server_from_assuan_fd(env):
fd = env.get('_assuan_connection_fd')
if fd is None:
return None
log.info('using fd=%r for UNIX socket server', fd)
return server.unix_domain_socket_server_from_fd(int(fd))
def _server_from_sock_path(env):
sock_path = keyring.get_agent_sock_path(env=env)
return server.unix_domain_socket_server(sock_path)
def run_agent(device_type):
"""Run a simple GPG-agent server."""
p = argparse.ArgumentParser()
p.add_argument('--homedir', default=os.environ.get('GNUPGHOME'))
p.add_argument('-v', '--verbose', default=0, action='count')
p.add_argument('--server', default=False, action='store_true',
help='Use stdin/stdout for communication with GPG.')
p.add_argument('--pin-entry-binary', type=str, default='pinentry',
help='Path to PIN entry UI helper.')
p.add_argument('--passphrase-entry-binary', type=str, default='pinentry',
help='Path to passphrase entry UI helper.')
p.add_argument('--cache-expiry-seconds', type=float, default=float('inf'),
help='Expire passphrase from cache after this duration.')
args, _ = p.parse_known_args()
assert args.homedir
log_file = os.path.join(args.homedir, 'gpg-agent.log')
util.setup_logging(verbosity=args.verbose, filename=log_file)
log.debug('sys.argv: %s', sys.argv)
log.debug('os.environ: %s', os.environ)
log.debug('pid: %d, parent pid: %d', os.getpid(), os.getppid())
try:
env = {'GNUPGHOME': args.homedir, 'PATH': os.environ['PATH']}
pubkey_bytes = keyring.export_public_keys(env=env)
device_type.ui = device.ui.UI(device_type=device_type,
config=vars(args))
device_type.cached_passphrase_ack = util.ExpiringCache(
seconds=float(args.cache_expiry_seconds))
handler = agent.Handler(device=device_type(),
pubkey_bytes=pubkey_bytes)
sock_server = _server_from_assuan_fd(os.environ)
if sock_server is None:
sock_server = _server_from_sock_path(env)
with sock_server as sock:
for conn in agent.yield_connections(sock):
with contextlib.closing(conn):
try:
handler.handle(conn)
except agent.AgentStop:
log.info('stopping gpg-agent')
return
except IOError as e:
log.info('connection closed: %s', e)
return
except Exception as e: # pylint: disable=broad-except
log.exception('handler failed: %s', e)
except Exception as e: # pylint: disable=broad-except
log.exception('gpg-agent failed: %s', e)
def main(device_type):
"""Parse command-line arguments."""
epilog = ('See https://github.com/romanz/trezor-agent/blob/master/'
'doc/README-GPG.md for usage examples.')
parser = argparse.ArgumentParser(epilog=epilog)
agent_package = device_type.package_name()
resources_map = {r.key: r for r in pkg_resources.require(agent_package)}
resources = [resources_map[agent_package], resources_map['libagent']]
versions = '\n'.join('{}={}'.format(r.key, r.version) for r in resources)
parser.add_argument('--version', help='print the version info',
action='version', version=versions)
subparsers = parser.add_subparsers(title='Action', dest='action')
subparsers.required = True
p = subparsers.add_parser('init',
help='initialize hardware-based GnuPG identity')
p.add_argument('user_id')
p.add_argument('-e', '--ecdsa-curve', default='nist256p1')
p.add_argument('-t', '--time', type=int, default=int(time.time()))
p.add_argument('-v', '--verbose', default=0, action='count')
p.add_argument('-s', '--subkey', default=False, action='store_true')
p.add_argument('--homedir', type=str, default=os.environ.get('GNUPGHOME'),
help='Customize GnuPG home directory for the new identity.')
p.add_argument('--pin-entry-binary', type=str, default='pinentry',
help='Path to PIN entry UI helper.')
p.add_argument('--passphrase-entry-binary', type=str, default='pinentry',
help='Path to passphrase entry UI helper.')
p.add_argument('--cache-expiry-seconds', type=float, default=float('inf'),
help='Expire passphrase from cache after this duration.')
p.set_defaults(func=run_init)
p = subparsers.add_parser('unlock', help='unlock the hardware device')
p.add_argument('-v', '--verbose', default=0, action='count')
p.set_defaults(func=run_unlock)
args = parser.parse_args()
device_type.ui = device.ui.UI(device_type=device_type, config=vars(args))
device_type.cached_passphrase_ack = util.ExpiringCache(
seconds=float(args.cache_expiry_seconds))
return args.func(device_type=device_type, args=args)

@ -0,0 +1,247 @@
"""GPG-agent utilities."""
import binascii
import logging
from . import client, decode, keyring, protocol
from .. import util
log = logging.getLogger(__name__)
def yield_connections(sock):
"""Run a server on the specified socket."""
while True:
log.debug('waiting for connection on %s', sock.getsockname())
try:
conn, _ = sock.accept()
except KeyboardInterrupt:
return
conn.settimeout(None)
log.debug('accepted connection on %s', sock.getsockname())
yield conn
def sig_encode(r, s):
"""Serialize ECDSA signature data into GPG S-expression."""
r = util.assuan_serialize(util.num2bytes(r, 32))
s = util.assuan_serialize(util.num2bytes(s, 32))
return b'(7:sig-val(5:ecdsa(1:r32:' + r + b')(1:s32:' + s + b')))'
def _serialize_point(data):
prefix = '{}:'.format(len(data)).encode('ascii')
# https://www.gnupg.org/documentation/manuals/assuan/Server-responses.html
return b'(5:value' + util.assuan_serialize(prefix + data) + b')'
def parse_ecdh(line):
"""Parse ECDH request and return remote public key."""
prefix, line = line.split(b' ', 1)
assert prefix == b'D'
exp, leftover = keyring.parse(keyring.unescape(line))
log.debug('ECDH s-exp: %r', exp)
assert not leftover
label, exp = exp
assert label == b'enc-val'
assert exp[0] == b'ecdh'
items = exp[1:]
log.debug('ECDH parameters: %r', items)
return dict(items)[b'e']
def _key_info(conn, args):
"""
Dummy reply (mainly for 'gpg --edit' to succeed).
For details, see GnuPG agent KEYINFO command help.
https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=agent/command.c;h=c8b34e9882076b1b724346787781f657cac75499;hb=refs/heads/master#l1082
"""
fmt = 'S KEYINFO {0} X - - - - - - -'
keygrip, = args
keyring.sendline(conn, fmt.format(keygrip).encode('ascii'))
class AgentError(Exception):
"""GnuPG agent-related error."""
class AgentStop(Exception):
"""Raised to close the agent."""
# pylint: disable=too-many-instance-attributes
class Handler:
"""GPG agent requests' handler."""
def _get_options(self):
return self.options
def __init__(self, device, pubkey_bytes):
"""C-tor."""
self.reset()
self.options = []
device.ui.options_getter = self._get_options
self.client = client.Client(device=device)
# Cache public keys from GnuPG
self.pubkey_bytes = pubkey_bytes
# "Clone" existing GPG version
self.version = keyring.gpg_version()
self.handlers = {
b'RESET': lambda *_: self.reset(),
b'OPTION': lambda _, args: self.handle_option(*args),
b'SETKEYDESC': None,
b'NOP': None,
b'GETINFO': self.handle_getinfo,
b'AGENT_ID': lambda conn, _: keyring.sendline(conn, b'D TREZOR'), # "Fake" agent ID
b'SIGKEY': lambda _, args: self.set_key(*args),
b'SETKEY': lambda _, args: self.set_key(*args),
b'SETHASH': lambda _, args: self.set_hash(*args),
b'PKSIGN': lambda conn, _: self.pksign(conn),
b'PKDECRYPT': lambda conn, _: self.pkdecrypt(conn),
b'HAVEKEY': lambda _, args: self.have_key(*args),
b'KEYINFO': _key_info,
b'SCD': self.handle_scd,
b'GET_PASSPHRASE': self.handle_get_passphrase,
}
def reset(self):
"""Reset agent's state variables."""
self.keygrip = None
self.digest = None
self.algo = None
def handle_option(self, opt):
"""Store GPG agent-related options (e.g. for pinentry)."""
self.options.append(opt)
log.debug('options: %s', self.options)
def handle_get_passphrase(self, conn, _):
"""Allow simple GPG symmetric encryption (using a passphrase)."""
p1 = self.client.device.ui.get_passphrase('Symmetric encryption')
p2 = self.client.device.ui.get_passphrase('Re-enter encryption')
if p1 == p2:
result = b'D ' + util.assuan_serialize(p1.encode('ascii'))
keyring.sendline(conn, result, confidential=True)
else:
log.warning('Passphrase does not match!')
def handle_getinfo(self, conn, args):
"""Handle some of the GETINFO messages."""
result = None
if args[0] == b'version':
result = self.version
elif args[0] == b's2k_count':
# Use highest number of S2K iterations.
# https://www.gnupg.org/documentation/manuals/gnupg/OpenPGP-Options.html
# https://tools.ietf.org/html/rfc4880#section-3.7.1.3
result = '{}'.format(64 << 20).encode('ascii')
else:
log.warning('Unknown GETINFO command: %s', args)
if result:
keyring.sendline(conn, b'D ' + result)
def handle_scd(self, conn, args):
"""No support for smart-card device protocol."""
reply = {
(b'GETINFO', b'version'): self.version,
}.get(args)
if reply is None:
raise AgentError(b'ERR 100696144 No such device <SCD>')
keyring.sendline(conn, b'D ' + reply)
@util.memoize_method # global cache for key grips
def get_identity(self, keygrip):
"""
Returns device.interface.Identity that matches specified keygrip.
In case of missing keygrip, KeyError will be raised.
"""
keygrip_bytes = binascii.unhexlify(keygrip)
pubkey_dict, user_ids = decode.load_by_keygrip(
pubkey_bytes=self.pubkey_bytes, keygrip=keygrip_bytes)
# We assume the first user ID is used to generate TREZOR-based GPG keys.
user_id = user_ids[0]['value'].decode('utf-8')
curve_name = protocol.get_curve_name_by_oid(pubkey_dict['curve_oid'])
ecdh = (pubkey_dict['algo'] == protocol.ECDH_ALGO_ID)
identity = client.create_identity(user_id=user_id, curve_name=curve_name)
verifying_key = self.client.pubkey(identity=identity, ecdh=ecdh)
pubkey = protocol.PublicKey(
curve_name=curve_name, created=pubkey_dict['created'],
verifying_key=verifying_key, ecdh=ecdh)
assert pubkey.key_id() == pubkey_dict['key_id']
assert pubkey.keygrip() == keygrip_bytes
return identity
def pksign(self, conn):
"""Sign a message digest using a private EC key."""
log.debug('signing %r digest (algo #%s)', self.digest, self.algo)
identity = self.get_identity(keygrip=self.keygrip)
r, s = self.client.sign(identity=identity,
digest=binascii.unhexlify(self.digest))
result = sig_encode(r, s)
log.debug('result: %r', result)
keyring.sendline(conn, b'D ' + result)
def pkdecrypt(self, conn):
"""Handle decryption using ECDH."""
for msg in [b'S INQUIRE_MAXLEN 4096', b'INQUIRE CIPHERTEXT']:
keyring.sendline(conn, msg)
line = keyring.recvline(conn)
assert keyring.recvline(conn) == b'END'
remote_pubkey = parse_ecdh(line)
identity = self.get_identity(keygrip=self.keygrip)
ec_point = self.client.ecdh(identity=identity, pubkey=remote_pubkey)
keyring.sendline(conn, b'D ' + _serialize_point(ec_point))
def have_key(self, *keygrips):
"""Check if any keygrip corresponds to a TREZOR-based key."""
for keygrip in keygrips:
try:
self.get_identity(keygrip=keygrip)
break
except KeyError as e:
log.warning('HAVEKEY(%s) failed: %s', keygrip, e)
else:
raise AgentError(b'ERR 67108881 No secret key <GPG Agent>')
def set_key(self, keygrip):
"""Set hexadecimal keygrip for next operation."""
self.keygrip = keygrip
def set_hash(self, algo, digest):
"""Set algorithm ID and hexadecimal digest for next operation."""
self.algo = algo
self.digest = digest
def handle(self, conn):
"""Handle connection from GPG binary using the ASSUAN protocol."""
keyring.sendline(conn, b'OK')
for line in keyring.iterlines(conn):
parts = line.split(b' ')
command = parts[0]
args = tuple(parts[1:])
if command == b'BYE':
return
elif command == b'KILLAGENT':
keyring.sendline(conn, b'OK')
raise AgentStop()
if command not in self.handlers:
log.error('unknown request: %r', line)
continue
handler = self.handlers[command]
if handler:
try:
handler(conn, args)
except AgentError as e:
msg, = e.args
keyring.sendline(conn, msg)
continue
keyring.sendline(conn, b'OK')

@ -0,0 +1,48 @@
"""Device abstraction layer for GPG operations."""
import logging
from .. import formats, util
from ..device import interface
log = logging.getLogger(__name__)
def create_identity(user_id, curve_name):
"""Create GPG identity for hardware device."""
result = interface.Identity(identity_str='gpg://', curve_name=curve_name)
result.identity_dict['host'] = user_id
return result
class Client:
"""Sign messages and get public keys from a hardware device."""
def __init__(self, device):
"""C-tor."""
self.device = device
def pubkey(self, identity, ecdh=False):
"""Return public key as VerifyingKey object."""
with self.device:
pubkey = self.device.pubkey(ecdh=ecdh, identity=identity)
return formats.decompress_pubkey(
pubkey=pubkey, curve_name=identity.curve_name)
def sign(self, identity, digest):
"""Sign the digest and return a serialized signature."""
log.info('please confirm GPG signature on %s for "%s"...',
self.device, identity.to_string())
if identity.curve_name == formats.CURVE_NIST256:
digest = digest[:32] # sign the first 256 bits
log.debug('signing digest: %s', util.hexlify(digest))
with self.device:
sig = self.device.sign(blob=digest, identity=identity)
return (util.bytes2num(sig[:32]), util.bytes2num(sig[32:]))
def ecdh(self, identity, pubkey):
"""Derive shared secret using ECDH from remote public key."""
log.info('please confirm GPG decryption on %s for "%s"...',
self.device, identity.to_string())
with self.device:
return self.device.ecdh(pubkey=pubkey, identity=identity)

@ -0,0 +1,320 @@
"""Decoders for GPG v2 data structures."""
import base64
import functools
import hashlib
import io
import logging
import struct
import ecdsa
import ed25519
from . import protocol
from .. import util
log = logging.getLogger(__name__)
def parse_subpackets(s):
"""See https://tools.ietf.org/html/rfc4880#section-5.2.3.1 for details."""
subpackets = []
total_size = s.readfmt('>H')
data = s.read(total_size)
s = util.Reader(io.BytesIO(data))
while True:
try:
first = s.readfmt('B')
except EOFError:
break
if first < 192:
subpacket_len = first
elif first < 255:
subpacket_len = ((first - 192) << 8) + s.readfmt('B') + 192
else: # first == 255
subpacket_len = s.readfmt('>L')
subpackets.append(s.read(subpacket_len))
return subpackets
def parse_mpi(s):
"""See https://tools.ietf.org/html/rfc4880#section-3.2 for details."""
bits = s.readfmt('>H')
blob = bytearray(s.read(int((bits + 7) // 8)))
return sum(v << (8 * i) for i, v in enumerate(reversed(blob)))
def parse_mpis(s, n):
"""Parse multiple MPIs from stream."""
return [parse_mpi(s) for _ in range(n)]
def _parse_nist256p1_pubkey(mpi):
prefix, x, y = util.split_bits(mpi, 4, 256, 256)
if prefix != 4:
raise ValueError('Invalid MPI prefix: {}'.format(prefix))
point = ecdsa.ellipticcurve.Point(curve=ecdsa.NIST256p.curve,
x=x, y=y)
return ecdsa.VerifyingKey.from_public_point(
point=point, curve=ecdsa.curves.NIST256p,
hashfunc=hashlib.sha256)
def _parse_ed25519_pubkey(mpi):
prefix, value = util.split_bits(mpi, 8, 256)
if prefix != 0x40:
raise ValueError('Invalid MPI prefix: {}'.format(prefix))
return ed25519.VerifyingKey(util.num2bytes(value, size=32))
SUPPORTED_CURVES = {
b'\x2A\x86\x48\xCE\x3D\x03\x01\x07':
(_parse_nist256p1_pubkey, protocol.keygrip_nist256),
b'\x2B\x06\x01\x04\x01\xDA\x47\x0F\x01':
(_parse_ed25519_pubkey, protocol.keygrip_ed25519),
b'\x2B\x06\x01\x04\x01\x97\x55\x01\x05\x01':
(_parse_ed25519_pubkey, protocol.keygrip_curve25519),
}
RSA_ALGO_IDS = {1, 2, 3}
ELGAMAL_ALGO_ID = 16
DSA_ALGO_ID = 17
ECDSA_ALGO_IDS = {18, 19, 22} # {ecdsa, nist256, ed25519}
def _parse_embedded_signatures(subpackets):
for packet in subpackets:
data = bytearray(packet)
if data[0] == 32:
# https://tools.ietf.org/html/rfc4880#section-5.2.3.26
stream = io.BytesIO(data[1:])
yield _parse_signature(util.Reader(stream))
def has_custom_subpacket(signature_packet):
"""Detect our custom public keys by matching subpacket data."""
return any(protocol.CUSTOM_KEY_LABEL == subpacket[1:]
for subpacket in signature_packet['unhashed_subpackets'])
def _parse_signature(stream):
"""See https://tools.ietf.org/html/rfc4880#section-5.2 for details."""
p = {'type': 'signature'}
to_hash = io.BytesIO()
with stream.capture(to_hash):
p['version'] = stream.readfmt('B')
p['sig_type'] = stream.readfmt('B')
p['pubkey_alg'] = stream.readfmt('B')
p['hash_alg'] = stream.readfmt('B')
p['hashed_subpackets'] = parse_subpackets(stream)
# https://tools.ietf.org/html/rfc4880#section-5.2.4
tail_to_hash = b'\x04\xff' + struct.pack('>L', to_hash.tell())
p['_to_hash'] = to_hash.getvalue() + tail_to_hash
p['unhashed_subpackets'] = parse_subpackets(stream)
embedded = list(_parse_embedded_signatures(p['unhashed_subpackets']))
if embedded:
log.debug('embedded sigs: %s', embedded)
p['embedded'] = embedded
p['hash_prefix'] = stream.readfmt('2s')
if p['pubkey_alg'] in ECDSA_ALGO_IDS:
p['sig'] = (parse_mpi(stream), parse_mpi(stream))
elif p['pubkey_alg'] in RSA_ALGO_IDS: # RSA
p['sig'] = (parse_mpi(stream),)
elif p['pubkey_alg'] == DSA_ALGO_ID:
p['sig'] = (parse_mpi(stream), parse_mpi(stream))
else:
log.error('unsupported public key algo: %d', p['pubkey_alg'])
assert not stream.read()
return p
def _parse_pubkey(stream, packet_type='pubkey'):
"""See https://tools.ietf.org/html/rfc4880#section-5.5 for details."""
p = {'type': packet_type}
packet = io.BytesIO()
with stream.capture(packet):
p['version'] = stream.readfmt('B')
p['created'] = stream.readfmt('>L')
p['algo'] = stream.readfmt('B')
if p['algo'] in ECDSA_ALGO_IDS:
log.debug('parsing elliptic curve key')
# https://tools.ietf.org/html/rfc6637#section-11
oid_size = stream.readfmt('B')
oid = stream.read(oid_size)
assert oid in SUPPORTED_CURVES, util.hexlify(oid)
p['curve_oid'] = oid
mpi = parse_mpi(stream)
log.debug('mpi: %x (%d bits)', mpi, mpi.bit_length())
leftover = stream.read()
if leftover:
leftover = io.BytesIO(leftover)
# https://tools.ietf.org/html/rfc6637#section-8
# should be b'\x03\x01\x08\x07': SHA256 + AES128
size, = util.readfmt(leftover, 'B')
p['kdf'] = leftover.read(size)
p['secret'] = leftover.read()
parse_func, keygrip_func = SUPPORTED_CURVES[oid]
keygrip = keygrip_func(parse_func(mpi))
log.debug('keygrip: %s', util.hexlify(keygrip))
p['keygrip'] = keygrip
elif p['algo'] == DSA_ALGO_ID:
parse_mpis(stream, n=4) # DSA keys are not supported
elif p['algo'] == ELGAMAL_ALGO_ID:
parse_mpis(stream, n=3) # ElGamal keys are not supported
else: # assume RSA
parse_mpis(stream, n=2) # RSA keys are not supported
assert not stream.read()
# https://tools.ietf.org/html/rfc4880#section-12.2
packet_data = packet.getvalue()
data_to_hash = (b'\x99' + struct.pack('>H', len(packet_data)) +
packet_data)
p['key_id'] = hashlib.sha1(data_to_hash).digest()[-8:]
p['_to_hash'] = data_to_hash
log.debug('key ID: %s', util.hexlify(p['key_id']))
return p
_parse_subkey = functools.partial(_parse_pubkey, packet_type='subkey')
def _parse_user_id(stream, packet_type='user_id'):
"""See https://tools.ietf.org/html/rfc4880#section-5.11 for details."""
value = stream.read()
to_hash = b'\xb4' + util.prefix_len('>L', value)
return {'type': packet_type, 'value': value, '_to_hash': to_hash}
# User attribute is handled as an opaque user ID
_parse_attribute = functools.partial(_parse_user_id,
packet_type='user_attribute')
PACKET_TYPES = {
2: _parse_signature,
5: _parse_pubkey,
6: _parse_pubkey,
7: _parse_subkey,
13: _parse_user_id,
14: _parse_subkey,
17: _parse_attribute,
}
def parse_packets(stream):
"""
Support iterative parsing of available GPG packets.
See https://tools.ietf.org/html/rfc4880#section-4.2 for details.
"""
reader = util.Reader(stream)
while True:
try:
value = reader.readfmt('B')
except EOFError:
return
log.debug('prefix byte: %s', bin(value))
assert util.bit(value, 7) == 1
tag = util.low_bits(value, 6)
if util.bit(value, 6) == 0:
length_type = util.low_bits(tag, 2)
tag = tag >> 2
fmt = {0: '>B', 1: '>H', 2: '>L'}[length_type]
packet_size = reader.readfmt(fmt)
else:
first = reader.readfmt('B')
if first < 192:
packet_size = first
elif first < 224:
packet_size = ((first - 192) << 8) + reader.readfmt('B') + 192
elif first == 255:
packet_size = reader.readfmt('>L')
else:
log.error('Partial Body Lengths unsupported')
log.debug('packet length: %d', packet_size)
packet_data = reader.read(packet_size)
packet_type = PACKET_TYPES.get(tag)
p = {'type': 'unknown', 'tag': tag, 'raw': packet_data}
if packet_type is not None:
try:
p = packet_type(util.Reader(io.BytesIO(packet_data)))
p['tag'] = tag
except ValueError:
log.exception('Skipping packet: %s', util.hexlify(packet_data))
log.debug('packet "%s": %s', p['type'], p)
yield p
def digest_packets(packets, hasher):
"""Compute digest on specified packets, according to '_to_hash' field."""
data_to_hash = io.BytesIO()
for p in packets:
data_to_hash.write(p['_to_hash'])
hasher.update(data_to_hash.getvalue())
return hasher.digest()
HASH_ALGORITHMS = {
1: 'md5',
2: 'sha1',
3: 'ripemd160',
8: 'sha256',
9: 'sha384',
10: 'sha512',
11: 'sha224',
}
def load_by_keygrip(pubkey_bytes, keygrip):
"""Return public key and first user ID for specified keygrip."""
stream = io.BytesIO(pubkey_bytes)
packets = list(parse_packets(stream))
packets_per_pubkey = []
for p in packets:
if p['type'] == 'pubkey':
# Add a new packet list for each pubkey.
packets_per_pubkey.append([])
packets_per_pubkey[-1].append(p)
for packets in packets_per_pubkey:
user_ids = [p for p in packets if p['type'] == 'user_id']
for p in packets:
if p.get('keygrip') == keygrip:
return p, user_ids
raise KeyError('{} keygrip not found'.format(util.hexlify(keygrip)))
def load_signature(stream, original_data):
"""Load signature from stream, and compute GPG digest for verification."""
signature, = list(parse_packets((stream)))
hash_alg = HASH_ALGORITHMS[signature['hash_alg']]
digest = digest_packets([{'_to_hash': original_data}, signature],
hasher=hashlib.new(hash_alg))
assert signature['hash_prefix'] == digest[:2]
return signature, digest
def remove_armor(armored_data):
"""Decode armored data into its binary form."""
stream = io.BytesIO(armored_data)
lines = stream.readlines()[3:-1]
data = base64.b64decode(b''.join(lines))
payload, checksum = data[:-3], data[-3:]
assert util.crc24(payload) == checksum
return payload

@ -0,0 +1,103 @@
"""Create GPG ECDSA signatures and public keys using TREZOR device."""
import io
import logging
from . import decode, keyring, protocol
from .. import util
log = logging.getLogger(__name__)
def create_primary(user_id, pubkey, signer_func, secret_bytes=b''):
"""Export new primary GPG public key, ready for "gpg2 --import"."""
pubkey_packet = protocol.packet(tag=(5 if secret_bytes else 6),
blob=(pubkey.data() + secret_bytes))
user_id_bytes = user_id.encode('utf-8')
user_id_packet = protocol.packet(tag=13, blob=user_id_bytes)
data_to_sign = (pubkey.data_to_hash() + user_id_packet[:1] +
util.prefix_len('>L', user_id_bytes))
hashed_subpackets = [
protocol.subpacket_time(pubkey.created), # signature time
# https://tools.ietf.org/html/rfc4880#section-5.2.3.7
protocol.subpacket_byte(0x0B, 9), # preferred symmetric algo (AES-256)
# https://tools.ietf.org/html/rfc4880#section-5.2.3.4
protocol.subpacket_byte(0x1B, 1 | 2), # key flags (certify & sign)
# https://tools.ietf.org/html/rfc4880#section-5.2.3.21
protocol.subpacket_bytes(0x15, [8, 9, 10]), # preferred hash
# https://tools.ietf.org/html/rfc4880#section-5.2.3.8
protocol.subpacket_bytes(0x16, [2, 3, 1]), # preferred compression
# https://tools.ietf.org/html/rfc4880#section-5.2.3.9
protocol.subpacket_byte(0x17, 0x80), # key server prefs (no-modify)
# https://tools.ietf.org/html/rfc4880#section-5.2.3.17
protocol.subpacket_byte(0x1E, 0x01), # advanced features (MDC)
# https://tools.ietf.org/html/rfc4880#section-5.2.3.24
]
unhashed_subpackets = [
protocol.subpacket(16, pubkey.key_id()), # issuer key id
protocol.CUSTOM_SUBPACKET]
signature = protocol.make_signature(
signer_func=signer_func,
public_algo=pubkey.algo_id,
data_to_sign=data_to_sign,
sig_type=0x13, # user id & public key
hashed_subpackets=hashed_subpackets,
unhashed_subpackets=unhashed_subpackets)
sign_packet = protocol.packet(tag=2, blob=signature)
return pubkey_packet + user_id_packet + sign_packet
def create_subkey(primary_bytes, subkey, signer_func, secret_bytes=b''):
"""Export new subkey to GPG primary key."""
subkey_packet = protocol.packet(tag=(7 if secret_bytes else 14),
blob=(subkey.data() + secret_bytes))
packets = list(decode.parse_packets(io.BytesIO(primary_bytes)))
primary, user_id, signature = packets[:3]
data_to_sign = primary['_to_hash'] + subkey.data_to_hash()
if subkey.ecdh:
embedded_sig = None
else:
# Primary Key Binding Signature
hashed_subpackets = [
protocol.subpacket_time(subkey.created)] # signature time
unhashed_subpackets = [
protocol.subpacket(16, subkey.key_id())] # issuer key id
embedded_sig = protocol.make_signature(
signer_func=signer_func,
data_to_sign=data_to_sign,
public_algo=subkey.algo_id,
sig_type=0x19,
hashed_subpackets=hashed_subpackets,
unhashed_subpackets=unhashed_subpackets)
# Subkey Binding Signature
# Key flags: https://tools.ietf.org/html/rfc4880#section-5.2.3.21
# (certify & sign) (encrypt)
flags = (2) if (not subkey.ecdh) else (4 | 8)
hashed_subpackets = [
protocol.subpacket_time(subkey.created), # signature time
protocol.subpacket_byte(0x1B, flags)]
unhashed_subpackets = []
unhashed_subpackets.append(protocol.subpacket(16, primary['key_id']))
if embedded_sig is not None:
unhashed_subpackets.append(protocol.subpacket(32, embedded_sig))
unhashed_subpackets.append(protocol.CUSTOM_SUBPACKET)
if not decode.has_custom_subpacket(signature):
signer_func = keyring.create_agent_signer(user_id['value'])
signature = protocol.make_signature(
signer_func=signer_func,
data_to_sign=data_to_sign,
public_algo=primary['algo'],
sig_type=0x18,
hashed_subpackets=hashed_subpackets,
unhashed_subpackets=unhashed_subpackets)
sign_packet = protocol.packet(tag=2, blob=signature)
return primary_bytes + subkey_packet + sign_packet

@ -0,0 +1,261 @@
"""Tools for doing signature using gpg-agent."""
from __future__ import absolute_import, print_function, unicode_literals
import binascii
import io
import logging
import os
import re
import socket
import subprocess
from .. import util
log = logging.getLogger(__name__)
def check_output(args, env=None, sp=subprocess):
"""Call an external binary and return its stdout."""
log.debug('calling %s with env %s', args, env)
output = sp.check_output(args=args, env=env)
log.debug('output: %r', output)
return output
def get_agent_sock_path(env=None, sp=subprocess):
"""Parse gpgconf output to find out GPG agent UNIX socket path."""
args = [util.which('gpgconf'), '--list-dirs']
output = check_output(args=args, env=env, sp=sp)
lines = output.strip().split(b'\n')
dirs = dict(line.split(b':', 1) for line in lines)
log.debug('%s: %s', args, dirs)
return dirs[b'agent-socket']
def connect_to_agent(env=None, sp=subprocess):
"""Connect to GPG agent's UNIX socket."""
sock_path = get_agent_sock_path(sp=sp, env=env)
# Make sure the original gpg-agent is running.
check_output(args=['gpg-connect-agent', '/bye'], sp=sp)
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(sock_path)
return sock
def communicate(sock, msg):
"""Send a message and receive a single line."""
sendline(sock, msg.encode('ascii'))
return recvline(sock)
def sendline(sock, msg, confidential=False):
"""Send a binary message, followed by EOL."""
log.debug('<- %r', ('<snip>' if confidential else msg))
sock.sendall(msg + b'\n')
def recvline(sock):
"""Receive a single line from the socket."""
reply = io.BytesIO()
while True:
c = sock.recv(1)
if not c:
return None # socket is closed
if c == b'\n':
break
reply.write(c)
result = reply.getvalue()
log.debug('-> %r', result)
return result
def iterlines(conn):
"""Iterate over input, split by lines."""
while True:
line = recvline(conn)
if line is None:
break
yield line
def unescape(s):
"""Unescape ASSUAN message (0xAB <-> '%AB')."""
s = bytearray(s)
i = 0
while i < len(s):
if s[i] == ord('%'):
hex_bytes = bytes(s[i+1:i+3])
value = int(hex_bytes.decode('ascii'), 16)
s[i:i+3] = [value]
i += 1
return bytes(s)
def parse_term(s):
"""Parse single s-expr term from bytes."""
size, s = s.split(b':', 1)
size = int(size)
return s[:size], s[size:]
def parse(s):
"""Parse full s-expr from bytes."""
if s.startswith(b'('):
s = s[1:]
name, s = parse_term(s)
values = [name]
while not s.startswith(b')'):
value, s = parse(s)
values.append(value)
return values, s[1:]
return parse_term(s)
def _parse_ecdsa_sig(args):
(r, sig_r), (s, sig_s) = args
assert r == b'r'
assert s == b's'
return (util.bytes2num(sig_r),
util.bytes2num(sig_s))
# DSA and EDDSA happen to have the same structure as ECDSA signatures
_parse_dsa_sig = _parse_ecdsa_sig
_parse_eddsa_sig = _parse_ecdsa_sig
def _parse_rsa_sig(args):
(s, sig_s), = args
assert s == b's'
return (util.bytes2num(sig_s),)
def parse_sig(sig):
"""Parse signature integer values from s-expr."""
label, sig = sig
assert label == b'sig-val'
algo_name = sig[0]
parser = {b'rsa': _parse_rsa_sig,
b'ecdsa': _parse_ecdsa_sig,
b'eddsa': _parse_eddsa_sig,
b'dsa': _parse_dsa_sig}[algo_name]
return parser(args=sig[1:])
def sign_digest(sock, keygrip, digest, sp=subprocess, environ=None):
"""Sign a digest using specified key using GPG agent."""
hash_algo = 8 # SHA256
assert len(digest) == 32
assert communicate(sock, 'RESET').startswith(b'OK')
ttyname = check_output(args=['tty'], sp=sp).strip()
options = ['ttyname={}'.format(ttyname)] # set TTY for passphrase entry
display = (environ or os.environ).get('DISPLAY')
if display is not None:
options.append('display={}'.format(display))
for opt in options:
assert communicate(sock, 'OPTION {}'.format(opt)) == b'OK'
assert communicate(sock, 'SIGKEY {}'.format(keygrip)) == b'OK'
hex_digest = binascii.hexlify(digest).upper().decode('ascii')
assert communicate(sock, 'SETHASH {} {}'.format(hash_algo,
hex_digest)) == b'OK'
assert communicate(sock, 'SETKEYDESC '
'Sign+a+new+TREZOR-based+subkey') == b'OK'
assert communicate(sock, 'PKSIGN') == b'OK'
while True:
line = recvline(sock).strip()
if line.startswith(b'S PROGRESS'):
continue
else:
break
line = unescape(line)
log.debug('unescaped: %r', line)
prefix, sig = line.split(b' ', 1)
if prefix != b'D':
raise ValueError(prefix)
sig, leftover = parse(sig)
assert not leftover, leftover
return parse_sig(sig)
def get_gnupg_components(sp=subprocess):
"""Parse GnuPG components' paths."""
args = [util.which('gpgconf'), '--list-components']
output = check_output(args=args, sp=sp)
components = dict(re.findall('(.*):.*:(.*)', output.decode('utf-8')))
log.debug('gpgconf --list-components: %s', components)
return components
@util.memoize
def get_gnupg_binary(sp=subprocess, neopg_binary=None):
"""Starting GnuPG 2.2.x, the default installation uses `gpg`."""
if neopg_binary:
return neopg_binary
return get_gnupg_components(sp=sp)['gpg']
def gpg_command(args, env=None):
"""Prepare common GPG command line arguments."""
if env is None:
env = os.environ
cmd = get_gnupg_binary(neopg_binary=env.get('NEOPG_BINARY'))
return [cmd] + args
def get_keygrip(user_id, sp=subprocess):
"""Get a keygrip of the primary GPG key of the specified user."""
args = gpg_command(['--list-keys', '--with-keygrip', user_id])
output = check_output(args=args, sp=sp).decode('utf-8')
return re.findall(r'Keygrip = (\w+)', output)[0]
def gpg_version(sp=subprocess):
"""Get a keygrip of the primary GPG key of the specified user."""
args = gpg_command(['--version'])
output = check_output(args=args, sp=sp)
line = output.split(b'\n')[0] # b'gpg (GnuPG) 2.1.11'
line = line.split(b' ')[-1] # b'2.1.11'
line = line.split(b'-')[0] # remove trailing version parts
return line.split(b'v')[-1] # remove 'v' prefix
def export_public_key(user_id, env=None, sp=subprocess):
"""Export GPG public key for specified `user_id`."""
args = gpg_command(['--export', user_id])
result = check_output(args=args, env=env, sp=sp)
if not result:
log.error('could not find public key %r in local GPG keyring', user_id)
raise KeyError(user_id)
return result
def export_public_keys(env=None, sp=subprocess):
"""Export all GPG public keys."""
args = gpg_command(['--export'])
result = check_output(args=args, env=env, sp=sp)
if not result:
raise KeyError('No GPG public keys found at env: {!r}'.format(env))
return result
def create_agent_signer(user_id):
"""Sign digest with existing GPG keys using gpg-agent tool."""
sock = connect_to_agent(env=os.environ)
keygrip = get_keygrip(user_id)
def sign(digest):
"""Sign the digest and return an ECDSA/RSA/DSA signature."""
return sign_digest(sock=sock, keygrip=keygrip, digest=digest)
return sign

@ -0,0 +1,276 @@
"""GPG protocol utilities."""
import base64
import hashlib
import logging
import struct
from .. import formats, util
log = logging.getLogger(__name__)
def packet(tag, blob):
"""Create small GPG packet."""
assert len(blob) < 2**32
if len(blob) < 2**8:
length_type = 0
elif len(blob) < 2**16:
length_type = 1
else:
length_type = 2
fmt = ['>B', '>H', '>L'][length_type]
leading_byte = 0x80 | (tag << 2) | (length_type)
return struct.pack('>B', leading_byte) + util.prefix_len(fmt, blob)
def subpacket(subpacket_type, fmt, *values):
"""Create GPG subpacket."""
blob = struct.pack(fmt, *values) if values else fmt
return struct.pack('>B', subpacket_type) + blob
def subpacket_long(subpacket_type, value):
"""Create GPG subpacket with 32-bit unsigned integer."""
return subpacket(subpacket_type, '>L', value)
def subpacket_time(value):
"""Create GPG subpacket with time in seconds (since Epoch)."""
return subpacket_long(2, value)
def subpacket_byte(subpacket_type, value):
"""Create GPG subpacket with 8-bit unsigned integer."""
return subpacket(subpacket_type, '>B', value)
def subpacket_bytes(subpacket_type, values):
"""Create GPG subpacket with 8-bit unsigned integers."""
return subpacket(subpacket_type, '>' + 'B'*len(values), *values)
def subpacket_prefix_len(item):
"""Prefix subpacket length according to RFC 4880 section-5.2.3.1."""
n = len(item)
if n >= 8384:
prefix = b'\xFF' + struct.pack('>L', n)
elif n >= 192:
n = n - 192
prefix = struct.pack('BB', (n // 256) + 192, n % 256)
else:
prefix = struct.pack('B', n)
return prefix + item
def subpackets(*items):
"""Serialize several GPG subpackets."""
prefixed = [subpacket_prefix_len(item) for item in items]
return util.prefix_len('>H', b''.join(prefixed))
def mpi(value):
"""Serialize multipresicion integer using GPG format."""
bits = value.bit_length()
data_size = (bits + 7) // 8
data_bytes = bytearray(data_size)
for i in range(data_size):
data_bytes[i] = value & 0xFF
value = value >> 8
data_bytes.reverse()
return struct.pack('>H', bits) + bytes(data_bytes)
def _serialize_nist256(vk):
return mpi((4 << 512) |
(vk.pubkey.point.x() << 256) |
(vk.pubkey.point.y()))
def _serialize_ed25519(vk):
return mpi((0x40 << 256) |
util.bytes2num(vk.to_bytes()))
def _compute_keygrip(params):
parts = []
for name, value in params:
exp = '{}:{}{}:'.format(len(name), name, len(value))
parts.append(b'(' + exp.encode('ascii') + value + b')')
return hashlib.sha1(b''.join(parts)).digest()
def keygrip_nist256(vk):
"""Compute keygrip for NIST256 curve public keys."""
curve = vk.curve.curve
gen = vk.curve.generator
g = (4 << 512) | (gen.x() << 256) | gen.y()
point = vk.pubkey.point
q = (4 << 512) | (point.x() << 256) | point.y()
return _compute_keygrip([
['p', util.num2bytes(curve.p(), size=32)],
['a', util.num2bytes(curve.a() % curve.p(), size=32)],
['b', util.num2bytes(curve.b() % curve.p(), size=32)],
['g', util.num2bytes(g, size=65)],
['n', util.num2bytes(vk.curve.order, size=32)],
['q', util.num2bytes(q, size=65)],
])
def keygrip_ed25519(vk):
"""Compute keygrip for Ed25519 public keys."""
# pylint: disable=line-too-long
return _compute_keygrip([
['p', util.num2bytes(0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED, size=32)], # nopep8
['a', b'\x01'],
['b', util.num2bytes(0x2DFC9311D490018C7338BF8688861767FF8FF5B2BEBE27548A14B235ECA6874A, size=32)], # nopep8
['g', util.num2bytes(0x04216936D3CD6E53FEC0A4E231FDD6DC5C692CC7609525A7B2C9562D608F25D51A6666666666666666666666666666666666666666666666666666666666666658, size=65)], # nopep8
['n', util.num2bytes(0x1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED, size=32)], # nopep8
['q', vk.to_bytes()],
])
def keygrip_curve25519(vk):
"""Compute keygrip for Curve25519 public keys."""
# pylint: disable=line-too-long
return _compute_keygrip([
['p', util.num2bytes(0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED, size=32)], # nopep8
['a', b'\x01\xDB\x41'],
['b', b'\x01'],
['g', util.num2bytes(0x04000000000000000000000000000000000000000000000000000000000000000920ae19a1b8a086b4e01edd2c7748d14c923d4d7e6d7c61b229e9c5a27eced3d9, size=65)], # nopep8
['n', util.num2bytes(0x1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED, size=32)], # nopep8
['q', vk.to_bytes()],
])
SUPPORTED_CURVES = {
formats.CURVE_NIST256: {
# https://tools.ietf.org/html/rfc6637#section-11
'oid': b'\x2A\x86\x48\xCE\x3D\x03\x01\x07',
'algo_id': 19,
'serialize': _serialize_nist256,
'keygrip': keygrip_nist256,
},
formats.CURVE_ED25519: {
'oid': b'\x2B\x06\x01\x04\x01\xDA\x47\x0F\x01',
'algo_id': 22,
'serialize': _serialize_ed25519,
'keygrip': keygrip_ed25519,
},
formats.ECDH_CURVE25519: {
'oid': b'\x2B\x06\x01\x04\x01\x97\x55\x01\x05\x01',
'algo_id': 18,
'serialize': _serialize_ed25519,
'keygrip': keygrip_curve25519,
},
}
ECDH_ALGO_ID = 18
CUSTOM_KEY_LABEL = b'TREZOR-GPG' # marks "our" pubkey
CUSTOM_SUBPACKET_ID = 26 # use "policy URL" subpacket
CUSTOM_SUBPACKET = subpacket(CUSTOM_SUBPACKET_ID, CUSTOM_KEY_LABEL)
def get_curve_name_by_oid(oid):
"""Return curve name matching specified OID, or raise KeyError."""
for curve_name, info in SUPPORTED_CURVES.items():
if info['oid'] == oid:
return curve_name
raise KeyError('Unknown OID: {!r}'.format(oid))
class PublicKey:
"""GPG representation for public key packets."""
def __init__(self, curve_name, created, verifying_key, ecdh=False):
"""Contruct using a ECDSA VerifyingKey object."""
self.curve_name = curve_name
self.curve_info = SUPPORTED_CURVES[curve_name]
self.created = int(created) # time since Epoch
self.verifying_key = verifying_key
self.ecdh = bool(ecdh)
if ecdh:
self.algo_id = ECDH_ALGO_ID
self.ecdh_packet = b'\x03\x01\x08\x07'
else:
self.algo_id = self.curve_info['algo_id']
self.ecdh_packet = b''
def keygrip(self):
"""Compute GPG keygrip of the verifying key."""
return self.curve_info['keygrip'](self.verifying_key)
def data(self):
"""Data for packet creation."""
header = struct.pack('>BLB',
4, # version
self.created, # creation
self.algo_id) # public key algorithm ID
oid = util.prefix_len('>B', self.curve_info['oid'])
blob = self.curve_info['serialize'](self.verifying_key)
return header + oid + blob + self.ecdh_packet
def data_to_hash(self):
"""Data for digest computation."""
return b'\x99' + util.prefix_len('>H', self.data())
def _fingerprint(self):
return hashlib.sha1(self.data_to_hash()).digest()
def key_id(self):
"""Short (8 byte) GPG key ID."""
return self._fingerprint()[-8:]
def __repr__(self):
"""Short (8 hexadecimal digits) GPG key ID."""
hex_key_id = util.hexlify(self.key_id())[-8:]
return 'GPG public key {}/{}'.format(self.curve_name, hex_key_id)
__str__ = __repr__
def _split_lines(body, size):
lines = []
for i in range(0, len(body), size):
lines.append(body[i:i+size] + '\n')
return ''.join(lines)
def armor(blob, type_str):
"""See https://tools.ietf.org/html/rfc4880#section-6 for details."""
head = '-----BEGIN PGP {}-----\nVersion: GnuPG v2\n\n'.format(type_str)
body = base64.b64encode(blob).decode('ascii')
checksum = base64.b64encode(util.crc24(blob)).decode('ascii')
tail = '-----END PGP {}-----\n'.format(type_str)
return head + _split_lines(body, 64) + '=' + checksum + '\n' + tail
def make_signature(signer_func, data_to_sign, public_algo,
hashed_subpackets, unhashed_subpackets, sig_type=0):
"""Create new GPG signature."""
# pylint: disable=too-many-arguments
header = struct.pack('>BBBB',
4, # version
sig_type, # rfc4880 (section-5.2.1)
public_algo,
8) # hash_alg (SHA256)
hashed = subpackets(*hashed_subpackets)
unhashed = subpackets(*unhashed_subpackets)
tail = b'\x04\xff' + struct.pack('>L', len(header) + len(hashed))
data_to_hash = data_to_sign + header + hashed + tail
log.debug('hashing %d bytes', len(data_to_hash))
digest = hashlib.sha256(data_to_hash).digest()
log.debug('signing digest: %s', util.hexlify(digest))
params = signer_func(digest=digest)
sig = b''.join(mpi(p) for p in params)
return bytes(header + hashed + unhashed +
digest[:2] + # used for decoder's sanity check
sig) # actual ECDSA signature

@ -0,0 +1 @@
"""Tests for GPG module."""

@ -0,0 +1,11 @@
from .. import agent
def test_sig_encode():
SIG = (
b'(7:sig-val(5:ecdsa(1:r32:\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x0c)(1:s32:\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00")))')
assert agent.sig_encode(12, 34) == SIG

@ -0,0 +1,62 @@
import glob
import io
import os
import pytest
from .. import decode, protocol
from ... import util
def test_subpackets():
s = io.BytesIO(b'\x00\x05\x02\xAB\xCD\x01\xEF')
assert decode.parse_subpackets(util.Reader(s)) == [b'\xAB\xCD', b'\xEF']
def test_subpackets_prefix():
for n in [0, 1, 2, 4, 5, 10, 191, 192, 193,
255, 256, 257, 8383, 8384, 65530]:
item = b'?' * n # create dummy subpacket
prefixed = protocol.subpackets(item)
result = decode.parse_subpackets(util.Reader(io.BytesIO(prefixed)))
assert [item] == result
def test_mpi():
s = io.BytesIO(b'\x00\x09\x01\x23')
assert decode.parse_mpi(util.Reader(s)) == 0x123
s = io.BytesIO(b'\x00\x09\x01\x23\x00\x03\x05')
assert decode.parse_mpis(util.Reader(s), n=2) == [0x123, 5]
cwd = os.path.join(os.path.dirname(__file__))
input_files = glob.glob(os.path.join(cwd, '*.gpg'))
@pytest.fixture(params=input_files)
def public_key_path(request):
return request.param
def test_gpg_files(public_key_path): # pylint: disable=redefined-outer-name
with open(public_key_path, 'rb') as f:
assert list(decode.parse_packets(f))
def test_has_custom_subpacket():
sig = {'unhashed_subpackets': []}
assert not decode.has_custom_subpacket(sig)
custom_markers = [
protocol.CUSTOM_SUBPACKET,
protocol.subpacket(10, protocol.CUSTOM_KEY_LABEL),
]
for marker in custom_markers:
sig = {'unhashed_subpackets': [marker]}
assert decode.has_custom_subpacket(sig)
def test_load_by_keygrip_missing():
with pytest.raises(KeyError):
decode.load_by_keygrip(pubkey_bytes=b'', keygrip=b'')

@ -0,0 +1,101 @@
import io
import mock
from .. import keyring
def test_unescape_short():
assert keyring.unescape(b'abc%0AX%0D %25;.-+()') == b'abc\nX\r %;.-+()'
def test_unescape_long():
escaped = (b'D (7:sig-val(3:dsa(1:r32:\x1d\x15.\x12\xe8h\x19\xd9O\xeb\x06'
b'yD?a:/\xae\xdb\xac\x93\xa6\x86\xcbs\xb8\x03\xf1\xcb\x89\xc7'
b'\x1f)(1:s32:%25\xb5\x04\x94\xc7\xc4X\xc7\xe0%0D\x08\xbb%0DuN'
b'\x9c6}[\xc2=t\x8c\xfdD\x81\xe8\xdd\x86=\xe2\xa9)))')
unescaped = (b'D (7:sig-val(3:dsa(1:r32:\x1d\x15.\x12\xe8h\x19\xd9O\xeb'
b'\x06yD?a:/\xae\xdb\xac\x93\xa6\x86\xcbs\xb8\x03\xf1\xcb\x89'
b'\xc7\x1f)(1:s32:%\xb5\x04\x94\xc7\xc4X\xc7\xe0\r\x08\xbb\ru'
b'N\x9c6}[\xc2=t\x8c\xfdD\x81\xe8\xdd\x86=\xe2\xa9)))')
assert keyring.unescape(escaped) == unescaped
def test_parse_term():
assert keyring.parse(b'4:abcdXXX') == (b'abcd', b'XXX')
def test_parse_ecdsa():
sig, rest = keyring.parse(b'(7:sig-val(5:ecdsa'
b'(1:r2:\x01\x02)(1:s2:\x03\x04)))')
values = [[b'r', b'\x01\x02'], [b's', b'\x03\x04']]
assert sig == [b'sig-val', [b'ecdsa'] + values]
assert rest == b''
assert keyring.parse_sig(sig) == (0x102, 0x304)
def test_parse_rsa():
sig, rest = keyring.parse(b'(7:sig-val(3:rsa(1:s4:\x01\x02\x03\x04)))')
assert sig == [b'sig-val', [b'rsa', [b's', b'\x01\x02\x03\x04']]]
assert rest == b''
assert keyring.parse_sig(sig) == (0x1020304,)
class FakeSocket:
def __init__(self):
self.rx = io.BytesIO()
self.tx = io.BytesIO()
def recv(self, n):
return self.rx.read(n)
def sendall(self, data):
self.tx.write(data)
def test_sign_digest():
sock = FakeSocket()
sock.rx.write(b'OK Pleased to meet you, process XYZ\n')
sock.rx.write(b'OK\n' * 6)
sock.rx.write(b'D (7:sig-val(3:rsa(1:s16:0123456789ABCDEF)))\n')
sock.rx.seek(0)
keygrip = '1234'
digest = b'A' * 32
sp = mock.Mock(spec=['check_output'])
sp.check_output.return_value = '/dev/pts/0'
sig = keyring.sign_digest(sock=sock, keygrip=keygrip,
digest=digest, sp=sp,
environ={'DISPLAY': ':0'})
assert sig == (0x30313233343536373839414243444546,)
assert sock.tx.getvalue() == b'''RESET
OPTION ttyname=/dev/pts/0
OPTION display=:0
SIGKEY 1234
SETHASH 8 4141414141414141414141414141414141414141414141414141414141414141
SETKEYDESC Sign+a+new+TREZOR-based+subkey
PKSIGN
'''
def test_iterlines():
sock = FakeSocket()
sock.rx.write(b'foo\nbar\nxyz')
sock.rx.seek(0)
assert list(keyring.iterlines(sock)) == [b'foo', b'bar']
def test_get_agent_sock_path():
sp = mock.Mock(spec=['check_output'])
sp.check_output.return_value = b'''sysconfdir:/usr/local/etc/gnupg
bindir:/usr/local/bin
libexecdir:/usr/local/libexec
libdir:/usr/local/lib/gnupg
datadir:/usr/local/share/gnupg
localedir:/usr/local/share/locale
dirmngr-socket:/run/user/1000/gnupg/S.dirmngr
agent-ssh-socket:/run/user/1000/gnupg/S.gpg-agent.ssh
agent-socket:/run/user/1000/gnupg/S.gpg-agent
homedir:/home/roman/.gnupg
'''
expected = b'/run/user/1000/gnupg/S.gpg-agent'
assert keyring.get_agent_sock_path(sp=sp) == expected

@ -0,0 +1,107 @@
import ecdsa
import ed25519
import pytest
from .. import protocol
from ... import formats
def test_packet():
assert protocol.packet(1, b'') == b'\x84\x00'
assert protocol.packet(2, b'A') == b'\x88\x01A'
blob = b'B' * 0xAB
assert protocol.packet(3, blob) == b'\x8c\xAB' + blob
blob = b'C' * 0x1234
assert protocol.packet(3, blob) == b'\x8d\x12\x34' + blob
blob = b'D' * 0x12345678
assert protocol.packet(4, blob) == b'\x92\x12\x34\x56\x78' + blob
def test_subpackets():
assert protocol.subpacket(1, b'') == b'\x01'
assert protocol.subpacket(2, '>H', 0x0304) == b'\x02\x03\x04'
assert protocol.subpacket_long(9, 0x12345678) == b'\x09\x12\x34\x56\x78'
assert protocol.subpacket_time(0x12345678) == b'\x02\x12\x34\x56\x78'
assert protocol.subpacket_byte(0xAB, 0xCD) == b'\xAB\xCD'
assert protocol.subpackets() == b'\x00\x00'
assert protocol.subpackets(b'ABC', b'12345') == b'\x00\x0A\x03ABC\x0512345'
def test_mpi():
assert protocol.mpi(0x123) == b'\x00\x09\x01\x23'
def test_armor():
data = bytearray(range(256))
assert protocol.armor(data, 'TEST') == '''-----BEGIN PGP TEST-----
Version: GnuPG v2
AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4v
MDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5f
YGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6P
kJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/
wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v
8PHy8/T19vf4+fr7/P3+/w==
=W700
-----END PGP TEST-----
'''
def test_make_signature():
def signer_func(digest):
assert digest == (b'\xd0\xe5]|\x8bP\xe6\x91\xb3\xe8+\xf4A\xf0`(\xb1'
b'\xc7\xf4;\x86\x97s\xdb\x9a\xda\xee< \xcb\x9e\x00')
return (7, 8)
sig = protocol.make_signature(
signer_func=signer_func,
data_to_sign=b'Hello World!',
public_algo=22,
hashed_subpackets=[protocol.subpacket_time(1)],
unhashed_subpackets=[],
sig_type=25)
assert sig == (b'\x04\x19\x16\x08\x00\x06\x05\x02'
b'\x00\x00\x00\x01\x00\x00\xd0\xe5\x00\x03\x07\x00\x04\x08')
def test_nist256p1():
sk = ecdsa.SigningKey.from_secret_exponent(secexp=1, curve=ecdsa.NIST256p)
vk = sk.get_verifying_key()
pk = protocol.PublicKey(curve_name=formats.CURVE_NIST256,
created=42, verifying_key=vk)
assert repr(pk) == 'GPG public key nist256p1/F82361D9'
assert pk.keygrip() == b'\x95\x85.\x91\x7f\xe2\xc3\x91R\xba\x99\x81\x92\xb5y\x1d\xb1\\\xdc\xf0'
def test_nist256p1_ecdh():
sk = ecdsa.SigningKey.from_secret_exponent(secexp=1, curve=ecdsa.NIST256p)
vk = sk.get_verifying_key()
pk = protocol.PublicKey(curve_name=formats.CURVE_NIST256,
created=42, verifying_key=vk, ecdh=True)
assert repr(pk) == 'GPG public key nist256p1/5811DF46'
assert pk.keygrip() == b'\x95\x85.\x91\x7f\xe2\xc3\x91R\xba\x99\x81\x92\xb5y\x1d\xb1\\\xdc\xf0'
def test_ed25519():
sk = ed25519.SigningKey(b'\x00' * 32)
vk = sk.get_verifying_key()
pk = protocol.PublicKey(curve_name=formats.CURVE_ED25519,
created=42, verifying_key=vk)
assert repr(pk) == 'GPG public key ed25519/36B40FE6'
assert pk.keygrip() == b'\xbf\x01\x90l\x17\xb64\xa3-\xf4\xc0gr\x99\x18<\xddBQ?'
def test_curve25519():
sk = ed25519.SigningKey(b'\x00' * 32)
vk = sk.get_verifying_key()
pk = protocol.PublicKey(curve_name=formats.ECDH_CURVE25519,
created=42, verifying_key=vk)
assert repr(pk) == 'GPG public key curve25519/69460384'
assert pk.keygrip() == b'x\xd6\x86\xe4\xa6\xfc;\x0fY\xe1}Lw\xc4\x9ed\xf1Q\x8a\x00'
def test_get_curve_name_by_oid():
for name, info in protocol.SUPPORTED_CURVES.items():
assert protocol.get_curve_name_by_oid(info['oid']) == name
with pytest.raises(KeyError):
protocol.get_curve_name_by_oid('BAD_OID')

@ -0,0 +1,166 @@
"""UNIX-domain socket server for ssh-agent implementation."""
import contextlib
import logging
import os
import socket
import subprocess
import threading
from . import util
log = logging.getLogger(__name__)
def remove_file(path, remove=os.remove, exists=os.path.exists):
"""Remove file, and raise OSError if still exists."""
try:
remove(path)
except OSError:
if exists(path):
raise
@contextlib.contextmanager
def unix_domain_socket_server(sock_path):
"""
Create UNIX-domain socket on specified path.
Listen on it, and delete it after the generated context is over.
"""
log.debug('serving on %s', sock_path)
remove_file(sock_path)
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(sock_path)
server.listen(1)
try:
yield server
finally:
remove_file(sock_path)
class FDServer:
"""File-descriptor based server (for NeoPG)."""
def __init__(self, fd):
"""C-tor."""
self.fd = fd
self.sock = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
def accept(self):
"""Use the same socket for I/O."""
return self, None
def recv(self, n):
"""Forward to underlying socket."""
return self.sock.recv(n)
def sendall(self, data):
"""Forward to underlying socket."""
return self.sock.sendall(data)
def close(self):
"""Not needed."""
def settimeout(self, _):
"""Not needed."""
def getsockname(self):
"""Simple representation."""
return '<fd: {}>'.format(self.fd)
@contextlib.contextmanager
def unix_domain_socket_server_from_fd(fd):
"""Build UDS-based socket server from a file descriptor."""
yield FDServer(fd)
def handle_connection(conn, handler, mutex):
"""
Handle a single connection using the specified protocol handler in a loop.
Since this function may be called concurrently from server_thread,
the specified mutex is used to synchronize the device handling.
Exit when EOFError is raised.
All other exceptions are logged as warnings.
"""
try:
log.debug('welcome agent')
with contextlib.closing(conn):
while True:
msg = util.read_frame(conn)
with mutex:
reply = handler.handle(msg=msg)
util.send(conn, reply)
except EOFError:
log.debug('goodbye agent')
except Exception as e: # pylint: disable=broad-except
log.warning('error: %s', e, exc_info=True)
def retry(func, exception_type, quit_event):
"""
Run the function, retrying when the specified exception_type occurs.
Poll quit_event on each iteration, to be responsive to an external
exit request.
"""
while True:
if quit_event.is_set():
raise StopIteration
try:
return func()
except exception_type:
pass
def server_thread(sock, handle_conn, quit_event):
"""Run a server on the specified socket."""
log.debug('server thread started')
def accept_connection():
conn, _ = sock.accept()
conn.settimeout(None)
return conn
while True:
log.debug('waiting for connection on %s', sock.getsockname())
try:
conn = retry(accept_connection, socket.timeout, quit_event)
except StopIteration:
log.debug('server stopped')
break
# Handle connections from SSH concurrently.
threading.Thread(target=handle_conn,
kwargs=dict(conn=conn)).start()
log.debug('server thread stopped')
@contextlib.contextmanager
def spawn(func, kwargs):
"""Spawn a thread, and join it after the context is over."""
t = threading.Thread(target=func, kwargs=kwargs)
t.start()
yield
t.join()
def run_process(command, environ):
"""
Run the specified process and wait until it finishes.
Use environ dict for environment variables.
"""
log.info('running %r with %r', command, environ)
env = dict(os.environ)
env.update(environ)
try:
p = subprocess.Popen(args=command, env=env)
except OSError as e:
raise OSError('cannot run %r: %s' % (command, e))
log.debug('subprocess %d is running', p.pid)
ret = p.wait()
log.debug('subprocess %d exited: %d', p.pid, ret)
return ret

@ -0,0 +1,313 @@
"""SSH-agent implementation using hardware authentication devices."""
import contextlib
import functools
import io
import logging
import os
import re
import signal
import subprocess
import sys
import tempfile
import threading
import pkg_resources
import configargparse
import daemon
from .. import device, formats, server, util
from . import client, protocol
log = logging.getLogger(__name__)
UNIX_SOCKET_TIMEOUT = 0.1
def ssh_args(conn):
"""Create SSH command for connecting specified server."""
I, = conn.identities
identity = I.identity_dict
pubkey_tempfile, = conn.public_keys_as_files()
args = []
if 'port' in identity:
args += ['-p', identity['port']]
if 'user' in identity:
args += ['-l', identity['user']]
args += ['-o', 'IdentityFile={}'.format(pubkey_tempfile.name)]
args += ['-o', 'IdentitiesOnly=true']
return args + [identity['host']]
def mosh_args(conn):
"""Create SSH command for connecting specified server."""
I, = conn.identities
identity = I.identity_dict
args = []
if 'port' in identity:
args += ['-p', identity['port']]
if 'user' in identity:
args += [identity['user']+'@'+identity['host']]
else:
args += [identity['host']]
return args
def _to_unicode(s):
try:
return unicode(s, 'utf-8')
except NameError:
return s
def create_agent_parser(device_type):
"""Create an ArgumentParser for this tool."""
epilog = ('See https://github.com/romanz/trezor-agent/blob/master/'
'doc/README-SSH.md for usage examples.')
p = configargparse.ArgParser(default_config_files=['~/.ssh/agent.config'],
epilog=epilog)
p.add_argument('-v', '--verbose', default=0, action='count')
agent_package = device_type.package_name()
resources_map = {r.key: r for r in pkg_resources.require(agent_package)}
resources = [resources_map[agent_package], resources_map['libagent']]
versions = '\n'.join('{}={}'.format(r.key, r.version) for r in resources)
p.add_argument('--version', help='print the version info',
action='version', version=versions)
curve_names = [name for name in formats.SUPPORTED_CURVES]
curve_names = ', '.join(sorted(curve_names))
p.add_argument('-e', '--ecdsa-curve-name', metavar='CURVE',
default=formats.CURVE_NIST256,
help='specify ECDSA curve name: ' + curve_names)
p.add_argument('--timeout',
default=UNIX_SOCKET_TIMEOUT, type=float,
help='timeout for accepting SSH client connections')
p.add_argument('--debug', default=False, action='store_true',
help='log SSH protocol messages for debugging.')
p.add_argument('--log-file', type=str,
help='Path to the log file (to be written by the agent).')
p.add_argument('--sock-path', type=str,
help='Path to the UNIX domain socket of the agent.')
p.add_argument('--pin-entry-binary', type=str, default='pinentry',
help='Path to PIN entry UI helper.')
p.add_argument('--passphrase-entry-binary', type=str, default='pinentry',
help='Path to passphrase entry UI helper.')
p.add_argument('--cache-expiry-seconds', type=float, default=float('inf'),
help='Expire passphrase from cache after this duration.')
g = p.add_mutually_exclusive_group()
g.add_argument('-d', '--daemonize', default=False, action='store_true',
help='Daemonize the agent and print its UNIX socket path')
g.add_argument('-f', '--foreground', default=False, action='store_true',
help='Run agent in foreground with specified UNIX socket path')
g.add_argument('-s', '--shell', default=False, action='store_true',
help=('run ${SHELL} as subprocess under SSH agent, allowing '
'regular SSH-based tools to be used in the shell'))
g.add_argument('-c', '--connect', default=False, action='store_true',
help='connect to specified host via SSH')
g.add_argument('--mosh', default=False, action='store_true',
help='connect to specified host via using Mosh')
p.add_argument('identity', type=_to_unicode, default=None,
help='proto://[user@]host[:port][/path]')
p.add_argument('command', type=str, nargs='*', metavar='ARGUMENT',
help='command to run under the SSH agent')
return p
@contextlib.contextmanager
def serve(handler, sock_path, timeout=UNIX_SOCKET_TIMEOUT):
"""
Start the ssh-agent server on a UNIX-domain socket.
If no connection is made during the specified timeout,
retry until the context is over.
"""
ssh_version = subprocess.check_output(['ssh', '-V'],
stderr=subprocess.STDOUT)
log.debug('local SSH version: %r', ssh_version)
environ = {'SSH_AUTH_SOCK': sock_path, 'SSH_AGENT_PID': str(os.getpid())}
device_mutex = threading.Lock()
with server.unix_domain_socket_server(sock_path) as sock:
sock.settimeout(timeout)
quit_event = threading.Event()
handle_conn = functools.partial(server.handle_connection,
handler=handler,
mutex=device_mutex)
kwargs = dict(sock=sock,
handle_conn=handle_conn,
quit_event=quit_event)
with server.spawn(server.server_thread, kwargs):
try:
yield environ
finally:
log.debug('closing server')
quit_event.set()
def run_server(conn, command, sock_path, debug, timeout):
"""Common code for run_agent and run_git below."""
ret = 0
try:
handler = protocol.Handler(conn=conn, debug=debug)
with serve(handler=handler, sock_path=sock_path,
timeout=timeout) as env:
if command:
ret = server.run_process(command=command, environ=env)
else:
signal.pause() # wait for signal (e.g. SIGINT)
except KeyboardInterrupt:
log.info('server stopped')
return ret
def handle_connection_error(func):
"""Fail with non-zero exit code."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except device.interface.NotFoundError as e:
log.error('Connection error (try unplugging and replugging your device): %s', e)
return 1
return wrapper
def parse_config(contents):
"""Parse config file into a list of Identity objects."""
for identity_str, curve_name in re.findall(r'\<(.*?)\|(.*?)\>', contents):
yield device.interface.Identity(identity_str=identity_str,
curve_name=curve_name)
def import_public_keys(contents):
"""Load (previously exported) SSH public keys from a file's contents."""
for line in io.StringIO(contents):
# Verify this line represents valid SSH public key
formats.import_public_key(line)
yield line
class JustInTimeConnection:
"""Connect to the device just before the needed operation."""
def __init__(self, conn_factory, identities, public_keys=None):
"""Create a JIT connection object."""
self.conn_factory = conn_factory
self.identities = identities
self.public_keys_cache = public_keys
self.public_keys_tempfiles = []
def public_keys(self):
"""Return a list of SSH public keys (in textual format)."""
if not self.public_keys_cache:
conn = self.conn_factory()
self.public_keys_cache = conn.export_public_keys(self.identities)
return self.public_keys_cache
def parse_public_keys(self):
"""Parse SSH public keys into dictionaries."""
public_keys = [formats.import_public_key(pk)
for pk in self.public_keys()]
for pk, identity in zip(public_keys, self.identities):
pk['identity'] = identity
return public_keys
def public_keys_as_files(self):
"""Store public keys as temporary SSH identity files."""
if not self.public_keys_tempfiles:
for pk in self.public_keys():
f = tempfile.NamedTemporaryFile(prefix='trezor-ssh-pubkey-', mode='w')
f.write(pk)
f.flush()
self.public_keys_tempfiles.append(f)
return self.public_keys_tempfiles
def sign(self, blob, identity):
"""Sign a given blob using the specified identity on the device."""
conn = self.conn_factory()
return conn.sign_ssh_challenge(blob=blob, identity=identity)
@contextlib.contextmanager
def _dummy_context():
yield
def _get_sock_path(args):
sock_path = args.sock_path
if not sock_path:
if args.foreground:
log.error('running in foreground mode requires specifying UNIX socket path')
sys.exit(1)
else:
sock_path = tempfile.mktemp(prefix='trezor-ssh-agent-')
return sock_path
@handle_connection_error
def main(device_type):
"""Run ssh-agent using given hardware client factory."""
args = create_agent_parser(device_type=device_type).parse_args()
util.setup_logging(verbosity=args.verbose, filename=args.log_file)
public_keys = None
filename = None
if args.identity.startswith('/'):
filename = args.identity
contents = open(filename, 'rb').read().decode('utf-8')
# Allow loading previously exported SSH public keys
if filename.endswith('.pub'):
public_keys = list(import_public_keys(contents))
identities = list(parse_config(contents))
else:
identities = [device.interface.Identity(
identity_str=args.identity, curve_name=args.ecdsa_curve_name)]
for index, identity in enumerate(identities):
identity.identity_dict['proto'] = u'ssh'
log.info('identity #%d: %s', index, identity.to_string())
# override default PIN/passphrase entry tools (relevant for TREZOR/Keepkey):
device_type.ui = device.ui.UI(device_type=device_type, config=vars(args))
device_type.cached_passphrase_ack = util.ExpiringCache(
args.cache_expiry_seconds)
conn = JustInTimeConnection(
conn_factory=lambda: client.Client(device_type()),
identities=identities, public_keys=public_keys)
sock_path = _get_sock_path(args)
command = args.command
context = _dummy_context()
if args.connect:
command = ['ssh'] + ssh_args(conn) + args.command
elif args.mosh:
command = ['mosh'] + mosh_args(conn) + args.command
elif args.daemonize:
out = 'SSH_AUTH_SOCK={0}; export SSH_AUTH_SOCK;\n'.format(sock_path)
sys.stdout.write(out)
sys.stdout.flush()
context = daemon.DaemonContext()
log.info('running the agent as a daemon on %s', sock_path)
elif args.foreground:
log.info('running the agent on %s', sock_path)
use_shell = bool(args.shell)
if use_shell:
command = os.environ['SHELL']
sys.stdin.close()
if command or args.daemonize or args.foreground:
with context:
return run_server(conn=conn, command=command, sock_path=sock_path,
debug=args.debug, timeout=args.timeout)
else:
for pk in conn.public_keys():
sys.stdout.write(pk)
return 0 # success exit code

@ -0,0 +1,65 @@
"""
Connection to hardware authentication device.
It is used for getting SSH public keys and ECDSA signing of server requests.
"""
import io
import logging
from . import formats, util
log = logging.getLogger(__name__)
class Client:
"""Client wrapper for SSH authentication device."""
def __init__(self, device):
"""Connect to hardware device."""
self.device = device
def export_public_keys(self, identities):
"""Export SSH public keys from the device."""
public_keys = []
with self.device:
for i in identities:
pubkey = self.device.pubkey(identity=i)
vk = formats.decompress_pubkey(pubkey=pubkey,
curve_name=i.curve_name)
public_key = formats.export_public_key(vk=vk,
label=i.to_string())
public_keys.append(public_key)
return public_keys
def sign_ssh_challenge(self, blob, identity):
"""Sign given blob using a private key on the device."""
msg = _parse_ssh_blob(blob)
log.debug('%s: user %r via %r (%r)',
msg['conn'], msg['user'], msg['auth'], msg['key_type'])
log.debug('nonce: %r', msg['nonce'])
fp = msg['public_key']['fingerprint']
log.debug('fingerprint: %s', fp)
log.debug('hidden challenge size: %d bytes', len(blob))
log.info('please confirm user "%s" login to "%s" using %s...',
msg['user'].decode('ascii'), identity.to_string(),
self.device)
with self.device:
return self.device.sign(blob=blob, identity=identity)
def _parse_ssh_blob(data):
res = {}
i = io.BytesIO(data)
res['nonce'] = util.read_frame(i)
i.read(1) # SSH2_MSG_USERAUTH_REQUEST == 50 (from ssh2.h, line 108)
res['user'] = util.read_frame(i)
res['conn'] = util.read_frame(i)
res['auth'] = util.read_frame(i)
i.read(1) # have_sig == 1 (from sshconnect2.c, line 1056)
res['key_type'] = util.read_frame(i)
public_key = util.read_frame(i)
res['public_key'] = formats.parse_pubkey(public_key)
assert not i.read()
return res

@ -0,0 +1,160 @@
"""
SSH-agent protocol implementation library.
See https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.agent and
http://ptspts.blogspot.co.il/2010/06/how-to-use-ssh-agent-programmatically.html
for more details.
The server's source code can be found here:
https://github.com/openssh/openssh-portable/blob/master/authfd.c
"""
import io
import logging
from . import formats, util
log = logging.getLogger(__name__)
# Taken from https://github.com/openssh/openssh-portable/blob/master/authfd.h
COMMANDS = dict(
SSH_AGENTC_REQUEST_RSA_IDENTITIES=1,
SSH_AGENT_RSA_IDENTITIES_ANSWER=2,
SSH_AGENTC_RSA_CHALLENGE=3,
SSH_AGENT_RSA_RESPONSE=4,
SSH_AGENT_FAILURE=5,
SSH_AGENT_SUCCESS=6,
SSH_AGENTC_ADD_RSA_IDENTITY=7,
SSH_AGENTC_REMOVE_RSA_IDENTITY=8,
SSH_AGENTC_REMOVE_ALL_RSA_IDENTITIES=9,
SSH2_AGENTC_REQUEST_IDENTITIES=11,
SSH2_AGENT_IDENTITIES_ANSWER=12,
SSH2_AGENTC_SIGN_REQUEST=13,
SSH2_AGENT_SIGN_RESPONSE=14,
SSH2_AGENTC_ADD_IDENTITY=17,
SSH2_AGENTC_REMOVE_IDENTITY=18,
SSH2_AGENTC_REMOVE_ALL_IDENTITIES=19,
SSH_AGENTC_ADD_SMARTCARD_KEY=20,
SSH_AGENTC_REMOVE_SMARTCARD_KEY=21,
SSH_AGENTC_LOCK=22,
SSH_AGENTC_UNLOCK=23,
SSH_AGENTC_ADD_RSA_ID_CONSTRAINED=24,
SSH2_AGENTC_ADD_ID_CONSTRAINED=25,
SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED=26,
)
def msg_code(name):
"""Convert string name into a integer message code."""
return COMMANDS[name]
def msg_name(code):
"""Convert integer message code into a string name."""
ids = {v: k for k, v in COMMANDS.items()}
return ids[code]
def failure():
"""Return error code to SSH binary."""
error_msg = util.pack('B', msg_code('SSH_AGENT_FAILURE'))
return util.frame(error_msg)
def _legacy_pubs(buf):
"""SSH v1 public keys are not supported."""
leftover = buf.read()
if leftover:
log.warning('skipping leftover: %r', leftover)
code = util.pack('B', msg_code('SSH_AGENT_RSA_IDENTITIES_ANSWER'))
num = util.pack('L', 0) # no SSH v1 keys
return util.frame(code, num)
class Handler:
"""ssh-agent protocol handler."""
def __init__(self, conn, debug=False):
"""
Create a protocol handler with specified public keys.
Use specified signer function to sign SSH authentication requests.
"""
self.conn = conn
self.debug = debug
self.methods = {
msg_code('SSH_AGENTC_REQUEST_RSA_IDENTITIES'): _legacy_pubs,
msg_code('SSH2_AGENTC_REQUEST_IDENTITIES'): self.list_pubs,
msg_code('SSH2_AGENTC_SIGN_REQUEST'): self.sign_message,
}
def handle(self, msg):
"""Handle SSH message from the SSH client and return the response."""
debug_msg = ': {!r}'.format(msg) if self.debug else ''
log.debug('request: %d bytes%s', len(msg), debug_msg)
buf = io.BytesIO(msg)
code, = util.recv(buf, '>B')
if code not in self.methods:
log.warning('Unsupported command: %s (%d)', msg_name(code), code)
return failure()
method = self.methods[code]
log.debug('calling %s()', method.__name__)
reply = method(buf=buf)
debug_reply = ': {!r}'.format(reply) if self.debug else ''
log.debug('reply: %d bytes%s', len(reply), debug_reply)
return reply
def list_pubs(self, buf):
"""SSH v2 public keys are serialized and returned."""
assert not buf.read()
keys = self.conn.parse_public_keys()
code = util.pack('B', msg_code('SSH2_AGENT_IDENTITIES_ANSWER'))
num = util.pack('L', len(keys))
log.debug('available keys: %s', [k['name'] for k in keys])
for i, k in enumerate(keys):
log.debug('%2d) %s', i+1, k['fingerprint'])
pubs = [util.frame(k['blob']) + util.frame(k['name']) for k in keys]
return util.frame(code, num, *pubs)
def sign_message(self, buf):
"""
SSH v2 public key authentication is performed.
If the required key is not supported, raise KeyError
If the signature is invalid, raise ValueError
"""
key = formats.parse_pubkey(util.read_frame(buf))
log.debug('looking for %s', key['fingerprint'])
blob = util.read_frame(buf)
assert util.read_frame(buf) == b''
assert not buf.read()
for k in self.conn.parse_public_keys():
if (k['fingerprint']) == (key['fingerprint']):
log.debug('using key %r (%s)', k['name'], k['fingerprint'])
key = k
break
else:
raise KeyError('key not found')
label = key['name'].decode('utf-8')
log.debug('signing %d-byte blob with "%s" key', len(blob), label)
try:
signature = self.conn.sign(blob=blob, identity=key['identity'])
except IOError:
return failure()
log.debug('signature: %r', signature)
try:
sig_bytes = key['verifier'](sig=signature, msg=blob)
log.info('signature status: OK')
except formats.ecdsa.BadSignatureError:
log.exception('signature status: ERROR')
raise ValueError('invalid ECDSA signature')
log.debug('signature size: %d bytes', len(sig_bytes))
data = util.frame(util.frame(key['type']), util.frame(sig_bytes))
code = util.pack('B', msg_code('SSH2_AGENT_SIGN_RESPONSE'))
return util.frame(code, data)

@ -0,0 +1 @@
"""Unit-tests for this package."""

@ -0,0 +1,72 @@
import io
import mock
import pytest
from .. import client, device, formats, util
ADDR = [2147483661, 2810943954, 3938368396, 3454558782, 3848009040]
CURVE = 'nist256p1'
PUBKEY = (b'\x03\xd8(\xb5\xa6`\xbet0\x95\xac:[;]\xdc,\xbd\xdc?\xd7\xc0\xec'
b'\xdd\xbc+\xfar~\x9dAis')
PUBKEY_TEXT = ('ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzd'
'HAyNTYAAABBBNgotaZgvnQwlaw6Wztd3Cy93D/XwOzdvCv6cn6dQWlzNMEQeW'
'VUfhvrGljR2Z/CMRONY6ejB+9PnpUOPuzYqi8= <localhost:22|nist256p1>\n')
class MockDevice(device.interface.Device): # pylint: disable=abstract-method
def connect(self): # pylint: disable=no-self-use
return mock.Mock()
def pubkey(self, identity, ecdh=False): # pylint: disable=unused-argument
assert self.conn
return PUBKEY
def sign(self, identity, blob):
"""Sign given blob and return the signature (as bytes)."""
assert self.conn
assert blob == BLOB
return SIG
BLOB = (b'\x00\x00\x00 \xce\xe0\xc9\xd5\xceu/\xe8\xc5\xf2\xbfR+x\xa1\xcf\xb0'
b'\x8e;R\xd3)m\x96\x1b\xb4\xd8s\xf1\x99\x16\xaa2\x00\x00\x00\x05roman'
b'\x00\x00\x00\x0essh-connection\x00\x00\x00\tpublickey'
b'\x01\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00h\x00\x00\x00'
b'\x13ecdsa-sha2-nistp256\x00\x00\x00\x08nistp256\x00\x00\x00A'
b'\x04\xd8(\xb5\xa6`\xbet0\x95\xac:[;]\xdc,\xbd\xdc?\xd7\xc0\xec'
b'\xdd\xbc+\xfar~\x9dAis4\xc1\x10yeT~\x1b\xeb\x1aX\xd1\xd9\x9f\xc21'
b'\x13\x8dc\xa7\xa3\x07\xefO\x9e\x95\x0e>\xec\xd8\xaa/')
SIG = (b'R\x19T\xf2\x84$\xef#\x0e\xee\x04X\xc6\xc3\x99T`\xd1\xd8\xf7!'
b'\x862@cx\xb8\xb9i@1\x1b3#\x938\x86]\x97*Y\xb2\x02Xa\xdf@\xecK'
b'\xdc\xf0H\xab\xa8\xac\xa7? \x8f=C\x88N\xe2')
def test_ssh_agent():
identity = device.interface.Identity(identity_str='localhost:22',
curve_name=CURVE)
c = client.Client(device=MockDevice())
assert c.export_public_keys([identity]) == [PUBKEY_TEXT]
signature = c.sign_ssh_challenge(blob=BLOB, identity=identity)
key = formats.import_public_key(PUBKEY_TEXT)
serialized_sig = key['verifier'](sig=signature, msg=BLOB)
stream = io.BytesIO(serialized_sig)
r = util.read_frame(stream)
s = util.read_frame(stream)
assert not stream.read()
assert r[:1] == b'\x00'
assert s[:1] == b'\x00'
assert r[1:] + s[1:] == SIG
# pylint: disable=unused-argument
def cancel_sign(identity, blob):
raise IOError(42, 'ERROR')
c.device.sign = cancel_sign
with pytest.raises(IOError):
c.sign_ssh_challenge(blob=BLOB, identity=identity)

@ -0,0 +1,109 @@
import mock
import pytest
from .. import device, formats, protocol
# pylint: disable=line-too-long
NIST256_KEY = 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEUksojS/qRlTKBKLQO7CBX7a7oqFkysuFn1nJ6gzlR3wNuQXEgd7qb2bjmiiBHsjNxyWvH5SxVi3+fghrqODWo= ssh://localhost' # nopep8
NIST256_BLOB = b'\x00\x00\x00 !S^\xe7\xf8\x1cKN\xde\xcbo\x0c\x83\x9e\xc48\r\xac\xeb,]"\xc1\x9bA\x0eit\xc1\x81\xd4E2\x00\x00\x00\x05roman\x00\x00\x00\x0essh-connection\x00\x00\x00\tpublickey\x01\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00h\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00\x08nistp256\x00\x00\x00A\x04E$\xb2\x88\xd2\xfe\xa4eL\xa0J-\x03\xbb\x08\x15\xfbk\xba*\x16L\xac\xb8Y\xf5\x9c\x9e\xa0\xceTw\xc0\xdb\x90\\H\x1d\xee\xa6\xf6n9\xa2\x88\x11\xec\x8c\xdcrZ\xf1\xf9K\x15b\xdf\xe7\xe0\x86\xba\x8e\rj' # nopep8
NIST256_SIG = b'\x88G!\x0c\n\x16:\xbeF\xbe\xb9\xd2\xa9&e\x89\xad\xc4}\x10\xf8\xbc\xdc\xef\x0e\x8d_\x8a6.\xb6\x1fq\xf0\x16>,\x9a\xde\xe7(\xd6\xd7\x93\x1f\xed\xf9\x94ddw\xfe\xbdq\x13\xbb\xfc\xa9K\xea\x9dC\xa1\xe9' # nopep8
LIST_MSG = b'\x0b'
LIST_NIST256_REPLY = b'\x00\x00\x00\x84\x0c\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00\x08nistp256\x00\x00\x00A\x04E$\xb2\x88\xd2\xfe\xa4eL\xa0J-\x03\xbb\x08\x15\xfbk\xba*\x16L\xac\xb8Y\xf5\x9c\x9e\xa0\xceTw\xc0\xdb\x90\\H\x1d\xee\xa6\xf6n9\xa2\x88\x11\xec\x8c\xdcrZ\xf1\xf9K\x15b\xdf\xe7\xe0\x86\xba\x8e\rj\x00\x00\x00\x0fssh://localhost' # nopep8
NIST256_SIGN_MSG = b'\r\x00\x00\x00h\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00\x08nistp256\x00\x00\x00A\x04E$\xb2\x88\xd2\xfe\xa4eL\xa0J-\x03\xbb\x08\x15\xfbk\xba*\x16L\xac\xb8Y\xf5\x9c\x9e\xa0\xceTw\xc0\xdb\x90\\H\x1d\xee\xa6\xf6n9\xa2\x88\x11\xec\x8c\xdcrZ\xf1\xf9K\x15b\xdf\xe7\xe0\x86\xba\x8e\rj\x00\x00\x00\xd1\x00\x00\x00 !S^\xe7\xf8\x1cKN\xde\xcbo\x0c\x83\x9e\xc48\r\xac\xeb,]"\xc1\x9bA\x0eit\xc1\x81\xd4E2\x00\x00\x00\x05roman\x00\x00\x00\x0essh-connection\x00\x00\x00\tpublickey\x01\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00h\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00\x08nistp256\x00\x00\x00A\x04E$\xb2\x88\xd2\xfe\xa4eL\xa0J-\x03\xbb\x08\x15\xfbk\xba*\x16L\xac\xb8Y\xf5\x9c\x9e\xa0\xceTw\xc0\xdb\x90\\H\x1d\xee\xa6\xf6n9\xa2\x88\x11\xec\x8c\xdcrZ\xf1\xf9K\x15b\xdf\xe7\xe0\x86\xba\x8e\rj\x00\x00\x00\x00' # nopep8
NIST256_SIGN_REPLY = b'\x00\x00\x00j\x0e\x00\x00\x00e\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00J\x00\x00\x00!\x00\x88G!\x0c\n\x16:\xbeF\xbe\xb9\xd2\xa9&e\x89\xad\xc4}\x10\xf8\xbc\xdc\xef\x0e\x8d_\x8a6.\xb6\x1f\x00\x00\x00!\x00q\xf0\x16>,\x9a\xde\xe7(\xd6\xd7\x93\x1f\xed\xf9\x94ddw\xfe\xbdq\x13\xbb\xfc\xa9K\xea\x9dC\xa1\xe9' # nopep8
def fake_connection(keys, signer):
c = mock.Mock()
c.parse_public_keys.return_value = keys
c.sign = signer
return c
def test_list():
key = formats.import_public_key(NIST256_KEY)
key['identity'] = device.interface.Identity('ssh://localhost', 'nist256p1')
h = protocol.Handler(fake_connection(keys=[key], signer=None))
reply = h.handle(LIST_MSG)
assert reply == LIST_NIST256_REPLY
def test_list_legacy_pubs_with_suffix():
h = protocol.Handler(fake_connection(keys=[], signer=None))
suffix = b'\x00\x00\x00\x06foobar'
reply = h.handle(b'\x01' + suffix)
assert reply == b'\x00\x00\x00\x05\x02\x00\x00\x00\x00' # no legacy keys
def test_unsupported():
h = protocol.Handler(fake_connection(keys=[], signer=None))
reply = h.handle(b'\x09')
assert reply == b'\x00\x00\x00\x01\x05'
def ecdsa_signer(identity, blob):
assert identity.to_string() == '<ssh://localhost|nist256p1>'
assert blob == NIST256_BLOB
return NIST256_SIG
def test_ecdsa_sign():
key = formats.import_public_key(NIST256_KEY)
key['identity'] = device.interface.Identity('ssh://localhost', 'nist256p1')
h = protocol.Handler(fake_connection(keys=[key], signer=ecdsa_signer))
reply = h.handle(NIST256_SIGN_MSG)
assert reply == NIST256_SIGN_REPLY
def test_sign_missing():
h = protocol.Handler(fake_connection(keys=[], signer=ecdsa_signer))
with pytest.raises(KeyError):
h.handle(NIST256_SIGN_MSG)
def test_sign_wrong():
def wrong_signature(identity, blob):
assert identity.to_string() == '<ssh://localhost|nist256p1>'
assert blob == NIST256_BLOB
return b'\x00' * 64
key = formats.import_public_key(NIST256_KEY)
key['identity'] = device.interface.Identity('ssh://localhost', 'nist256p1')
h = protocol.Handler(fake_connection(keys=[key], signer=wrong_signature))
with pytest.raises(ValueError):
h.handle(NIST256_SIGN_MSG)
def test_sign_cancel():
def cancel_signature(identity, blob): # pylint: disable=unused-argument
raise IOError()
key = formats.import_public_key(NIST256_KEY)
key['identity'] = device.interface.Identity('ssh://localhost', 'nist256p1')
h = protocol.Handler(fake_connection(keys=[key], signer=cancel_signature))
assert h.handle(NIST256_SIGN_MSG) == protocol.failure()
ED25519_KEY = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFBdF2tjfSO8nLIi736is+f0erq28RTc7CkM11NZtTKR ssh://localhost' # nopep8
ED25519_SIGN_MSG = b'''\r\x00\x00\x003\x00\x00\x00\x0bssh-ed25519\x00\x00\x00 P]\x17kc}#\xbc\x9c\xb2"\xef~\xa2\xb3\xe7\xf4z\xba\xb6\xf1\x14\xdc\xec)\x0c\xd7SY\xb52\x91\x00\x00\x00\x94\x00\x00\x00 i3\xae}yk\\\xa1L\xb9\xe1\xbf\xbc\x8e\x87\r\x0e\xc0\x9f\x97\x0fTC!\x80\x07\x91\xdb^8\xc1\xd62\x00\x00\x00\x05roman\x00\x00\x00\x0essh-connection\x00\x00\x00\tpublickey\x01\x00\x00\x00\x0bssh-ed25519\x00\x00\x003\x00\x00\x00\x0bssh-ed25519\x00\x00\x00 P]\x17kc}#\xbc\x9c\xb2"\xef~\xa2\xb3\xe7\xf4z\xba\xb6\xf1\x14\xdc\xec)\x0c\xd7SY\xb52\x91\x00\x00\x00\x00''' # nopep8
ED25519_SIGN_REPLY = b'''\x00\x00\x00X\x0e\x00\x00\x00S\x00\x00\x00\x0bssh-ed25519\x00\x00\x00@\x8eb)\xa6\xe9P\x83VE\xfbq\xc6\xbf\x1dV3\xe3<O\x11\xc0\xfa\xe4\xed\xb8\x81.\x81\xc8\xa6\xba\x10RA'a\xbc\xa9\xd3\xdb\x98\x07\xf0\x1a\x9c4\x84<\xaf\x99\xb7\xe5G\xeb\xf7$\xc1\r\x86f\x16\x8e\x08\x05''' # nopep8
ED25519_BLOB = b'''\x00\x00\x00 i3\xae}yk\\\xa1L\xb9\xe1\xbf\xbc\x8e\x87\r\x0e\xc0\x9f\x97\x0fTC!\x80\x07\x91\xdb^8\xc1\xd62\x00\x00\x00\x05roman\x00\x00\x00\x0essh-connection\x00\x00\x00\tpublickey\x01\x00\x00\x00\x0bssh-ed25519\x00\x00\x003\x00\x00\x00\x0bssh-ed25519\x00\x00\x00 P]\x17kc}#\xbc\x9c\xb2"\xef~\xa2\xb3\xe7\xf4z\xba\xb6\xf1\x14\xdc\xec)\x0c\xd7SY\xb52\x91''' # nopep8
ED25519_SIG = b'''\x8eb)\xa6\xe9P\x83VE\xfbq\xc6\xbf\x1dV3\xe3<O\x11\xc0\xfa\xe4\xed\xb8\x81.\x81\xc8\xa6\xba\x10RA'a\xbc\xa9\xd3\xdb\x98\x07\xf0\x1a\x9c4\x84<\xaf\x99\xb7\xe5G\xeb\xf7$\xc1\r\x86f\x16\x8e\x08\x05''' # nopep8
def ed25519_signer(identity, blob):
assert identity.to_string() == '<ssh://localhost|ed25519>'
assert blob == ED25519_BLOB
return ED25519_SIG
def test_ed25519_sign():
key = formats.import_public_key(ED25519_KEY)
key['identity'] = device.interface.Identity('ssh://localhost', 'ed25519')
h = protocol.Handler(fake_connection(keys=[key], signer=ed25519_signer))
reply = h.handle(ED25519_SIGN_MSG)
assert reply == ED25519_SIGN_REPLY

@ -0,0 +1 @@
"""Unit-tests for this package."""

@ -0,0 +1,103 @@
import binascii
import pytest
from .. import formats
def test_fingerprint():
fp = '5d:41:40:2a:bc:4b:2a:76:b9:71:9d:91:10:17:c5:92'
assert formats.fingerprint(b'hello') == fp
_point = (
44423495295951059636974944244307637263954375053872017334547086177777411863925, # nopep8
111713194882028655451852320740440245619792555065469028846314891587105736340201 # nopep8
)
_public_key = (
'ecdsa-sha2-nistp256 '
'AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTY'
'AAABBBGI2zqveJSB+geQEWG46OvGs2h3+0qu7tIdsH8Wylr'
'V19vttd7GR5rKvTWJt8b9ErthmnFALelAFKOB/u50jsuk= '
'home\n'
)
def test_parse_public_key():
key = formats.import_public_key(_public_key)
assert key['name'] == b'home'
assert key['point'] == _point
assert key['curve'] == 'nist256p1'
assert key['fingerprint'] == '4b:19:bc:0f:c8:7e:dc:fa:1a:e3:c2:ff:6f:e0:80:a2' # nopep8
assert key['type'] == b'ecdsa-sha2-nistp256'
def test_decompress():
blob = '036236ceabde25207e81e404586e3a3af1acda1dfed2abbbb4876c1fc5b296b575'
vk = formats.decompress_pubkey(binascii.unhexlify(blob),
curve_name=formats.CURVE_NIST256)
assert formats.export_public_key(vk, label='home') == _public_key
def test_parse_ed25519():
pubkey = ('ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFBdF2tj'
'fSO8nLIi736is+f0erq28RTc7CkM11NZtTKR hello\n')
p = formats.import_public_key(pubkey)
assert p['name'] == b'hello'
assert p['curve'] == 'ed25519'
BLOB = (b'\x00\x00\x00\x0bssh-ed25519\x00\x00\x00 P]\x17kc}#'
b'\xbc\x9c\xb2"\xef~\xa2\xb3\xe7\xf4z\xba\xb6\xf1\x14'
b'\xdc\xec)\x0c\xd7SY\xb52\x91')
assert p['blob'] == BLOB
assert p['fingerprint'] == '6b:b0:77:af:e5:3a:21:6d:17:82:9b:06:19:03:a1:97' # nopep8
assert p['type'] == b'ssh-ed25519'
def test_export_ed25519():
pub = (b'\x00P]\x17kc}#\xbc\x9c\xb2"\xef~\xa2\xb3\xe7\xf4'
b'z\xba\xb6\xf1\x14\xdc\xec)\x0c\xd7SY\xb52\x91')
vk = formats.decompress_pubkey(pub, formats.CURVE_ED25519)
result = formats.serialize_verifying_key(vk)
assert result == (b'ssh-ed25519',
b'\x00\x00\x00\x0bssh-ed25519\x00\x00\x00 P]\x17kc}#\xbc'
b'\x9c\xb2"\xef~\xa2\xb3\xe7\xf4z\xba\xb6\xf1\x14\xdc'
b'\xec)\x0c\xd7SY\xb52\x91')
def test_decompress_error():
with pytest.raises(ValueError):
formats.decompress_pubkey('', formats.CURVE_NIST256)
def test_curve_mismatch():
# NIST256 public key
blob = '036236ceabde25207e81e404586e3a3af1acda1dfed2abbbb4876c1fc5b296b575'
with pytest.raises(ValueError):
formats.decompress_pubkey(binascii.unhexlify(blob),
curve_name=formats.CURVE_ED25519)
blob = '00' * 33 # Dummy public key
with pytest.raises(ValueError):
formats.decompress_pubkey(binascii.unhexlify(blob),
curve_name=formats.CURVE_NIST256)
blob = 'FF' * 33 # Unsupported prefix byte
with pytest.raises(ValueError):
formats.decompress_pubkey(binascii.unhexlify(blob),
curve_name=formats.CURVE_NIST256)
def test_serialize_error():
with pytest.raises(TypeError):
formats.serialize_verifying_key(None)
def test_get_ecdh_curve_name():
for c in [formats.CURVE_NIST256, formats.ECDH_CURVE25519]:
assert c == formats.get_ecdh_curve_name(c)
assert (formats.ECDH_CURVE25519 ==
formats.get_ecdh_curve_name(formats.CURVE_ED25519))

@ -0,0 +1,7 @@
from ..device import interface
def test_unicode():
i = interface.Identity(u'ko\u017eu\u0161\u010dek@host', 'ed25519')
assert i.to_bytes() == b'kozuscek@host'
assert sorted(i.items()) == [('host', 'host'), ('user', 'kozuscek')]

@ -0,0 +1,135 @@
import io
import os
import socket
import tempfile
import threading
import mock
import pytest
from .. import server, util
from ..ssh import protocol
def test_socket():
path = tempfile.mktemp()
with server.unix_domain_socket_server(path):
pass
assert not os.path.isfile(path)
class FakeSocket:
def __init__(self, data=b''):
self.rx = io.BytesIO(data)
self.tx = io.BytesIO()
def sendall(self, data):
self.tx.write(data)
def recv(self, size):
return self.rx.read(size)
def close(self):
pass
def settimeout(self, value):
pass
def empty_device():
c = mock.Mock(spec=['parse_public_keys'])
c.parse_public_keys.return_value = []
return c
def test_handle():
mutex = threading.Lock()
handler = protocol.Handler(conn=empty_device())
conn = FakeSocket()
server.handle_connection(conn, handler, mutex)
msg = bytearray([protocol.msg_code('SSH_AGENTC_REQUEST_RSA_IDENTITIES')])
conn = FakeSocket(util.frame(msg))
server.handle_connection(conn, handler, mutex)
assert conn.tx.getvalue() == b'\x00\x00\x00\x05\x02\x00\x00\x00\x00'
msg = bytearray([protocol.msg_code('SSH2_AGENTC_REQUEST_IDENTITIES')])
conn = FakeSocket(util.frame(msg))
server.handle_connection(conn, handler, mutex)
assert conn.tx.getvalue() == b'\x00\x00\x00\x05\x0C\x00\x00\x00\x00'
msg = bytearray([protocol.msg_code('SSH2_AGENTC_ADD_IDENTITY')])
conn = FakeSocket(util.frame(msg))
server.handle_connection(conn, handler, mutex)
conn.tx.seek(0)
reply = util.read_frame(conn.tx)
assert reply == util.pack('B', protocol.msg_code('SSH_AGENT_FAILURE'))
conn_mock = mock.Mock(spec=FakeSocket)
conn_mock.recv.side_effect = [Exception, EOFError]
server.handle_connection(conn=conn_mock, handler=None, mutex=mutex)
def test_server_thread():
sock = FakeSocket()
connections = [sock]
quit_event = threading.Event()
class FakeServer:
def accept(self): # pylint: disable=no-self-use
if not connections:
raise socket.timeout()
return connections.pop(), 'address'
def getsockname(self): # pylint: disable=no-self-use
return 'fake_server'
def handle_conn(conn):
assert conn is sock
quit_event.set()
server.server_thread(sock=FakeServer(),
handle_conn=handle_conn,
quit_event=quit_event)
quit_event.wait()
def test_spawn():
obj = []
def thread(x):
obj.append(x)
with server.spawn(thread, dict(x=1)):
pass
assert obj == [1]
def test_run():
assert server.run_process(['true'], environ={}) == 0
assert server.run_process(['false'], environ={}) == 1
assert server.run_process(command=['bash', '-c', 'exit $X'],
environ={'X': '42'}) == 42
with pytest.raises(OSError):
server.run_process([''], environ={})
def test_remove():
path = 'foo.bar'
def remove(p):
assert p == path
server.remove_file(path, remove=remove)
def remove_raise(_):
raise OSError('boom')
server.remove_file(path, remove=remove_raise, exists=lambda _: False)
with pytest.raises(OSError):
server.remove_file(path, remove=remove_raise, exists=lambda _: True)

@ -0,0 +1,146 @@
import io
import mock
import pytest
from .. import util
def test_bytes2num():
assert util.bytes2num(b'\x12\x34') == 0x1234
def test_num2bytes():
assert util.num2bytes(0x1234, size=2) == b'\x12\x34'
def test_pack():
assert util.pack('BHL', 1, 2, 3) == b'\x01\x00\x02\x00\x00\x00\x03'
def test_frames():
msgs = [b'aaa', b'bb', b'c' * 0x12340]
f = util.frame(*msgs)
assert f == b'\x00\x01\x23\x45' + b''.join(msgs)
assert util.read_frame(io.BytesIO(f)) == b''.join(msgs)
class FakeSocket:
def __init__(self):
self.buf = io.BytesIO()
def sendall(self, data):
self.buf.write(data)
def recv(self, size):
return self.buf.read(size)
def test_send_recv():
s = FakeSocket()
util.send(s, b'123')
util.send(s, b'*')
assert s.buf.getvalue() == b'123*'
s.buf.seek(0)
assert util.recv(s, 2) == b'12'
assert util.recv(s, 2) == b'3*'
pytest.raises(EOFError, util.recv, s, 1)
def test_crc24():
assert util.crc24(b'') == b'\xb7\x04\xce'
assert util.crc24(b'1234567890') == b'\x8c\x00\x72'
def test_bit():
assert util.bit(6, 3) == 0
assert util.bit(6, 2) == 1
assert util.bit(6, 1) == 1
assert util.bit(6, 0) == 0
def test_split_bits():
assert util.split_bits(0x1234, 4, 8, 4) == [0x1, 0x23, 0x4]
def test_hexlify():
assert util.hexlify(b'\x12\x34\xab\xcd') == '1234ABCD'
def test_low_bits():
assert util.low_bits(0x1234, 12) == 0x234
assert util.low_bits(0x1234, 32) == 0x1234
assert util.low_bits(0x1234, 0) == 0
def test_readfmt():
stream = io.BytesIO(b'ABC\x12\x34')
assert util.readfmt(stream, 'B') == (65,)
assert util.readfmt(stream, '>2sH') == (b'BC', 0x1234)
def test_prefix_len():
assert util.prefix_len('>H', b'ABCD') == b'\x00\x04ABCD'
def test_reader():
stream = io.BytesIO(b'ABC\x12\x34')
r = util.Reader(stream)
assert r.read(1) == b'A'
assert r.readfmt('2s') == b'BC'
dst = io.BytesIO()
with r.capture(dst):
assert r.readfmt('>H') == 0x1234
assert dst.getvalue() == b'\x12\x34'
with pytest.raises(EOFError):
r.read(1)
def test_setup_logging():
util.setup_logging(verbosity=10, filename='/dev/null')
def test_memoize():
f = mock.Mock(side_effect=lambda x: x)
def func(x):
# mock.Mock doesn't work with functools.wraps()
return f(x)
g = util.memoize(func)
assert g(1) == g(1)
assert g(1) != g(2)
assert f.mock_calls == [mock.call(1), mock.call(2)]
def test_assuan_serialize():
assert util.assuan_serialize(b'') == b''
assert util.assuan_serialize(b'123\n456') == b'123%0A456'
assert util.assuan_serialize(b'\r\n') == b'%0D%0A'
def test_cache():
timer = mock.Mock(side_effect=range(7))
c = util.ExpiringCache(seconds=2, timer=timer) # t=0
assert c.get() is None # t=1
obj = 'foo'
c.set(obj) # t=2
assert c.get() is obj # t=3
assert c.get() is obj # t=4
assert c.get() is None # t=5
assert c.get() is None # t=6
def test_cache_inf():
timer = mock.Mock(side_effect=range(6))
c = util.ExpiringCache(seconds=float('inf'), timer=timer)
obj = 'foo'
c.set(obj)
assert c.get() is obj
assert c.get() is obj
assert c.get() is obj
assert c.get() is obj

@ -0,0 +1,280 @@
"""Various I/O and serialization utilities."""
import binascii
import contextlib
import functools
import io
import logging
import struct
import time
log = logging.getLogger(__name__)
def send(conn, data):
"""Send data blob to connection socket."""
conn.sendall(data)
def recv(conn, size):
"""
Receive bytes from connection socket or stream.
If size is struct.calcsize()-compatible format, use it to unpack the data.
Otherwise, return the plain blob as bytes.
"""
try:
fmt = size
size = struct.calcsize(fmt)
except TypeError:
fmt = None
try:
_read = conn.recv
except AttributeError:
_read = conn.read
res = io.BytesIO()
while size > 0:
buf = _read(size)
if not buf:
raise EOFError
size = size - len(buf)
res.write(buf)
res = res.getvalue()
if fmt:
return struct.unpack(fmt, res)
else:
return res
def read_frame(conn):
"""Read size-prefixed frame from connection."""
size, = recv(conn, '>L')
return recv(conn, size)
def bytes2num(s):
"""Convert MSB-first bytes to an unsigned integer."""
res = 0
for i, c in enumerate(reversed(bytearray(s))):
res += c << (i * 8)
return res
def num2bytes(value, size):
"""Convert an unsigned integer to MSB-first bytes with specified size."""
res = []
for _ in range(size):
res.append(value & 0xFF)
value = value >> 8
assert value == 0
return bytes(bytearray(list(reversed(res))))
def pack(fmt, *args):
"""Serialize MSB-first message."""
return struct.pack('>' + fmt, *args)
def frame(*msgs):
"""Serialize MSB-first length-prefixed frame."""
res = io.BytesIO()
for msg in msgs:
res.write(msg)
msg = res.getvalue()
return pack('L', len(msg)) + msg
def crc24(blob):
"""See https://tools.ietf.org/html/rfc4880#section-6.1 for details."""
CRC24_INIT = 0x0B704CE
CRC24_POLY = 0x1864CFB
crc = CRC24_INIT
for octet in bytearray(blob):
crc ^= (octet << 16)
for _ in range(8):
crc <<= 1
if crc & 0x1000000:
crc ^= CRC24_POLY
assert 0 <= crc < 0x1000000
crc_bytes = struct.pack('>L', crc)
assert crc_bytes[:1] == b'\x00'
return crc_bytes[1:]
def bit(value, i):
"""Extract the i-th bit out of value."""
return 1 if value & (1 << i) else 0
def low_bits(value, n):
"""Extract the lowest n bits out of value."""
return value & ((1 << n) - 1)
def split_bits(value, *bits):
"""
Split integer value into list of ints, according to `bits` list.
For example, split_bits(0x1234, 4, 8, 4) == [0x1, 0x23, 0x4]
"""
result = []
for b in reversed(bits):
mask = (1 << b) - 1
result.append(value & mask)
value = value >> b
assert value == 0
result.reverse()
return result
def readfmt(stream, fmt):
"""Read and unpack an object from stream, using a struct format string."""
size = struct.calcsize(fmt)
blob = stream.read(size)
return struct.unpack(fmt, blob)
def prefix_len(fmt, blob):
"""Prefix `blob` with its size, serialized using `fmt` format."""
return struct.pack(fmt, len(blob)) + blob
def hexlify(blob):
"""Utility for consistent hexadecimal formatting."""
return binascii.hexlify(blob).decode('ascii').upper()
class Reader:
"""Read basic type objects out of given stream."""
def __init__(self, stream):
"""Create a non-capturing reader."""
self.s = stream
self._captured = None
def readfmt(self, fmt):
"""Read a specified object, using a struct format string."""
size = struct.calcsize(fmt)
blob = self.read(size)
obj, = struct.unpack(fmt, blob)
return obj
def read(self, size=None):
"""Read `size` bytes from stream."""
blob = self.s.read(size)
if size is not None and len(blob) < size:
raise EOFError
if self._captured:
self._captured.write(blob)
return blob
@contextlib.contextmanager
def capture(self, stream):
"""Capture all data read during this context."""
self._captured = stream
try:
yield
finally:
self._captured = None
def setup_logging(verbosity, filename=None):
"""Configure logging for this tool."""
levels = [logging.WARNING, logging.INFO, logging.DEBUG]
level = levels[min(verbosity, len(levels) - 1)]
logging.root.setLevel(level)
fmt = logging.Formatter('%(asctime)s %(levelname)-12s %(message)-100s '
'[%(filename)s:%(lineno)d]')
hdlr = logging.StreamHandler() # stderr
hdlr.setFormatter(fmt)
logging.root.addHandler(hdlr)
if filename:
hdlr = logging.FileHandler(filename, 'a')
hdlr.setFormatter(fmt)
logging.root.addHandler(hdlr)
def memoize(func):
"""Simple caching decorator."""
cache = {}
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""Caching wrapper."""
key = (args, tuple(sorted(kwargs.items())))
if key in cache:
return cache[key]
else:
result = func(*args, **kwargs)
cache[key] = result
return result
return wrapper
def memoize_method(method):
"""Simple caching decorator."""
cache = {}
@functools.wraps(method)
def wrapper(self, *args, **kwargs):
"""Caching wrapper."""
key = (args, tuple(sorted(kwargs.items())))
if key in cache:
return cache[key]
else:
result = method(self, *args, **kwargs)
cache[key] = result
return result
return wrapper
@memoize
def which(cmd):
"""Return full path to specified command, or raise OSError if missing."""
try:
# For Python 3
from shutil import which as _which
except ImportError:
# For Python 2
from backports.shutil_which import which as _which # pylint: disable=relative-import
full_path = _which(cmd)
if full_path is None:
raise OSError('Cannot find {!r} in $PATH'.format(cmd))
log.debug('which %r => %r', cmd, full_path)
return full_path
def assuan_serialize(data):
"""Serialize data according to ASSUAN protocol (for GPG daemon communication)."""
for c in [b'%', b'\n', b'\r']:
escaped = '%{:02X}'.format(ord(c)).encode('ascii')
data = data.replace(c, escaped)
return data
class ExpiringCache:
"""Simple cache with a deadline."""
def __init__(self, seconds, timer=time.time):
"""C-tor."""
self.duration = seconds
self.timer = timer
self.value = None
self.set(None)
def get(self):
"""Returns existing value, or None if deadline has expired."""
if self.timer() > self.deadline:
self.value = None
return self.value
def set(self, value):
"""Set new value and reset the deadline for expiration."""
self.deadline = self.timer() + self.duration
self.value = value

@ -0,0 +1,6 @@
#!/bin/bash
set -eux
rm -rv dist/*
python3 setup.py sdist
gpg2 -v --detach-sign -a dist/*.tar.gz
twine upload dist/*

@ -2,28 +2,43 @@
from setuptools import setup
setup(
name='trezor_agent',
version='0.4.1',
description='Using Trezor as hardware SSH agent',
name='libagent',
version='0.12.0',
description='Using hardware wallets as SSH/GPG agent',
author='Roman Zeyde',
author_email='roman.zeyde@gmail.com',
license='MIT',
url='http://github.com/romanz/trezor-agent',
packages=['trezor_agent', 'trezor_agent.trezor'],
install_requires=['ecdsa>=0.13', 'trezor>=0.6.6'],
packages=[
'libagent',
'libagent.device',
'libagent.gpg',
'libagent.ssh'
],
install_requires=[
'backports.shutil_which>=3.5.1',
'ConfigArgParse>=0.12.0',
'python-daemon>=2.1.2',
'ecdsa>=0.13',
'ed25519>=1.4',
'mnemonic>=0.18',
'pymsgbox>=1.0.6',
'semver>=2.2',
'unidecode>=0.4.20',
],
platforms=['POSIX'],
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Console',
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'License :: OSI Approved :: MIT License',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',
'Operating System :: POSIX',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3 :: Only',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: System :: Networking',
'Topic :: Communications',
'Topic :: Security',
'Topic :: Utilities',
],
entry_points={'console_scripts': [
'trezor-agent = trezor_agent.__main__:trezor_agent'
]},
)

@ -1,17 +1,24 @@
[tox]
envlist = py27,py34
skipsdist = True
envlist = py3
[pycodestyle]
max-line-length = 100
[pep257]
add-ignore = D401
[testenv]
deps=
pytest
mock
pep8
pycodestyle
coverage
pylint
six
ecdsa
semver
pydocstyle
isort
commands=
pep8 trezor_agent
pylint --report=no --rcfile .pylintrc trezor_agent
coverage run --omit='trezor_agent/__main__.py,trezor_agent/trezor/_library.py' --source trezor_agent/ -m py.test -v
pycodestyle libagent
# isort --skip-glob .tox -c -r libagent
pylint --reports=no --rcfile .pylintrc libagent
pydocstyle libagent
coverage run --source libagent -m py.test -v libagent
coverage report
coverage html

@ -1,116 +0,0 @@
import os
import re
import sys
import argparse
import subprocess
from . import trezor
from . import server
import logging
log = logging.getLogger(__name__)
def identity_from_gitconfig():
out = subprocess.check_output(args='git config --list --local'.split())
config = [line.split('=', 1) for line in out.strip().split('\n')]
config_dict = dict(item for item in config if len(item) == 2)
name_regex = re.compile(r'^remote\..*\.trezor$')
names = [item[0] for item in config if name_regex.match(item[0])]
if len(names) != 1:
log.error('please add "trezor" key '
'to a single remote section at .git/config')
sys.exit(1)
key_name = names[0]
identity_label = config_dict.get(key_name)
if identity_label:
return identity_label
# extract remote name marked as TREZOR's
section_name, _ = key_name.rsplit('.', 1)
key_name = section_name + '.url'
url = config_dict[key_name]
log.debug('using "%s=%s" from git-config', key_name, url)
user, url = url.split('@', 1)
host, path = url.split(':', 1)
return 'ssh://{0}@{1}/{2}'.format(user, host, path)
def create_agent_parser():
p = argparse.ArgumentParser()
p.add_argument('-v', '--verbose', default=0, action='count')
p.add_argument('identity', type=str, default=None,
help='proto://[user@]host[:port][/path]')
g = p.add_mutually_exclusive_group()
g.add_argument('-s', '--shell', default=False, action='store_true',
help='run $SHELL as subprocess under SSH agent')
g.add_argument('-c', '--connect', default=False, action='store_true',
help='connect to specified host via SSH')
p.add_argument('command', type=str, nargs='*', metavar='ARGUMENT',
help='command to run under the SSH agent')
return p
def setup_logging(verbosity):
fmt = ('%(asctime)s %(levelname)-12s %(message)-100s '
'[%(filename)s:%(lineno)d]')
levels = [logging.WARNING, logging.INFO, logging.DEBUG]
level = levels[min(verbosity, len(levels) - 1)]
logging.basicConfig(format=fmt, level=level)
def ssh_command(identity):
command = ['ssh', identity.host]
if identity.user:
command += ['-l', identity.user]
if identity.port:
command += ['-p', identity.port]
return command
def trezor_agent():
args = create_agent_parser().parse_args()
setup_logging(verbosity=args.verbose)
with trezor.Client() as client:
label = args.identity
command = args.command
if label == 'git':
label = identity_from_gitconfig()
log.info('using identity %r for git command %r', label, command)
if command:
command = ['git'] + command
identity = client.get_identity(label=label)
public_key = client.get_public_key(identity=identity)
use_shell = False
if args.connect:
command = ssh_command(identity) + args.command
log.debug('SSH connect: %r', command)
if args.shell:
command, use_shell = os.environ['SHELL'], True
log.debug('using shell: %r', command)
if not command:
sys.stdout.write(public_key)
return
def signer(label, blob):
identity = client.get_identity(label=label)
return client.sign_ssh_challenge(identity=identity, blob=blob)
try:
with server.serve(public_keys=[public_key], signer=signer) as env:
return server.run_process(command=command, environ=env,
use_shell=use_shell)
except KeyboardInterrupt:
log.info('server stopped')

@ -1,92 +0,0 @@
import io
import hashlib
import base64
import ecdsa
from . import util
import logging
log = logging.getLogger(__name__)
DER_OCTET_STRING = b'\x04'
ECDSA_KEY_PREFIX = b'ecdsa-sha2-'
ECDSA_CURVE_NAME = b'nistp256'
hashfunc = hashlib.sha256
def fingerprint(blob):
digest = hashlib.md5(blob).digest()
return ':'.join('{:02x}'.format(c) for c in bytearray(digest))
def parse_pubkey(blob, curve=ecdsa.NIST256p):
s = io.BytesIO(blob)
key_type = util.read_frame(s)
log.debug('key type: %s', key_type)
curve_name = util.read_frame(s)
log.debug('curve name: %s', curve_name)
point = util.read_frame(s)
assert s.read() == b''
_type, point = point[:1], point[1:]
assert _type == DER_OCTET_STRING
size = len(point) // 2
assert len(point) == 2 * size
coords = (util.bytes2num(point[:size]), util.bytes2num(point[size:]))
log.debug('coordinates: %s', coords)
fp = fingerprint(blob)
point = ecdsa.ellipticcurve.Point(curve.curve, *coords)
vk = ecdsa.VerifyingKey.from_public_point(point, curve, hashfunc)
result = {
'point': coords,
'curve': curve_name,
'fingerprint': fp,
'type': key_type,
'blob': blob,
'size': size,
'verifying_key': vk
}
return result
def decompress_pubkey(pub, curve=ecdsa.NIST256p):
P = curve.curve.p()
A = curve.curve.a()
B = curve.curve.b()
x = util.bytes2num(pub[1:33])
beta = pow(int(x*x*x+A*x+B), int((P+1)//4), int(P))
p0 = util.bytes2num(pub[:1])
y = (P-beta) if ((beta + p0) % 2) else beta
point = ecdsa.ellipticcurve.Point(curve.curve, x, y)
return ecdsa.VerifyingKey.from_public_point(point, curve=curve,
hashfunc=hashfunc)
def serialize_verifying_key(vk):
key_type = ECDSA_KEY_PREFIX + ECDSA_CURVE_NAME
curve_name = ECDSA_CURVE_NAME
key_blob = DER_OCTET_STRING + vk.to_string()
parts = [key_type, curve_name, key_blob]
return b''.join([util.frame(p) for p in parts])
def export_public_key(pubkey, label):
blob = serialize_verifying_key(decompress_pubkey(pubkey))
log.debug('fingerprint: %s', fingerprint(blob))
b64 = base64.b64encode(blob).decode('ascii')
key_type = ECDSA_KEY_PREFIX + ECDSA_CURVE_NAME
return '{} {} {}\n'.format(key_type.decode('ascii'), b64, label)
def import_public_key(line):
''' Parse public key textual format, as saved at .pub file '''
file_type, base64blob, name = line.split()
blob = base64.b64decode(base64blob)
result = parse_pubkey(blob)
result['name'] = name.encode('ascii')
assert result['type'] == file_type.encode('ascii')
log.debug('loaded %s %s', file_type, result['fingerprint'])
return result

@ -1,115 +0,0 @@
import io
from . import util
from . import formats
import logging
log = logging.getLogger(__name__)
SSH_AGENTC_REQUEST_RSA_IDENTITIES = 1
SSH_AGENT_RSA_IDENTITIES_ANSWER = 2
SSH_AGENTC_REMOVE_ALL_RSA_IDENTITIES = 9
SSH2_AGENTC_REQUEST_IDENTITIES = 11
SSH2_AGENT_IDENTITIES_ANSWER = 12
SSH2_AGENTC_SIGN_REQUEST = 13
SSH2_AGENT_SIGN_RESPONSE = 14
SSH2_AGENTC_ADD_IDENTITY = 17
SSH2_AGENTC_REMOVE_IDENTITY = 18
SSH2_AGENTC_REMOVE_ALL_IDENTITIES = 19
class Error(Exception):
pass
class BadSignature(Error):
pass
class MissingKey(Error):
pass
class Handler(object):
def __init__(self, keys, signer):
self.public_keys = keys
self.signer = signer
self.methods = {
SSH_AGENTC_REQUEST_RSA_IDENTITIES: Handler.legacy_pubs,
SSH2_AGENTC_REQUEST_IDENTITIES: self.list_pubs,
SSH2_AGENTC_SIGN_REQUEST: self.sign_message,
}
def handle(self, msg):
log.debug('request: %d bytes', len(msg))
buf = io.BytesIO(msg)
code, = util.recv(buf, '>B')
method = self.methods[code]
log.debug('calling %s()', method.__name__)
reply = method(buf=buf)
log.debug('reply: %d bytes', len(reply))
return reply
@staticmethod
def legacy_pubs(buf):
''' SSH v1 public keys are not supported '''
assert not buf.read()
code = util.pack('B', SSH_AGENT_RSA_IDENTITIES_ANSWER)
num = util.pack('L', 0) # no SSH v1 keys
return util.frame(code, num)
def list_pubs(self, buf):
''' SSH v2 public keys are serialized and returned. '''
assert not buf.read()
keys = self.public_keys
code = util.pack('B', SSH2_AGENT_IDENTITIES_ANSWER)
num = util.pack('L', len(keys))
log.debug('available keys: %s', [k['name'] for k in keys])
for i, k in enumerate(keys):
log.debug('%2d) %s', i+1, k['fingerprint'])
pubs = [util.frame(k['blob']) + util.frame(k['name']) for k in keys]
return util.frame(code, num, *pubs)
def sign_message(self, buf):
''' SSH v2 public key authentication is performed. '''
key = formats.parse_pubkey(util.read_frame(buf))
log.debug('looking for %s', key['fingerprint'])
blob = util.read_frame(buf)
assert util.read_frame(buf) == b''
assert not buf.read()
for k in self.public_keys:
if (k['fingerprint']) == (key['fingerprint']):
log.debug('using key %r (%s)', k['name'], k['fingerprint'])
key = k
break
else:
raise MissingKey('key not found')
log.debug('signing %d-byte blob', len(blob))
r, s = self.signer(label=key['name'], blob=blob)
signature = (r, s)
log.debug('signature: %s', signature)
try:
key['verifying_key'].verify(signature=signature, data=blob,
sigdecode=lambda sig, _: sig)
log.info('signature status: OK')
except formats.ecdsa.BadSignatureError:
log.exception('signature status: ERROR')
raise BadSignature('invalid ECDSA signature')
sig_bytes = io.BytesIO()
for x in signature:
x_frame = util.frame(b'\x00' + util.num2bytes(x, key['size']))
sig_bytes.write(x_frame)
sig_bytes = sig_bytes.getvalue()
log.debug('signature size: %d bytes', len(sig_bytes))
data = util.frame(util.frame(key['type']), util.frame(sig_bytes))
code = util.pack('B', SSH2_AGENT_SIGN_RESPONSE)
return util.frame(code, data)

@ -1,102 +0,0 @@
import socket
import os
import subprocess
import tempfile
import contextlib
import threading
from . import protocol
from . import formats
from . import util
import logging
log = logging.getLogger(__name__)
def remove_file(path, remove=os.remove, exists=os.path.exists):
try:
remove(path)
except OSError:
if exists(path):
raise
@contextlib.contextmanager
def unix_domain_socket_server(sock_path):
log.debug('serving on SSH_AUTH_SOCK=%s', sock_path)
remove_file(sock_path)
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(sock_path)
server.listen(1)
try:
yield server
finally:
remove_file(sock_path)
def handle_connection(conn, handler):
try:
log.debug('welcome agent')
while True:
msg = util.read_frame(conn)
reply = handler.handle(msg=msg)
util.send(conn, reply)
except EOFError:
log.debug('goodbye agent')
except:
log.exception('error')
raise
def server_thread(server, handler):
log.debug('server thread started')
while True:
log.debug('waiting for connection on %s', server.getsockname())
try:
conn, _ = server.accept()
except socket.error as e:
log.debug('server stopped: %s', e)
break
with contextlib.closing(conn):
handle_connection(conn, handler)
log.debug('server thread stopped')
@contextlib.contextmanager
def spawn(func, **kwargs):
t = threading.Thread(target=func, kwargs=kwargs)
t.start()
yield
t.join()
@contextlib.contextmanager
def serve(public_keys, signer, sock_path=None):
if sock_path is None:
sock_path = tempfile.mktemp(prefix='ssh-agent-')
keys = [formats.import_public_key(k) for k in public_keys]
environ = {'SSH_AUTH_SOCK': sock_path, 'SSH_AGENT_PID': str(os.getpid())}
with unix_domain_socket_server(sock_path) as server:
handler = protocol.Handler(keys=keys, signer=signer)
with spawn(server_thread, server=server, handler=handler):
try:
yield environ
finally:
log.debug('closing server')
server.shutdown(socket.SHUT_RD)
def run_process(command, environ, use_shell=False):
log.debug('running %r with %r', command, environ)
env = dict(os.environ)
env.update(environ)
try:
p = subprocess.Popen(args=command, env=env, shell=use_shell)
except OSError as e:
raise OSError('cannot run %r: %s' % (command, e))
log.debug('subprocess %d is running', p.pid)
ret = p.wait()
log.debug('subprocess %d exited: %d', p.pid, ret)
return ret

@ -1,39 +0,0 @@
import binascii
from .. import formats
def test_fingerprint():
fp = '5d:41:40:2a:bc:4b:2a:76:b9:71:9d:91:10:17:c5:92'
assert formats.fingerprint(b'hello') == fp
_point = (
44423495295951059636974944244307637263954375053872017334547086177777411863925, # nopep8
111713194882028655451852320740440245619792555065469028846314891587105736340201 # nopep8
)
_public_key = (
'ecdsa-sha2-nistp256 '
'AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTY'
'AAABBBGI2zqveJSB+geQEWG46OvGs2h3+0qu7tIdsH8Wylr'
'V19vttd7GR5rKvTWJt8b9ErthmnFALelAFKOB/u50jsuk= '
'home\n'
)
def test_parse_public_key():
key = formats.import_public_key(_public_key)
assert key['name'] == b'home'
assert key['point'] == _point
assert key['curve'] == b'nistp256'
assert key['fingerprint'] == '4b:19:bc:0f:c8:7e:dc:fa:1a:e3:c2:ff:6f:e0:80:a2' # nopep8
assert key['type'] == b'ecdsa-sha2-nistp256'
assert key['size'] == 32
def test_decompress():
blob = '036236ceabde25207e81e404586e3a3af1acda1dfed2abbbb4876c1fc5b296b575'
result = formats.export_public_key(binascii.unhexlify(blob), label='home')
assert result == _public_key

@ -1,56 +0,0 @@
from .. import protocol
from .. import formats
import pytest
# pylint: disable=line-too-long
KEY = 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEUksojS/qRlTKBKLQO7CBX7a7oqFkysuFn1nJ6gzlR3wNuQXEgd7qb2bjmiiBHsjNxyWvH5SxVi3+fghrqODWo= ssh://localhost' # nopep8
BLOB = b'\x00\x00\x00 !S^\xe7\xf8\x1cKN\xde\xcbo\x0c\x83\x9e\xc48\r\xac\xeb,]"\xc1\x9bA\x0eit\xc1\x81\xd4E2\x00\x00\x00\x05roman\x00\x00\x00\x0essh-connection\x00\x00\x00\tpublickey\x01\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00h\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00\x08nistp256\x00\x00\x00A\x04E$\xb2\x88\xd2\xfe\xa4eL\xa0J-\x03\xbb\x08\x15\xfbk\xba*\x16L\xac\xb8Y\xf5\x9c\x9e\xa0\xceTw\xc0\xdb\x90\\H\x1d\xee\xa6\xf6n9\xa2\x88\x11\xec\x8c\xdcrZ\xf1\xf9K\x15b\xdf\xe7\xe0\x86\xba\x8e\rj' # nopep8
SIG = (61640221631134565789126560951398335114074531708367858563384221818711312348703, 51535548700089687831159696283235534298026173963719263249292887877395159425513) # nopep8
LIST_MSG = b'\x0b'
LIST_REPLY = b'\x00\x00\x00\x84\x0c\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00\x08nistp256\x00\x00\x00A\x04E$\xb2\x88\xd2\xfe\xa4eL\xa0J-\x03\xbb\x08\x15\xfbk\xba*\x16L\xac\xb8Y\xf5\x9c\x9e\xa0\xceTw\xc0\xdb\x90\\H\x1d\xee\xa6\xf6n9\xa2\x88\x11\xec\x8c\xdcrZ\xf1\xf9K\x15b\xdf\xe7\xe0\x86\xba\x8e\rj\x00\x00\x00\x0fssh://localhost' # nopep8
SIGN_MSG = b'\r\x00\x00\x00h\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00\x08nistp256\x00\x00\x00A\x04E$\xb2\x88\xd2\xfe\xa4eL\xa0J-\x03\xbb\x08\x15\xfbk\xba*\x16L\xac\xb8Y\xf5\x9c\x9e\xa0\xceTw\xc0\xdb\x90\\H\x1d\xee\xa6\xf6n9\xa2\x88\x11\xec\x8c\xdcrZ\xf1\xf9K\x15b\xdf\xe7\xe0\x86\xba\x8e\rj\x00\x00\x00\xd1\x00\x00\x00 !S^\xe7\xf8\x1cKN\xde\xcbo\x0c\x83\x9e\xc48\r\xac\xeb,]"\xc1\x9bA\x0eit\xc1\x81\xd4E2\x00\x00\x00\x05roman\x00\x00\x00\x0essh-connection\x00\x00\x00\tpublickey\x01\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00h\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00\x08nistp256\x00\x00\x00A\x04E$\xb2\x88\xd2\xfe\xa4eL\xa0J-\x03\xbb\x08\x15\xfbk\xba*\x16L\xac\xb8Y\xf5\x9c\x9e\xa0\xceTw\xc0\xdb\x90\\H\x1d\xee\xa6\xf6n9\xa2\x88\x11\xec\x8c\xdcrZ\xf1\xf9K\x15b\xdf\xe7\xe0\x86\xba\x8e\rj\x00\x00\x00\x00' # nopep8
SIGN_REPLY = b'\x00\x00\x00j\x0e\x00\x00\x00e\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00J\x00\x00\x00!\x00\x88G!\x0c\n\x16:\xbeF\xbe\xb9\xd2\xa9&e\x89\xad\xc4}\x10\xf8\xbc\xdc\xef\x0e\x8d_\x8a6.\xb6\x1f\x00\x00\x00!\x00q\xf0\x16>,\x9a\xde\xe7(\xd6\xd7\x93\x1f\xed\xf9\x94ddw\xfe\xbdq\x13\xbb\xfc\xa9K\xea\x9dC\xa1\xe9' # nopep8
def test_list():
key = formats.import_public_key(KEY)
h = protocol.Handler(keys=[key], signer=None)
reply = h.handle(LIST_MSG)
assert reply == LIST_REPLY
def signer(label, blob):
assert label == b'ssh://localhost'
assert blob == BLOB
return SIG
def test_sign():
key = formats.import_public_key(KEY)
h = protocol.Handler(keys=[key], signer=signer)
reply = h.handle(SIGN_MSG)
assert reply == SIGN_REPLY
def test_sign_missing():
h = protocol.Handler(keys=[], signer=signer)
with pytest.raises(protocol.MissingKey):
h.handle(SIGN_MSG)
def test_sign_wrong():
def wrong_signature(label, blob):
assert label == b'ssh://localhost'
assert blob == BLOB
return (0, 0)
key = formats.import_public_key(KEY)
h = protocol.Handler(keys=[key], signer=wrong_signature)
with pytest.raises(protocol.BadSignature):
h.handle(SIGN_MSG)

@ -1,118 +0,0 @@
import tempfile
import socket
import os
import io
import pytest
from .. import server
from .. import protocol
from .. import util
def test_socket():
path = tempfile.mktemp()
with server.unix_domain_socket_server(path):
pass
assert not os.path.isfile(path)
class SocketMock(object):
def __init__(self, data=b''):
self.rx = io.BytesIO(data)
self.tx = io.BytesIO()
def sendall(self, data):
self.tx.write(data)
def recv(self, size):
return self.rx.read(size)
def close(self):
pass
def test_handle():
handler = protocol.Handler(keys=[], signer=None)
conn = SocketMock()
server.handle_connection(conn, handler)
msg = bytearray([protocol.SSH_AGENTC_REQUEST_RSA_IDENTITIES])
conn = SocketMock(util.frame(msg))
server.handle_connection(conn, handler)
assert conn.tx.getvalue() == b'\x00\x00\x00\x05\x02\x00\x00\x00\x00'
msg = bytearray([protocol.SSH2_AGENTC_REQUEST_IDENTITIES])
conn = SocketMock(util.frame(msg))
server.handle_connection(conn, handler)
assert conn.tx.getvalue() == b'\x00\x00\x00\x05\x0C\x00\x00\x00\x00'
with pytest.raises(AttributeError):
server.handle_connection(conn=None, handler=None)
class ServerMock(object):
def __init__(self, connections, name):
self.connections = connections
self.name = name
def getsockname(self):
return self.name
def accept(self):
if self.connections:
return self.connections.pop(), 'address'
raise socket.error('stop')
def test_server_thread():
s = ServerMock(connections=[SocketMock()], name='mock')
h = protocol.Handler(keys=[], signer=None)
server.server_thread(s, h)
def test_spawn():
obj = []
def thread(x):
obj.append(x)
with server.spawn(thread, x=1):
pass
assert obj == [1]
def test_run():
assert server.run_process(['true'], environ={}) == 0
assert server.run_process(['false'], environ={}) == 1
assert server.run_process(
command='exit $X',
environ={'X': '42'},
use_shell=True) == 42
with pytest.raises(OSError):
server.run_process([''], environ={})
def test_serve_main():
with server.serve(public_keys=[], signer=None, sock_path=None):
pass
def test_remove():
path = 'foo.bar'
def remove(p):
assert p == path
server.remove_file(path, remove=remove)
def remove_raise(_):
raise OSError('boom')
server.remove_file(path, remove=remove_raise, exists=lambda _: False)
with pytest.raises(OSError):
server.remove_file(path, remove=remove_raise, exists=lambda _: True)

@ -1,135 +0,0 @@
from ..trezor import client
from .. import formats
import mock
import pytest
ADDR = [2147483661, 2810943954, 3938368396, 3454558782, 3848009040]
CURVE = 'nist256p1'
PUBKEY = (b'\x03\xd8(\xb5\xa6`\xbet0\x95\xac:[;]\xdc,\xbd\xdc?\xd7\xc0\xec'
b'\xdd\xbc+\xfar~\x9dAis')
PUBKEY_TEXT = ('ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzd'
'HAyNTYAAABBBNgotaZgvnQwlaw6Wztd3Cy93D/XwOzdvCv6cn6dQWlzNMEQeW'
'VUfhvrGljR2Z/CMRONY6ejB+9PnpUOPuzYqi8= ssh://localhost:22\n')
class ConnectionMock(object):
def __init__(self, version):
self.features = mock.Mock(spec=[])
self.features.device_id = '123456789'
self.features.label = 'mywallet'
self.features.vendor = 'mock'
self.features.major_version = version[0]
self.features.minor_version = version[1]
self.features.patch_version = version[2]
self.features.revision = b'456'
self.closed = False
def close(self):
self.closed = True
def clear_session(self):
self.closed = True
def get_public_node(self, n, ecdsa_curve_name='secp256k1'):
assert not self.closed
assert n == ADDR
assert ecdsa_curve_name in {'secp256k1', 'nist256p1'}
result = mock.Mock(spec=[])
result.node = mock.Mock(spec=[])
result.node.public_key = PUBKEY
return result
def ping(self, msg):
assert not self.closed
return msg
class FactoryMock(object):
@staticmethod
def client():
return ConnectionMock(version=(1, 3, 4))
@staticmethod
def identity_type(**kwargs):
result = mock.Mock(spec=[])
result.index = 0
result.proto = result.user = result.host = result.port = None
result.path = None
for k, v in kwargs.items():
setattr(result, k, v)
return result
BLOB = (b'\x00\x00\x00 \xce\xe0\xc9\xd5\xceu/\xe8\xc5\xf2\xbfR+x\xa1\xcf\xb0'
b'\x8e;R\xd3)m\x96\x1b\xb4\xd8s\xf1\x99\x16\xaa2\x00\x00\x00\x05roman'
b'\x00\x00\x00\x0essh-connection\x00\x00\x00\tpublickey'
b'\x01\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00h\x00\x00\x00'
b'\x13ecdsa-sha2-nistp256\x00\x00\x00\x08nistp256\x00\x00\x00A'
b'\x04\xd8(\xb5\xa6`\xbet0\x95\xac:[;]\xdc,\xbd\xdc?\xd7\xc0\xec'
b'\xdd\xbc+\xfar~\x9dAis4\xc1\x10yeT~\x1b\xeb\x1aX\xd1\xd9\x9f\xc21'
b'\x13\x8dc\xa7\xa3\x07\xefO\x9e\x95\x0e>\xec\xd8\xaa/')
SIG = (b'\x00R\x19T\xf2\x84$\xef#\x0e\xee\x04X\xc6\xc3\x99T`\xd1\xd8\xf7!'
b'\x862@cx\xb8\xb9i@1\x1b3#\x938\x86]\x97*Y\xb2\x02Xa\xdf@\xecK'
b'\xdc\xf0H\xab\xa8\xac\xa7? \x8f=C\x88N\xe2')
def test_ssh_agent():
c = client.Client(factory=FactoryMock)
ident = c.get_identity(label='localhost:22')
assert ident.host == 'localhost'
assert ident.proto == 'ssh'
assert ident.port == '22'
assert ident.user is None
assert ident.path is None
with c:
assert c.get_public_key(ident) == PUBKEY_TEXT
def ssh_sign_identity(identity, challenge_hidden,
challenge_visual, ecdsa_curve_name):
assert identity is ident
assert challenge_hidden == BLOB
assert challenge_visual == identity.path
assert ecdsa_curve_name == 'nist256p1'
result = mock.Mock(spec=[])
result.public_key = PUBKEY
result.signature = SIG
return result
c.client.sign_identity = ssh_sign_identity
signature = c.sign_ssh_challenge(identity=ident, blob=BLOB)
key = formats.import_public_key(PUBKEY_TEXT)
assert key['verifying_key'].verify(signature=signature, data=BLOB,
sigdecode=lambda sig, _: sig)
def test_utils():
identity = mock.Mock(spec=[])
identity.proto = 'https'
identity.user = 'user'
identity.host = 'host'
identity.port = '443'
identity.path = '/path'
url = 'https://user@host:443/path'
assert client.identity_to_string(identity) == url
def test_old_version():
class OldFactoryMock(FactoryMock):
@staticmethod
def client():
return ConnectionMock(version=(1, 2, 3))
with pytest.raises(ValueError):
client.Client(factory=OldFactoryMock)

@ -1,47 +0,0 @@
import io
import pytest
from .. import util
def test_bytes2num():
assert util.bytes2num(b'\x12\x34') == 0x1234
def test_num2bytes():
assert util.num2bytes(0x1234, size=2) == b'\x12\x34'
def test_pack():
assert util.pack('BHL', 1, 2, 3) == b'\x01\x00\x02\x00\x00\x00\x03'
def test_frames():
msgs = [b'aaa', b'bb', b'c' * 0x12340]
f = util.frame(*msgs)
assert f == b'\x00\x01\x23\x45' + b''.join(msgs)
assert util.read_frame(io.BytesIO(f)) == b''.join(msgs)
class SocketMock(object):
def __init__(self):
self.buf = io.BytesIO()
def sendall(self, data):
self.buf.write(data)
def recv(self, size):
return self.buf.read(size)
def test_send_recv():
s = SocketMock()
util.send(s, b'123')
util.send(s, data=[42], fmt='B')
assert s.buf.getvalue() == b'123*'
s.buf.seek(0)
assert util.recv(s, 2) == b'12'
assert util.recv(s, 2) == b'3*'
pytest.raises(EOFError, util.recv, s, 1)

@ -1 +0,0 @@
from .client import Client

@ -1,18 +0,0 @@
''' Thin wrapper around trezorlib. '''
def client():
# pylint: disable=import-error
from trezorlib.client import TrezorClient
from trezorlib.transport_hid import HidTransport
devices = HidTransport.enumerate()
if len(devices) != 1:
msg = '{:d} Trezor devices found'.format(len(devices))
raise IOError(msg)
return TrezorClient(HidTransport(devices[0]))
def identity_type(**kwargs):
# pylint: disable=import-error
from trezorlib.types_pb2 import IdentityType
return IdentityType(**kwargs)

@ -1,152 +0,0 @@
import io
import re
import struct
import binascii
from .. import util
from .. import formats
from . import _factory as TrezorFactory
import logging
log = logging.getLogger(__name__)
class Client(object):
MIN_VERSION = [1, 3, 4]
def __init__(self, factory=TrezorFactory):
self.factory = factory
self.client = self.factory.client()
f = self.client.features
log.debug('connected to Trezor %s', f.device_id)
log.debug('label : %s', f.label)
log.debug('vendor : %s', f.vendor)
version = [f.major_version, f.minor_version, f.patch_version]
version_str = '.'.join([str(v) for v in version])
log.debug('version : %s', version_str)
log.debug('revision : %s', binascii.hexlify(f.revision))
if version < self.MIN_VERSION:
fmt = 'Please upgrade your TREZOR to v{}+ firmware'
version_str = '.'.join([str(v) for v in self.MIN_VERSION])
raise ValueError(fmt.format(version_str))
def __enter__(self):
msg = 'Hello World!'
assert self.client.ping(msg) == msg
return self
def __exit__(self, *args):
log.info('disconnected from Trezor')
self.client.clear_session() # forget PIN and shutdown screen
self.client.close()
def get_identity(self, label):
identity = string_to_identity(label, self.factory.identity_type)
identity.proto = 'ssh'
return identity
def get_public_key(self, identity):
assert identity.proto == 'ssh'
label = identity_to_string(identity)
log.info('getting "%s" public key from Trezor...', label)
addr = _get_address(identity)
node = self.client.get_public_node(n=addr,
ecdsa_curve_name='nist256p1')
pubkey = node.node.public_key
return formats.export_public_key(pubkey=pubkey, label=label)
def sign_ssh_challenge(self, identity, blob):
assert identity.proto == 'ssh'
label = identity_to_string(identity)
msg = _parse_ssh_blob(blob)
log.info('please confirm user "%s" login to "%s" using Trezor...',
msg['user'], label)
visual = identity.path # not signed when proto='ssh'
result = self.client.sign_identity(identity=identity,
challenge_hidden=blob,
challenge_visual=visual,
ecdsa_curve_name='nist256p1')
verifying_key = formats.decompress_pubkey(result.public_key)
public_key_blob = formats.serialize_verifying_key(verifying_key)
assert public_key_blob == msg['public_key']['blob']
assert len(result.signature) == 65
assert result.signature[:1] == bytearray([0])
return parse_signature(result.signature)
def parse_signature(blob):
sig = blob[1:]
r = util.bytes2num(sig[:32])
s = util.bytes2num(sig[32:])
return (r, s)
_identity_regexp = re.compile(''.join([
'^'
r'(?:(?P<proto>.*)://)?',
r'(?:(?P<user>.*)@)?',
r'(?P<host>.*?)',
r'(?::(?P<port>\w*))?',
r'(?P<path>/.*)?',
'$'
]))
def string_to_identity(s, identity_type):
m = _identity_regexp.match(s)
result = m.groupdict()
log.debug('parsed identity: %s', result)
kwargs = {k: v for k, v in result.items() if v}
return identity_type(**kwargs)
def identity_to_string(identity):
result = []
if identity.proto:
result.append(identity.proto + '://')
if identity.user:
result.append(identity.user + '@')
result.append(identity.host)
if identity.port:
result.append(':' + identity.port)
if identity.path:
result.append(identity.path)
return ''.join(result)
def _get_address(identity):
index = struct.pack('<L', identity.index)
addr = index + identity_to_string(identity).encode('ascii')
log.debug('address string: %r', addr)
digest = formats.hashfunc(addr).digest()
s = io.BytesIO(bytearray(digest))
hardened = 0x80000000
address_n = [13] + list(util.recv(s, '<LLLL'))
return [(hardened | value) for value in address_n]
def _parse_ssh_blob(data):
res = {}
if data:
i = io.BytesIO(data)
res['nonce'] = util.read_frame(i)
i.read(1) # TBD
res['user'] = util.read_frame(i)
res['conn'] = util.read_frame(i)
res['auth'] = util.read_frame(i)
i.read(1) # TBD
res['key_type'] = util.read_frame(i)
public_key = util.read_frame(i)
res['public_key'] = formats.parse_pubkey(public_key)
assert not i.read()
log.debug('%s: user %r via %r (%r)',
res['conn'], res['user'], res['auth'], res['key_type'])
log.debug('nonce: %s', binascii.hexlify(res['nonce']))
log.debug('fingerprint: %s', res['public_key']['fingerprint'])
return res

@ -1,66 +0,0 @@
import struct
import io
def send(conn, data, fmt=None):
if fmt:
data = struct.pack(fmt, *data)
conn.sendall(data)
def recv(conn, size):
try:
fmt = size
size = struct.calcsize(fmt)
except TypeError:
fmt = None
try:
_read = conn.recv
except AttributeError:
_read = conn.read
res = io.BytesIO()
while size > 0:
buf = _read(size)
if not buf:
raise EOFError
size = size - len(buf)
res.write(buf)
res = res.getvalue()
if fmt:
return struct.unpack(fmt, res)
else:
return res
def read_frame(conn):
size, = recv(conn, '>L')
return recv(conn, size)
def bytes2num(s):
res = 0
for i, c in enumerate(reversed(bytearray(s))):
res += c << (i * 8)
return res
def num2bytes(value, size):
res = []
for _ in range(size):
res.append(value & 0xFF)
value = value >> 8
assert value == 0
return bytearray(list(reversed(res)))
def pack(fmt, *args):
return struct.pack('>' + fmt, *args)
def frame(*msgs):
res = io.BytesIO()
for msg in msgs:
res.write(msg)
msg = res.getvalue()
return pack('L', len(msg)) + msg
Loading…
Cancel
Save