ProtonMail / go-crypto

Fork of go/x/crypto, providing an up-to-date OpenPGP implementation
https://pkg.go.dev/github.com/ProtonMail/go-crypto
BSD 3-Clause "New" or "Revised" License
328 stars 99 forks source link

Loading encryption subkey to smartcard mutates fingerprint #199

Closed alex-u410 closed 5 months ago

alex-u410 commented 5 months ago

Hello, I am trying to generate a ed+cv25519 signing+encryption key on a smartcard. When I push the keys to the smartcard using gpg-connect-agent, the encryption subkey fingerprint gets mutated relative to the one in my keyring.

Here is my debugging code to create an entity and output a secret key file

ent := []byte{
    0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
    0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
    0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
    0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
    0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
    0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
    0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
    0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40,
}
now := time.Unix(1709329403, 0)
cfgTime := func() time.Time { return now }
cfg := &propacket.Config{
    Rand:            bytes.NewReader(ent),
    Algorithm:       propacket.PubKeyAlgoEdDSA,
    Curve:           propacket.Curve25519,
    KeyLifetimeSecs: 0, // no expiration
    Time:            cfgTime,
    V5Keys:          false,
}
entity, err := propgp.NewEntity(keyID, "", "", cfg)

...

serNew := bytes.NewBuffer(nil)
err := entity.SerializePrivate(serNew, nil)
if err != nil {
    panic("SerializePrivate new" + err.Error())
}
_ = os.WriteFile(secretPath, serNew.Bytes(), 0644)

I load that key into my keyring and get the fingerprints + keygrips

>gpg --import <secretPath>
gpg: key EA0063F6C7ACB985: public key "test-sc" imported
gpg: key EA0063F6C7ACB985: secret key imported
gpg: Total number processed: 1
gpg:               imported: 1
gpg:       secret keys read: 1
gpg:  secret keys unchanged: 1

>gpg --show-key --with-colons <secretPath>                                                                                                  
sec:-:255:22:EA0063F6C7ACB985:1709329403:::-:::scESC:::::ed25519:::0:
fpr:::::::::D3A4BD8DD05557DC6C469D37EA0063F6C7ACB985:
grp:::::::::11FA927B453C99646DCE42A4EC33E7CDE9B5D80A:
uid:-::::1709329403::26898656831648DBFB10988947B1523808F13B24::test-sc::::::::::0:
ssb:-:255:18:9090FA627420EF33:1709329403::::::e:::::cv25519::
fpr:::::::::3CDC4501333D5FA52B98F28A9090FA627420EF33:
grp:::::::::6BD4B392CE9CA0994038B4ACB2A8EEA33C42B73B:

> gpg --with-subkey-fingerprints --list-key test-sc
pub   ed25519 2024-03-01 [SC]
      D3A4BD8DD05557DC6C469D37EA0063F6C7ACB985
uid           [ unknown] test-sc
sub   cv25519 2024-03-01 [E]
      3CDC4501333D5FA52B98F28A9090FA627420EF33

The fingerprints from the file match the ones in my keyring, as expected.

I then load the keys to the card:

NOTE: I am formatting the key's timestamp by converting it to a time.Time object and formatting with keyTimestamp.UTC().Format("20060102T150405")

>gpg-connect-agent 
> KEYTOCARD 11FA927B453C99646DCE42A4EC33E7CDE9B5D80A D2760001240100000006199714170000 OPENPGP.1 20240301T214323
> KEYTOCARD 6BD4B392CE9CA0994038B4ACB2A8EEA33C42B73B D2760001240100000006199714170000 OPENPGP.2 20240301T214323 
> /bye

However, when I run gpg --card-status, I see the following fingerprints:

Signature key ....: D3A4 BD8D D055 57DC 6C46  9D37 EA00 63F6 C7AC B985
      created ....: 2024-03-01 21:43:23
Encryption key....: 7684 69AC 284E EABB 2000  4B7C CFF6 8A7F 6CBA 9554
      created ....: 2024-03-01 21:43:23

The signature key's fingerprint matches the one in my keyring, but the encryption subkey does not.

If I instead generate a key with gpg, both fingerprints stay the same when loading to a smartcard:

>gpg --batch --passphrase "" --quick-generate-key test-sc-2 ed25519 sign never                                                                      
gpg: revocation certificate stored as '~/.gnupg/openpgp-revocs.d/0A842270AB61EFC33F2E298B1717B3D9583B854F.rev'

>gpg --batch --passphrase "" --quick-add-key 0A842270AB61EFC33F2E298B1717B3D9583B854F cv25519 encr never

> gpg --export-secret-key 0A842270AB61EFC33F2E298B1717B3D9583B854F > foo

> gpg --show-key --with-colons foo
sec:u:255:22:1717B3D9583B854F:1709585144:::u:::scESC:::::ed25519:::0:
fpr:::::::::0A842270AB61EFC33F2E298B1717B3D9583B854F:
grp:::::::::6D886491D5771C42D8ADF0BE748E79B86F7D8B4E:
uid:u::::1709585144::6B8541341C8D57622DF9AD9AE28036BCD5DB5D97::test-sc-2::::::::::0:
ssb:u:255:18:C5721B49EBE22FE3:1709585186::::::e:::::cv25519::
fpr:::::::::3F718B3457D3AC317031DCA2C5721B49EBE22FE3:
grp:::::::::7038DDE1064A68FB96BAF95D0EC985E309290599:

> gpg-connect-agent 
> KEYTOCARD 6D886491D5771C42D8ADF0BE748E79B86F7D8B4E D2760001240100000006199714170000 OPENPGP.1 20240304T204544
> KEYTOCARD 7038DDE1064A68FB96BAF95D0EC985E309290599 D2760001240100000006199714170000 OPENPGP.2 20240304T204626
> /bye

> gpg --card-status

...
Signature key ....: 0A84 2270 AB61 EFC3 3F2E  298B 1717 B3D9 583B 854F
      created ....: 2024-03-04 20:45:44
Encryption key....: 3F71 8B34 57D3 AC31 7031  DCA2 C572 1B49 EBE2 2FE3
      created ....: 2024-03-04 20:46:26
...

This behavior indicates some discrepancy being created in the go-crypto serialized secret file, like perhaps a field is not getting set, and then gets set by gpg-connect-agent when loading. However, I've tried a lot of different flags and the depth of Entity configuration is a bit dizzying so I thought I'd see if any of the maintainers have thoughts here.

A few notes:

Any help would be appreciated! Hopefully I've just overlooked something obvious in the configuration.

alex-u410 commented 5 months ago

The problem was two-fold:

  1. I was not adding the ECDH params to KEYTOCARD. I should have been using e.g. KEYTOCARD 6D886491D5771C42D8ADF0BE748E79B86F7D8B4E D2760001240100000006199714170000 OPENPGP.1 20240304T204544 03010A09
  2. Even with the added ECDH params, I needed to update gpg, as older versions (2.3) do not work with this argument.

Hope this helps someone! I will close the issue as it is unrelated to this repo :)