Using PGP signatures with bitcoin script OP_CHECKDATASIG
Dr. Mark B. Lundeberg, 2018 August 30
Since version 2.1, GnuPG is able to use the very same secp256k1 elliptic curve signature algorithm (ECDSA) as used in bitcoin. Quite soon Bitcoin Cash will add a new script opcode OP_CHECKDATASIG that is able to check signatures not just on the containing transaction, but also on arbitrary data. For fun, let's try to intersect the two signature systems and see what can be done!
This new opcode will allow scripts to fail/succeed on the basis of whether a given message is signed with a given key. When a script invokes this opcode, it pops three items (byte arrays) off the stack:
pubkey. It checks the result of
Verify_ECDSA_secp256k1(pubkey, digest, sig), where
digest=SHA256(msg). This is similar to the transaction signature system, except note that:
sigstores the signature (r,s) values in the regular ~70 byte DER format used for the OP_CHECKSIG signatures. A hashtype byte suffix is however not added.
- Unlike with transaction signing,
msgis not altered in any way before inputting into the hash, and
msgis hashed once with SHA256, not twice.
pubkeyis a standard bitcoin public key, uncompressed or compressed.
PGP elliptic curve signatures
When you run a command like
gpg --detached-sign stuff.dat, a number of things happen behind the scenes in accordance with the OpenPGP standard. Focussing on the case of an ECDSA key,
- Construct a partial 'signature packet' that includes: a version number (0x04 currently), the type of signed data (0x00 for binary document), the public key algorithm type (0x13 for ECDSA), the hash type (0x08 for SHA256), and various subpackets of metadata including the signature creation time.
- Read in the file "stuff.dat", for example this byte string of ASCII characters:
data = b'Hello world!\n'.
- Appends the partial signature packet to the message data, plus an extra 6 bytes encoding the length of the partial signature packet. In hex:
msg = 48656c6c6f20776f726c64210a04001308000605025e0c518004ff0000000cwhere the bolded part is the original
- Compute the digest according to the selected hash type, e.g.:
digest = SHA256(msg)
r, s = Sign_ECDSA(secret_key, digest).
- Take the partial signature packet from step 1, and complete it by appending: various other subpackets of metadata (like the key ID used for the signature), the leftmost 2 bytes of
digest, and finally the integers (r,s) stored in PGP's multiprecision integer format.
- This yields the completed signature data, which for ECDSA is typically 96 bytes. It may at this point be converted to ascii armor "-----BEGIN PGP SIGNATURE----- ...".
To verify, GnuPG extracts out the partial signature packet, appends it to the data as above to get
msg, then computes
digest. It checks that it matches the 2 bytes and tries to find the public key matching this key ID. Finally it checks
Verify_ECDSA(pubkey, digest, sig).
For key certifications / self-signatures, the
msg passed into the hash is a bit more complicated. It is a concatenation of the following byte strings:
- A 3-byte key header.
- The key packet being signed (using 'old style' header).
- A 5-byte user ID header.
- The user ID packet being signed.
- The partial signature packet plus extra 6 bytes, as above.
Although OP_CHECKDATASIG and PGP store the signatures in different format, and appear to compute the digest differently, they are not entirely incompatible. We must create secp256k1 keys (currently requires
gpg --full-gen-key --expert) and when we sign documents we should make sure we use SHA256 (
gpg --digest-algo SHA256 -b stuff.dat).
Let's say we have such a PGP signature and want to convert it to be verifiable by OP_CHECKDATASIG. We can do the following:
- Extract the partial signature packet and append it to our
data, to obtain
- Extract the signature data -- the (r,s) multiprecision integers -- and convert it to the bitcoin DER representation. Ensure that s is 'low S' by possibly subtracting it from the group order.
- Get the public key file and extract they key data (PGP stores this in uncompressed 04... format), in order to get
One major limitation is the following point: In the bitcoin script interpreter, a single stack element cannot in any circumstances exceed 520 bytes in size. This means that the entirety of
msg must fit within 520 bytes. Practically, this means:
- Signed documents need to be limited to <~ 500 bytes, to leave enough room for the appended metadata. If you need a signature on a large file, you will need to instead create a small document including the hash of the large file. This is perhaps not so inconvenient -- it is quite common in software distributions to sign a 'SHA256SUMS' file, for which ~4 to 6 files can be included within 500 bytes.
- Key certifications on large keys can easily exceed 500 bytes. The following keys are small enough to sign while keeping
msgunder 520 bytes:
- All ECDSA/Ed25519 keys.
- DSA up to 1024 bits.
- RSA up to ~3500 bits.
Oracle wants to use PGP
The classic use envisioned for OP_CHECKDATASIG is that a trusted Oracle would sign some message using bitcoin's
signmessage or some variant, and this information could be more-or-less directly included and processed in script. It is now clear that Oracles also have the option to use PGP.
The fact that
msg includes a mandatory appendix (partial signature packet) does add a wrinkle. In particular the appendix includes a creation time (unix seconds since epoch) and may or may not include other metadata subpackets.
The redeem script can be constructed so that the appendix is omitted. For example
msg = (data + appendix) may be split into:
data = b'Broncos win!\n' and
appendix = 04001308000605025e0c518004ff0000000c. Then the following redeem script would reconstruct them, given the signature and appendix as an input:
[sig and appendix get pushed from scriptSig] ... (..., sig, appendix) <data> (..., sig, appendix, data) SWAP (..., sig, data, appendix) CAT (..., sig, msg) <pubkey> (..., sig, msg, pubkey) CHECKDATASIG (..., 0 / 1) ... [do stuff based on CHECKDATASIG result]
(items in parentheses show the stack state after performing the operation)
Note however this means a PGP signature from the oracle on any message starting with "Broncos win!\n" would work, even if this message came from a previous year or if the signed message was "Broncos win!\nJust kidding!".
If the appendix is not truncated, then a timestamp is automatically included. However, the exact appendix contents (including timestamp set with 1-ssecond accuracy) must be known ahead of time. If this is done, the Oracle should use the
gpg --faked-system-time flag and other options to make sure that the appendix has precisely the pre-announced binary form.
Atomic swap of BCH for a PGP key certification
Alice wants to pay Bob some bitcoin in exchange for a PGP certification but doesn't quite trust that he won't run off with her money. What a bizarre situation. But don't fret, now there is a way for them to do a trustless swap!
Bob performs the certification but keeps it secret, only sharing
msg. Alice parses the
msg to make sure it would be satisfactory and computes
digest=SHA256(msg). She then funds a P2SH address with the following 151-byte redeem script:
IF <alice_bitcoin_pubkey> CHECKSIGVERIFY <alice_recovery_time> CHECKLOCKTIMEVERIFY ELSE <bob_bitcoin_pubkey> CHECKSIGVERIFY DUP HASH256 <digest> EQUALVERIFY <bob_pgp_pubkey> CHECKDATASIG ENDIF
Bob can spend from this address using the following scriptSig stack: (
0), where he constructed
sig from the PGP signature's (r, s) parameters. He must also sign the transaction with his bitcoin key
bob_bitcoin_pubkey, as this ensures that he is in control of where the funds go.
Once Bob has spent from this address, which requires him to reveal r, s, Alice now can take this signature together with
msg, to reconstruct the valid key certification.
If Bob stops responding then Alice can recover her funds with just the stack (
1) -- i.e., the transaction is just signed by her key
alice_bitcoin_pubkey. She must however wait until
alice_recovery_time to do so.
Note 1: I used
digest here in the redeemscript instead of including
msg, since the entire redeem script must fit into 520 bytes. This lets us use a full 520 byte
msg by having it pushed from the scriptsig.
Note 2: In practice, Alice might demand that Bob sign with a 'non-revocable' signature, which she can confirm by checking the metadata packets in
msg. Quite a variety of customizations can be done with PGP signatures/certifications, and Alice must make sure to be aware of them.
Note 3: What if the participants don't want to publish this obvious exchange of a PGP certification on the blockchain? No problem -- like many other contracts, this one can be hidden inside of a regular payment channel.
Files available at github. Includes a python script that extracts PGP data and creates a bitcoin transaction.
Worked example of basic signature
A secp256k1 ECDSA PGP key was created using
gpg --full-gen-key --expert:
984f045b745e9813052b8104000a020304a788d320086c086c71e15d34e6 2a2308240afccc2325bb34f07edf56706f62a60330ea3e1df9ee826afa2e b4f382d681c99f27bccc486a9390d65b305215f978b40b4d792054657374 204b6579887904131308002105025b745e98021b03050b09080702061508 090a0b020416020301021e01021780000a09102d91cad9873ecc6a5e8c01 00c1c98722bb5ff8ecd80219b04e72134a604a4395f0eb387cfa86a2b909 4e73bd0100bf29cdacee283c3faaba9a480b0f0761a1d3377d8e0056a6e3 72977f1f068ba0
Three pieces of information are bolded above:
- 0x13 indicates the public key is ECDSA.
- 2b8104000a is an OID indicating the curve is secp256k1.
- 04a78...978 is the public key in uncompressed format.
The rest of the info is the PGP key's user ID and self-signature (including various algorithm preference fields), which can be seen by pasting the above hex into
cat | xxd -r -p | gpg --list-packets.
PGP's ECDSA doesn't use compressed keys, however it is easy to convert to compressed form:
02a788d320086c086c71e15d34e62a2308240afccc2325bb34f07edf56706f62a6. This compressed form will be used in the bitcoin script, for space savings.
A file containing binary hex
b"Hello world!\n", was then signed using
gpg -b file.dat, yielding the following detached signature file:
885e04001308000605025e0c5180000a09102d91cad9873ecc6a4f3c00fe 2caa3f50ec00dcf2eff56bb9e14d4af2b54f0f7ecbdee576c8e5f128a503 82b400ff6eaa0117977269a2ba9eb5698861141681a81479e5f32e8f9bce bacc07062616
Here I have highlighted four important parts:
- The signature 'hashed part' which includes pubkey algorithm-id 0x13 (ECDSA), hash algorithm-id 0x08 (SHA256) and the signature creation time 0x5e0c5180,
- the key ID 2D91CAD9873ECC6A (orange),
- signature integer r = 0x2ca...2b4,
- signature integer s = 0x6ea...616.
The 'hashed part' and then six more bytes get appended to the signed data:
msg = 48656c6c6f20776f726c64210a04001308000605025e0c518004ff0000000c. The SHA256 digest of this
4f3c2588e3a7c508fc16e71b418c20625fff5de633f61be116d94e1b86a5c449, and is used in signing but will not appear explicitly anywhere else. Note however that for pre-checking, PGP included the leftmost 16 bits
4f3c in the signature packet, just after the key ID.
pubkey from above, I created a custom redeem script
PUSH<msg> PUSH<pubkey> OP_CHECKDATASIG. Note that it ends with opcode 0xba (OP_CHECKDATASIG):
1f48656c6c6f20776f726c64210a04001308000605025e0c518004ff0000 000c2102a788d320086c086c71e15d34e62a2308240afccc2325bb34f07e df56706f62a6ba
I used Electron Cash in testnet mode to fund the P2SH address
bchtest:pqgsc6ej2yqsrdlrlaqf3lphyznps50zgvwdaxc3e4 (corresponding to the redeem script hash) with 0.3 BCH. I set up a bitcoin-ABC with early activation for OP_CHECKDATASIG and an ElectrumX server. I then successfully spent the utxo using the following scriptSig that pushes the (r, s) signature and redeem script:
46304402202caa3f50ec00dcf2eff56bb9e14d4af2b54f0f7ecbdee576c8 e5f128a50382b402206eaa0117977269a2ba9eb5698861141681a81479e5 f32e8f9bcebacc07062616431f48656c6c6f20776f726c64210a04001308 000605025e0c518004ff0000000c2102a788d320086c086c71e15d34e62a 2308240afccc2325bb34f07edf56706f62a6ba
As can be seen, the bolded parts are directly the integers r, s that appeared in the PGP signature above.
- In general, it may be necessary to convert s to 'Low S' form but in this case PGP made it that way already.
- This is a poor example of redeem script, since it requires no transaction signature and hence has no replay protection. Once the scriptsig is known, anyone can construct a new transaction that spends from this address, and send the funds to themselves. In fact, there is at least one bot on the bitcoin network that is designed to automatically respond in this way!
This hefty 679 byte single-P2SH-input transaction successfully confirmed on the testnet with OP_CHECKDATASIG enabled. It is left as an exercise to the reader to figure out what it is.
0100000001aec61bd1c2a6da1e61d957e4a36526f7652128c39c5337fef7 0cc85ae7b546a500000000fd5002473045022100f98cb8534576e1ce59f9 6257c4d8cb720efd3e1914ed9f4c9bc84fa1aa65de5802203fc114cd42da 1428070c9c3123bc3b4c8e7c589a01f2d265cadf934a014a78bc4d05024d df019901a2044909faa7110400ca0d37199580d1b4fd50eff4a1bb3049cd 487d868555a30a207f65b4ed4e86f41a8a5b6e2ba623b162612b8cc15fbc db68ccd93edf771dfecab7822d393b520c999ce0dab6c5e277b9b778a995 79cdbb3361fa3fba571c40d7a18f841469aef9e783cafd19800b2d4a68f5 7935eca138dbd618285815bed4cfb441949693b51300a0ec59967416d1a7 89a7e467f140d4f5873a7d5a0903ff7a59d1c0799b87a7cdfca53875e645 f24a04c174d990e7b18d46a9e1effcb8e4842bac4e9daacf8b612f271838 528698075da559ccb04fd36192e0a6cc63ac9b2b10f3527685e4dc88044a da309978d37ebf7134eac1db4424d1938ed668a9396320b1ee0f988bb4d9 7db839aa56b7285d1ec5f7a012dcc037647bc68480087c03fb0794aedf2f 5062f2b173c7a85bfb6c88943fdf6b330155951ed2bdaea3b95da84cf9f8 67848811628277791c786a8c9c3cad42647de641a132989432a8ed70eb4d 1c5fcb336e6593f104b4e3fa4798f16818850e4c5ac482d8c5fb1591da25 d7586b4a73526159990874c9dba55544272cacd14ae9ec8c63a79b189605 9c1be9b4000000235361746f736869204e616b616d6f746f203c7361746f 7368696e40676d782e636f6d3e04101308000605025b83907404ff000000 0c2102a788d320086c086c71e15d34e62a2308240afccc2325bb34f07edf 56706f62a6baffffffff0198bfc901000000001976a9142b23deef077156 90e9c94c4bed6ce1601e15d43f88ac00000000