romanz / trezor-agent

Hardware-based SSH/GPG/age agent
GNU Lesser General Public License v3.0
568 stars 152 forks source link

FR: Authentication, Signing Subkeys #355

Open jrruethe opened 3 years ago

jrruethe commented 3 years ago

While playing with this, I noticed that the only subkey that is generated is the Encryption subkey. Signing and Authentication are tied to the master key. Example:

$ gpg -k
/home/user/Downloads/tmp/trezor/pubring.kbx
-------------------------------------------
pub   nistp256 1970-01-01 [SCA]
      39597EBBBC8A6E2B7B8C29929DB331349C28E5AA
uid           [ultimate] test@example.com
sub   nistp256 1970-01-01 [E]

From my Yubikey experience, the best practice is to have the primary key only support Certify, with 3 subkeys that support Encryption, Signing, and Authentication. Then, the primary key is backed up to cold storage, and the subkeys are moved/copied to a few Yubikeys. One example of this documentation is here.

I'm familiar with python and I might take a stab at implementing this and doing a pull request, but I wanted to discuss the feasibility with you first. From what I can tell, this line is where the primary key is set to both certify and sign (I'm not sure how Authenticate is also set, still looking), while this line is where the subkey type is set. Then here is where the primary and single subkey are created.

It appears that the modification would be to simply create two more subkeys in that location, passing in the key flags, however the protocol.PublicKey object only takes in a flag ecdh=False indicating whether the key is for encryption or not. This interface appears to be everywhere. I think if I tried to modify this, I would change that bool to an enumeration of encryption, authentication, and signing.

This leads to my next question about feasibility. I need to research this a bit more, but if I understand correctly, the signing and authentication keys act very similarly. I'm not exactly sure if the nature of Trezor's deterministic key generation implies that the two would end up being the same "key". Likewise, if I were to move the signing key from the primary to the subkey, would it actually just be the same key material? From what I can see in the current code, the only differentiation between the primary and subkeys is the ecdh boolean. See here.

Anyway, any help or guidance on this would be greatly appreciated.

jrruethe commented 3 years ago

I did a little more digging, and I think I answered my own question. It looks like the derivation index is set here, and SLIP 13/17 is set here.

It looks to me like I can use SLIP 13, index 0 for Certify, index 1 for Signing, and index 2 for Authentication. That would create 3 different keys, plus the one for encryption using SLIP 17.

I might try to do a POC.

jrruethe commented 3 years ago

I was able to get something working. Here is the result:

2021.07.13 12:54:23 0
user@thinkpad:~/Downloads/tmp
$ rm -rf trezor/; trezor-gpg init "test@example.com"
2021-07-13 12:54:50,071 WARNING      This GPG tool is still in EXPERIMENTAL mode, so please note that the API and features may change without backwards compatibility! [__init__.py:148]
2021-07-13 12:54:50,080 WARNING      NOTE: in order to re-generate the exact same GPG key later, run this command with "--time=0" commandline flag (to set the timestamp of the GPG key manually). [__init__.py:32]
gpg: inserting ownertrust of 6
gpg: checking the trustdb
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: depth: 0  valid:   1  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 1u
sec   nistp256 1970-01-01 [SCA]
      39597EBBBC8A6E2B7B8C29929DB331349C28E5AA
uid           [ultimate] test@example.com
ssb   nistp256 1970-01-01 [S]
ssb   nistp256 1970-01-01 [A]
ssb   nistp256 1970-01-01 [E]

2021.07.13 12:55:15 0
user@thinkpad:~/Downloads/tmp
$ gpg -k --with-fingerprint --with-subkey-fingerprint --with-keygrip
/home/user/Downloads/tmp/trezor/pubring.kbx
-------------------------------------------
pub   nistp256 1970-01-01 [SCA]
      3959 7EBB BC8A 6E2B 7B8C  2992 9DB3 3134 9C28 E5AA
      Keygrip = CF5C5563251A4CF60704CDCB536A020911054CB9
uid           [ultimate] test@example.com
sub   nistp256 1970-01-01 [S]
      D5DB 0875 4D13 FEEC E3BC  2053 86D4 8592 B3F7 0311
      Keygrip = C52FB3B8725694DF3E8044F6A9BF4E0E6827FEA0
sub   nistp256 1970-01-01 [A]
      D093 D21E C582 81AA 8660  659E 3F75 37A7 3375 8B7A
      Keygrip = 55B3B462CB8F843B3D0123EA28DB49E89AF81891
sub   nistp256 1970-01-01 [E]
      78AB E1E8 DC68 39AA 9315  93B4 618A 2AAA 69DA AA38
      Keygrip = 5D17BDC19CDCCA819DB43723D828CD55FF9F25B4

The trezor asked me to sign 6 times, presumably 3 times for the primary, and 3 times for the subkeys. After that, I deleted the trezor folder and did it again, all the fingerprints and keygrips were the same.

Here are some tests for signing and encryption:

2021.07.13 12:55:23 0
user@thinkpad:~/Downloads/tmp
$ echo "This is a test" > test.txt

2021.07.13 12:59:07 0
user@thinkpad:~/Downloads/tmp
$ gpg -a -b test.txt 
gpg: using "test@example.com" as default secret key for signing

2021.07.13 12:59:42 0
user@thinkpad:~/Downloads/tmp
$ gpg -v test.txt.asc 
gpg: WARNING: no command supplied.  Trying to guess what you mean ...
gpg: assuming signed data in 'test.txt'
gpg: Signature made Tue 13 Jul 2021 12:59:39 PM CDT
gpg:                using ECDSA key 39597EBBBC8A6E2B7B8C29929DB331349C28E5AA
gpg:                issuer "test@example.com"
gpg: Good signature from "test@example.com" [uncertain]
gpg: using pgp trust model
gpg: binary signature, digest algorithm SHA512, key algorithm nistp256

It looks like it used the primary key for signing this, not the subkey. I think that is because the primary key is still flagged as "SCA", not as "C". I'll see if I can fix that.

Encryption works as expected, though nothing really changed here:

2021.07.13 13:01:27 0
user@thinkpad:~/Downloads/tmp
$ gpg --encrypt test.txt
You did not specify a user ID. (you may use "-r")

Current recipients:

Enter the user ID.  End with an empty line: test

Current recipients:
nistp256/618A2AAA69DAAA38 1970-01-01 "test@example.com"

Enter the user ID.  End with an empty line: 

2021.07.13 13:04:47 0
user@thinkpad:~/Downloads/tmp
$ ls
test.txt  test.txt.asc  test.txt.gpg  trezor

2021.07.13 13:04:49 0
user@thinkpad:~/Downloads/tmp
$ gpg --decrypt test.txt.gpg 
gpg: WARNING: cipher algorithm AES256 not found in recipient preferences
gpg: encrypted with 256-bit ECDH key, ID 618A2AAA69DAAA38, created 1970-01-01
      "test@example.com"
This is a test

My modifications for this are here. I plan on doing a proper pull request, but I want to clean up and comment my modifications first. Also, I only have a Trezor, so I don't know how to make the same modifications for the other devices and test.

I plan on also updating this script to allow exporting the Trezor-generated subkeys so I can load them onto a Yubikey. Of course, that action is dangerous and should only be performed on an offline LiveCD like Tails.

Anyway, hope this is useful for someone besides me. Thanks.

jrruethe commented 3 years ago

Hmm, I'm not sure why the primary key is still set to "SCA". When I look at the packets themselves, the keyflags for the primary key are properly set to 0x01, which should indicate Certify only.

2021.07.13 16:35:36 0
user@thinkpad:~/Downloads/tmp
$ gpg -a --export test | gpg --list-packets --verbose
# off=0 ctb=98 tag=6 hlen=2 plen=82
:public key packet:
    version 4, algo 19, created 0, expires 0
    pkey[0]: 082A8648CE3D030107 nistp256 (1.2.840.10045.3.1.7)
    pkey[1]: 040DA3A191F01DC8D9061F9048E4460C3979BE13A07F65ACE4843AB1C82EE9253A2C976485A80DCE0D4F07C09B1E79444C99B64B5D8A9F9F4BA60BC0687461C101
    keyid: 9DB331349C28E5AA
# off=84 ctb=b4 tag=13 hlen=2 plen=16
:user ID packet: "test@example.com"
# off=102 ctb=88 tag=2 hlen=2 plen=128
:signature packet: algo 19, keyid 9DB331349C28E5AA
    version 4, created 0, md5len 0, sigclass 0x13
    digest algo 8, begin of digest 97 ad
    hashed subpkt 2 len 4 (sig created 1970-01-01)
    hashed subpkt 11 len 1 (pref-sym-algos: 9)
    hashed subpkt 27 len 1 (key flags: 01)
    hashed subpkt 21 len 3 (pref-hash-algos: 8 9 10)
    hashed subpkt 22 len 3 (pref-zip-algos: 2 3 1)
    hashed subpkt 23 len 1 (keyserver preferences: 80)
    hashed subpkt 30 len 1 (features: 01)
    subpkt 16 len 8 (issuer key ID 9DB331349C28E5AA)
    subpkt 26 len 10 (policy: TREZOR-GPG)
    data: A9930A75937BB135F8DFD0607474DA8B09CD0BDB29E7826FB3F4E39C871070DE
    data: 38F314F95E1F26B098F81853A1F28B66628CE348815D4612F10EEC410E6CB989
# off=232 ctb=b8 tag=14 hlen=2 plen=82
:public sub key packet:
    version 4, algo 19, created 0, expires 0
    pkey[0]: 082A8648CE3D030107 nistp256 (1.2.840.10045.3.1.7)
    pkey[1]: 0471EF74CCED04A5AD4C232345611A43FDA802DFF7736FF1CFFD738F2CC57E4C1EEBE22873B33748459E0A38ED9841C3289D092F41112EC36E462C0000A753E668
    keyid: 86D48592B3F70311
# off=316 ctb=88 tag=2 hlen=2 plen=205
:signature packet: algo 19, keyid 9DB331349C28E5AA
    version 4, created 0, md5len 0, sigclass 0x18
    digest algo 8, begin of digest 6a a7
    hashed subpkt 2 len 4 (sig created 1970-01-01)
    hashed subpkt 27 len 1 (key flags: 02)
    subpkt 16 len 8 (issuer key ID 9DB331349C28E5AA)
    subpkt 32 len 94 (signature: v4, class 0x19, algo 19, digest algo 8)
    subpkt 26 len 10 (policy: TREZOR-GPG)
    data: 456299E45F32EE5793123C8FED527AE00C25484DF338C8C42D9BC29947B41767
    data: 47B9B51668A401325BE59E2B5A140F5939D475A0DF427D6669ADE1BFE1083B32
# off=523 ctb=b8 tag=14 hlen=2 plen=82
:public sub key packet:
    version 4, algo 19, created 0, expires 0
    pkey[0]: 082A8648CE3D030107 nistp256 (1.2.840.10045.3.1.7)
    pkey[1]: 0491D76708DF0DA620BB5B1ABFAC689E92FD1992F0EE2A6D6EE6197B90173BDCE8435998AA4FBC15AC472381F95DCFD2573F27084ECD722E4EEF419D685579ED3C
    keyid: 3F7537A733758B7A
# off=607 ctb=88 tag=2 hlen=2 plen=205
:signature packet: algo 19, keyid 9DB331349C28E5AA
    version 4, created 0, md5len 0, sigclass 0x18
    digest algo 8, begin of digest 30 2f
    hashed subpkt 2 len 4 (sig created 1970-01-01)
    hashed subpkt 27 len 1 (key flags: 20)
    subpkt 16 len 8 (issuer key ID 9DB331349C28E5AA)
    subpkt 32 len 94 (signature: v4, class 0x19, algo 19, digest algo 8)
    subpkt 26 len 10 (policy: TREZOR-GPG)
    data: E2BB7E4CB034FA79ACF93BC17AE08F75DA34C4E3C7567EB58E35058A68D901F6
    data: 12AF215B19361F3AE41DB95568825FD8B891FF78E81082BA615B48B1A2ECAEC7
# off=814 ctb=b8 tag=14 hlen=2 plen=86
:public sub key packet:
    version 4, algo 18, created 0, expires 0
    pkey[0]: 082A8648CE3D030107 nistp256 (1.2.840.10045.3.1.7)
    pkey[1]: 04A143ABC26CF74C5C71EC214AC62FC655BEB2E5FBEC24E1C23D200DFF29E28C9DA9CFF9329D4D4B91446FD982902FE33A8C5E06C64ECC7294F616863C13E2C7D5
    pkey[2]: 03010807
    keyid: 618A2AAA69DAAA38
# off=902 ctb=88 tag=2 hlen=2 plen=109
:signature packet: algo 19, keyid 9DB331349C28E5AA
    version 4, created 0, md5len 0, sigclass 0x18
    digest algo 8, begin of digest 9a 5b
    hashed subpkt 2 len 4 (sig created 1970-01-01)
    hashed subpkt 27 len 1 (key flags: 0C)
    subpkt 16 len 8 (issuer key ID 9DB331349C28E5AA)
    subpkt 26 len 10 (policy: TREZOR-GPG)
    data: C734D6C7ABED68CC6D88AAEF07B8F01DD5C5B2C6A856DF78C82C775184955442
    data: 31B4B176B4D5C883CB044A1C0AB0B27F88CFFB068F55E88C05C8B02B6E9036CB
jrruethe commented 3 years ago

Ok, I believe I have this feature working. There were some nuances I needed to figure out. I'm doing some code cleanup, testing, and ensuring I didn't break any backward compatibility.

I intend to submit a pull request soon.

jrruethe commented 3 years ago

I have submitted a pull request for this feature: https://github.com/romanz/trezor-agent/pull/358

doolio commented 1 year ago

For me only Signing (S) and Certification (C) are tied to the primary key and not Authentication (A). In any case, to remove the signing permission tied to the primary key the following worked for me:

$ gpg --edit-key '<fingerprint>'
gpg> change-usage
S
Q
gpg> save

where $ is your terminal user prompt, gpg> is your gpg prompt, S is the selection to toggle the sign capability, and Q is selection to specify you have finished with the change-usage command. I hope this is clear. I believe the same process can be used to add a permission to a specific key if desired.