Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Using PGP signatures with bitcoin script OP_CHECKDATASIG

Using PGP signatures with bitcoin script OP_CHECKDATASIG

Dr. Mark B. Lundeberg, 2018 August 30 bitcoincash:qqy9myvyt7qffgye5a2mn2vn8ry95qm6asy40ptgx2

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: sig, msg, 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:

  • sig stores 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, msg is not altered in any way before inputting into the hash, and msg is hashed once with SHA256, not twice.
  • pubkey is a standard bitcoin public key, uncompressed or compressed.

PGP elliptic curve signatures

This document only focusses on the "version 4" PGP signature system. See the RFC4880 section on computing signatures. The specification for ECDSA encoding in PGP is RFC6637.

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,

  1. 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.
  2. Read in the file "stuff.dat", for example this byte string of ASCII characters: data = b'Hello world!\n'.
  3. 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 = 48656c6c6f20776f726c64210a04001308000605025e0c518004ff0000000c where the bolded part is the original data.
  4. Compute the digest according to the selected hash type, e.g.: digest = SHA256(msg)
  5. Run r, s = Sign_ECDSA(secret_key, digest).
  6. 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.
  7. 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).

Key-signing signatures

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.

Compatibility? Interconversion?

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:

  1. Extract the partial signature packet and append it to our data, to obtain msg.
  2. 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.
  3. Get the public key file and extract they key data (PGP stores this in uncompressed 04... format), in order to get pubkey.

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 msg under 520 bytes:
    • All ECDSA/Ed25519 keys.
    • DSA up to 1024 bits.
    • RSA up to ~3500 bits.

Use cases

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:


Bob can spend from this address using the following scriptSig stack: (sig, msg, tx_sig, 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 (tx_sig, 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:


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 48656c6c6f20776f726c64210a, or, b"Hello world!\n", was then signed using gpg -b file.dat, yielding the following detached signature file:


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 msg is 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.

Using the msg and pubkey from above, I created a custom redeem script PUSH<msg> PUSH<pubkey> OP_CHECKDATASIG. Note that it ends with opcode 0xba (OP_CHECKDATASIG):


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:


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!

Mystery example

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.