I'm in the process of building a tool to sign cryptocurrency transactions with my Yubikey. For that I need to sign arbitrary data using the SHA512 digest algo on the secp256k1 curve. I dug out the OpenPGP specification which the Yubikey implements along with the required APDUs that are needed to sign hashed data directly in the Yubikey. I get a signature, however it is flagged as invalid in most Javascript libraries and this online verification tool for secp256k1 keys and this one for ed25519 keys.
The APDUs I send to my Yubikey are:
00A4040006D27600012401
00478100000002B6000000
0020008106313233343536
002A9E9A409b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec04300
The OpenPGP APDU makes the Yubikey select the OpenPGP applet.
The GET PUBLIC KEY APDU means:
c=00
: Default command classi=47
: Generate asymmetric key pairp1=81
: Reading of actual public keyp2=00
lc=000002
le=B600
: Signature keyem=0000
Extracting the public key in is optional but useful for verification.
(Reference 7.2.14 GENERATE ASYMMETRIC KEY PAIR on page 74 in the OpenPGP Smart Card Spec)
The PIN APDU means:
c=00
: Default command classi=20
: Instruction byte for VERIFYp1=00
p2=81
: Must be 81 for SIGN as specified in 7.2.10lc=06
: Pin length in bytes, hex encodeddata=313233343536
: hex-encoded default pin 123456
Verifying the pin is a prerequisite for the following signing operation to work.
(Reference 7.2.2 VERIFY on page 52 in the OpenPGP Smart Card Spec)
The SIGN APDU means:
c=00
: Default command classi=2A
: Instruction byte for PERFORM SECURITY OPERATIONp1=9E
: 9E specifies that the operation is to compute a digital signaturep2=9A
: 9A specifies that the data is a hash that needs to be signedlc=40
: hex for 64, so 512 bits for a SHA512data=9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043
: hex-encoded SHA512 of UTF8-encoded string hello
used as digest to signle=00
(Reference 7.2.10 PSO: COMPUTE DIGITAL SIGNATURE on page 63 in the OpenPGP Smart Card Spec)
When I run this on my Yubikey, I get the following responses:
OpenPGP APDU Executed: 00a4040006d27600012401
-> Response: 9000
: Success
GET PKEY APDU Executed: 00478100000002b6000000
-> Response: 7f494386410498a5ecbb2e3738c1021f980017a2f47314288e41ab1c435cfceef00a5e63276933ff480a6bd607f80729204c16c9d2d092a187767c2928008d146197f5fe43c39000
Deconstructed this means:
7f49
: Response success43
: Length in bytes of remaining result. 43
in hex is 67 in decimal.86
: Constant for ECDSA Signature, as the Signature key on this Yubikey is of type secp256k1
41
: Length of following key. 41
in hex is 65 in decimal04
: First part of the public key, with 04
indicating an uncompressed public key98a5ecbb2e3738c1021f980017a2f47314288e41ab1c435cfceef00a5e63276933ff480a6bd607f80729204c16c9d2d092a187767c2928008d146197f5fe43c3
: The 64 byte public key containing r
and s
(both 32 bytes) according to my tests9000
Success status wordPIN APDU Executed: 0020008106313233343536
-> Response: 9000
: Success
SIGN APDU Executed: 002a9e9a409b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec04300
-> Response: af679c9010968004f5b3b6dc23795a743676dee39c4f214426f43b703a244448129ce9b72177bc6647ae2cb9caa0af7d26336a4daa8e67fad253b1b8c02a3f829000
Deconstructed this means:
af679c9010968004f5b3b6dc23795a743676dee39c4f214426f43b703a244448129ce9b72177bc6647ae2cb9caa0af7d26336a4daa8e67fad253b1b8c02a3f82
: The 64 byte digital signature where I can't find any specification of the encoding9000
Success status wordSo all together we have:
68656c6c6f
(=hello
)SHA512
digest to sign: 9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043
0498a5ecbb2e3738c1021f980017a2f47314288e41ab1c435cfceef00a5e63276933ff480a6bd607f80729204c16c9d2d092a187767c2928008d146197f5fe43c3
(with out without 04
prefix)af679c9010968004f5b3b6dc23795a743676dee39c4f214426f43b703a244448129ce9b72177bc6647ae2cb9caa0af7d26336a4daa8e67fad253b1b8c02a3f82
But for some reason, most verifier do not accept this signature as valid. I tried with both secp256k1 type sig keys and ed25519 type signature keys.
Where did I go wrong here?
Links for online verification:
68656c6c6f
, SHA512, secp256k1, no 04 prefix68656c6c6f
, SHA512, secp256k1, 04 prefixhello
, SHA512, secp256k1, no 04 prefixhello
, SHA512, secp256k1, 04 prefix68656c6c6f
, SHA512, ed25519, a different key pairI also checked this using the scdaemon log file while using gpg --sign --digest-algo SHA512
, where gpg sends the following APDU to sign a 64 byte SHA512 digest:
DBG: send apdu: c=00 i=20 p1=00 p2=81 lc=6 le=-1 em=0
DBG: PCSC_data: 00 20 00 81 06 [hidden]
DBG: response: sw=9000 datalen=0
DBG: chan_0x00000324 -> S PINCACHE_PUT 0/openpgp/1 3ED853D574CD8F2CF0B5E1EF602E296AF823642870AFD078
DBG: send apdu: c=00 i=2A p1=9E p2=9A lc=64 le=256 em=0
DBG: PCSC_data: 002a9e9a409fb0ff1714c8c6cc7e01399c2d95bde1d60e76d738bc340c9f67f0 \
DBG: 62bf82ef51a44cd17e9ddb0cce9560e3efba0c2d17844a4f7f69d22f8c0fbedb \
DBG: e39adf434500
DBG: response: sw=9000 datalen=64
DBG: dump: 6a1a138e709c5615547281ac3236b35f6dbf952c0b1f4e8e47b07074acf18bed \
DBG: c742874929e8152b6578bb53b7c040d7e5efb63d87aee5daf9d6d8645e4c7acc
operation sign result: Success
Then in the gpg --list-packets --verbose
of the generated PGP message, that generated signature can be found in the data
fields with digest 9f b0
matching the PCSC_data
as well:
# off=0 ctb=a3 tag=8 hlen=1 plen=0 indeterminate
:compressed packet: algo=1
# off=2 ctb=90 tag=4 hlen=2 plen=13
:onepass_sig packet: keyid 9B70B27E1322DFEE
version 3, sigclass 0x00, digest 10, pubkey 19, last=1
# off=17 ctb=cb tag=11 hlen=2 plen=19 new-ctb
:literal data packet:
mode b (62), created 1725290603, name="",
raw data: 13 bytes
# off=38 ctb=88 tag=2 hlen=2 plen=117
:signature packet: algo 19, keyid 9B70B27E1322DFEE
version 4, created 1725290603, md5len 0, sigclass 0x00
digest algo 10, begin of digest 9f b0
hashed subpkt 33 len 21 (issuer fpr v4 79825D85C8A09191DB3C53DC9B70B27E1322DFEE)
hashed subpkt 2 len 4 (sig created 2024-09-02)
subpkt 16 len 8 (issuer key ID 9B70B27E1322DFEE)
data: 6A1A138E709C5615547281AC3236B35F6DBF952C0B1F4E8E47B07074ACF18BED
data: C742874929E8152B6578BB53B7C040D7E5EFB63D87AEE5DAF9D6D8645E4C7ACC
If even GPG does it this way, how is the generated signature invalid for other verifiers? The only verifier that indicates a valid signature is the noble-secp256k1 Javascript that says about half of the generated signatures are valid and I don't see why.
Verifying ECDSA signatures is tricky, as there are different encodings for keys and signatures. The online tools you are referring to apparently uses signatures wrapped in an ASN1 struture. See rfc5480.
Your PGP applet is however returning the raw signature (64 bytes) af679c9010968004f5b3b6dc23795a743676dee39c4f214426f43b703a244448129ce9b72177bc6647ae2cb9caa0af7d26336a4daa8e67fad253b1b8c02a3f82
Your signature will validate using that tool if you convert the signature to ASN1: (3045022100af679c9010968004f5b3b6dc23795a743676dee39c4f214426f43b703a2444480220129ce9b72177bc6647ae2cb9caa0af7d26336a4daa8e67fad253b1b8c02a3f82
)
See also this discussion.