Gradle: Dependency verification: Work with PGP fingerprints instead of long key IDs

Created on 29 Dec 2019  路  36Comments  路  Source: gradle/gradle

First of all thanks to @melix and @vlsi for their work on dependency verification. Very cool!

I think this issue should be discussed before the signature verifying part of the dependency verification feature goes public.

Expected Behavior

The part of the dependency verification that verifies PGP signatures should work with PGP Fingerprints instead of Long Key IDs.
Maybe like this:
<pgp fingerprint="B801E2F8EF035068EC1139CC29579F18FA8FD93B"/>

PGP fingerprints are often displayed using upper case letters, so I did it here as well for cosmetic reasons. However, since it's not feasible for humans to compare fingerprints by eye it's not that important.

Current Behavior

The current (unreleased) implementation for verifying PGP signatures seems to use "Long Key IDs". (Long Key IDs are the 64 lowest bits of the PGP Key fingerprint)
Like this:
<pgp value="29579f18fa8fd93b"/>

The problem with long key IDs (as with short Key IDs) is that collisions are possible:

So, while of course having the PGP signature of our dependencies verified is much better than not doing any signature verification, it still would be better if the gradle dependency verification would work with PGP fingerprints instead of long key IDs.

The above notes are of importance if the public keys are fetched from a key server by their long key id. If the trusted public keys are stored locally (e.g. in the project's repo) this is not an issue. Locally stored public keys is probably the preferred way to go for projects with high security requirements (which also eliminates connectivity issues with key servers).

feature contributor

Most helpful comment

I think I have all the required information now, we all agree I think, thanks for the clarification, very helpful!

All 36 comments

I think this is a reasonable requirement, I'll try to do something.

@p- There's something which is unclear to me. Unless I'm mistaken, a PGPSignature only gives access to the _long_ key id of the signer. This means that while I can have a trusted keyring using fingerprints, in practice when I see a signature, I can only get the long id so I have to search publicly with the long id. Is this correct? If so, it seems that for ignored key, I can only use long ids (and not fingerprints). Then for keys downloaded from public servers, I can download the public key and do the verification using the full fingerprint.

Maybe I'm missing something obvious around fingerprints in signatures?

@melix , I can look into it if you don't mind (e.g. try implementing it for checksum-dependency-plugin).

Is this correct?

The key feature here is as follows:
1) The system downloads the public key
2) As it has the key it can verify its full fingerprint
3) It can reject any keys that do not match the desired full fingerprint
4) Then, if the validation passes, then it means the archive is signed with the desired key (that has its fingerprint listed in the config file)

I'm probably missing something obvious. A signature uses a _long_ key id, not the full pringerprint. This means that the only thing we have is the long key id. From this, we can either:

  • use the local keyring to find the key (feature provided by Gradle) or
  • download the key from a public key server

even if there's a collision, what we get in the end is a public key. So far so good. Then, what we do in any case is verifying the signature using the found public key. I assume the signature uses in any case the _full_ fingerprint, so even if there's a collision, verification would fail if it wasn't the "real" public key, no?

So I would be tempted to think that it doesn't really matter what we use as the key here, as we would fail if it's not verified.

Of course, I can explicitly use the full fingerprint in the configuration file, but it wouldn't mean it would be any more secure. Also as I said for _ignored_ keys you have no option but using the long id.

A signature uses a long key id, not the full pringerprint.

That is false.
Signature is using private key material.
The fingerprint is just a SHA1 of the private material (or something like that)
key id is the last bits of the fingerprint.

In other words, both key id and fingerprint are the ways for humans to tell if the key is "the same or not".
However, signing is performed with the full key, not just its fingerprint.

In the same way, when you validate the signature, you need the full public key, not just its fingerprint.

You download the full public key from the internet, then you validate if it confirms the signature is valid.

Does that make sense?

Also as I said for ignored keys you have no option but using the long id.

Why's that?

Signature is using private key material.
The fingerprint is just a SHA1 of the private material (or something like that)

Yeah sorry wrong wording on my side. It's not necessarily the SHA1, can be something else.

What I'm saying is that when I read a PGPSignature, I can get access to the long key id, but not the full fingerprint. I don't know if it's a limitation of Bouncycastle, or that the information is not available because of a limitation of the signature format itself. I'm unfortunately not fully understanding what I'm doing here.

Now, what I'm also saying is that in any case, we get a long id, a full fingerprint or just an boolean, whatever public key we get is going to be used for verification and there it _should fail_.

So I can understand you want to make sure the key you downloaded from a public server is actually the key you expected using its full fingerprint, but if it wasn't, verification would still fail in any case, no?

So I can understand you want to make sure the key you downloaded from a public server is actually the key you expected using its full fingerprint

I'm not sure if all the servers allow downloading a key by its full fingerprint, however, you can download key(s) by its key id, and filter out the ones that do not match the full fingerprint.

The problem is not org.bouncycastle.openpgp.PGPPublicKey#getFingerprint but on on PGPSignature. As far as I can tell, a signature _always_ uses a long ID. So yes, I can deal with fingerprints for the public keys, downloading, whatever. But, when it comes to figuring out what keys were used to sign, what you get is a long id.

PGPSignature

Signature itself is nothing.
You can't even validate the signature without a public key.

So when you configure "I trust PGP with fingerprint cafebabecafebabe" it really should mean "the signature verification must pass if I use a public key with fingerprint cafebabecafebabe".

As far as I can tell, a signature always uses a long ID

I just tried pgpdump, and it looks like .asc does not have full fingerprint data:

$ pgpdump apache-jmeter-5.3.tgz.asc
Old: Signature Packet(tag 2)(284 bytes)
    Ver 4 - new
    Sig type - Signature of a binary document(0x00).
    Pub alg - RSA Encrypt or Sign(pub 1)
    Hash alg - SHA512(hash 10)
    Hashed Sub: signature creation time(sub 2)(4 bytes)
        Time - Sun Dec 29 22:14:11 +03 2019
    Sub: issuer key ID(sub 16)(8 bytes)
        Key ID - 0x105CB91CAC2AEE0E
    Hash left 2 bytes - ce d7
    RSA m^d mod n(2045 bits) - ...
        -> PKCS-1

So the way to proceed is to get 8-byte keyid from asc, download whatever keys exist for that keyid, and use only those that match the configured fingerptints.

WDYT?

Ok so that's exactly what I was saying: in any case, the only thing you can get is a 64-bit key id from a signature (this is also why I said that for _ignored keys_ you need to use the long ID). So, we _can_ add the fingerprint to the configuration, but it doesn't add much value, because verification would fail in any case if it's not the right key:

  • public keys PK1 and PK2 have different fingerprints, F1 and F2
  • PK1 and PK2 have a collision long key ID, ID3
  • for artifact A signed with the private key for PK1, when we see the signature and what we get is ID3
  • we either look for ID3 in the local keyring _or_ download a key using ID3 from a public key server
  • assume the key server returns PK2, not PK1
  • because A was signed with the private key corresponding to PK1, verification _will fail_
  • if A was signed with the private key corresponding to PK2, verification _would pass_

So, yes, we can put in the configuration that we want that the key for A _has a full fingerprint F1`, but it doesn't add much, does it?

for artifact A signed with the private key for PK1, when we see the signature and what we get is ID3

That is false.
The signature is RSA m^d mod n(2045 bits) - ...-> PKCS-1 packet which is much longer than a key id.

For instance:

$ pgpdump -i apache-jmeter-5.3.tgz.asc
Old: Signature Packet(tag 2)(284 bytes)
    Ver 4 - new
    Sig type - Signature of a binary document(0x00).
    Pub alg - RSA Encrypt or Sign(pub 1)
    Hash alg - SHA512(hash 10)
    Hashed Sub: signature creation time(sub 2)(4 bytes)
        Time - Sun Dec 29 22:14:11 +03 2019
    Sub: issuer key ID(sub 16)(8 bytes)
        Key ID - 0x105CB91CAC2AEE0E
    Hash left 2 bytes - ce d7
    RSA m^d mod n(2045 bits) - 1a 39 b2 19 60 52 88 13 e3 ca 12 8a ad cf 94 f6 2c b5 9a 51 b8 78 41 a0 f3 6c 0d 84 86 b3 71 83 8d dd cc 5c 3e da 79 72 94 75 07 37 2d 08 2f c1 b9 3e 4c fc aa 04 bd 3f 96 33 3b 98 03 d8 3f 09 25 cb 04 df 80 85 70 8a 17 ee f0 03 d6 db 71 f2 bc 3b 11 1a 2c 20 ec 70 99 8f 51 72 4c ed 85 f5 2f fe 1b 7a b1 8f 18 09 d2 4b 5e a3 b0 e1 ca d5 15 67 cd 65 e1 c3 66 03 ac 68 73 55 95 32 1d 64 22 29 d2 87 41 d3 72 bd d7 a7 75 bc 01 68 ca fa 7f 1e c2 fe f9 29 a0 d5 30 fe 56 3d 31 f2 f9 ff a7 49 9f 99 15 f5 a3 e0 c1 b7 21 a4 3b 02 04 57 ad cc 8d 92 c8 5f 76 64 8a 5e fc ec 18 82 74 3c 05 06 5c 42 d8 18 56 5b 05 d6 0f 0f b9 42 9e 53 0b e0 3c 07 69 ca b7 42 69 7c c8 70 08 20 e0 c5 d4 73 13 90 2c 54 d2 0c ef 16 23 71 ac c9 56 ea f1 3b cd 69 1f db 08 f7 f7 b1 f8 04 c2 91 f1 2b

That's not what I'm saying. I'm saying that _in the signature_, the key ID we receive is 64-bit (long) and will be ID3. So we have no guarantee that a public server will return PK1. It can return PK1 _or_ PK2. But, whatever key we download and use, I _think_ that verification would fail if it's not the correct key.

But, whatever key we download and use, I think that verification would fail if it's not the correct key.

Exactly. In other words, having full fingerprints in the configuration does improve security.

Does it? If verification fails if it's not the correct key, we fail. It's already secure.

It would improve security if, for the 2 keys, verification _passed_, but it seems extremely unlikely that for 2 different keys which happen to collide on the long id, would still pass verification of the signature. That's why I'm quite confused.

That's why I'm quite confused.

Ok. Suppose you've configured "trust keyid=0xcafebabecafebabe" by which you mean "trust artifacts signed by Gradle Inc." (assuming Gradle's key has keyid of 0xcafebabecafebabe).

What the attacker can do is they can generate a collision (this alone is highly unlikely, but it is possible), so an attacker produces a brand new key which happens to have the same keyid of 0xcafebabecafebabe.

Then the attacker produces gradle-core-42.0.jar, and signs it with their evil key.
Then they publish gradle-core-42.0.jar, the signature, and the relevant public key to public servers.

Of course, that jar is signed with one signature only which is an evil one.

Then you happen to add a dependency on gradle:42:jar, and you download that jar.
The system downloads the public key, it verifies the signature, and it believes the file is OK because it does not know that key ids can collide.

On the other hand, if the system used full fingerprint for public key configuration, it would identify that the configured fingerprint for gradle... does not match the actual one.

TL;DR: full fingerprint protects users from the case when the only available public key (or signature) is the evil one.

Right, but this assumes that you _also_ published the gradle jar with the baked key. I understand this, but I also understand it _doesn't_ help for the "bootstrapping" use case. And because if you're serious about contents you should also verify the checksum I'm not convinced it's a big win.

That doesn't mean we shouldn't do this, but I'm just not convinced it's actually useful if you do automatic key downloading. In the end, I think you do agree that for ignored keys we have no choice but using 64-bit ids, though?

I also understand it doesn't help for the "bootstrapping" use case

It does help.

At PGP signing key party they verify full fingerprints.
I know certain developers in person, and I know the full fingerprints they use for signing artifacts.

Why should I tolerate to keyid collisions?

I think you do agree that for ignored keys we have no choice but using 64-bit ids, though?

No. I do not see why's that.

Of course, it is easier to implement 64bit keys, so the system does not even try to resolve full public keys.

However, it could easily be like "ignore by fingerprint" where the system still resolves the key, takes its fingerprints and filters accordingly.

That's not the point, the point is, if you have a valid artifact and you got the wrong public key, it will fail, so it doesn't _effectively_ improve security, because we would already fail.

It helps identifying the _correct_ key, though. So it helps the _machine_ (instead of having to try multiple keys, you tell me the correct one).

The point of provenance is to ensure who signed the artifact.

Having the full fingerprint helps that dramatically.

PS. Do you think we could just call with something like https://meet.jit.si/ ?

No. I do not see why's that.

Ok, so: I download this .asc file. It tells me (remember it's the only thing we have), this was signed with this "64-bit public key id".

The key can't be found on any public key server. How do you know what was the origin, full, fingerprint? The key is lost, or cannot be found anywere.

The key can't be found on any public key server. How do you know what was the origin, full, fingerprint? The key is lost, or cannot be found anywere.

If the fingerprint was configured for "ignored list", then you ignore public key resolution error and assume the signature never existed.

Again I do not contest the fact that the full fingerprint is _better_ and clearly identifies the author.

If the fingerprint was configured for "ignored list",

Where do you get that fingerprint from? If you have a KEYS file, you're lucky. Most folks will just have the .asc file and all you get is that long key id.

Alternatively we could support ignoring _both_ by fingerprint and long id, which would solve both cases (you know the original key fingerprint or you don't).

Where do you get that fingerprint from?

I mean from the configuration.

For instance:

      <ignored-keys>
         <ignored-key id="74dafdfd6dae2441...fullfingerprint...ehere" reason="Key couldn't be downloaded from any key server"/>
      </ignored-keys>

It still doesn't tell me where you got that fingerprint in the configuration file from :)

It still doesn't tell me where you got that fingerprint in the configuration file from :)

Ok. Of course, if there's an artifact that is signed with no-one-knows-which PGP key, then we have to ignore it by 64bit key :)

Alternatively we could support ignoring both by fingerprint and long id, which would solve both cases

Frankly speaking, I'm for transparently supporting both keyid and fingerprint in id=".." fields.
In other words, support both of them, and update to full fingerprints automatically when possible.

I think I have all the required information now, we all agree I think, thanks for the clarification, very helpful!

@melix Wow, very fast! Awesome, thanks!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lacasseio picture lacasseio  路  102Comments

cjdxhjj picture cjdxhjj  路  43Comments

JehandadK picture JehandadK  路  36Comments

johnzielke picture johnzielke  路  52Comments

bmuschko picture bmuschko  路  45Comments