Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tox test build failing (can't reproduce, maybe related to GPG) #171

Closed
lukpueh opened this issue Jan 17, 2018 · 21 comments
Closed

Tox test build failing (can't reproduce, maybe related to GPG) #171

lukpueh opened this issue Jan 17, 2018 · 21 comments

Comments

@lukpueh
Copy link
Member

lukpueh commented Jan 17, 2018

Description of issue or feature request:
I keep getting build errors for different tox jobs, locally and on Travis CI. But when I re-run tox (locally) or restart the Travis build (via web UI) the builds pass.

Here's parts of a local tox build for commit 7016b95 (PR #170) , where py3.6 failed:

py36 runtests: commands[2] | coverage run test/runtests.py

[OUTPUT STRIPPED FOR READABILITY]

----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/lukp/in-toto/in-toto/in_toto/in_toto_verify.py", line 105, in in_toto_verify
    verifylib.in_toto_verify(layout, layout_key_dict)
  File "/Users/lukp/in-toto/in-toto/in_toto/verifylib.py", line 1315, in in_toto_verify
    verify_layout_signatures(layout, layout_key_dict)
  File "/Users/lukp/in-toto/in-toto/in_toto/verifylib.py", line 251, in verify_layout_signatures
    layout.verify_signatures(keys_dict)
  File "/Users/lukp/in-toto/in-toto/in_toto/models/metadata.py", line 255, in verify_signatures
    raise SignatureVerificationError("Invalid signature")
in_toto.exceptions.SignatureVerificationError: Invalid signature

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/lukp/in-toto/in-toto/test/test_in_toto_verify.py", line 209, in test_gpg_signed_layout_with_gpg_functionary_keys
    in_toto_verify_main()
  File "/Users/lukp/in-toto/in-toto/in_toto/in_toto_verify.py", line 161, in main
    in_toto_verify(args.layout, args.layout_keys, args.gpg, args.gpg_home)
  File "/Users/lukp/in-toto/in-toto/in_toto/in_toto_verify.py", line 109, in in_toto_verify
    sys.exit(1)
SystemExit: 1

----------------------------------------------------------------------
Ran 189 tests in 14.527s

[OUTPUT STRIPPED FOR READABILITY]

ERROR: InvocationError: '/Users/lukp/in-toto/in-toto/.tox/py36/bin/coverage run test/runtests.py'
___________________________________ summary ____________________________________
  py27: commands succeeded
  py34: commands succeeded
  py35: commands succeeded
ERROR:   py36: commands failed

I tried the following things, none of which could reproduce the problem:

  • Run tox with -r option (forcing recreation of virtual environments)
  • Restart gpg agent and re-run tox
  • Restarting system and re-run tox
  • Push a new test branch, triggering a new travis build.

The problem first occurred on a Travis build adding Python3 support. Since I restarted the build manually and it passed afterwards the error message is lost.

@lukpueh
Copy link
Member Author

lukpueh commented Jan 18, 2018

Probably related:
https://travis-ci.org/in-toto/in-toto/jobs/330453422

======================================================================
FAIL: test_gpg_sign_and_verify_object (tests.test_gpg.TestGPGRSA)
Create a signature using a specific key on the keyring
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/travis/build/in-toto/in-toto/tests/test_gpg.py", line 121, in test_gpg_sign_and_verify_object
    self.assertTrue(gpg_verify_signature(signature, key_data, test_data))
AssertionError: False is not true
----------------------------------------------------------------------
Ran 189 tests in 14.675s
FAILED (failures=1)
ERROR: InvocationError: '/home/travis/build/in-toto/in-toto/.tox/py34/bin/coverage run tests/runtests.py'
___________________________________ summary ____________________________________
ERROR:   py34: commands failed

@vladimir-v-diaz vladimir-v-diaz mentioned this issue May 23, 2018
3 tasks
@vladimir-v-diaz
Copy link
Contributor

vladimir-v-diaz commented May 23, 2018

@lukpueh @SantiagoTorres

Is it possible that the subprocess call to GPG is not completing successfully in certain cases, and is returning empty data? For instance:

command = GPG_EXPORT_PUBKEY_COMMAND.format(keyid=keyid, homearg=homearg)
process = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE,
stdin=subprocess.PIPE, stderr=subprocess.PIPE)
key_packet, junk = process.communicate()
key_bundle = in_toto.gpg.common.parse_pubkey_bundle(key_packet, keyid)

Popen.communicate() returns a (stdoutdata, stderrdata) tuple, but the code doesn't really check for GPG errors nor does it validate the stdoutdata (see parse_signature_bundle()).

https://docs.python.org/2/library/subprocess.html#subprocess.Popen.communicate

@vladimir-v-diaz
Copy link
Contributor

@SantiagoTorres @lukpueh

I was able to trigger the following exception on my local machine:

Exception TypeError: "'NoneType' object is not callable" in <function _removeHandlerRef at 0x104b3fe60> ignored

The affected lines are in in_toto/gpg/common.py, meaning that lines 195-196 were not executed by coverage.

coverage output:

in_toto/gpg/common.py           84      1     20      1    98%   196, 195->196

for idx, public_key in enumerate(
[master_public_key] + list(sub_public_keys.values())):
if public_key and public_key["keyid"].endswith(keyid.lower()):
if idx > 1:
log.warning("Exporting master key '{}' including subkeys '{}'. For"

@SantiagoTorres
Copy link
Member

I'm unsure on how to move forward with this. One option would be to move to gpgme (which was a long-term goal), but I don't know if that'll fix it. Do you have anything in mind?

@vladimir-v-diaz vladimir-v-diaz mentioned this issue Jun 1, 2018
3 tasks
lukpueh added a commit to lukpueh/in-toto that referenced this issue Nov 2, 2018
log.debug stdout and stderr from running gpg commands in a
subprocess to troubleshoot:
in-toto#171
lukpueh added a commit that referenced this issue Nov 2, 2018
Add log.debug stdout and stderr for all subprocess calls to gpg
command to troubleshoot #171.
lukpueh added a commit to lukpueh/in-toto that referenced this issue Nov 2, 2018
Add log.debug stdout and stderr for all subprocess calls to gpg
command to troubleshoot in-toto#171 (failing ci builds).
@lukpueh
Copy link
Member Author

lukpueh commented Nov 15, 2018

Hooray, this is the first travis log that actually gives us some context (courtesy of #240) on these obscure failures. Unfortunately, it's still not obvious why the signature verification fails.

@lukpueh
Copy link
Member Author

lukpueh commented Nov 16, 2018

I investigated the failing travis build a little further by manually parsing the relevant signature and key bytes, added to the log through the log.debug statements in gpg_sign_object and gpg_export_pubkey, using parse_signature_packet and parse_pubkey_bundle.

I tried both key and signature parsed from the failed test log with a corresponding signature and key respectively created outside of the failed test. I was able to use the key from the failed test to verify the separately created signature, which rules out that the error is related to the key export. However, verifying the signature from the failed test with a separately created key failed.

FWIW, the signature bytes array from the failed test log is shorter (len(bad_sig_bytes) == 286) than a similar signature created outside of the test (len(good_sig_bytes) == 310), which also leads to a shorter hex string in the resulting signature dict, i.e. len(bad_sig_dict["signature"]) == 510 as opposed to len(good_sig_dict["signature"]) == 512.

(to be continued)

@alyptik
Copy link

alyptik commented Nov 16, 2018

@lukpueh @vladimir-v-diaz @SantiagoTorres

Dang, you guys always happen upon the weirdest parts of gnupg:

Subpacket 33 (Issuer Fingerprint) from [1]:

On Thu, 25 Jan 2018 05:43, gnupg-users at gnupg.org said:

After looking at the content of subpacket 33, it appears to be the signing-key's fingerprint prepended by '0x04'.

So I'm guessing subpacket 33 is to be a more robust version of subpacket 16 (Issuer)?

Right. From RFC-4880bis (draft -03)

5.2.3.5. {5.2.3.5} Issuer

(8-octet Key ID)

The OpenPGP Key ID of the key issuing the signature. If the version
of that key is greater than 4, this subpacket MUST NOT be included in
the signature.

5.2.3.28. Issuer Fingerprint

(1 octet key version number, N octets of fingerprint)

The OpenPGP Key fingerprint of the key issuing the signature. This
subpacket SHOULD be included in all signatures. If the version of
the issuing key is 4 and an Issuer subpacket is also included in the
signature, the key ID of the Issuer subpacket MUST match the low 64
bits of the fingerprint.

Note that the length N of the fingerprint for a version 4 key is 20
octets; for a version 5 key N is 32.

And here it is in the source [2].

That maths don't come out as perfect as I was expecting, but from what I can tell your missing bytes are because the older gpg didn't include this packet (possibly the bytes unaccounted for can be blamed on python's len() implementation or even random padding bytes).

As for a solution, maybe update gnupg to the latest version before running any gpg commands in your Travis CI script; I believe that should be enough for the signature generation to come out consistent.

If that's not possible then we have to figure out how to prod older gpg into generating subpacket 33.

[1] https://lists.gnupg.org/pipermail/gnupg-users/2018-January/059881.html
[2] https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=common/openpgpdefs.h;hb=0a641ad25d8c3b91dc32bb9f3f1ae49ae539a4f7#l118

@lukpueh
Copy link
Member Author

lukpueh commented Nov 16, 2018

Yes, you're absolutely right. The different versions definitely account for the different lengths of the raw bytes (because of the missing subpackt 33 in older versions). I do wonder why the actual signature data, which does not include the subpackets, is also 2 bytes shorter. But maybe there was also a change between versions.

At any rate, I don't think that the missing subpacket 33 is the reason for these failures. For one, we have a workaround if the signature only has subpacket 16 (see gpg_sign_object#L106-L110 and parse_signature_packet#L306-L323, and also, if this were the problem, it should always fail on old versions, right?

But thanks a lot for chiming in here. I could use some help on this.

@SantiagoTorres
Copy link
Member

yeah, I do see the error log mentioning that subpacket 33 is missing. That in and by itself should be enough to be worried of using the wrong underlying gpg library. I wonder if somehow this is using <2.1 sometimes...

@SantiagoTorres
Copy link
Member

On a related note, lukpueh. I'll dig into the signature itself and try to figure out what's exactly the discrepancy, I suspect it's just padding in PKCS1.5

@alyptik
Copy link

alyptik commented Nov 16, 2018

Well, from experience Travis is known to have some pretty ancient defaults, heh

@alyptik
Copy link

alyptik commented Nov 16, 2018

@lukpueh Ah, I see. Yeah, that missing 2 bytes is very strange.

@SantiagoTorres
Copy link
Member

From reviewing this, actually the math makes sense perfectly for the signature packet:

[santiago@LykOS gpg-error-in-toto]$ cat ayy.sig | gpg --list-packets
# off=0 ctb=89 tag=2 hlen=3 plen=283
:signature packet: algo 1, keyid 9BE9DF5131D924E9
        version 4, created 1542299731, md5len 0, sigclass 0x00
        digest algo 8, begin of digest cf 99
        hashed subpkt 2 len 4 (sig created 2018-11-15)
        subpkt 16 len 8 (issuer key ID 9BE9DF5131D924E9)
        data: [2040 bits]

This is 286 bytes, plus 24 bytes from the missing subpacket 33 (the subpacket 33 would be 33 04 21 [21 bytes of data]. It all ends up accounting for 310 bytes total.

If that's not possible then we have to figure out how to prod older gpg into generating subpacket 33.

I tried this, I ended up settling for mocking our signing method:

newsig_obj = gpg_sign_object(signable_bytes)

# to trick the signing method into thinking longkeyid wasn't available
backup_keyid = newsig_obj['keyid']
newsig_obj['keyid'] = ''
newsig_obj['short_keyid'] = backup_keyid[-16:]

with mock.patch('in_toto.gpg.common.parse_signature_packet') as mocked_method:
    mocked_method.return_value = newsig_obj 
    failing_signature_object = gpg_sign_object(signable_bytes, keyid=newkey_sub)
    print(failing_signature_object)

This will trigger our replacement method, which does actually work, it appears...

Further

However, verifying the signature from the failed test with a separately created key failed.

This doesn't happen to me if I manually embed the keyid myself:

# embedded from the logs
pubkey_info = parse_pubkey_bundle(pubkey, subkey)
signature_obj = parse_signature_packet(sig)
signature_obj['keyid'] = subkey
gpg_verify_signature(signature_obj, pubkey_info, signable_bytes)
# passes

I wonder if I'm doing something wrong.

If I compare two raw exported signature packets I see that the data: field changes size in about a byte or so:

# off=0 ctb=89 tag=2 hlen=3 plen=307
:signature packet: algo 1, keyid 3A06AB9E509F3220
        version 4, created 1542406427, md5len 0, sigclass 0x00
        digest algo 2, begin of digest f2 f8
        hashed subpkt 33 len 21 (issuer fpr v4 8DE91900FF8DB2F8A3B375F83A06AB9E509F3220)
        hashed subpkt 2 len 4 (sig created 2018-11-16)
        subpkt 16 len 8 (issuer key ID 3A06AB9E509F3220)
        data: [2047 bits]

vs

[santiago@LykOS gpg-error-in-toto]$ cat ayy.sig | gpg --list-packets
# off=0 ctb=89 tag=2 hlen=3 plen=283
:signature packet: algo 1, keyid 9BE9DF5131D924E9
        version 4, created 1542299731, md5len 0, sigclass 0x00
        digest algo 8, begin of digest cf 99
        hashed subpkt 2 len 4 (sig created 2018-11-15)
        subpkt 16 len 8 (issuer key ID 9BE9DF5131D924E9)
        data: [2040 bits]

funnily enough, I do see that those two signatures have that two-byte discrepancy when using our impl

(Pdb) len(newsig_obj['signature'])
512
(Pdb) len(signature_obj['signature'])
510

Now maybe I'm seeing things, but the probability of our tests failing look a little familiar* when I do this:

>>> import in_toto.gpg.functions as f
>>> from test_data import signable_bytes
>>> from collections import Counter
>>> counter = Counter()
>>> for i in range(1,10000):
...     sig = f.gpg_sign_object(signable_bytes)
...     counter[len(sig['signature'])] += 1
...     
... 
>>> counter
Counter({512: 9833, 510: 166})
>>> 166/9833
0.016881928200955965

I'm utterly confused, but also very curious to see if we're onto something.

  • This relation is just assumption by me, I don't think we have the numbers on how often do tests fail here...

@lukpueh
Copy link
Member Author

lukpueh commented Nov 19, 2018

Thanks, @SantiagoTorres for digging into this. I do think we are on to something. I updated your awesome script to see if signature length and verification failure are related. It seems that they are:

>>> # In the in-toto project root (using the test keychain)
>>> import in_toto.gpg.functions as f
>>> from collections import Counter
>>> counter = Counter()
>>>
>>> keyid = "8465A1E2E0FB2B40ADB2478E18FB3F537E0C8A17"
>>> homedir = "tests/gpg_keyrings/rsa"
>>> pubkey = f.gpg_export_pubkey(keyid, homedir=homedir)
>>> data = b"hello"
>>>
>>> for i in range(1,1000):
>>>   sig = f.gpg_sign_object(data, homedir=homedir)
>>>   valid = f.gpg_verify_signature(sig, pubkey, data)
>>>   counter[(len(sig['signature']), valid)] += 1
>>>
>>> print(counter)
Counter({(512, True): 9842, (510, False): 157})

We see that signatures that fail are two bytes shorter, than signatures that pass.

@lukpueh
Copy link
Member Author

lukpueh commented Nov 19, 2018

I wrote another test script to investigate failing/passing signatures and signature data length outside of our Python gpg interface. It repeatedly creates detached signatures for some test data, then verifies each signature and prints the (unique) results, i.e. pass or fail together with the signature data length.
I did one run with 10000 iterations and the signature verification never failed. However, the signature lengths varied between 2041 and 2048 bits.

# In in-toto root
mkdir sigs
echo data > data.txt

# Create N signatures over data.txt and store them in `sigs` dir
for (( i=1; i<=10000; i++ ))
do
  cat data.txt | gpg2 --detach-sign --digest-algo SHA256\
      --default-key 8465A1E2E0FB2B40ADB2478E18FB3F537E0C8A17\
      --homedir tests/gpg_keyrings/rsa > sigs/${i}.sig 2> /dev/null
done

# Verify each detached signature and echo (unique) results + signature length
for sig in sigs/*
do
  gpg2 --homedir tests/gpg_keyrings/rsa\
      --verify $sig data.txt &> /dev/null

  echo $([[ $? -eq 0 ]] && echo "PASS" || echo "FAIL")\
      $(cat $sig | gpg2 --list-packets | grep data)

done | sort | uniq

# Clean up
rm -rf sigs
rm data.txt

Here's the result of a run on above script:

PASS data: [2041 bits]
PASS data: [2043 bits]
PASS data: [2044 bits]
PASS data: [2045 bits]
PASS data: [2046 bits]
PASS data: [2047 bits]
PASS data: [2048 bits]

I did a similar thing with our implementation and the Python snippet from above, where I patched gpg.functions.gpg_sign_object to also return the raw signature, and then ran gpg --list-packets over it to look at the signature data length, which was always below 2041 bits for failing signatures.

@lukpueh
Copy link
Member Author

lukpueh commented Nov 19, 2018

Btw. I doubt that it has something to do with the gpg version and the presence or absence of subpacket 33, as above Python test snippet had a similar pass/fail ratio on a system with old gpg and a system with new gpg.

I really think it has something to do with gpg returning an incomplete signature (i.e. where signature data length <= 2040 bits). I wonder why this only seems to happen when we run gpg in a Python subprocess.

@aaaaalbert
Copy link

I'm manually replaying @lukpueh's bash script and can report that the signature timestamp does not seem to play a role. A few iterations of gpg2 --detach-sign --digest-algo SHA256 --faked-system-time 1542632964 (i.e. the signing time held constant) still yield different data lengths.

@lukpueh
Copy link
Member Author

lukpueh commented Nov 19, 2018

I also ran the results from the bash script (removing uniq) through R to see the frequencies of the different signature bit lengths and also if there are interesting patterns over time... But it doesn't really make sense.

@lukpueh
Copy link
Member Author

lukpueh commented Nov 19, 2018

I think I am getting closer...
I went back to the failing signature from the travis build log, and realized that it only fails when verifying with in-toto, which does not use gpg for verification but pyca/cryptography. So either our signatur parser, which prepares the gpg sig for pyca/cryptogrpahy or pyca/cryptography itself can't handle corner cases, i.e. slightly shorter signatures.

Here's a Python script to reproduce my finding:

import os
import in_toto.gpg.functions as gpgf
import in_toto.gpg.common as gpgc
import in_toto.process

# Works when being in the in-toto project repo root
IN_TOTO_TEST_GPG_KEYRING = "tests/gpg_keyrings/rsa"

# Signature copied from travis log
# https://travis-ci.org/in-toto/in-toto/jobs/455583004#L951
sig_raw = b'\x89\x01\x1b\x04\x00\x01\x08\x00\x06\x05\x02[\xed\xa0S\x00\n\t\x10\x9b\xe9\xdfQ1\xd9$\xe9\xcf\x99\x07\xf8\xd2C\xd1\xe5\x9cR\xfd/\x89U\x13\xe0S\x1d\xc4\x03\x0c\xa5\x9f\xec\x94I\xd9\xaf\xc5\xd7\xd7\x9b\'\x9e9\xd0\'H\x9d\x16\xbc\xa8\xf8\x9fmH\x16([\x1fpi\nj\x84Z\x90\xbb\x95[\x9b\x07\x98:\xd4y\xf8\x9e\xb4\xd7\xe7\x82\x7f\xefX\xfcx)\x7fz\x9e}\xd8\x8b\x14\xe1\xd0\r\xea\xd7\xe8z\x86\xfd\xd0\xbe\xe7\xd7\xa0UY\xdf\xed\xf7gM+1(\x07\x0bWMY\xb2G\xb7\x96?\xab\x91\xeef\x12\x89\x1f\x8ak\xd6\xf5\xbf\x92vB_\x0b\x86]\xfd\xa4\xe3\x83}\xa8\xedG\x9fk3\x841&\xb6\\\x0bKt\xceg\x99\x11\x9d\xc1pGi"\x01\r\xb5\\M\x12?\x1fuD\xe1\x95\x94\x9a\x1cr\xed>\xc9\xcf\x12~\x02\xea\xc9\xb5\x81\x06\xcd\xcd"\xfbM\xcd\xc0CClMw\x1d\xdb=#\x05\xdbO\xfb@\xe9\x94\xf1"\xa7[f`\x0f\x0c\x0e\xbb_\xfd\x19 2\xbf\x82\xf6\x14[\xf5\x1b\xc3\x17\x9b\x8c%h\x05>E\x0b\xe0t`$\xab}n\xdc@'

# Key and link data as used in the failing unittest, i.e.
# TestInTotoVerifyThresholdsGpgSubkeys.test_verify_link_signature_thresholds__S_S_S
content = b'{"_type":"link","byproducts":{},"command":[],"environment":{},"materials":{},"name":"name","products":{}}'
keyid = "c5a0abe6ec19d0d65f85e2c39be9df5131d924e9"
key = {'method': 'pgp+rsa-pkcsv1.5', 'type': 'rsa', 'hashes': ['pgp+SHA2'], 'keyid': '8465a1e2e0fb2b40adb2478e18fb3f537e0c8a17', 'keyval': {'private': '', 'public': {'e': '010001', 'n': 'da59409e6ede307a52f6851954a7bd4b9e309bd40a390f8c0de9722b6310110ef0b095bf1c473e33db97150edae05c63dda70c03902701b15f3c5c308947e1b06675b4f1112030f1145be84ae1562e9120c2d429b20d5056337cbc97fc8b5db5704a21db635d00b2157ed68a403c793e9958b77e00163f99b01809e08ee9099b99b117c086501e79eb947f760a0715bead0024c48d81f9000671c4306a93725965f3ff2dc9806eaf081357f0268cab8ba7582d2e95e51225a9dc7ed31a9568c45568d7917b05e7c954d561cd084291e77a7bdd69e3ac2f9091de55fe3f4e730147e880e2fc044c5f7c04c75ce33a3c0b52380f4d60309708c56185f3bce6703b'}}, 'subkeys': {'6a112fd3390b2e53afc2e57f8fc8e12099aeceea': {'method': 'pgp+rsa-pkcsv1.5', 'type': 'rsa', 'hashes': ['pgp+SHA2'], 'keyid': '6a112fd3390b2e53afc2e57f8fc8e12099aeceea', 'keyval': {'private': '', 'public': {'e': '010001', 'n': 'a40806286bfeb2bee9f36d9953ef2f83265f2b802024981775ce8498f6f08a6d7ea14dcecc1b82013b71e7d93d6ecb294f8880ee703e01a73cb45d8740221671a04f4b6cdf79f44438c7b8b8b4963d4308e0f40dbe18aee79e48fdf9fecb02661b76da7ba17a5dffa88afc8984743d3a033879a26384cce12350e25068ec38d4e3d7075176b6020882ca81a12d174df864dc59f11e19f98eec5e5e22d0ab6cc32bf1dd70cf4c2ca7c6aa1be48da0f94dcebc2f3c425d0d1e536e01f01ce5dfdc655f2994ccadd120469d55e2ad2f1e9c3483abcfefcf959d600a2ebb7990b46841b15d25537d8f1fbeda9347ffa466ccfcceca4567f9a7d84dd767fdf037fe97'}}}, 'c5a0abe6ec19d0d65f85e2c39be9df5131d924e9': {'method': 'pgp+rsa-pkcsv1.5', 'type': 'rsa', 'hashes': ['pgp+SHA2'], 'keyid': 'c5a0abe6ec19d0d65f85e2c39be9df5131d924e9', 'keyval': {'private': '', 'public': {'e': '010001', 'n': 'c152fc1f1535a6d3c1e8c0dece7f0a1d09324466e10e4ea51d5d7223ab125c1743393eebca73ccb1022d44c379fae30ef63b263d0a793882a7332ef06f28a4b9ae777f5d2d8d289167e86c162df1b9a9e127acb26803688556ecb08492d071f06caf88cea95571354349d8ef131eff03b0d259fae30ebf8dac9ab5acd6f26f4770fe2f30fcd0a3c54f03463a3094aa6524e39027a625108f04e12475da248fb3b536df61b0f6e2954739b8828c61171f66f8e176823e1c887e65fa0aec081013b2a50ed60515f7e3b3291ca443e1222b9b625005dba045a7208188fb88d436d473f6340348953e891354c7a5734bf64e6274e196db3074a7ce3607960baacb1b'}}}}}

# Create in-toto formatted signature from raw signature
sig = gpgc.parse_signature_packet(sig_raw)
sig["keyid"] = keyid
del sig["short_keyid"]

# Verify signature with in-toto (pyca cryptography)
result = gpgf.gpg_verify_signature(sig, key, content)
if not result:
  print("FAIL (using pyca/cryptography)")
else:
  print("PASS (using pyca/cryptography)") # It does not :'(

# Verify signature with gpg
with open("failing.sig", "wb") as f: f.write(sig_raw)
with open("content.txt", "wb") as f: f.write(content)
cmd = ["gpg2", "--homedir", IN_TOTO_TEST_GPG_KEYRING, "--verify", "failing.sig", "content.txt"]
in_toto.process.run(cmd)
# gpg: WARNING: unsafe permissions on homedir '[...]tests/gpg_keyrings/rsa'
# gpg: Signature made Thu Nov 15 17:35:31 2018 CET
# gpg:                using RSA key 9BE9DF5131D924E9
# gpg: Good signature from "Joan Doe <test@donotuseth.is>" [ultimate]

# Clean up
os.remove("failing.sig")
os.remove("content.txt")

@SantiagoTorres
Copy link
Member

I think I found a fix to this issue. I have a trove of reasons this makes sense but what led me to try this out was the following:

  • It appears the bug is triggered with a probability of 1/256, e.g., see a couple of runs of the script above:
>>> (318/(99682+318))*100
0.318

Other runs yielded: 0.49500000000000005 and 0.47400000000000003

  • It only happens with signatures that are 8 bits shorter than the length of the key.

After adding Lukas's changes, I could verify that indeed only signature verification fails when the signature is 510 bytes and 510 bytes only (corresponding to a 2040-bit signature).

This led me to believe that this was related to a wrong handling of the MPI parsing routine in in_toto, but I discarded it after realizing that the bitlength of the signature actually matches what the --list-packages flag says.

Instread, the issue seems to be a discrepancy in how MPI numbers are represented between the openssl and the gpg backends. It appears that the length of the signature varies as long as there are eight-or-more leading 0's in the signature. This yields the "shorter" signature in the gpg backend, which throws off the openssl pyca/cryptography backend.

A simple solution to this issue is to add the leading zeroes when serializing the signature field to an in-toto compliant signature object:

sig['signature'] = "00{}".format(sig['signature']

I know it sounds dumb, but you can try it yourself today:

import in_toto.gpg.functions as f
import in_toto.gpg.functions as f
from in_toto.gpg.common import (parse_pubkey_bundle, parse_signature_packet)
from test_data import signable_bytes, pubkey
from collections import Counter
master = "8DE91900FF8DB2F8A3B375F83A06AB9E509F3220" # use your own dumbly-generated key
counter = Counter()
fixed_signatures = Counter()
parsed_pubkey = f.gpg_export_pubkey(master)
for i in range(1,100001):
    sig = f.gpg_sign_object(signable_bytes)
    valid = f.gpg_verify_signature(sig, parsed_pubkey, signable_bytes)
    counter[(len(sig['signature']), valid)] += 1
    if not valid and len(sig['signature']) == 510:
        sig['signature'] = "00{}".format(sig['signature'])
        valid = f.gpg_verify_signature(sig, parsed_pubkey, signable_bytes)
        fixed_signatures[valid] += 1 


print("signatures computed {}".format(counter))
print("signatures fixed by 0-padding {}".format(fixed_signatures))

Of course this is not the final fix, as we need to be more clever about it. Technically, there may be signatures with 16 leading 0 bits (and I think our computer should be awarded some bitcoin when this happens), and thus we will have to 0-pad on the leading signature in a more clever way. Expect a patch in the coming days about it :)

@SantiagoTorres
Copy link
Member

@lukpueh @alyptik @aaaaalbert Please take a look 👍

SantiagoTorres added a commit that referenced this issue Nov 20, 2018
It appears the specification for PKCSv1.5 doesn't say how long should a
signature should be (i.e., should we zero-pad the signature or should we
use the minimum number of bits required). In this case, signatures
generated using gnupg would sometimes be 8*n bits shorter than the
public key length, which would result in signatures that fail to verify
due to missing leading zeroes (the leading zeroes existed at the bit
level however).

Zero-left-pad signature objects that do not match the length of the exponent
`n` on a gpg rsa public key object before verification.
michizhou pushed a commit to michizhou/in-toto that referenced this issue Nov 30, 2018
Add log.debug stdout and stderr for all subprocess calls to gpg
command to troubleshoot in-toto#171 (failing ci builds).
michizhou pushed a commit to michizhou/in-toto that referenced this issue Nov 30, 2018
It appears the specification for PKCSv1.5 doesn't say how long should a
signature should be (i.e., should we zero-pad the signature or should we
use the minimum number of bits required). In this case, signatures
generated using gnupg would sometimes be 8*n bits shorter than the
public key length, which would result in signatures that fail to verify
due to missing leading zeroes (the leading zeroes existed at the bit
level however).

Zero-left-pad signature objects that do not match the length of the exponent
`n` on a gpg rsa public key object before verification.
lukpueh added a commit to lukpueh/securesystemslib that referenced this issue Aug 8, 2019
Add log.debug stdout and stderr for all subprocess calls to gpg
command to troubleshoot in-toto/in-toto#171 (failing ci builds).
lukpueh added a commit to lukpueh/securesystemslib that referenced this issue Aug 14, 2019
Add log.debug stdout and stderr for all subprocess calls to gpg
command to troubleshoot in-toto/in-toto#171 (failing ci builds).
lukpueh added a commit to lukpueh/securesystemslib that referenced this issue Sep 4, 2019
Add log.debug stdout and stderr for all subprocess calls to gpg
command to troubleshoot in-toto/in-toto#171 (failing ci builds).
lukpueh added a commit to lukpueh/securesystemslib that referenced this issue Sep 4, 2019
Add log.debug stdout and stderr for all subprocess calls to gpg
command to troubleshoot in-toto/in-toto#171 (failing ci builds).
tanishqjasoria pushed a commit to tanishqjasoria/securesystemslib that referenced this issue Jan 30, 2020
Add log.debug stdout and stderr for all subprocess calls to gpg
command to troubleshoot in-toto/in-toto#171 (failing ci builds).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants