Compare commits

...

882 Commits

Author SHA1 Message Date
Roman Zeyde
18c80b4cca Bump version: 0.13.1 → 0.14.0 2020-04-17 14:31:41 +03:00
Roman Zeyde
7eab4933ed Add more Python version to Travis 2020-04-17 14:30:36 +03:00
Roman Zeyde
d103ebee6f Fix pylint warning 2020-04-17 14:28:50 +03:00
matejcik
d8bcca3ccb support trezorlib 0.12 2020-04-09 14:41:56 +02:00
Roman Zeyde
67ef11419a Merge pull request #320 from eli-b/patch-5
docs: Install libagent from source too
2020-04-06 23:30:46 +03:00
Eli Boyarski
d4d168c746 docs: Install libagent from source too
Installing the trezor/ledger agent from source installs the libagent module from PyPI unless libagent is already installed from source beforehand.
2020-04-06 20:44:25 +03:00
Roman Zeyde
61cfcef35c Merge branch 'NTICompass/keepkey-webusb' 2020-03-16 23:21:01 +02:00
Eric Siegel
0f627e8322 Clean up code... 2020-03-16 15:26:15 -04:00
Eric Siegel (Rocket Hazmat)
7bdfa7609d Upgrade KeepKey for new libagent code
Add get_public_node for KeepKey
2020-03-13 13:50:09 -04:00
Eric Siegel (Rocket Hazmat)
53b08f4968 Fix detecting KeepKey USB device
The new KeepKey firmware uses WebUSB instead of HID
2020-03-13 13:05:08 -04:00
Roman Zeyde
15b0218bf2 Default GPG key creation time to 0 (i.e. Jan 1 1970) 2019-10-29 09:14:26 +02:00
Roman Zeyde
f52e959639 Merge branch 'patch-2' of https://github.com/zack-shoylev/trezor-agent 2019-10-29 09:12:18 +02:00
Roman Zeyde
d98f49445e Merge branch 'patch-1' of https://github.com/korzq/trezor-agent 2019-10-26 13:52:02 +03:00
Roman Zeyde
ab6892f42f Fix pylint warnings 2019-10-26 13:47:29 +03:00
Eric Zhu
f03312d61f Update README-SSH.md 2019-10-02 17:41:51 -04:00
Roman Zeyde
b75cf74976 Merge pull request #301 from hkjn/20190925-describe-versioning
Add components section
2019-09-25 18:44:47 +03:00
Henrik Jonsson
363b4d633f Add components section 2019-09-25 12:19:33 +02:00
Zack Shoylev
b7d0ef0f94 Update README-SSH.md
Fix typo
2019-08-27 15:33:02 -05:00
Zack Shoylev
8c3744c30c Update README-SSH.md
Small systemd doc improvements.
2019-06-25 13:24:32 -05:00
Roman Zeyde
513b1259c4 Bump version: 0.13.0 → 0.13.1 2019-03-10 18:41:14 +02:00
Roman Zeyde
5984a58f65 Update .bumpversion.cfg 2019-03-10 18:41:07 +02:00
Roman Zeyde
e437591dd5 Fix prompt for symmetric encryption passphrase 2019-03-03 22:51:15 +02:00
André Vitor de Lima Matos
94ad9648f8 Fix passphrase cache
Broken since 2cb64991c3
Fix #284
2019-02-23 17:42:08 -03:00
Roman Zeyde
ed64f94bd3 Merge pull request #281 from eli-b/patch-4
Fix header numbering
2018-12-20 07:46:33 +02:00
Eli Boyarski
bf9f2593b5 Fix header numbering 2018-12-19 13:06:24 +02:00
Roman Zeyde
995fba3e93 Drop compatibility with <0.11 trezorlib 2018-12-13 00:05:47 +02:00
Roman Zeyde
34b269be1e Bump library and TREZOR-related agent versions 2018-12-12 23:59:15 +02:00
matejcik
5cfdc7734b fix style complaints 2018-12-10 16:30:56 +01:00
matejcik
2cb64991c3 Trezor: restructure code to support python-trezor 0.11 2018-12-10 16:10:55 +01:00
matejcik
a30cab1156 Trezor: bump version requirement to 0.10.1
because 0.9 doesn't work anyway due to the hidapi extra,
and there's no point of supporting 0.10.0 that doesn't have state
handling
2018-12-10 16:10:55 +01:00
matejcik
b30e6a8408 Allow devices to override connection closing 2018-12-10 16:10:55 +01:00
Roman Zeyde
8041ed883f Ignore cyclic imports pylint warning 2018-11-30 11:29:40 +02:00
walkjivefly
a71fa8de9e Add missing pre-reqs
Attempting to install 0.10.2 from PyPI failed because docutils and wheel were not installed.
2018-11-30 12:31:54 +07:00
Roman Zeyde
ddd823d976 Bump version: 0.12.0 → 0.12.1 2018-11-17 23:48:23 +02:00
Roman Zeyde
fec84288be gpg: --homedir should come before --list-secret-keys 2018-10-27 18:15:29 +03:00
Roman Zeyde
71f357c1bf Add 'hidapi' dependency 2018-08-18 12:55:46 +03:00
Eli Boyarski
8f1d008eb2 fixed typo + missing word 2018-08-06 23:19:32 +03:00
Roman Zeyde
7a351acf15 Merge remote-tracking branch 'matejcik/master' 2018-08-02 22:01:31 +03:00
Roman Zeyde
7f9aa2b147 Bump version: 0.11.3 → 0.12.0 2018-07-25 13:47:48 +03:00
Roman Zeyde
eed168341c Don't inheric from 'object' (after deprecating Python 2.x support) 2018-07-25 13:44:55 +03:00
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.
2018-07-17 16:50:53 +02:00
matejcik
8708b1e16d trezor: use TREZOR_PATH environment variable to specify device path 2018-07-17 16:45:09 +02:00
Roman Zeyde
03e7fc48e9 Improve Git-related documentation 2018-07-12 12:10:48 +03:00
Roman Zeyde
4968ca7ff3 Merge branch 'master' into neopg-wip 2018-07-01 13:52:37 +03:00
Roman Zeyde
6b6d9f5d20 Add a link to neopg-trezor wrapper at documentation 2018-07-01 13:17:13 +03:00
Roman Zeyde
c22109df24 Document argv[0] hack for NeoPG 2018-07-01 13:15:04 +03:00
Roman Zeyde
47ce035e79 Remove unused import 2018-07-01 12:52:08 +03:00
Roman Zeyde
36cbba6c57 Fix a few lint issues 2018-07-01 12:49:39 +03:00
Roman Zeyde
6afe20350b Simplify GPG command generation 2018-07-01 12:12:16 +03:00
Roman Zeyde
fa171e8923 Add short example for NeoPG usage 2018-07-01 12:08:46 +03:00
Roman Zeyde
f0bda9a3e6 Allow using $PATH when looking for GPG binary
It's needed for running neopg (instead of gnupg).
2018-07-01 12:05:25 +03:00
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].
2018-07-01 12:04:32 +03:00
Roman Zeyde
3b9c00e02a Default to $GNUPGHOME when not specified on commandline 2018-07-01 11:46:16 +03:00
Roman Zeyde
dcee59a19e Assume NeoPG binary runs GnuPG functionality 2018-07-01 11:32:02 +03:00
Roman Zeyde
a274de30b8 Parse NeoPG development versions
e.g. v0.0.5-37-g1fe5046-dirty
2018-06-30 13:05:21 +03:00
Roman Zeyde
4fe9e437ad Simplify GPG homedir setting 2018-06-30 13:03:30 +03:00
Roman Zeyde
d04527a8ed Replace GPG version assertion by an error log
since NeoPG uses different versioning
2018-06-30 13:02:50 +03:00
Roman Zeyde
3329c29cb4 Use gpg_command() for identity generation 2018-06-30 12:50:55 +03:00
Roman Zeyde
df2cb52f8d fixup! Reply with an ERR to SCD SERIALNO openpgp ASSUAN command 2018-06-30 12:49:59 +03:00
Roman Zeyde
f36ef4ffe0 Allow running NeoPG binary (instead of GnuPG) 2018-06-30 12:44:17 +03:00
Roman Zeyde
f74de828fc Reply with an ERR to SCD SERIALNO openpgp ASSUAN command
(for NeoPG)
2018-06-30 12:10:37 +03:00
Roman Zeyde
912b1cde7a Add support for file-descriptor-based socket server
(for NeoPG)
2018-06-30 12:10:03 +03:00
Roman Zeyde
b7a8c42893 Merge pull request #153 from romanz/drop-py2
setup: deprecate Python2 support
2018-06-30 11:24:52 +03:00
Roman Zeyde
1e6c4e6930 Add links to SSH/GPG usage examples 2018-06-30 11:21:47 +03:00
Roman Zeyde
a8f19e4150 Comment about SSH argument separation 2018-06-30 11:12:43 +03:00
Roman Zeyde
6a9fdf75e2 Bump version: 0.11.2 → 0.11.3 2018-06-19 21:15:14 +03:00
Roman Zeyde
6bc5b6af5e Add small example for IdentityOnly use-case 2018-06-19 19:04:05 +03:00
Roman Zeyde
8672a6901a Document IdentitiesOnly support 2018-06-19 18:49:36 +03:00
Roman Zeyde
672af98ad7 Explicitly use IdentityFile option when connecting to specific host 2018-06-19 18:38:57 +03:00
Roman Zeyde
ed531cfff8 Remove trailing whitespace
git ls-files | xargs -n1 sed -e's/[[:space:]]*$//' -i
2018-05-25 08:43:22 +03:00
Bram
bd1ae0f091 Update INSTALL.md
I've sorted out the Formula for Homebrew and it's been merged.
2018-05-24 14:01:40 +03:00
Roman Zeyde
0c762e8998 Use pinentry homebrew formula on macOS 2018-05-23 08:35:34 +03:00
Roman Zeyde
bd0df4f801 trezor: update setup.py for latest libagent and trezorlib 2018-05-05 21:05:02 +03:00
Roman Zeyde
3d1639d271 gpg: require symmetric passphrase re-entry 2018-04-25 11:18:13 +03:00
Roman Zeyde
bea899d1ef gpg: allow symmetric encryption with a passphrase 2018-04-25 11:09:58 +03:00
Roman Zeyde
ccc2174775 gpg: allow more verbose output during GnuPG pubkey import 2018-04-25 00:16:27 +03:00
Roman Zeyde
afa3fdb89c gpg: allow setting passphrase cache expriration duration 2018-04-25 00:12:34 +03:00
Roman Zeyde
2ca3941cfa ssh: allow setting passphrase cache expriration duration 2018-04-25 00:02:21 +03:00
Roman Zeyde
b1bd6cb690 gpg: refactor GETINFO handling into a separate method 2018-04-23 22:59:11 +03:00
Roman Zeyde
766536d2c4 trezor: allow expiring cached passphrase 2018-04-23 22:55:10 +03:00
Roman Zeyde
91f70e7a96 Merge pull request #238 from pruflyos/patch-1
Update INSTALL.md
2018-04-22 09:37:09 +03:00
Roman Zeyde
cf5bfd960a Merge pull request #237 from menteb/patch-2
Update to Install.md reflecting Homebrew formula
2018-04-22 09:36:45 +03:00
pruflyos
4bd769f138 Update INSTALL.md
On Fedora `python3-tk` is called `python3-tkinter`
2018-04-21 16:15:44 -04:00
Bram
91b850f184 Update to Install.md reflecting Homebrew formula 2018-04-21 13:20:22 +03:00
Roman Zeyde
c6bb090dfc Merge pull request #235 from timthelion/git-email-readme
Document the configuration of the git email setting and errors
2018-04-18 22:32:17 +03:00
Timothy Hobbs
fef4fd06c9 Document the configuration of the git email setting and errors
Signed-off-by: Timothy Hobbs <timothyhobbs@seznam.cz>
2018-04-18 12:41:38 +02:00
Roman Zeyde
bc691ae795 gpg: fix method's caching 2018-04-16 12:38:28 +03:00
Roman Zeyde
61e516e200 Add link to Ledger Nano S guide 2018-04-09 22:10:38 +03:00
Roman Zeyde
543ff7021d doc: explain how to reset cached passphrase 2018-04-08 16:33:34 +03:00
Roman Zeyde
2e0cfc8088 gpg: fail if new identity is missing 2018-04-08 16:20:55 +03:00
Roman Zeyde
18f33f8a08 README: document PIN entry depedencies 2018-04-08 10:16:58 +03:00
Roman Zeyde
2973413995 Merge pull request #227 from kvbik/patch-1
mention brew install libusb on macOS
2018-03-29 20:52:11 +03:00
Jakub Vysoký
2360693dc5 mention brew install libusb on macOS 2018-03-29 18:32:26 +02:00
Roman Zeyde
7443fc6512 Pass 'state' during TREZOR initialization 2018-03-27 16:06:18 +03:00
Roman Zeyde
5efb752979 doc: update Fedora installation instructions 2018-03-22 14:31:36 +02:00
Roman Zeyde
4546cd674b Bump version: 0.11.1 → 0.11.2 2018-03-19 09:45:56 +02:00
Roman Zeyde
5dba12f144 gpg: don't clear options on RESET assuan command 2018-03-14 13:55:59 +02:00
Roman Zeyde
887561de9f pylint: skip 'fixme' warnings 2018-03-14 12:17:07 +02:00
Roman Zeyde
6d730e0a5b ui: subprocess.Popen doesn't have 'args' attribute in Python 2 2018-03-14 12:15:08 +02:00
Roman Zeyde
d0732d16e8 ui: don't log passphrases (since the log may be persisted) 2018-03-14 12:13:44 +02:00
Roman Zeyde
dafb80ad7a trezor: don't retry on PIN/passphrase entry cancellation 2018-03-13 16:52:04 +02:00
Roman Zeyde
df6249b071 Merge remote-tracking branch 'rendaw/pinentry-docs' 2018-03-13 15:35:00 +02:00
rendaw
942f01418b Also set DISPLAY in SSH unit 2018-03-13 16:31:52 +09:00
rendaw
93b548b737 Add docs to show using the gpg agent with systemd; set PATH for ssh unit 2018-03-13 16:28:36 +09:00
rendaw
329f07249a Small reword 2018-03-13 05:57:39 +09:00
rendaw
a1f7088d33 Remove pin entry instructions from INSTALL, didn't seem that relevant 2018-03-13 05:47:31 +09:00
rendaw
25f066e113 Document --pin-entry-binary with usage guide 2018-03-13 05:43:18 +09:00
Roman Zeyde
0699273d49 util: move ASSUAN serialization to break circular import 2018-03-11 15:11:02 +02:00
Roman Zeyde
92c352e860 Bump version: 0.11.0 → 0.11.1 2018-03-11 14:40:04 +02:00
Roman Zeyde
34c03a462c ui: merge into a single module 2018-03-11 14:33:54 +02:00
Roman Zeyde
51dbecd4c2 Bump version: 0.10.0 → 0.11.0 2018-03-11 13:35:23 +02:00
Roman Zeyde
ceae65aa5a ui: use {} as default config 2018-03-11 13:34:50 +02:00
Roman Zeyde
d0497b0137 pinentry: specify device name at PIN/passphrase entry UI 2018-03-10 22:11:26 +02:00
Roman Zeyde
870152a7af gpg: allow specifying custom homedir during init 2018-03-10 18:39:56 +02:00
Roman Zeyde
cbdc52c0a4 trezor: handle passphrase on-device entry (for Model T) 2018-03-10 09:35:23 +02:00
Roman Zeyde
0c9fc33757 gpg: replace gpg-agent.conf by run-agent.sh 2018-03-08 17:47:16 +02:00
Roman Zeyde
17ea941add gpg: use pinentry UI for initialization and agent 2018-03-08 17:47:14 +02:00
Roman Zeyde
64064b5ecc ssh: use pinentry UI 2018-03-08 17:13:19 +02:00
Roman Zeyde
601a2b1336 device: refactor PIN/passphrase UI into a separate class
This would allow easier customization.
2018-03-08 17:13:16 +02:00
Roman Zeyde
2e688ccac9 setup: deprecate Python2 support 2018-03-08 09:18:37 +02:00
Roman Zeyde
b6181bb5b5 trezor: replace tk-based pinentry with GnuPG pinentry 2018-03-07 13:42:59 +02:00
Roman Zeyde
b6da299cb0 pinentry: add simple wrapper for PIN/passphrase entry 2018-03-07 13:42:56 +02:00
Roman Zeyde
04627f0899 gpg: collect OPTIONs from agent 2018-03-06 11:02:39 +02:00
Roman Zeyde
54ce6f2cec trezor: limit passphrase length 2018-03-06 10:06:00 +02:00
Roman Zeyde
a1047ba7b1 Bump version: 0.9.8 → 0.10.0 2018-03-03 21:02:10 +02:00
Roman Zeyde
e90bd0cd81 trezor: refactor transport enumeration a bit 2018-03-03 20:22:22 +02:00
slush
66e3e60370 trezor: Use composite transport for device detection. 2018-03-03 01:25:19 +01:00
slush
3f1604d609 Use Python3 by default 2018-03-03 01:24:08 +01:00
slush
d0f4cccfd2 trezor: Both Trezor One and Model T are supported. 2018-03-03 01:23:35 +01:00
Roman Zeyde
08d81c992c trezor: split pinentry tool into a separate file 2018-02-27 11:17:53 +02:00
Roman Zeyde
55a899f929 trezor: initialize cached_passphrase_ack with None (instead of 0) 2018-02-27 10:33:19 +02:00
Roman Zeyde
e7604dff68 ssh: fix small commandline documentation nits 2018-02-27 09:53:37 +02:00
rendaw
8849545700 Clarify a couple sentences 2018-02-27 03:00:16 +09:00
rendaw
d109cd73b5 Adjust ssh systemd instructions; cleanup 2018-02-27 02:48:23 +09:00
rendaw
95e98d6eda Merge remote-tracking branch 'upstream/master' into doc-enhancements2 2018-02-27 02:30:00 +09:00
rendaw
9e78d52721 SSH clarification 2018-02-27 02:29:54 +09:00
Roman Zeyde
2a76ef6819 gpg: notice encryption of gpg-agent logs (for privacy) 2018-02-24 23:49:35 +02:00
Roman Zeyde
654a3c465a Merge remote-tracking branch 'rendaw/systemd-instructions' 2018-02-24 21:35:30 +02:00
Roman Zeyde
2168115b06 ssh: fixup small refactoring bug 2018-02-24 21:23:47 +02:00
Roman Zeyde
4a9140c42d Merge branch 'serge' 2018-02-24 21:07:42 +02:00
Roman Zeyde
b20d98bf57 gpg: move socket path generation into a helper function 2018-02-24 21:03:30 +02:00
Roman Zeyde
199fb299c3 gpg: use 'sys.exit' and log homedir 2018-02-24 20:46:04 +02:00
rendaw
06e169f141 Add instructions for using SSH agent as systemd unit 2018-02-24 03:08:46 +09:00
rendaw
131111bc0e Add additional information to docs; collect and reorganize sections 2018-02-23 20:55:46 +09:00
Roman Zeyde
f4208009e0 trezor: init_device before failing PIN/passphrase entry 2018-02-22 11:19:26 +02:00
Roman Zeyde
73d60dbec0 gpg: cache the passphrase for better UX 2018-02-21 11:35:38 +02:00
Roman Zeyde
34ea224290 gpg: the scripts should be only user-readable 2018-02-20 21:21:45 +02:00
Roman Zeyde
7803026f61 gpg: allow setting passphrase from environment variable
as done by TREZOR's client library
2018-02-20 09:59:15 +02:00
Roman Zeyde
34ce1005fd build: add simple script for PyPI release 2018-02-19 15:12:01 +02:00
Roman Zeyde
8677c8ebaa trezor_agent: fix broken PyPI package 2018-02-19 13:15:49 +02:00
Serge Pokhodyaev
6363eb0d4a add -f/--foreground option to run as systemd service 2018-02-18 21:08:36 +03:00
Serge Pokhodyaev
a32bfc749b don't overwrite homedir 2018-02-18 20:24:22 +03:00
Roman Zeyde
75d117ad0d Bump version: 0.9.7 → 0.9.8 2018-02-15 19:10:40 +02:00
Roman Zeyde
cefc5f180a ssh: add --sock-path flag to explicitly specify SSH agent's UNIX socket 2018-02-15 19:08:13 +02:00
Roman Zeyde
0f5c71b748 ssh: add --log-file flag 2018-02-15 18:50:50 +02:00
Roman Zeyde
d5f97b7efa Update README title 2018-02-15 15:15:16 +02:00
Roman Zeyde
4a12bfa0b7 Allow SSH agent to daemonize when invoked with -d flag
This change adds the support for "eval `trezor-agent -d`" invocation.
2018-02-15 15:10:34 +02:00
Roman Zeyde
cac889ff7d Update trezorlib dependency for trezor_agent 2018-02-15 14:27:29 +02:00
Roman Zeyde
92c6e680ed doc: add python-tk dependency
Following #194
2018-02-04 11:49:21 +02:00
Roman Zeyde
bf294beb56 gpg: decode stdout as UTF-8 2018-01-30 20:18:19 +02:00
Roman Zeyde
713345918e ssh: document sub-shell mode 2018-01-26 11:20:14 +02:00
Roman Zeyde
eb60c2f475 fix more pylint issues 2018-01-24 18:37:30 +02:00
Roman Zeyde
6d8d43db9b fix pylint issues 2018-01-22 21:24:16 +02:00
Roman Zeyde
3e67bc9f0e gpg: log GnuPG commands' output 2018-01-22 20:16:32 +02:00
Roman Zeyde
38b50485de ssh: remove old demo from README 2018-01-20 18:29:39 +02:00
Roman Zeyde
9cba27b31a Merge pull request #188 from eli-b/patch-2
README-SSH.md: spelling
2018-01-14 11:50:01 -08:00
Eli Boyarski
00a65a9820 README-SSH.md: spelling 2018-01-13 20:27:52 +02:00
Roman Zeyde
52ad601e66 Merge pull request #187 from eli-b/patch-1
INSTALL.md: update the Ledger Nano S udev link
2018-01-13 10:15:22 -08:00
Eli Boyarski
d96a2820ff INSTALL.md: update the Ledger Nano S udev link 2018-01-13 18:12:59 +02:00
Roman Zeyde
29aaf777ad Bump version: 0.9.6 → 0.9.7 2018-01-10 21:35:40 +02:00
Roman Zeyde
385fc9457b Support multiple devices 2018-01-05 20:53:42 +02:00
Jonathan Roelofs
9cf73f677a Show libagent version too 2018-01-01 23:05:37 +00:00
Jonathan Roelofs
ec97cd0c44 Implement #182 2017-12-29 09:23:06 -07:00
Jonathan Roelofs
4cd7dc02eb Fix an argparse nuance
https://bugs.python.org/issue16308
2017-12-29 08:54:31 -07:00
Roman Zeyde
8fe9460ed6 trezor: allow UDP connection (for emulator) 2017-12-23 16:59:12 +02:00
Roman Zeyde
db16aa3d1c trezor: update to latest trezorlib version 2017-12-23 16:51:37 +02:00
Roman Zeyde
41ccd2f332 fix new pylint warning 2017-12-22 17:10:27 +02:00
Roman Zeyde
cb14d1e00b Bump version: 0.9.5 → 0.9.6 2017-12-14 20:45:42 +02:00
Roman Zeyde
cc6ee31deb add .bumpversion.cfg 2017-12-14 20:36:00 +02:00
Roman Zeyde
b1f302151b tests: fix test_server.py 2017-12-13 21:12:29 +02:00
Roman Zeyde
fde50f04ab Merge branch 'config' 2017-12-08 21:50:22 +02:00
Roman Zeyde
7e42e455a1 gpg: add an example for adding new user ID 2017-12-06 20:31:10 +02:00
Roman Zeyde
13cd6be2d1 travis: pep8 -> pycodestyle 2017-12-03 21:53:42 +02:00
Roman Zeyde
40469c4100 tox: pep8 -> pycodestyle 2017-12-02 21:14:03 +02:00
Roman Zeyde
0d059587a7 ssh: allow configuration from a file 2017-12-02 21:04:57 +02:00
Roman Zeyde
283cb3d7e8 setup: add ConfigArgParse 2017-12-02 20:43:04 +02:00
Roman Zeyde
51cc716e3f Merge pull request #169 from dirkx/master
Some background and designn rationale for the use of derived keys
2017-11-25 09:20:31 +02:00
Roman Zeyde
8b4850b0ce Explain rationale better, several typos fixed, include warning about keepkey not yet supporting encryption/decryption. 2017-11-24 15:27:06 +01:00
Roman Zeyde
f22c07e970 trezor: retry in case of invalid PIN 2017-11-18 20:37:06 +02:00
Roman Zeyde
29c7234ef4 trezor: make sure scrambled PIN is valid 2017-11-18 17:06:23 +02:00
Roman Zeyde
1942e3999b ssh: fix exception type for missing device 2017-11-18 16:50:21 +02:00
Roman Zeyde
f2e52a88be ` -> ' 2017-11-16 23:04:12 +02:00
Roman Zeyde
b26a4cc7b0 A few small fixes 2017-11-16 23:01:30 +02:00
Roman Zeyde
c4dfca04f2 trezor: use UI-based passphrase entry
Now TREZOR_PASSPHRASE environment variable is ignored.
2017-11-16 22:36:19 +02:00
Roman Zeyde
a1ecbf447e gpg: return correct keygrip for KEYINFO assuan command 2017-11-16 22:36:17 +02:00
Roman Zeyde
1f9d457e92 gpg: no need to remove the UNIX socket
Our agent should be invoked and re-used when running 'gpg --import'.
2017-11-16 21:53:38 +02:00
Roman Zeyde
cb3477fc69 Merge branch 'which-fix' 2017-11-16 21:04:12 +02:00
Roman Zeyde
9bbc66cc16 util: add backport for shutil.which() 2017-11-16 20:59:58 +02:00
Roman Zeyde
06afc971db Merge pull request #166 from aitorpazos/master
doc: Include python3 support in OpenSUSE instructions
2017-11-15 13:11:29 -08:00
Dirk-Willem van Gulik
2b51a85c26 Rework order of paragraphs for clarity II 2017-11-15 19:57:09 +01:00
Dirk-Willem van Gulik
1906e6d9b0 Rework order of paragraphs for clarity. 2017-11-15 19:56:10 +01:00
Dirk-Willem van Gulik
b3f6e39b48 First cut at a design rationale 2017-11-15 19:53:23 +01:00
Aitor Pazos
8b03b649d5 doc: Include python3 support in OpenSUSE instructions 2017-11-12 20:16:34 +00:00
Roman Zeyde
90cbc41b17 gpg: refactor messagebox UI from PIN entry 2017-11-11 11:16:31 +02:00
Roman Zeyde
4926d4f4d3 gpg: set PATH explicitly for $DEVICE-gpg-agent 2017-11-09 22:13:28 +02:00
Roman Zeyde
d52f295326 gpg: use shutil.which() for Python 3 2017-11-04 17:48:00 +02:00
Max Pixel
47a8a53247 Add OpenSUSE-specific installation instructions
The packages required are slightly different on OpenSUSE.
2017-11-04 17:45:57 +02:00
Roman Zeyde
9530c4d7db gpg: use gpgconf for getting gpg binary path 2017-11-03 10:52:27 +02:00
Roman Zeyde
a2d0c1067d gpg: don't hardcode Python binary 2017-10-27 11:27:40 +03:00
Roman Zeyde
3d5717dca1 gpg: use a separate process for PIN entrering UI 2017-10-27 10:43:50 +03:00
Roman Zeyde
08fef24e39 gpg: use pymsgbox for PIN entrering UI 2017-10-21 21:18:00 +03:00
Roman Zeyde
bab46dae5c gpg-agent: use correct GNUPGHOME when getting public keys 2017-10-21 20:49:27 +03:00
Roman Zeyde
e2625cc521 gpg: fail if no public keys are found 2017-10-21 19:16:48 +03:00
Roman Zeyde
7ed76fe472 gpg: use correct GNUPGHOME for gpgconf 2017-10-21 18:46:04 +03:00
Roman Zeyde
a5929eed62 gpg: create config files first 2017-10-21 18:21:38 +03:00
Roman Zeyde
5f722f8ae1 logging: add more DEBUG information 2017-10-21 18:08:40 +03:00
Roman Zeyde
7212b2fa37 Merge pull request #156 from codeHatcher/feature/155
add documentation related to #155 to help other macOS/OSX users who are
2017-10-18 01:57:51 -07:00
Avishaan
55e1c614a7 add documentation related to #155 to help other macOS/OSX users who are
still using system python
2017-10-17 15:37:11 -04:00
Roman Zeyde
8cf1f0463a device: release HID handle before failing 2017-10-14 17:34:02 +03:00
Roman Zeyde
f177b0b55a bump version 2017-10-11 21:44:53 +03:00
Roman Zeyde
b2450d448c Merge branch 'gpg-init' 2017-10-11 21:43:30 +03:00
Roman Zeyde
93e5f0cd8b gpg: update README for latest CLI 2017-10-11 21:40:05 +03:00
Roman Zeyde
9998456fe0 ledger: add DEBUG logging 2017-10-11 21:15:44 +03:00
Roman Zeyde
0f85ae6e2c Rewrite gpg-init Bash script in Python 2017-10-11 20:26:06 +03:00
Roman Zeyde
44cdeed024 Merge branch 'fix-gpg-prefs' 2017-10-10 20:46:44 +03:00
Roman Zeyde
867e2cfd1b gpg: add MDC support 2017-10-10 20:43:45 +03:00
Roman Zeyde
df6ddab2cf gpg: add compression and stronger digests 2017-10-10 20:43:45 +03:00
Roman Zeyde
5b9f03d198 gpg: show warnings while importing new pubkey 2017-10-10 20:29:31 +03:00
Roman Zeyde
06ea890095 gpg: add note regarding Pinentry 2017-10-10 15:10:56 +03:00
Roman Zeyde
0999a85529 gpg: add documentation for subkey generation 2017-10-10 14:20:17 +03:00
Roman Zeyde
835f283ccf gpg: support multiple keygrips for HAVEKEY command 2017-10-10 13:40:59 +03:00
Roman Zeyde
f57dbb553f gpg: allow setting trezor-gpg arguments via gpg-init script 2017-10-10 13:20:25 +03:00
Roman Zeyde
a890dcc085 gpg: add MDC support 2017-10-10 11:56:25 +03:00
Roman Zeyde
c8ed4a223a gpg: add compression and stronger digests 2017-10-10 11:56:23 +03:00
Roman Zeyde
1ef96bed03 gpg: handle NOP assuan command 2017-10-10 10:04:43 +03:00
Roman Zeyde
e4fdca08e5 ssh: fix identity stringification 2017-10-09 16:24:11 +03:00
Roman Zeyde
51b297e93b doc: add Enigmail tutorial 2017-10-09 10:02:09 +03:00
Roman Zeyde
c22c959cf9 doc: move READMEs to separate directory 2017-10-09 09:37:22 +03:00
Roman Zeyde
3199cb964a Merge branch 'timestamplookup' 2017-10-09 09:34:59 +03:00
Roman Zeyde
c5f245957d README: add spaces around '|' operators 2017-10-09 09:33:09 +03:00
Chris Cowan
fbb3059a0b README: Add a note about how to fetch a key's timestamp. 2017-10-08 17:58:21 -07:00
Roman Zeyde
e922f45871 bump version 2017-10-08 13:49:44 +03:00
Roman Zeyde
377af1466c gpg: detect installed GnuPG binary 2017-10-07 20:46:33 +03:00
Roman Zeyde
b7743e12a5 gpg: support GnuPG 2.2.x default installation 2017-10-07 20:29:01 +03:00
Roman Zeyde
48d5630561 gpg: fix identity stringification 2017-10-07 19:18:58 +03:00
Roman Zeyde
b88dff8430 ssh: fix identity stringification 2017-10-05 18:16:15 +03:00
Roman Zeyde
2af1086ed8 bump version 2017-10-02 20:46:30 +03:00
Roman Zeyde
7e95179128 ssh: fix unicode identity handling for Python 2 2017-09-22 14:11:21 +03:00
Roman Zeyde
ac8898a434 gpg: allow using 'gpg' instead of 'gpg2' 2017-09-09 21:31:25 +03:00
Roman Zeyde
0b829636e1 ssh: close stdin when running subshell 2017-08-31 17:00:27 +03:00
Roman Zeyde
7598f6cdbf Merge branch 'travis' 2017-08-18 11:50:33 +03:00
Roman Zeyde
c8bf57cbcc travis: fix setuptools issue 2017-08-18 11:46:08 +03:00
Roman Zeyde
62af49236c Merge pull request #132 from romanzolotarev/patch-1
Fix link in README-GPG.md
2017-08-15 21:41:41 +03:00
Roman Zolotarev
af3f669780 Update README-GPG.md 2017-08-15 19:20:31 +03:00
Roman Zeyde
1520dbd8b9 gpg: add a screencast for identity re-generation 2017-08-07 21:15:36 +03:00
Roman Zeyde
4f05d51e9b bump version 2017-07-16 15:29:49 +03:00
Roman Zeyde
9d38c26a0f gpg: add 'fake' device for GPG-integration testing 2017-07-16 15:29:47 +03:00
Roman Zeyde
3a9330b995 gpg: return SCD version from agent 2017-07-16 15:29:21 +03:00
Roman Zeyde
f904aac92e Allow unicode in identity string for SSH and GPG 2017-06-24 14:26:51 +03:00
Roman Zeyde
ca67923fe8 INSTALL: add instructions for Fedora/RedHat 2017-06-15 21:13:42 +03:00
Roman Zeyde
ce90e61eb2 README: add troubleshooting section for GPG 2017-06-14 21:49:20 +03:00
Roman Zeyde
90dc124e8d install: note '~/.local/bin' issue with pip 2017-06-12 22:39:11 +03:00
Roman Zeyde
442bf725ef gpg: fail SCD-related requests 2017-06-07 23:11:57 +03:00
Roman Zeyde
5820480052 GPG: set default user ID for signing during gpg-init 2017-06-05 22:04:43 +03:00
Roman Zeyde
ae2a84e168 INSTALL: comment about Ledger's 'SSH/PGP Agent' app requirement 2017-05-26 09:59:23 +03:00
Roman Zeyde
f6911a0016 pin: use PyQt only when running with no TTY 2017-05-21 21:03:04 +03:00
Roman Zeyde
69c54eb425 device: allow Qt-based PIN entry for Trezor/Keepkey 2017-05-21 17:29:57 +03:00
Roman Zeyde
931573f32b gpg: echo during identity initialization 2017-05-20 12:22:10 +03:00
Roman Zeyde
1e8363d4fc gpg: refactor client usage at agent module
This allows caching public keys (as done in the SSH agent).
2017-05-20 12:22:08 +03:00
Roman Zeyde
b7113083b4 README: add 'git config' for enabling commit signing 2017-05-19 21:29:57 +03:00
Roman Zeyde
b5c4eca0d2 README: elaborate trezor/ledger usage for GPG 2017-05-17 22:01:59 +03:00
Roman Zeyde
8aa08d0862 INSTALL: remove duplicate line 2017-05-17 21:53:35 +03:00
Roman Zeyde
b452b49f4c README: note qtpass for password management 2017-05-15 20:13:13 +03:00
Roman Zeyde
639c4efb6d README: add links to products 2017-05-14 21:08:55 +03:00
Roman Zeyde
6f1686c614 README: remove extra parentheses 2017-05-14 14:06:19 +03:00
Roman Zeyde
300d9a7140 README: update GPG screencasts and other examples 2017-05-14 10:58:56 +03:00
Roman Zeyde
b143bafc70 README: remove PyPI badges 2017-05-14 10:51:07 +03:00
Roman Zeyde
f2c6b6b9c1 README: move installation to a separate file 2017-05-14 10:44:50 +03:00
Roman Zeyde
e0507b1508 tests: cover file-based logging case 2017-05-14 10:04:01 +03:00
Roman Zeyde
85274d8374 bump version 2017-05-13 13:18:25 +03:00
Roman Zeyde
f358ca29d4 Allow loading previously exported SSH public keys from a file 2017-05-13 12:47:48 +03:00
Roman Zeyde
53d43cba29 gpg: update README after re-packaging 2017-05-06 21:13:07 +03:00
Roman Zeyde
214b556f83 ssh: simplify argparse creation 2017-05-06 20:55:38 +03:00
Roman Zeyde
051a3fd4ab ssh: remove unused git-related code 2017-05-06 20:40:23 +03:00
Roman Zeyde
91050ee64a ssh: remove unused git wrapper 2017-05-06 15:02:46 +03:00
Roman Zeyde
257992d04c ssh: move related code to a separate subdirectory 2017-05-05 11:22:00 +03:00
Roman Zeyde
6c2273387d util: extend logging.basicConfig() 2017-05-03 21:30:33 +03:00
Roman Zeyde
b6ad8207ba setup: add Python 3.6 support 2017-05-01 21:37:32 +03:00
Roman Zeyde
3a93fc859d setup: fix indentation for lists 2017-05-01 21:35:05 +03:00
Roman Zeyde
7d9b3ff1d0 README: update device-related info 2017-04-29 20:51:18 +03:00
Roman Zeyde
4af881b3cb Split the package into a shared library and separate per-device packages 2017-04-29 18:34:46 +03:00
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.
2017-04-26 23:12:09 +03:00
Roman Zeyde
02c8e729b7 ssh: retrieve all keys using a single device session 2017-04-25 20:43:19 +03:00
Roman Zeyde
12359938ad keepkey: fix transport import 2017-04-23 21:27:02 +03:00
Roman Zeyde
93cd3e688b travis: add Python 3.6 2017-04-22 20:19:06 +03:00
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.
2017-04-22 14:47:30 +03:00
Tomás Rojas
0d5c3a9ca7 Allow using TREZOR bridge (instead of HID transport) 2017-04-22 11:16:30 +03:00
Roman Zeyde
97ec6b2719 travis: fix dependency 2017-04-22 10:53:39 +03:00
Roman Zeyde
8ba9be1780 fix pylint warnings 2017-04-21 22:56:50 +03:00
Roman Zeyde
b2bc87c0c7 fix pydocstyle warnings 2017-04-21 22:51:11 +03:00
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.
2017-04-10 10:40:27 +03:00
Roman Zeyde
c796a3b01d README: password manager usage example 2017-03-28 21:21:35 +03:00
Roman Zeyde
a3362bbf3e bump version 2017-03-18 16:49:09 +02:00
Nubis
9a271d115b Allow contents in buffer when using _legacy_pubs 2017-03-09 20:13:23 +02:00
Roman Zeyde
6a7165298f decode: skip invalid pubkeys (instead of crashing) 2017-03-07 21:43:32 +02:00
Roman Zeyde
c4f3fa6e04 agent: reply correctly to HAVEKEY requests 2017-02-21 13:12:02 +02:00
Roman Zeyde
8a77fa519f decode: raise an error when keygrip is missing 2017-02-21 13:12:00 +02:00
Roman Zeyde
59560ec0b0 util: add simple memoization decorator 2017-02-21 13:05:48 +02:00
Roman Zeyde
7a91196dd5 agent: add link to HAVEKEY implementation 2017-02-21 11:48:22 +02:00
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.
2017-01-07 18:29:12 +02:00
Roman Zeyde
6672ea9bc4 device: set passphrase from environment 2017-01-06 12:52:45 +02:00
Roman Zeyde
002dc2a0e0 tox: order imports 2017-01-06 12:37:14 +02:00
Roman Zeyde
61ced2808f device: allow non-empty passphrases 2017-01-06 11:59:57 +02:00
Roman Zeyde
71a8930021 bump version 2017-01-05 23:12:54 +02:00
Roman Zeyde
74e8f21a22 gpg: export secret subkey 2017-01-01 18:14:52 +02:00
Roman Zeyde
897236d556 gpg: allow decoding secret keys 2017-01-01 18:14:28 +02:00
Roman Zeyde
5bec0e8382 README: upgrade also pip 2017-01-01 10:08:19 +02:00
Roman Zeyde
3cb7f6fd21 gpg: export secret primary key 2016-12-30 18:55:18 +02:00
Roman Zeyde
cad2ec1239 device: import device-specific defs module lazily
It may fail on unsupported platforms (e.g. keepkeylib does not supoprt Python 3)
2016-12-27 12:34:07 +02:00
Roman Zeyde
604b2b7e99 gpg: allow GPG 2.1.11+ (to support Ubuntu 16.04 & Mint 18) 2016-12-27 10:12:34 +02:00
Roman Zeyde
159bd79b5f gpg: list fingerpints explicitly during init 2016-12-27 10:04:48 +02:00
Roman Zeyde
dde0b60e83 Merge pull request #87 from aceat64/bugfix/mosh
Mosh doesn't support "-l" for user, only user@host for args
2016-12-15 19:25:19 +02:00
Andrew LeCody
109bb3b47f Mosh doesn't support "-l" for user, only user@host for args 2016-12-14 23:59:45 -06:00
Roman Zeyde
0f20bfa239 README: add a note about udev 2016-12-06 18:36:14 +02:00
Roman Zeyde
798597c436 bump version 2016-12-06 10:19:23 +02:00
Roman Zeyde
a13b1103f7 setup: temporary pin dependency for keepkey compatibilty 2016-12-06 10:07:18 +02:00
Roman Zeyde
9fe1a235c1 gpg: check that the configuration is in place 2016-12-02 13:10:33 +02:00
Roman Zeyde
f86aae9a40 gpg: check that GnuPG v2 is installed 2016-12-02 12:48:56 +02:00
Roman Zeyde
fc070e3ca0 GPG: add OS X installation link to README 2016-11-30 23:08:56 +02:00
Roman Zeyde
05fac995eb README: add 'setup.py' installation 2016-11-26 14:01:06 +02:00
Roman Zeyde
188b74b327 gpg: use explicit '--subkey' flag for adding a subkey to an existing GPG key 2016-11-25 19:35:40 +02:00
Roman Zeyde
fc31847f8e decode: add test for custom markers 2016-11-19 20:06:29 +02:00
Roman Zeyde
0faf21a102 README: easier tag signature verification 2016-11-19 13:37:33 +02:00
Roman Zeyde
6b82f8b9b7 keyring: add test for get_agent_sock_path() 2016-11-12 20:51:35 +02:00
Roman Zeyde
fabfcaaae2 keyring: fix test case for iterlines() 2016-11-12 20:51:14 +02:00
Roman Zeyde
f0f89310ac main: add '--mosh' for better SSH client 2016-11-11 22:26:22 +02:00
Roman Zeyde
47ff7c5cb3 gpg: add usage example for GPA 2016-11-11 20:05:47 +02:00
Roman Zeyde
0440025083 gpg: use explicit function to check for custom subpacket marker 2016-11-11 13:02:02 +02:00
Roman Zeyde
c49fe97f63 gpg: remove unused parser for literal packets 2016-11-11 13:01:54 +02:00
Roman Zeyde
7f8abcb5c5 client: remove unused code 2016-11-11 13:01:47 +02:00
Roman Zeyde
e13039e52d gpg: remove property method and unused member variable from PublicKey 2016-11-11 13:01:33 +02:00
Roman Zeyde
c420571eb8 gpg: import test coverage for protocol 2016-11-11 09:14:33 +02:00
Roman Zeyde
827119a18d gpg: handle KILLAGENT command
so `gpg-connect-agent KILLAGENT` should stop the running agent
2016-11-10 23:29:47 +02:00
Roman Zeyde
9be6504658 util: import test coverage 2016-11-10 14:33:41 +02:00
Roman Zeyde
07cbe65875 formats: improve test coverage 2016-11-10 14:33:27 +02:00
Roman Zeyde
180120e787 README: link to trezor-ssh-agent for Windows support 2016-11-10 13:09:53 +02:00
Roman Zeyde
f4ce81fa94 README: add 1.4.0-related post 2016-11-10 13:03:12 +02:00
Roman Zeyde
176bf4ef7c README: add Hg demo 2016-11-08 21:45:42 +02:00
Roman Zeyde
d22cd7512d gpg: launch agent before starting the shell 2016-11-06 22:39:45 +02:00
Roman Zeyde
83f17704cb server: remove 'SSH_AUTH_SOCK=' from logging 2016-11-06 22:02:35 +02:00
Roman Zeyde
92f6751ccb README: multiple SSH identities from config file 2016-11-06 20:46:42 +02:00
Roman Zeyde
abe80533eb README: trim whitespace 2016-11-06 20:43:45 +02:00
Roman Zeyde
de51665c71 bump version 2016-11-04 21:16:15 +02:00
Roman Zeyde
c30e5f5a67 gpg: add optional arguments to gpg-shell 2016-11-04 19:33:05 +02:00
Roman Zeyde
2eab2a152c device: verify keepkey constraints 2016-11-04 19:31:54 +02:00
Roman Zeyde
5e93d97be3 Merge branch 'ssh-ids' 2016-11-04 16:07:29 +02:00
Roman Zeyde
4c8fcd6714 ssh: use special UNIX socket name 2016-11-04 10:36:53 +02:00
Roman Zeyde
ee593bc66e gpg: show user ID on a single line 2016-11-03 23:36:11 +02:00
Roman Zeyde
dbed773e54 fix pylint and tests 2016-11-03 23:29:45 +02:00
Roman Zeyde
ac4a86d312 ssh: remove git utility 2016-11-03 23:12:59 +02:00
Roman Zeyde
021831073e ssh: simple support for multiple public keys loading 2016-11-03 23:05:27 +02:00
Roman Zeyde
6a5acba0b0 gpg: decouple identity from device 2016-11-03 22:00:56 +02:00
Roman Zeyde
9123cef810 ssh: decouple identity from device 2016-11-03 22:00:43 +02:00
Roman Zeyde
6f6e7c0bcc device: allow loading identities from a file (instead of argument) 2016-11-03 22:00:22 +02:00
Roman Zeyde
47ff081525 bump version 2016-11-03 16:46:14 +02:00
Roman Zeyde
6d53baafe2 setup: add 'device' package 2016-11-03 16:45:39 +02:00
Roman Zeyde
317b672add setup: support Python 3.5 2016-10-31 23:00:19 +02:00
Roman Zeyde
9964c200ff travis: install keepkey from GitHub 2016-10-31 22:40:23 +02:00
Roman Zeyde
75405b4944 gpg: allow PIN entry before starting GPG shell 2016-10-30 22:03:39 +02:00
Roman Zeyde
e74b9c77af gpg: rename gpg.device into gpg.client 2016-10-30 22:03:12 +02:00
Roman Zeyde
c2158947c8 Merge branch 'refactor-device' 2016-10-30 20:29:44 +02:00
Roman Zeyde
e39d5025d5 travis: use pip to install trezor-agent 2016-10-30 20:23:20 +02:00
Roman Zeyde
efdb9fcfb5 gpg: fix bytes/str issue with GPG user ID 2016-10-30 20:23:20 +02:00
Roman Zeyde
a20b1ed2a8 factory: remove obsolete code 2016-10-30 20:23:20 +02:00
Roman Zeyde
ca507126d6 gpg: use new device package (instead of factory) 2016-10-30 20:23:20 +02:00
Roman Zeyde
0f79b5ff2e ssh: use new device package (instead of factory) 2016-10-30 20:23:19 +02:00
Roman Zeyde
946ab633d4 device: move device-related code into a separate package 2016-10-30 20:23:19 +02:00
Roman Zeyde
4108c9287f bump version 2016-10-29 22:50:43 +03:00
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.
2016-10-29 22:28:57 +03:00
Roman Zeyde
2cecd2ed08 pylint: enable "fixme" 2016-10-29 19:44:54 +03:00
Roman Zeyde
05f40085b2 gpg: remove spurious space 2016-10-29 17:25:22 +03:00
Roman Zeyde
c7346d621d gpg: use policy URI subpacket for marking our public keys
keybase.io does not support experimental/private subpacket IDs
2016-10-29 17:16:36 +03:00
Roman Zeyde
0342b39465 gpg: allow setting key creation timestamp 2016-10-29 17:16:20 +03:00
Roman Zeyde
fa6d8564b9 gpg: use SHA-512 as default hash for signatures 2016-10-29 11:25:53 +03:00
Roman Zeyde
e09712c793 gpg: add OS X support for gpg-init 2016-10-25 19:55:48 +03:00
Roman Zeyde
0cbb3bb9fa Merge pull request #67 from romanz/concurrent-handler
Concurrent SSH handler
2016-10-24 21:51:19 +03:00
Roman Zeyde
d7a6641ffa gpg: update screencasts 2016-10-24 20:13:54 +03:00
Roman Zeyde
6fe89241c4 Merge branch 'auto-spawn-agent' 2016-10-24 19:29:45 +03:00
Roman Zeyde
c5262d075b gpg: use 'gpg-agent.conf' to configure trezor-gpg-agent
currently support logfile and logging verbosity
2016-10-24 17:55:35 +03:00
Roman Zeyde
683d24f4eb gpg: use gpg.conf to automatically spawn trezor-gpg-agent 2016-10-24 17:54:39 +03:00
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.
2016-10-24 16:41:12 +03:00
Roman Zeyde
3f784289d8 gpg: allow setting CURVE from environment 2016-10-24 11:01:37 +03:00
Roman Zeyde
04d790767d gpg: don't fail on non-zero shell exit code 2016-10-23 21:37:09 +03:00
Roman Zeyde
97efdf4a45 ssh: handle connections concurrently 2016-10-23 17:35:12 +03:00
Roman Zeyde
ee2f6b75dc server: log SSH version for debugging 2016-10-23 17:05:20 +03:00
Roman Zeyde
a26f0ea034 README: make tag example clearer 2016-10-23 13:56:22 +03:00
Roman Zeyde
a68f1e5c26 gpg: update README for easier usage 2016-10-22 22:46:18 +03:00
Roman Zeyde
93e3c66a15 gpg: notify the user for confirmation 2016-10-22 22:35:49 +03:00
Roman Zeyde
44eaaa6b9c gpg: don't spawn gpg-shell automatically 2016-10-22 22:35:08 +03:00
Roman Zeyde
b83d4960e7 gpg: run gpg-shell in verbose mode 2016-10-22 22:34:55 +03:00
Roman Zeyde
75fe7b4e05 gpg: improve shell helper scripts
- explicit trust configuration
- less debug prints
2016-10-22 21:41:23 +03:00
Roman Zeyde
742136b22d gpg: add helper scripts 2016-10-21 23:19:32 +03:00
Roman Zeyde
513e99dd57 server: refactor server_thread() to decouple it from handle_connection() 2016-10-21 22:09:47 +03:00
Roman Zeyde
1bd6775c35 gpg: replace -s flag by implicit adding to existing GPG key 2016-10-21 21:25:22 +03:00
Roman Zeyde
aaade1737f gpg: comment about digest size 2016-10-21 19:02:47 +03:00
Roman Zeyde
fe185c190e ledger: move factory-related code to a separate file 2016-10-21 18:34:36 +03:00
Roman Zeyde
1bc0165368 setup: update trezorlib dependency 2016-10-21 18:07:04 +03:00
Roman Zeyde
0f841ffbc4 factory: add Python 3 support for Ledger 2016-10-21 17:33:26 +03:00
Roman Zeyde
b2942035a3 gpg: skip "progress" status messages 2016-10-20 22:46:39 +03:00
Roman Zeyde
215b64f253 gpg: fix comment 2016-10-18 22:23:40 +03:00
Roman Zeyde
79e68b29c2 bump version 2016-10-18 22:16:54 +03:00
Roman Zeyde
8265515641 gpg: fix small Python2/3 issue 2016-10-18 22:15:58 +03:00
Roman Zeyde
749799845d bump version 2016-10-18 21:37:01 +03:00
Roman Zeyde
eaea35003e gpg: remove unused function (_time_format) 2016-10-18 21:25:31 +03:00
Roman Zeyde
eefb38ce83 gpg: remove unused function (_verify_keygrip) 2016-10-18 21:19:09 +03:00
Roman Zeyde
0730eb7223 gpg: use same logging configuration as in SSH 2016-10-18 21:02:49 +03:00
Roman Zeyde
5b61702205 gpg: don't crash gpg-agent on error 2016-10-18 20:56:17 +03:00
Roman Zeyde
0ad0ca3b9a README: add a note about SSH incompatible options 2016-10-18 19:46:43 +03:00
Roman Zeyde
2843cdcf41 ssh: pretty-print user name 2016-10-18 18:28:21 +03:00
Roman Zeyde
c7bc78ebe7 Merge pull request #58 from romanz/keygrip-agent
gpg: replace TREZOR_GPG_USER_ID usage in gpg-agent mode
2016-10-18 18:15:00 +03:00
Roman Zeyde
a6d9edcb0b README: update for new user ID specification for GPG 2016-10-18 18:12:42 +03:00
Roman Zeyde
bc64205a85 gpg: replace TREZOR_GPG_USER_ID usage in gpg-agent mode
Use the keygrip to find the correct public key instead.
2016-10-18 18:05:51 +03:00
Roman Zeyde
34dc803856 README: add "user@" for SSH example usage
This should help when local username and remote username are different.
2016-10-18 15:07:40 +03:00
Roman Zeyde
f7ebb02799 isort: fix imports 2016-10-18 12:10:28 +03:00
Roman Zeyde
0ba33a5bc4 gpg: document agent responses 2016-10-18 12:08:28 +03:00
Roman Zeyde
13752ddcd5 gpg: require latest GPG version 2016-10-18 12:05:44 +03:00
Roman Zeyde
487a8e56c4 gpg: add keygrip logic into decoding 2016-10-17 23:30:50 +03:00
Roman Zeyde
ef56ee4602 gpg: remove verifying logic from decoding 2016-10-17 23:08:16 +03:00
Roman Zeyde
ae381a38e5 gpg: export keygrips from protocol 2016-10-17 22:57:40 +03:00
Roman Zeyde
446ec99bf4 gpg: remove complex pubkey parsing code 2016-10-17 22:51:11 +03:00
Roman Zeyde
80c6f10533 README: correct pip commands order 2016-10-17 18:01:38 +03:00
Roman Zeyde
ff984c60e4 README: link to PIN entering instructions 2016-10-17 17:54:59 +03:00
Roman Zeyde
c9bc079dc9 gpg: add file:line to logging format 2016-10-17 11:58:03 +03:00
Roman Zeyde
65d2c04478 gpg: fix agent module to work with Python 3 2016-10-17 11:47:22 +03:00
Roman Zeyde
2d57bf4453 gpg: beter logging while search for GPG key 2016-10-17 11:46:58 +03:00
Roman Zeyde
79b6d31dfe gpg: raise proper exception when keygrip mismatch is detected 2016-10-17 11:08:06 +03:00
Roman Zeyde
7de88a3980 gpg: add comment for stopping current gpg-agent 2016-10-16 22:40:16 +03:00
Roman Zeyde
6f8d0df116 bump version 2016-10-16 22:17:53 +03:00
Roman Zeyde
b4a382d22e Merge pull request #51 from romanz/curve25519
Curve25519
2016-10-15 16:19:50 +03:00
Roman Zeyde
d236f4667e gpg: allow Curve25519 for ECDH 2016-10-15 16:10:16 +03:00
Roman Zeyde
42813ddbb4 gpg: parse curve OID from public key to select curve name 2016-10-15 16:10:16 +03:00
Roman Zeyde
8f19690943 gpg: support Curve25519 for creating encryption subkeys 2016-10-15 16:10:16 +03:00
Roman Zeyde
5047805385 gpg: move HardwareSigner to device module 2016-10-15 16:10:16 +03:00
Roman Zeyde
915b326da7 gpg: simplify AgentSigner and move to keyring module 2016-10-15 15:57:45 +03:00
Roman Zeyde
e7b8379a97 factory: explicitly only the first interface 2016-10-14 20:58:42 +03:00
Roman Zeyde
26435130d7 factory: emit warning (instead of exception) when an import fails 2016-10-12 21:15:21 +03:00
Roman Zeyde
dfde6dbee4 bump version 2016-10-12 19:12:04 +03:00
Cédric Félizard
085a3e81c7 Add explanation about entering PIN (#47)
[ci skip]
2016-10-11 21:31:25 +03:00
Cédric Félizard
3082d61deb Fix typo (#48) 2016-10-11 21:29:54 +03:00
Roman Zeyde
e3286a4510 gpg: don't clear the session after PIN is entered
This would allow single PIN entry when running multiple GPG commands.
2016-10-11 08:43:39 +03:00
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.
2016-10-04 23:11:12 +03:00
Roman Zeyde
1454d2f4d7 Merge pull request #43 from romanz/fix-gpg-manual
GPG: fix installation instructions
2016-10-04 20:01:44 +03:00
Roman Zeyde
9b395363a3 GPG: fix installation instructions 2016-10-04 19:59:08 +03:00
Roman Zeyde
5bb9dd7770 Merge pull request #42 from romanz/ssh-git
README: add an example for remote git repository
2016-10-04 11:43:55 +03:00
Roman Zeyde
51df023a23 README: add an example for remote git repository 2016-10-04 11:40:14 +03:00
Roman Zeyde
d74f375637 Merge pull request #41 from romanz/protobuf-note
README: add note about protobuf permissions issue
2016-10-04 11:27:14 +03:00
Roman Zeyde
1fd0659051 README: add note about protobuf permissions issue 2016-10-04 11:24:39 +03:00
Roman Zeyde
18be290bd6 Merge branch 'fix_agent' of https://github.com/Solution4Future/trezor-agent into Solution4Future-fix_agent 2016-10-04 11:16:56 +03:00
Roman Zeyde
a1ab496bf4 Merge branch 'ledger' 2016-10-04 10:39:08 +03:00
Roman Zeyde
784e14647a Merge branch 'master' into HEAD
Conflicts:
	trezor_agent/factory.py
2016-10-04 10:37:52 +03:00
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 2016-10-01 12:38:16 +02:00
Dominik Kozaczko
cf27b345f6 better handling of keepkey dependency; fixes #36 2016-10-01 12:30:00 +02:00
Dominik Kozaczko
386ed5a81f Merge pull request #1 from romanz/master
pull changes from upstream
2016-10-01 12:10:18 +02:00
Roman Zeyde
5a64954324 Merge pull request #37 from Solution4Future/fix_agent
removed .decode('ascii') and added missing bytestrings and also some missing deps
2016-10-01 11:42:47 +03:00
Dominik Kozaczko
3aebd137b0 removed .decode('ascii') and added missing bytestrings 2016-10-01 10:02:46 +02:00
Nicolas Pouillard
1fa35e7f1a Fix the URL for the TREZOR firmware 2016-09-30 21:07:35 +03:00
Roman Zeyde
aeda85275d bump version 2016-09-28 18:32:19 +03:00
Roman Zeyde
e41206b350 setup: update trezorlib version 2016-09-28 18:31:13 +03:00
Roman Zeyde
03650550dd Use latest protobuf library (for native Python 3 support) 2016-09-28 18:18:09 +03:00
Roman Zeyde
f7b07070da README: update setuptools to the latest version 2016-09-28 18:08:09 +03:00
Roman Zeyde
96eede9c83 Merge branch 'np-encode-subpackets' 2016-09-28 17:27:48 +03:00
Roman Zeyde
91146303a3 Follow GPG implementation for subpacket prefix encoding.
Conflicts:
	trezor_agent/gpg/protocol.py
2016-09-28 17:26:50 +03:00
Roman Zeyde
bf598435fb client: keep the session open (doesn't forget PIN) 2016-09-26 22:27:47 +03:00
Roman Zeyde
459b882b89 ledger: don't use debug=True 2016-09-14 23:07:27 +03:00
Roman Zeyde
998c9ee958 README: update usage section 2016-09-11 23:38:21 +03:00
Roman Zeyde
d408a592aa README: get only the first lines of 'trezorctl get_features' 2016-09-11 23:35:10 +03:00
Roman Zeyde
282e91ace3 update README about protobuf issueOF 2016-09-11 23:22:10 +03:00
Roman Zeyde
23c37cf1e3 README: update TREZOR's required version 2016-09-11 23:17:47 +03:00
Roman Zeyde
5c5c6f9cbb bump version 2016-09-11 23:08:11 +03:00
Roman Zeyde
17c8bd0e92 gpg: add experimental warning 2016-09-11 23:06:47 +03:00
Nicolas Pouillard
016e864503 Attempt at fixing issue #32 2016-09-06 00:45:51 +02:00
Roman Zeyde
57e09248db Merge pull request #31 from romanz/master
Update ledger branch with the latest changes from master branch
2016-09-05 22:28:07 +03:00
Roman Zeyde
0c4e67c837 Merge pull request #30 from np/decode-subpackets
gpg/decode/parse_subpackets: parse subpacket length according to RFC
2016-09-05 20:52:48 +03:00
Nicolas Pouillard
adcbe6e7b2 gpg/decode/parse_subpackets: parse subpacket length according to RFC 2016-09-05 17:13:33 +02:00
Roman Zeyde
73bdf417e4 factory: require TREZOR firmware v1.4.0+ for GPG signatures and decryption 2016-09-02 11:38:59 +03:00
Roman Zeyde
ee347252b4 README: update gitter badge position 2016-09-01 22:02:32 +03:00
Roman Zeyde
d63f048b78 gpg: update trezor-agent installation instruction (using pip) 2016-08-27 20:59:25 +03:00
Roman Zeyde
05fada91d2 gpg: use gpgconf to get correct GPG agent UNIX socket path 2016-08-15 21:54:01 +03:00
Roman Zeyde
27a3fddfa2 gpg: add a note about restoring GPG keys with --time command-line flag 2016-08-15 21:39:34 +03:00
Roman Zeyde
030ae4c3f6 gpg: include unsupport hash algorithm ID in exception message 2016-08-13 10:06:52 +03:00
Roman Zeyde
4897b70888 factory: fix pylint import-error warnings 2016-08-11 22:38:12 +03:00
Roman Zeyde
f4ecd47ed6 factory: fix pep8 and pylint warnings 2016-08-11 22:31:24 +03:00
Roman Zeyde
c4bbac0e77 util: move BIP32 address related functions 2016-08-11 22:30:59 +03:00
BTChip
5d0b0f65d3 Merge branch 'ledger' of https://github.com/btchip/trezor-agent into ledger 2016-08-09 13:02:47 +02:00
BTChip
33747592ca Fix eddsa, SSH optimization with signature + key, cleanup 2016-08-09 13:01:57 +02:00
BTChip
adb09cd8ca Ledger integration 2016-08-09 13:01:57 +02:00
Roman Zeyde
45f6f1a3d8 gpg: allow setting GPG home directory via $GNUPGHOME 2016-08-05 13:48:47 +03:00
Roman Zeyde
c4c56b9faf gpg: no support for empty user_id 2016-08-05 11:57:12 +03:00
BTChip
bc1d7a5448 Fix eddsa, SSH optimization with signature + key, cleanup 2016-08-03 15:17:04 +02:00
BTChip
8fe16d24c2 Ledger integration 2016-07-31 10:00:46 +02:00
Roman Zeyde
1704ae7683 gpg: add '--user' flag to pip install command 2016-07-30 14:53:54 +03:00
Roman Zeyde
a7190223fd gpg: note WIP status in README 2016-07-30 14:52:50 +03:00
Roman Zeyde
220735c6ad gpg: note WIP status in README 2016-07-30 14:45:50 +03:00
Roman Zeyde
82e08d073b gpg: rename proto -> protocol 2016-07-26 19:37:42 +03:00
Roman Zeyde
8ab0908388 proto: don't hardcode name length 2016-07-26 17:59:06 +03:00
Roman Zeyde
fd3183d71c gitignore: sublime text project files 2016-07-26 17:57:34 +03:00
Roman Zeyde
295d52ef10 gpg: move 'iterlines' to keyring 2016-07-26 17:50:49 +03:00
Roman Zeyde
8a51099488 gpg: remove unused "sign_message" 2016-07-26 17:42:14 +03:00
Roman Zeyde
f4dd1eacdd gpg: allow parsing multiple keys 2016-07-26 17:35:07 +03:00
Roman Zeyde
024b5f131f README: reformat links 2016-07-22 23:14:25 +03:00
Roman Zeyde
b9b7b8dafd gpg: re-structure public key packets for easier parsing 2016-07-22 23:14:25 +03:00
Roman Zeyde
744696fdee gpg: decode user_attribute packets 2016-07-22 22:44:54 +03:00
Roman Zeyde
ccdbc7abfc gpg: parse_packets() should get file-like stream
and wrap it with util.Reader()
2016-07-22 21:46:38 +03:00
Roman Zeyde
e70f0ec681 gpg: refactor hash algorithm handling 2016-07-09 12:25:05 +03:00
Roman Zeyde
aeaf978d8e gpg: add mulitple GPG public keys as test vectors 2016-07-09 12:08:07 +03:00
Roman Zeyde
d60fff202a gpg: don't validate non-ECDSA signatures 2016-07-09 11:26:48 +03:00
Roman Zeyde
9171dd08c8 README: update posts 2016-06-23 22:25:27 +03:00
Roman Zeyde
4c5004d838 Merge pull request #16 from jhoenicke/master
More robust gpg key parsing
2016-06-22 20:58:34 +03:00
Jochen Hoenicke
a2e46048a1 Use TREZOR_GPG_USER_ID in agent 2016-06-22 02:31:57 +02:00
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.
2016-06-22 02:31:22 +02:00
Roman Zeyde
db874ad98f README: add GPG part 2016-06-20 22:43:13 +03:00
Roman Zeyde
ed2d71cc08 README: split into main and SSH parts 2016-06-20 22:36:07 +03:00
Roman Zeyde
59b39ce81f Merge branch 'gpg-agent' 2016-06-20 22:19:12 +03:00
Roman Zeyde
75f879edbb gpg: update README.md 2016-06-20 22:18:03 +03:00
Roman Zeyde
45a85a317b gpg: allow setting UNIX socket from command-line 2016-06-18 20:10:52 +03:00
Roman Zeyde
7b3874e6f7 gpg: fixup logging during key creation 2016-06-17 22:05:13 +03:00
Roman Zeyde
6c96cc37b9 gpg: add support for adding subkeys to EdDSA primary GPG keys 2016-06-17 21:59:13 +03:00
Roman Zeyde
c98cb22ba4 gpg: use separate derivations for GPG keys 2016-06-17 19:51:49 +03:00
Roman Zeyde
d9fbfccd35 gpg: load correct key if ECDH is requested 2016-06-17 19:51:49 +03:00
Roman Zeyde
fe4d9ed3c8 gpg: add SLIP-0017 support for ECDH session key generation 2016-06-17 09:29:53 +03:00
Roman Zeyde
092445af71 agent: handle connection errors 2016-06-11 20:26:10 +03:00
Roman Zeyde
602e867c7d gpg: add test for keygrip 2016-06-11 20:18:07 +03:00
Roman Zeyde
16de8cdabc agent: refactor signature and ECDH 2016-06-11 15:06:35 +03:00
Roman Zeyde
7bbf11b631 gpg: refactor key creation 2016-06-11 14:46:24 +03:00
Roman Zeyde
3e41fddcef gpg: add test for ECDH pubkey generation 2016-06-11 14:02:12 +03:00
Roman Zeyde
8108e5400d gpg: support TREZOR-based primary key 2016-06-11 13:47:56 +03:00
Roman Zeyde
a1659e0f0d gpg: add preferred symmetric algo 2016-06-11 10:34:59 +03:00
Roman Zeyde
3b139314b6 gpg: refactor sign_message method 2016-06-06 23:02:14 +03:00
Roman Zeyde
a05cff5079 gpg: use "gpg2" for 'git config --local gpg.program' 2016-06-06 23:02:14 +03:00
Roman Zeyde
694cee17ac gpg: refactor create_* methods 2016-06-04 20:54:07 +03:00
Roman Zeyde
bc281d4411 gpg: use local version 2016-06-04 19:45:03 +03:00
Roman Zeyde
04af6b737b gpg: remove extra param from Factory.from_public_key() 2016-06-04 09:53:43 +03:00
Roman Zeyde
171c746c7e gpg: move agent main code to __main__ 2016-06-04 09:53:23 +03:00
Roman Zeyde
8b5ac14150 gpg: add docstrings 2016-06-03 22:44:25 +03:00
Roman Zeyde
16090cebed pylint: ignore TODOs 2016-06-03 22:40:17 +03:00
Roman Zeyde
d2167cd4ff gpg: check keygrip on ECDH 2016-06-03 22:39:31 +03:00
Roman Zeyde
10cbe67c9a gpg: add TODO 2016-06-03 21:53:31 +03:00
Roman Zeyde
29a984eebb gpg: improve flags selection 2016-06-03 20:17:53 +03:00
Roman Zeyde
a6660fd5c5 gpg: handle BYE command 2016-06-03 17:43:46 +03:00
Roman Zeyde
2acd0bf3b7 gpg: fix keygrip computation 2016-06-03 17:41:31 +03:00
Roman Zeyde
e9f7894d62 ecdh: fixup pubkey ID 2016-06-03 15:05:45 +03:00
Roman Zeyde
56e9d7c776 gpg: allow graceful exit via Ctrl+C 2016-06-03 14:42:40 +03:00
Roman Zeyde
e7bacf829c gpg: refactor ecdh case 2016-06-03 14:39:16 +03:00
Roman Zeyde
c1c679b541 HACK: support ECDH in agent - note keygrip and ID errors. 2016-06-02 23:24:36 +03:00
Roman Zeyde
49c343df94 HACK: create subkey with ECDH support 2016-06-02 22:54:08 +03:00
Roman Zeyde
7da7f5c256 HACK: fixup tests 2016-06-02 22:25:44 +03:00
Roman Zeyde
39cb5565bf HACK: better line iteration 2016-06-02 21:39:48 +03:00
Roman Zeyde
f89c5bb125 HACK: better logging 2016-06-02 21:38:48 +03:00
Roman Zeyde
92649b290f HACK: add preliminary gpg support 2016-05-30 21:57:10 +03:00
Roman Zeyde
d9b07e2ac6 gpg: hack agent prototype 2016-05-28 23:02:45 +03:00
Roman Zeyde
6975671cc1 setup: fix protobuf dependency to allow 2.6.1+
protobuf 3.0+ is needed for Python 3 support.
2016-05-28 08:43:55 +03:00
Roman Zeyde
f0ea568bb8 gpg: add more UTs for decode 2016-05-27 22:07:50 +03:00
Roman Zeyde
34c614db6e gpg: add more UTs for decode 2016-05-27 21:52:00 +03:00
Roman Zeyde
2bbd335f7e pep8: use tox.ini for configuration 2016-05-27 19:48:28 +03:00
Roman Zeyde
af8ad99c7a gpg: add UTs for decode 2016-05-27 16:59:10 +03:00
Roman Zeyde
313271ac06 gpg: move signer.py to __main__.py 2016-05-27 14:38:38 +03:00
Roman Zeyde
969e08140b gpg: add more tests for keyring 2016-05-27 13:43:55 +03:00
Roman Zeyde
39f00af65d gpg: add help for sign arguments 2016-05-27 12:20:33 +03:00
Roman Zeyde
272759e907 gpg: allow dependency injection for subprocess module 2016-05-27 12:20:33 +03:00
Roman Zeyde
4be55156ed gpg: refactor pubkeys' parsing code 2016-05-27 11:28:23 +03:00
Roman Zeyde
80a5ea0f2a gpg: add UTs for keyring 2016-05-26 23:16:08 +03:00
Roman Zeyde
87e50449e5 travis: add Python 3.5 2016-05-26 22:31:39 +03:00
Roman Zeyde
dcf35c4267 decode: split _remove_armor() from verify() 2016-05-26 22:29:19 +03:00
Roman Zeyde
7570861765 gpg: fixup signer docstring 2016-05-26 22:29:19 +03:00
Roman Zeyde
339f61c071 gpg: better __repr__ and logging for public keys 2016-05-26 22:29:19 +03:00
Roman Zeyde
3c4fb7a17b gpg: allow pinentry UI via "display=" option 2016-05-25 18:52:20 +03:00
Roman Zeyde
a6a0c05f57 keyring: fix more Python 2/3 issues 2016-05-23 23:03:02 +03:00
Roman Zeyde
4c036d2ce7 gpg: fixup str/bytes issues 2016-05-22 23:10:12 +03:00
Roman Zeyde
eaa91cfdbd gpg: add tests for basic protocol utils 2016-05-22 23:06:12 +03:00
Roman Zeyde
fd61941d0f gpg: fixup subcommand for Python 3
http://bugs.python.org/issue9253#msg186387
2016-05-22 22:28:07 +03:00
Roman Zeyde
decd3ddf75 gpg: fixup str/bytes issues 2016-05-22 22:20:55 +03:00
Roman Zeyde
4c07b360cd gpg: fix pep8/pylint warning 2016-05-22 08:07:51 +03:00
Roman Zeyde
0b0f60dd89 gpg: rename load_from_gpg -> get_public_key 2016-05-21 20:23:48 +03:00
Roman Zeyde
db6903eab7 gpg: rename agent -> keyring 2016-05-21 20:17:58 +03:00
Roman Zeyde
171a0c2f6a gpg: remove agent's main 2016-05-21 20:12:41 +03:00
Roman Zeyde
a535b31a1b gpg: fixup lint/pep8 2016-05-21 20:00:38 +03:00
Roman Zeyde
ee4bcddd22 gpg: rename main API 2016-05-21 17:32:15 +03:00
Roman Zeyde
f626d34e21 gpg: using closing() context handler 2016-05-21 17:21:16 +03:00
Roman Zeyde
2cf081420f gpg: move armor to proto 2016-05-21 17:15:42 +03:00
Roman Zeyde
0e72e3b7ff gpg: move PublicKey to proto 2016-05-21 17:10:17 +03:00
Roman Zeyde
ce61c8b2ae gpg: move timeformat from util 2016-05-21 17:04:18 +03:00
Roman Zeyde
3192e570ed gpg: initial support for ElGamal and DSA
Doesn't verify anything (yet).
2016-05-21 16:54:38 +03:00
Roman Zeyde
bf8f516ef4 gpg: no visual challenge 2016-05-21 07:44:27 +03:00
Roman Zeyde
51f7d6120b client: not visual challength for SSH 2016-05-21 07:43:10 +03:00
Roman Zeyde
0cb7cf0746 Merge branch 'python3' 2016-05-18 18:42:00 +03:00
Roman Zeyde
b4ff31f816 gpg: handle ECDH keys 2016-05-12 22:15:05 +03:00
Roman Zeyde
6e9d6d6430 gpg: add URLs for subpackets 2016-05-12 21:55:26 +03:00
Roman Zeyde
fa9391ede6 gpg: update required firmware version 2016-05-08 21:19:28 +03:00
Roman Zeyde
ad8eafe6f8 Merge branch 'master' into python3
Conflicts:
	setup.py
2016-05-07 21:14:20 +03:00
Roman Zeyde
695079e4b9 agent: raise explicit error when signature fails 2016-05-07 20:49:51 +03:00
Roman Zeyde
9888ef971a gpg: add installation command to README 2016-05-07 20:41:34 +03:00
Roman Zeyde
04a878374f setup: add gpg subpackage 2016-05-07 20:38:19 +03:00
Roman Zeyde
4270d8464f gpg: add screencasts 2016-05-07 20:29:07 +03:00
Roman Zeyde
25a427081c gpg: add more output examples 2016-05-07 13:24:25 +03:00
Roman Zeyde
939fdbe829 gpg: add output examples 2016-05-07 13:15:29 +03:00
Roman Zeyde
1f126f3002 gpg: better logging 2016-05-07 13:05:25 +03:00
Roman Zeyde
78526d1379 gpg: install gpg-git wrapper script 2016-05-07 13:02:16 +03:00
Roman Zeyde
7e3c3b4f77 gpg: fixup README 2016-05-07 12:49:01 +03:00
Roman Zeyde
513c19bf1f gpg: remove unused files 2016-05-07 09:54:55 +03:00
Roman Zeyde
f1e75783c4 gpg: use environment variable for user_id 2016-05-07 09:41:58 +03:00
Roman Zeyde
68637525ea Merge branch 'master' into python3 2016-05-06 22:24:17 +03:00
Roman Zeyde
fce45832c2 gpg: fix small typo 2016-05-06 22:22:02 +03:00
Roman Zeyde
df001c4100 gpg: rename README 2016-05-06 22:20:50 +03:00
Roman Zeyde
1a228a1af6 gpg: refactor cli 2016-05-06 22:19:46 +03:00
Roman Zeyde
7a7c9efc47 pylint: disable unbalanced-tuple-unpacking warning 2016-05-06 21:17:10 +03:00
Roman Zeyde
859cee9757 travis: use latest tools 2016-05-06 21:15:04 +03:00
Roman Zeyde
2846c0bf1a util: add tests for gpg-related code 2016-05-06 14:28:15 +03:00
Roman Zeyde
b2147a8418 formats: curve name should be a string 2016-05-05 22:31:07 +03:00
Roman Zeyde
4cbf8a9f0a setup: WiP for Python 3 support 2016-05-05 22:17:04 +03:00
Roman Zeyde
d9c4e930f3 main: fixup str/bytes issue for curve_name 2016-05-05 21:42:11 +03:00
Roman Zeyde
6fd6fe6520 handle missing imports 2016-05-04 23:05:43 +03:00
Roman Zeyde
4a7fef3011 gpg: fix logging and arguments in demo 2016-04-30 22:20:50 +03:00
Roman Zeyde
a0e476ea19 gpg: remove unused code 2016-04-30 22:15:15 +03:00
Roman Zeyde
683aae7aa4 gpg: add logging for digest 2016-04-30 22:11:51 +03:00
Roman Zeyde
d369638c7b gpg: add a script for faster commit verification 2016-04-30 22:07:46 +03:00
Roman Zeyde
07c4100618 gpg: fixup logging and make sure it works with git 2016-04-30 21:55:37 +03:00
Roman Zeyde
b9f139b74a gpg: refactor subkey as pubkey 2016-04-30 21:34:12 +03:00
Roman Zeyde
3bf926620b gpg: handle multiple packets 2016-04-30 21:07:19 +03:00
Roman Zeyde
ab192619f4 gpg: move protocol utils to proto.py 2016-04-30 16:50:01 +03:00
Roman Zeyde
f982d785bd gpg: add marker to our pubkey signature packets 2016-04-30 16:27:43 +03:00
Roman Zeyde
38c1acf4db Merge branch 'subkey' 2016-04-30 15:42:21 +03:00
Roman Zeyde
31c3686fa4 gpg: small fixes 2016-04-30 15:39:32 +03:00
Roman Zeyde
87ca33c104 gpg: fixup encoding for large packets 2016-04-30 15:34:18 +03:00
Roman Zeyde
c3d23ea7f5 gpg: allow longer packets 2016-04-30 14:47:32 +03:00
Roman Zeyde
5c04d17c43 gpg: demo with ed25519 TREZOR-based keys 2016-04-30 13:32:20 +03:00
Roman Zeyde
2d2d6efa93 gpg: small refactoring 2016-04-30 13:25:14 +03:00
Roman Zeyde
131c30acca gpg: use explicit public key algo_id 2016-04-30 13:20:06 +03:00
Roman Zeyde
a7ef263954 gpg: generalize RSA/ECDSA signatures 2016-04-30 13:01:40 +03:00
Roman Zeyde
d486c1ee7b gpg: refactor agent rsa/ecdsa signature parsing 2016-04-30 12:33:01 +03:00
Roman Zeyde
f35b5be3ac gpg: 1st try for RSA primary key support 2016-04-30 11:40:02 +03:00
Roman Zeyde
9ed9781496 gpg: support RSA decode and verify 2016-04-30 11:02:41 +03:00
Roman Zeyde
5d007260e1 gpg: add docstrings 2016-04-30 10:04:44 +03:00
Roman Zeyde
7dfa3ab255 gpg: replace PublicKey.curve_name attribute 2016-04-30 09:29:04 +03:00
Roman Zeyde
b8eba72d0b gpg: fixup subkey/export handling 2016-04-29 22:46:02 +03:00
Roman Zeyde
492285de1b gpg: rename pubkey methods 2016-04-29 22:28:41 +03:00
Roman Zeyde
cc326b1f7d gpg: pubkey is not needed for make_signature 2016-04-29 22:25:08 +03:00
Roman Zeyde
169ff39b1a gpg: remove visual keyword for now 2016-04-29 22:23:12 +03:00
Roman Zeyde
dcc7ef2600 minor fixes 2016-04-29 22:10:04 +03:00
Roman Zeyde
ac2d12b354 It works again! 2016-04-29 17:45:16 +03:00
Roman Zeyde
f3b49ff553 gpg: use strict bash mode for demo 2016-04-29 11:14:27 +03:00
Roman Zeyde
12d640c66b fixup pep8 2016-04-29 10:25:46 +03:00
Roman Zeyde
32984d2d3f agent: add support for gpg passphrase entry 2016-04-29 10:16:58 +03:00
Roman Zeyde
a45c6c1300 horrible hack - but IT WORKS!!! 2016-04-28 22:17:08 +03:00
Roman Zeyde
1d3ba7e9b7 subkey: add backsig 2016-04-28 22:10:40 +03:00
Roman Zeyde
673b1df648 1st try 2016-04-28 21:31:01 +03:00
Roman Zeyde
e63f03354e gpg: refactor signing providers from actual Signer class 2016-04-28 14:56:58 +03:00
Roman Zeyde
3c9c1b4e14 gpg: export verifying_key from parsing 2016-04-28 14:44:52 +03:00
Roman Zeyde
5caf4728ee gpg: fixup comment 2016-04-28 12:56:06 +03:00
Roman Zeyde
dde6dcdaeb gpg: fix unpacking for subkey-case 2016-04-28 12:55:48 +03:00
Roman Zeyde
1f3c989884 gpg: 'dump' -> 'serialize' 2016-04-28 12:34:00 +03:00
Roman Zeyde
55dea41959 gpg: make sure gpg-agent is running before connecting 2016-04-28 12:09:45 +03:00
Roman Zeyde
ed01c00d0c gpg: add agent-signing tool 2016-04-27 21:01:21 +03:00
Roman Zeyde
e09571151c gpg: remove length type logging 2016-04-26 21:46:39 +03:00
Roman Zeyde
340aae4fb8 gpg: refactor decode to functional style 2016-04-26 21:38:59 +03:00
Roman Zeyde
9875c9927e gpg: demo for subkeys decoding 2016-04-26 21:12:02 +03:00
Roman Zeyde
d9862ae0e1 gpg: debug logging for ECDSA verification 2016-04-26 12:57:27 +03:00
Roman Zeyde
5fb8b0e047 decode: parse GPG subkeys 2016-04-26 12:54:10 +03:00
Roman Zeyde
324fc21a5c decode: refactor digest calculation 2016-04-26 12:34:50 +03:00
Roman Zeyde
e2f5ccafdf signer: allow importing to local keyring (using "-o" flag) 2016-04-26 10:26:12 +03:00
Roman Zeyde
a0b4776374 gpg: fixup exception message 2016-04-25 18:19:08 +03:00
Roman Zeyde
5abc3dc41b gpg: fix check script -v option 2016-04-24 21:56:09 +03:00
Roman Zeyde
3c2eb64e0d gpg: fixup demo script 2016-04-24 14:25:01 +03:00
Roman Zeyde
67d58a5ae0 Merge pull request #10 from romanz/gpg
GPG v2.1 support
2016-04-24 14:07:22 +03:00
Roman Zeyde
9a435ae23e gpg: minor renames and code refactoring 2016-04-24 14:05:30 +03:00
Roman Zeyde
d7913a84d5 gpg: pydocstyle fixes 2016-04-24 12:22:02 +03:00
Roman Zeyde
a114242243 gpg: small fixes before merging to master 2016-04-24 10:58:32 +03:00
Roman Zeyde
b6dbc4aa81 gpg: small fixes before merging to master 2016-04-23 23:37:11 +03:00
Roman Zeyde
6cc3a629a8 gpg: export git-gpg wrapper
should be used as 'gpg.program' in .git/config
2016-04-23 23:13:06 +03:00
Roman Zeyde
0c94363595 gpg: export command-line tool 2016-04-23 23:12:32 +03:00
Roman Zeyde
40377fc66b gpg: add __init__.py 2016-04-23 22:46:24 +03:00
Roman Zeyde
489c8fe357 gpg: rename git wrapper 2016-04-23 22:45:11 +03:00
Roman Zeyde
6f4f33bfa5 gpg: verify signature after signing 2016-04-23 22:41:43 +03:00
Roman Zeyde
76ce25fab1 gpg: fixup imports 2016-04-23 22:30:12 +03:00
Roman Zeyde
5506310239 gpg: move under trezor_agent 2016-04-23 21:47:30 +03:00
Roman Zeyde
9dc955aae8 gpg: fix signer logging 2016-04-23 19:31:23 +03:00
Roman Zeyde
80f29469d0 gpg: deduce curve name from existing pubkey information 2016-04-23 00:08:45 +03:00
Roman Zeyde
fb368d24eb gpg: use subprocess.call() 2016-04-22 23:48:01 +03:00
Roman Zeyde
8c0848b459 gpg: debug during check 2016-04-22 23:48:01 +03:00
Roman Zeyde
276dec5728 gpg: support ed25519 public keys and signatures 2016-04-22 23:47:25 +03:00
Roman Zeyde
74f7ebf228 gpg: support ed25519 decoding 2016-04-22 22:39:03 +03:00
Roman Zeyde
7ef0958c33 gpg: minor fixes 2016-04-22 21:37:23 +03:00
Roman Zeyde
1402918bb3 gpg: use user name instead of key id 2016-04-22 21:36:56 +03:00
Roman Zeyde
b6cfa0c03f main: show better error when no SSH remote is found 2016-04-22 11:31:00 +03:00
Roman Zeyde
33ff9ba667 signer: update required patch link 2016-04-20 22:42:47 +03:00
Roman Zeyde
ab64505cdb gpg: refactor hexlification of key_id 2016-04-20 21:41:46 +03:00
Roman Zeyde
5651452c0d gpg: rename GPG public key file 2016-04-20 21:41:31 +03:00
Roman Zeyde
af6d0caf33 Add GPG-wrapper script for Git 2016-04-18 23:02:14 +03:00
Roman Zeyde
96592269b6 signer: refactor a bit 2016-04-18 22:10:00 +03:00
Roman Zeyde
b2d078eec6 simplify signer usage
and make less INFO loggin
2016-04-18 21:55:23 +03:00
Roman Zeyde
01dafb0ebd signer: show key ID on TREZOR screen 2016-04-17 23:11:18 +03:00
Roman Zeyde
447faf973c signer should export public key or sign a file 2016-04-17 23:04:23 +03:00
Roman Zeyde
add90e3c51 signer: support armoring public keys 2016-04-17 22:40:02 +03:00
Roman Zeyde
34670c601d Fix PEP8 warnings 2016-04-17 22:19:55 +03:00
Roman Zeyde
b9ba4a3082 split decoding functionality 2016-04-17 22:18:42 +03:00
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
2016-04-16 21:21:12 +03:00
Roman Zeyde
861401e89a client: make get_address() public 2016-04-09 21:09:11 +03:00
Roman Zeyde
335d050212 formats: fixup comment 2016-04-09 20:40:32 +03:00
Roman Zeyde
6e1b08c27a README: fix links 2016-03-12 21:18:13 +02:00
Roman Zeyde
b3a6c76631 bump version 2016-03-12 21:08:52 +02:00
Roman Zeyde
f056f1fac5 fixup lint errors 2016-03-12 21:07:10 +02:00
Roman Zeyde
716dc82312 bump version 2016-03-12 20:58:38 +02:00
Roman Zeyde
0e2a19f7ce client: fixup UT 2016-03-12 20:57:16 +02:00
Roman Zeyde
2cdbc89d28 protocol: fixup UT 2016-03-12 20:57:09 +02:00
Roman Zeyde
1022e54d6a protocol: fail gracefully on cancellation 2016-03-12 20:42:14 +02:00
Roman Zeyde
ea88f425f5 protocol: fail on unsupported commands 2016-03-12 20:40:09 +02:00
Roman Zeyde
000860feaf main: add --test flag for verifying SSH configuration
https://help.github.com/articles/testing-your-ssh-connection/
2016-03-12 15:32:29 +02:00
Roman Zeyde
2a5196003e tests: update for CallException handling 2016-03-06 22:06:45 +02:00
Roman Zeyde
e10b42bbb5 client: catch CallException for cancellation handling 2016-03-06 21:59:17 +02:00
Roman Zeyde
b07d7e6535 server: handle IOError gracefully 2016-03-06 21:58:39 +02:00
Roman Zeyde
4838030be5 factory: add CallException type 2016-03-06 21:58:11 +02:00
Roman Zeyde
c9f341a42b main: handle 'pushurl' and 'url' remote settings 2016-03-06 21:21:25 +02:00
Roman Zeyde
bdd2568b2c main: log pubkey fingerprint on INFO level 2016-03-05 20:49:14 +02:00
Roman Zeyde
ae20ae4a04 bump version 2016-03-05 19:54:51 +02:00
Roman Zeyde
f15c2c7236 README: add trezor-git screencast 2016-03-05 15:12:30 +02:00
Roman Zeyde
e6ccc324a0 main: ignore path from git remote URL
It's much easier to use single keypair per user@host
2016-03-05 14:56:58 +02:00
Roman Zeyde
7c102e435e setup: add more classifiers 2016-03-05 11:29:05 +02:00
Roman Zeyde
7f6bb12b24 bump version 2016-03-05 11:20:11 +02:00
Roman Zeyde
98e875562e main: add trezor-git entry point 2016-03-05 11:18:24 +02:00
Roman Zeyde
4384b93c19 main: remove unneeded use_shell parameter 2016-03-05 11:03:10 +02:00
Roman Zeyde
8a90a8cd84 main: split git from ssh 2016-03-05 10:56:30 +02:00
Roman Zeyde
1e86983782 main: split argument parser 2016-03-05 10:46:36 +02:00
Roman Zeyde
c63201c90c client: show visual challenge 2016-03-05 10:39:47 +02:00
Roman Zeyde
19b00dc427 client: add logging for challenge sizes 2016-02-27 20:09:03 +02:00
Roman Zeyde
aa35981980 README: add 'apt-get' to installation section 2016-02-27 09:49:15 +02:00
Roman Zeyde
8909b38107 main: use command-line for git interaction 2016-02-20 18:24:14 +02:00
Roman Zeyde
6d9aa9cb8a README: license badge is broken most of the time 2016-02-19 20:54:36 +02:00
Roman Zeyde
d6532311b9 fix PEP8 & docstrings 2016-02-19 20:52:59 +02:00
Roman Zeyde
41b30b42b5 main: add git identity via "origin" remote 2016-02-19 20:48:16 +02:00
Roman Zeyde
5b0e56697f travis: add pydocstyle 2016-02-19 11:41:05 +02:00
Roman Zeyde
0e6d998b4c tox: add pydocstyle 2016-02-19 11:39:12 +02:00
Roman Zeyde
2c7fabfa35 tests: add docstrings 2016-02-19 11:35:34 +02:00
Roman Zeyde
1adccdbfe6 __init__: add docstrings 2016-02-19 11:35:27 +02:00
Roman Zeyde
04f4bbf2ac main: add docstrings 2016-02-19 11:35:16 +02:00
Roman Zeyde
bbe963d0ff util: rename UTs 2016-02-19 11:34:58 +02:00
Roman Zeyde
c49514754b util: add docstrings 2016-02-19 11:34:20 +02:00
Roman Zeyde
2ebefff909 server: add docstrings 2016-02-19 11:19:01 +02:00
Roman Zeyde
21e89014c9 protocol: add docstrings and replace custom exceptions 2016-02-19 10:49:39 +02:00
Roman Zeyde
566e4310e1 formats: add docstrings 2016-02-19 10:40:39 +02:00
Roman Zeyde
e1441518d4 factory: add docstrings 2016-02-19 10:08:36 +02:00
Roman Zeyde
5cb12a43de client: add docstrings 2016-02-19 10:07:33 +02:00
Roman Zeyde
df607f3665 pylint: add 'no-member' check 2016-02-18 14:28:16 +02:00
Roman Zeyde
d712509a4e client: show current time instead of identity.path 2016-02-17 15:04:10 +02:00
Roman Zeyde
40e2d9fb2c fixup imports order
isort -rc trezor_agent
2016-02-15 20:53:14 +02:00
Roman Zeyde
cd4cc059d6 main: remove git-config parsing code 2016-02-15 20:52:44 +02:00
Roman Zeyde
2b047f0525 main: refactor shell flag 2016-02-15 20:38:34 +02:00
Roman Zeyde
64776fd294 rename client test 2016-02-15 17:22:57 +02:00
Roman Zeyde
231995bd1a remove trezor module 2016-02-15 17:22:01 +02:00
Roman Zeyde
ff76f17c02 client: elaborate SSH blob parsing 2016-02-13 20:26:23 +02:00
Roman Zeyde
963e80b49b client: move logging from parsing code 2016-02-06 18:32:51 +02:00
Roman Zeyde
dee13b75ea client: remove unneeded 'if' 2016-02-06 18:27:46 +02:00
Roman Zeyde
be86507e00 client: pass index as default argument 2016-02-06 17:52:49 +02:00
Roman Zeyde
2f2663ef94 client: set identity index explicitly 2016-02-06 17:51:57 +02:00
Roman Zeyde
cafa218e19 server: pass handler and add debug option 2016-01-26 21:14:52 +02:00
Roman Zeyde
50b627ed45 protocol: allow debugging SSH message handler 2016-01-26 21:14:27 +02:00
Roman Zeyde
7f36097c15 tests: refactor mocks and fakes 2016-01-22 12:04:24 +02:00
Roman Zeyde
a4b905cd6f bump version 2016-01-19 22:56:54 +02:00
Roman Zeyde
2eff21f96c factory: refactor for easier testing 2016-01-19 22:52:52 +02:00
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.
2016-01-18 22:49:27 +02:00
Roman Zeyde
b101281a5b main: add command-line argument for setting UNIX socket timeout 2016-01-16 22:14:36 +02:00
Roman Zeyde
8c6ac43cf4 Merge Trezor and KeepKey functionality 2016-01-15 13:20:38 +02:00
Kenneth Heutmaker
5932a89dc5 Make it work with KeepKey 2016-01-14 13:28:32 -08:00
Roman Zeyde
2009160ff2 Revert "travis: test with tox"
This reverts commit 3d8072522c.
2016-01-09 17:46:07 +02:00
Roman Zeyde
3d8072522c travis: test with tox 2016-01-09 17:41:17 +02:00
Roman Zeyde
0c63aef719 sort imports using isort tool 2016-01-09 16:06:47 +02:00
Roman Zeyde
c454114c4e README: add gitter chat 2016-01-09 12:15:43 +02:00
Roman Zeyde
f9133f7e05 README: fixup license link 2016-01-09 11:19:33 +02:00
Roman Zeyde
33a6951a96 server: don't crash after single exception 2016-01-08 20:46:49 +02:00
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.
2016-01-08 20:28:38 +02:00
Roman Zeyde
7ea20c7009 test_trezor: verify serialized signature 2016-01-08 17:30:08 +02:00
Roman Zeyde
4247558166 README: add subshell demo 2016-01-08 16:07:29 +02:00
Roman Zeyde
fe1e1d2bb9 server: log command with INFO level 2016-01-08 16:04:57 +02:00
Roman Zeyde
1a5b8118ad setup.py: support for Python 3.4 2016-01-05 20:46:55 +02:00
Roman Zeyde
3a806c6d77 beta release 2016-01-05 19:54:20 +02:00
Roman Zeyde
3b61f86c25 README: fixup license to match the repository 2016-01-05 18:49:49 +02:00
Roman Zeyde
06d84c387c bump version 2016-01-04 22:49:28 +02:00
Roman Zeyde
8347142a99 setup.py: fixup license to match the repository 2016-01-04 21:26:17 +02:00
Roman Zeyde
7dabe2c555 test_protocol: fix bytes->str 2016-01-04 21:03:46 +02:00
Roman Zeyde
d6ee3d8995 tox: add py34 2016-01-04 21:03:27 +02:00
Roman Zeyde
c3fa79e450 Fix a few pylint issues 2016-01-04 19:21:56 +02:00
Roman Zeyde
15b10c9a7e bump version 2016-01-04 19:05:43 +02:00
Roman Zeyde
e19d76398e formats: verify public key according to requested ECDSA curve 2015-12-18 16:04:20 +02:00
Roman Zeyde
535b4d50c7 Fix SSH connection arguments handling 2015-11-27 17:26:06 +02:00
Roman Zeyde
461f38d599 travis: fix up dependency 2015-10-27 19:57:53 +02:00
Roman Zeyde
60571e65dd trezor: add support for Ed25519 SSH keys 2015-10-27 19:49:30 +02:00
Roman Zeyde
34cecb276a README: fix URL 2015-09-19 14:31:40 +03:00
Roman Zeyde
903ba919b3 README: fix whitespace 2015-09-19 14:15:17 +03:00
Roman Zeyde
3184d34440 README: update badges and blog post 2015-09-19 14:14:50 +03:00
Roman Zeyde
d7099cb863 bump version 2015-09-16 22:03:15 +03:00
Roman Zeyde
e3f04f3389 Merge pull request #2 from romanz/pr
trezor: don't ask for passphrase (always use empty one)
2015-09-16 21:59:31 +03:00
Roman Zeyde
e59404737d trezor: fix PEP8 2015-09-16 21:57:48 +03:00
Pavol Rusnak
ca30707789 don't ask for passphrase (always use empty one similarly to TREZOR Connect) 2015-09-16 15:32:47 +02:00
Roman Zeyde
5449411d09 README: update trezorlib version 2015-09-06 11:50:45 +03:00
Roman Zeyde
697d22fede bump version 2015-09-06 11:48:32 +03:00
Roman Zeyde
4f94c9459c setup.py: require up-to-date ecdsa and trezor packages 2015-09-06 11:47:19 +03:00
Roman Zeyde
f5577e1c15 README: verify firmware version 2015-09-04 22:20:33 +03:00
Roman Zeyde
803e3bb738 client: require TREZOR v1.3.4 firmware for SSH NIST256P1 curve support 2015-09-04 13:07:35 +03:00
Roman Zeyde
c11245ea69 README: fixup SSH example 2015-09-02 15:16:21 +03:00
Roman Zeyde
7b5dd3a51b README: update SSH pubkey handling demo 2015-09-02 15:15:06 +03:00
Roman Zeyde
4199c79074 README: update SSH example 2015-09-02 15:12:33 +03:00
Roman Zeyde
38fd938fd4 travis: test on Python 3.4 2015-08-24 16:07:39 +03:00
Roman Zeyde
ad35e03a9f README: add travis badge 2015-08-24 15:14:46 +03:00
Roman Zeyde
dd6fded82d travis: test without trezorlib 2015-08-24 15:13:28 +03:00
Roman Zeyde
8547d00b33 README: fix naming 2015-08-24 14:49:51 +03:00
Roman Zeyde
a4a0c6a802 README: expand 2015-08-24 14:49:05 +03:00
Roman Zeyde
38228baba9 tox: remove unused dependency 2015-08-21 21:24:46 +03:00
Roman Zeyde
a8c6e71825 rename package name to trezor_agent 2015-08-21 21:22:26 +03:00
Roman Zeyde
9a1e49190b setup: remove unused dependency 2015-08-21 21:18:47 +03:00
Roman Zeyde
e4a7d9aa06 trezor: simplify client API 2015-08-19 18:27:06 +03:00
Roman Zeyde
5d510c4a60 trezor: add ping for self-test 2015-08-18 21:30:58 +03:00
Roman Zeyde
783722edce trezor: rename wrapper 2015-08-18 21:30:42 +03:00
Roman Zeyde
592bc78391 trezor: update tests and remove identity issues 2015-08-18 18:52:29 +03:00
Roman Zeyde
bf6b58971a trezor: fix Python 3 bytes issue 2015-08-18 17:54:36 +03:00
Roman Zeyde
8d09d9e928 trezor: add UT for SSH signature 2015-08-18 17:52:07 +03:00
Roman Zeyde
8d45e42d10 trezor: fix UT 2015-08-18 17:05:27 +03:00
Roman Zeyde
dd93dea0a9 trezor: set NIST256P1 curve when needed 2015-08-18 17:03:38 +03:00
Roman Zeyde
5ba24c9386 trezor: forget PIN and shutdown screen on close() 2015-08-18 17:03:00 +03:00
Roman Zeyde
ef8c190c6c trezor: refactor common code 2015-08-18 16:53:16 +03:00
Roman Zeyde
d8489e6a5a trezor.client: set dependencies as keywords 2015-08-18 16:50:46 +03:00
Roman Zeyde
0b8e7be629 test_trezor: add UT 2015-08-18 16:46:56 +03:00
Roman Zeyde
396967433a trezor.client: fix string<>bytes error 2015-08-18 16:46:44 +03:00
Roman Zeyde
b71c1e132c main: rename parser creator 2015-08-17 18:09:45 +03:00
Roman Zeyde
b33a281762 main: simplify trezor_agent 2015-08-17 18:05:13 +03:00
Roman Zeyde
d47741e9ab trezor: __init__ should be short 2015-08-17 17:59:05 +03:00
Roman Zeyde
88e60b4338 util: move to_ascii() from __main__ 2015-08-17 17:56:17 +03:00
Roman Zeyde
b7a1dd61ea trezor: rename kwarg to 'label' 2015-08-17 17:36:25 +03:00
Roman Zeyde
0e28873e0a trezor: move into subpackage 2015-08-17 17:28:41 +03:00
Roman Zeyde
a09d8030f5 tox: omit trezor_library 2015-08-17 10:09:09 +03:00
Roman Zeyde
241b342f43 trezor: split trezorlib-specific code 2015-08-17 10:08:06 +03:00
Roman Zeyde
91d0905364 main: remove unneeded keyword argument 2015-08-17 10:07:32 +03:00
Roman Zeyde
0db45cbd7e tox: omit __main__.py entry point from coverage 2015-08-17 09:33:40 +03:00
Roman Zeyde
bc6bebc79e pylint: disable "locally-disabled" warning 2015-08-17 09:33:18 +03:00
Roman Zeyde
784357238a trezor: show BTC address on device 2015-08-14 09:26:44 +03:00
Roman Zeyde
0a94134d8a main: simplify verification 2015-08-13 18:37:14 +03:00
Roman Zeyde
fb89909146 main: fixup verification 2015-08-13 18:32:51 +03:00
Roman Zeyde
838df004f0 trezor: fix protocol defaults 2015-08-13 18:31:51 +03:00
Roman Zeyde
211c989b1a server: improve coverage 2015-08-13 18:03:25 +03:00
Roman Zeyde
6427ded731 tests: add protocol UT 2015-08-13 18:03:04 +03:00
Roman Zeyde
b634d7cdb9 trezor: fix typo 2015-08-13 17:49:08 +03:00
Roman Zeyde
d403e58be0 trezor: refactor protobuf specific code 2015-08-13 10:04:45 +03:00
Roman Zeyde
622363c1f1 tests: add server UT 2015-08-13 09:56:50 +03:00
Roman Zeyde
c367a1bd9d fix PEP8 2015-08-11 20:46:13 +03:00
Roman Zeyde
f462d49141 install bitcoin Python library 2015-08-11 20:46:00 +03:00
Roman Zeyde
cf572c7417 trezor: fixup sign_identity API 2015-07-22 14:30:36 +03:00
Roman Zeyde
362c2870ad formats: fixup typo 2015-07-22 14:30:12 +03:00
Roman Zeyde
524df20ad6 main: integration identity verification 2015-07-22 13:58:43 +03:00
Roman Zeyde
2ce448f736 formats: split serialization from decompression 2015-07-22 13:47:53 +03:00
Roman Zeyde
38136e76d5 main: rename entry point 2015-07-22 12:51:04 +03:00
Roman Zeyde
85e0cf903e formats: use curve as function argument 2015-07-22 11:58:17 +03:00
Roman Zeyde
e54fda332e trezor: fix assertions 2015-07-22 11:25:36 +03:00
Roman Zeyde
afa42d80a3 tox: skip full Trezor installation 2015-07-21 14:51:11 +03:00
Roman Zeyde
55a0c3ed97 add Tox 2015-07-21 14:38:46 +03:00
Roman Zeyde
e7585170ae fix Py3k issues 2015-07-21 14:38:40 +03:00
Roman Zeyde
b6d0efef9f fix PEP8 2015-07-21 14:38:26 +03:00
Roman Zeyde
709782cb41 tests: add a bit 2015-07-21 14:16:13 +03:00
Roman Zeyde
921b973075 bump version 2015-07-21 14:08:41 +03:00
Roman Zeyde
1a42dbc46d server: disable traceback 2015-07-20 18:29:36 +03:00
Roman Zeyde
86b166ba08 formats: refactor parsing code 2015-07-20 18:29:36 +03:00
Roman Zeyde
b7cc276259 main: fix invocation via ssh 2015-07-20 18:05:59 +03:00
Roman Zeyde
b3ccb4c729 __main__: move entry point 2015-07-20 18:01:34 +03:00
Roman Zeyde
e1e4a6b93e trezor: verify public key correctness 2015-07-20 17:58:52 +03:00
Roman Zeyde
00730069e7 formats: remove magic byte 2015-07-20 17:58:52 +03:00
Roman Zeyde
0c21e33324 trezor: keep session on 2015-07-20 17:58:52 +03:00
Roman Zeyde
1c6b949fe9 formats: log fingerprint for export 2015-07-20 17:58:33 +03:00
Roman Zeyde
7d2b9cf820 agent: allow identity label specification 2015-07-20 16:04:43 +03:00
Roman Zeyde
1296669512 trezor-agent: improve gitconfig parsing 2015-07-06 09:36:56 +03:00
Roman Zeyde
71fde5e704 trezor: fix confirmation message 2015-07-05 21:35:45 +03:00
Roman Zeyde
cdaf4cad36 trezor-agent: add invalid value for assertion 2015-07-05 15:40:38 +03:00
Roman Zeyde
63990bf698 trezor: show identity.path at challenge_visual
It won't be signed when using SSH-agent mode.
2015-07-05 15:35:25 +03:00
Roman Zeyde
ccdf37d371 trezor-agent: refactor identity_from_gitconfig() a bit 2015-07-05 15:10:24 +03:00
Roman Zeyde
df4f3a1b19 trezor-agent: automatic support for git identity
The agent uses local 'git config', and chooses the url marked as "trezor = true".
2015-07-05 14:52:31 +03:00
Roman Zeyde
04afefcba2 protocol: verify EOF after parsing at sign_message() 2015-07-04 21:15:49 +03:00
Roman Zeyde
bd22d7512b trezor: refactor label parsing and handling 2015-07-04 21:15:09 +03:00
76 changed files with 5499 additions and 580 deletions

7
.bumpversion.cfg Normal file
View File

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

3
.gitignore vendored
View File

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

View File

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

29
.travis.yml Normal file
View File

@@ -0,0 +1,29 @@
sudo: false
language: python
python:
- "3.5"
- "3.6"
- "3.7"
- "3.8"
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 -U -e .
script:
- pycodestyle libagent
- pylint --reports=no --rcfile .pylintrc libagent
- pydocstyle libagent
- coverage run --source libagent/ -m py.test -v
after_success:
- coverage report

View File

@@ -1,4 +1,41 @@
# 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)
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).
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.
See the following blog posts about this tool:
- [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)
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.
## Components
This repository contains source code for one library as well as
agents to interact with several different hardware devices:
* [`libagent`](https://pypi.org/project/libagent/): shared library
* [`trezor-agent`](https://pypi.org/project/trezor-agent/): Using Trezor as hardware-based SSH/PGP agent
* [`ledger_agent`](https://pypi.org/project/ledger_agent/): Using Ledger as hardware-based SSH/PGP agent
* [`keepkey_agent`](https://pypi.org/project/keepkey_agent/): Using KeepKey as hardware-based SSH/PGP agent
[![Demo](https://asciinema.org/a/22959.png)](https://asciinema.org/a/22959)
The [/releases](/releases) page on Github contains the `libagent`
releases.
## Documentation
* **Installation** instructions are [here](doc/INSTALL.md)
* **SSH** instructions and common use cases are [here](doc/README-SSH.md)
Note: If you're using Windows, see [trezor-ssh-agent](https://github.com/martin-lizner/trezor-ssh-agent) by Martin Lízner.
* **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)

View File

@@ -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)

42
agents/fake/setup.py Normal file
View File

@@ -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',
]},
)

View File

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

38
agents/keepkey/setup.py Normal file
View File

@@ -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',
]},
)

View File

@@ -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)

40
agents/ledger/setup.py Normal file
View File

@@ -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',
]},
)

38
agents/trezor/setup.py Normal file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python
from setuptools import setup
setup(
name='trezor_agent',
version='0.10.0',
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.13.0',
'trezor[hidapi]>=0.12.0,<0.13'
],
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 :: 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',
]},
)

View File

@@ -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)

15
contrib/neopg-trezor Executable file
View File

@@ -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:])

51
doc/DESIGN.md Normal file
View File

@@ -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

143
doc/INSTALL.md Normal file
View File

@@ -0,0 +1,143 @@
# 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
$ 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
$ 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).

251
doc/README-GPG.md Normal file
View File

@@ -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
```

31
doc/README-NeoPG.md Normal file
View File

@@ -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]
```

69
doc/README-PINENTRY.md Normal file
View File

@@ -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)
```

226
doc/README-SSH.md Normal file
View File

@@ -0,0 +1,226 @@
# 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.
Example:
```
(trezor|keepkey|ledger)-agent -e ed25519 bob@example.com -- rsync up/ bob@example.com:/home/bob
```
As a shortcut you can run
```
$ (trezor|keepkey|ledger)-agent identity@myhost -s
```
to start a shell with the proper environment.
##### 3. 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
Restart=always
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.
`IDENTITY` can be a path (starting with `/`) to a file containing a list of public keys
generated by Trezor. I.e. `/home/myUser/.ssh/trezor.conf` with one public key per line.
This is a more convenient way to have a systemd setup that has to handle multiple
keys/hosts.
When updating the file, make sure to restart trezor-agent.
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.*" | cut -d " " -f 1)
```
Make sure the SSH_AUTH_SOCK variable matches the location of the socket that trezor-agent
is listening on: `ps -x | grep trezor-agent`. In this setup trezor-agent should start
automatically when the socket is opened.
##### 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

26
doc/enigmail.md Normal file
View File

@@ -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)

1
libagent/__init__.py Normal file
View File

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

View File

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

View File

@@ -0,0 +1,70 @@
"""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 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()

View File

@@ -0,0 +1,151 @@
"""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 close(self):
"""Close connection to device.
By default, close the underlying connection. Overriding classes
can perform their own cleanup.
"""
self.conn.close()
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.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__)

View File

@@ -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)

View File

@@ -0,0 +1,23 @@
"""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.transport_webusb import WebUsbTransport
from keepkeylib.types_pb2 import IdentityType
get_public_node = Client.get_public_node
sign_identity = Client.sign_identity
Client.state = None
def find_device():
"""Returns first WebUSB or HID transport."""
for d in WebUsbTransport.enumerate():
return WebUsbTransport(d)
for d in HidTransport.enumerate():
return HidTransport(d)

122
libagent/device/ledger.py Normal file
View File

@@ -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)

137
libagent/device/trezor.py Normal file
View File

@@ -0,0 +1,137 @@
"""TREZOR-related code (see http://bitcointrezor.com/)."""
import binascii
import logging
import semver
from . import interface
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
cached_session_id = None
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,
ui=self.ui,
session_id=self.__class__.cached_session_id)
self._verify_version(connection)
try:
# unlock PIN and passphrase
self._defs.get_address(connection,
"Testnet",
self._defs.PASSPHRASE_TEST_PATH)
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.__class__.cached_session_id = self.conn.session_id
super().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._defs.get_public_node(
self.conn,
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._defs.sign_identity(
self.conn,
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.TrezorFailure 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._defs.get_ecdh_session_key(
self.conn,
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.TrezorFailure as e:
msg = '{} error: {}'.format(self, e)
log.debug(msg, exc_info=True)
raise interface.DeviceError(msg)

View File

@@ -0,0 +1,30 @@
"""TREZOR-related definitions."""
# pylint: disable=unused-import,import-error,no-name-in-module,no-member
import os
import logging
import mnemonic
import semver
import trezorlib
from trezorlib.client import TrezorClient as Client, PASSPHRASE_TEST_PATH
from trezorlib.exceptions import TrezorFailure, PinException
from trezorlib.transport import get_transport
from trezorlib.messages import IdentityType
from trezorlib.btc import get_address, get_public_node
from trezorlib.misc import sign_identity, get_ecdh_session_key
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)

152
libagent/device/ui.py Normal file
View File

@@ -0,0 +1,152 @@
"""UIs for PIN/passphrase entry."""
import logging
import os
import subprocess
try:
from trezorlib.client import PASSPHRASE_ON_DEVICE
except ImportError:
PASSPHRASE_ON_DEVICE = object()
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__
self.cached_passphrase_ack = None
def get_pin(self, _code=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(self.device_name),
prompt='PIN:',
description=description,
binary=self.pin_entry_binary,
options=self.options_getter())
def get_passphrase(self, prompt='Passphrase:', available_on_device=False):
"""Ask the user for passphrase."""
passphrase = None
if self.cached_passphrase_ack:
passphrase = self.cached_passphrase_ack.get()
if passphrase is None:
env_passphrase = os.environ.get("TREZOR_PASSPHRASE")
if env_passphrase is not None:
passphrase = env_passphrase
elif available_on_device:
passphrase = PASSPHRASE_ON_DEVICE
else:
passphrase = interact(
title='{} passphrase'.format(self.device_name),
prompt=prompt,
description=None,
binary=self.passphrase_entry_binary,
options=self.options_getter())
if self.cached_passphrase_ack:
self.cached_passphrase_ack.set(passphrase)
return passphrase
def button_request(self, _code=None):
"""Called by TrezorClient when device interaction is required."""
# XXX: show notification to the user?
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()

212
libagent/formats.py Normal file
View File

@@ -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]

324
libagent/gpg/__init__.py Normal file
View File

@@ -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(['--homedir', homedir,
'--list-secret-keys', args.user_id]))
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.ui.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=0)
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.ui.cached_passphrase_ack = util.ExpiringCache(
seconds=float(args.cache_expiry_seconds))
return args.func(device_type=device_type, args=args)

247
libagent/gpg/agent.py Normal file
View File

@@ -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')

48
libagent/gpg/client.py Normal file
View File

@@ -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)

320
libagent/gpg/decode.py Normal file
View File

@@ -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

103
libagent/gpg/encode.py Normal file
View File

@@ -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

259
libagent/gpg/keyring.py Normal file
View File

@@ -0,0 +1,259 @@
"""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 not line.startswith(b'S PROGRESS'):
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

276
libagent/gpg/protocol.py Normal file
View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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'')

View File

@@ -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

View File

@@ -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')

166
libagent/server.py Normal file
View File

@@ -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

312
libagent/ssh/__init__.py Normal file
View File

@@ -0,0 +1,312 @@
"""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 = ', '.join(sorted(formats.SUPPORTED_CURVES))
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.ui.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

65
libagent/ssh/client.py Normal file
View File

@@ -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

160
libagent/ssh/protocol.py Normal file
View File

@@ -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)

View File

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

View File

@@ -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)

View File

@@ -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

View File

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

View File

@@ -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))

View File

@@ -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')]

View File

@@ -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)

146
libagent/tests/test_util.py Normal file
View File

@@ -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

280
libagent/util.py Normal file
View File

@@ -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
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

6
release.sh Executable file
View File

@@ -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/*

41
setup.py Normal file → Executable file
View File

@@ -2,28 +2,45 @@
from setuptools import setup
setup(
name='sshagent',
version='0.2',
description='Using Trezor as hardware SSH agent',
name='libagent',
version='0.14.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=['sshagent'],
install_requires=['ecdsa', 'trezor'],
packages=[
'libagent',
'libagent.device',
'libagent.gpg',
'libagent.ssh'
],
install_requires=[
'docutils>=0.14',
'wheel>=0.32.3',
'backports.shutil_which>=3.5.1',
'ConfigArgParse>=0.12.1',
'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 = sshagent.trezor_agent:main'
]},
)

View File

View File

@@ -1,81 +0,0 @@
import io
import hashlib
import base64
import ecdsa
from . import util
import logging
log = logging.getLogger(__name__)
DER_OCTET_STRING = b'\x04'
curve = ecdsa.NIST256p
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):
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 parse_public_key(data):
file_type, base64blob, name = data.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
def decompress_pubkey(pub):
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))
y = (P-beta) if ((beta + ord(pub[0])) % 2) else beta
return (x, y)
def export_public_key(pubkey, label):
x, y = decompress_pubkey(pubkey)
point = ecdsa.ellipticcurve.Point(curve.curve, x, y)
vk = ecdsa.VerifyingKey.from_public_point(point, curve=curve,
hashfunc=hashfunc)
key_type = 'ecdsa-sha2-nistp256'
curve_name = 'nistp256'
parts = [key_type, curve_name, '\x04' + vk.to_string()]
b64 = base64.b64encode(''.join([util.frame(p) for p in parts]))
return '{} {} {}\n'.format(key_type, b64, label)

View File

@@ -1,100 +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 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''
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 ValueError('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)
success = key['verifying_key'].verify(signature=signature, data=blob,
sigdecode=lambda sig, _: sig)
log.info('signature status: %s', 'OK' if success else 'ERROR')
if not success:
raise ValueError('invalid 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)

View File

@@ -1,98 +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__)
@contextlib.contextmanager
def unix_domain_socket_server(sock_path):
log.debug('serving on SSH_AUTH_SOCK=%s', sock_path)
try:
os.remove(sock_path)
except OSError:
if os.path.exists(sock_path):
raise
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(sock_path)
server.listen(1)
try:
yield server
finally:
os.remove(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 error: %s', e, exc_info=True)
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.parse_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

View File

@@ -1,157 +0,0 @@
import io
import struct
import binascii
from . import util
from . import formats
import logging
log = logging.getLogger(__name__)
class TrezorLibrary(object):
@staticmethod
def client():
# pylint: disable=import-error
from trezorlib.client import TrezorClient
from trezorlib.transport_hid import HidTransport
devices = HidTransport.enumerate()
if len(devices) != 1:
raise ValueError('{:d} Trezor devices found'.format(len(devices)))
return TrezorClient(HidTransport(devices[0]))
@staticmethod
def parse_identity(s):
# pylint: disable=import-error
from trezorlib.types_pb2 import IdentityType
return IdentityType(**_string_to_identity(s))
class Client(object):
curve_name = 'nist256p1'
def __init__(self, factory=TrezorLibrary):
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]
log.debug('version : %s', '.'.join([str(v) for v in version]))
log.debug('revision : %s', binascii.hexlify(f.revision))
def __enter__(self):
return self
def __exit__(self, *args):
log.info('disconnected from Trezor')
self.client.clear_session()
self.client.close()
def get_identity(self, label):
return self.factory.parse_identity(label)
def get_public_key(self, identity):
label = _identity_to_string(identity)
log.info('getting "%s" public key from Trezor...', label)
addr = _get_address(identity)
node = self.client.get_public_node(addr, self.curve_name)
pubkey = node.node.public_key
return formats.export_public_key(pubkey=pubkey, label=label)
def sign_ssh_challenge(self, label, blob):
identity = self.factory.parse_identity(label)
msg = _parse_ssh_blob(blob)
log.info('confirm user %s connection to %r using Trezor...',
msg['user'], label)
s = self.client.sign_identity(identity=identity,
challenge_hidden=blob,
challenge_visual='',
ecdsa_curve_name=self.curve_name)
assert len(s.signature) == 65
assert s.signature[0] == b'\x00'
sig = s.signature[1:]
r = util.bytes2num(sig[:32])
s = util.bytes2num(sig[32:])
return (r, s)
def _lsplit(s, sep):
p = None
if sep in s:
p, s = s.split(sep, 1)
return (p, s)
def _rsplit(s, sep):
p = None
if sep in s:
s, p = s.rsplit(sep, 1)
return (s, p)
def _string_to_identity(s):
proto, s = _lsplit(s, '://')
user, s = _lsplit(s, '@')
s, path = _rsplit(s, '/')
host, port = _rsplit(s, ':')
if not proto:
proto = 'ssh' # otherwise, Trezor will use SECP256K1 curve
result = [
('proto', proto), ('user', user), ('host', host),
('port', port), ('path', path)
]
return {k: v for k, v in result if v}
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)
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)
res['pubkey'] = util.read_frame(i)
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']))
pubkey = formats.parse_pubkey(res['pubkey'])
log.debug('fingerprint: %s', pubkey['fingerprint'])
return res

View File

@@ -1,63 +0,0 @@
import os
import sys
import argparse
from . import trezor
from . import server
from . import formats
import logging
log = logging.getLogger(__name__)
def main():
fmt = '%(asctime)s %(levelname)-12s %(message)-100s [%(filename)s:%(lineno)d]'
p = argparse.ArgumentParser()
p.add_argument('-v', '--verbose', default=0, action='count')
p.add_argument('identity', type=str,
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')
args = p.parse_args()
levels = [logging.WARNING, logging.INFO, logging.DEBUG]
level = levels[min(args.verbose, len(levels))]
logging.basicConfig(level=level, format=fmt)
with trezor.Client(factory=trezor.TrezorLibrary) as client:
identity = client.get_identity(label=args.identity)
public_key = client.get_public_key(identity=identity)
command, use_shell = args.command, False
if args.connect:
to_ascii = lambda s: s.encode('ascii')
command = ['ssh', to_ascii(identity.host)]
if identity.user:
command += ['-l', to_ascii(identity.user)]
if identity.port:
command += ['-p', to_ascii(identity.port)]
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
signer = client.sign_ssh_challenge
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')

View File

@@ -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

24
tox.ini Normal file
View File

@@ -0,0 +1,24 @@
[tox]
envlist = py3
[pycodestyle]
max-line-length = 100
[pep257]
add-ignore = D401
[testenv]
deps=
pytest
mock
pycodestyle
coverage
pylint
semver
pydocstyle
isort
commands=
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