Trying to verify a Yubikey signature
This post is produced in collaboration with Resonance Security?—?cybersecurity for?all.
In my previous article on Yubikeys, I ended with a failed attempt to verify an assertion without using the fido2-assert tool, but by using openssl instead.
In this article, I am going to show you how I worked through the problem methodically to find the correct solution.
Introduction
If you are facing problems using command line tools and programming in dealing with hash functions, encryption and decryption, and digital signing, in my experience the problem usually lies with variations in data formats.
Data in cryptography can take the form of:
What’s more, data is sometimes encapsulated in a data representation format, rather than just raw numbers. For example, the ASN1 standard, the CBOR standard, and DER encoding are agreed-on methods in which extra data is added to your data to specify what is being represented, from simple formats that say things like “this is a 32-bit unsigned integer” to “this is a private key for the ECDSA signing algorithm using the secp256r1 curve”.
Therefore, when writing cryptographic code it is important to read the documentation (which can sometimes be wrong), and work methodically, testing each stage.
Check that I can use openssl to verify a general signature
I’m trying to verify an ECDSA signature generated by my Yubikey.
Observation: Obviously, if I cannot verify an es256 signature using a standard process, then I’m certainly not going to be able to do so for a FIDO2 assertion output.
Step 1: Generate my own es256 key (which is an ECDSA key using the secp256r1 curve, also known as the prime256v1 curve):
kf106@media-pc3:~/GIT/yubikey/openssl-test$ openssl ecparam -genkey -name prime256v1 > key.pem
kf106@media-pc3:~/GIT/yubikey/openssl-test$ cat key.pem
-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIO4p0pcQNNbJLvc67EhqXXMLMeWH1pmq4avTwcA/gRv/oAoGCCqGSM49
AwEHoUQDQgAE9CDk+AeP2Y452kAUTLOdrocdxg2hIRnR4n9yWcVMEJCZyS/Y6fmD
GCZEVg9HnKsUeH2BhQFFI4EDylU/AdR0sg==
-----END EC PRIVATE KEY-----
That worked. The key is in PEM format, as indicated by the lines starting with and ending with four minus signs. The key is encoded in base64.
Step 2: Extract the public key:
kf106@media-pc3:~/GIT/yubikey/openssl-test$ openssl ec -in key.pem -pubout > pub.pem
read EC key
writing EC key
kf106@media-pc3:~/GIT/yubikey/openssl-test$ cat pub.pem
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9CDk+AeP2Y452kAUTLOdrocdxg2h
IRnR4n9yWcVMEJCZyS/Y6fmDGCZEVg9HnKsUeH2BhQFFI4EDylU/AdR0sg==
-----END PUBLIC KEY-----
That worked too.
Step 3: Generate a message, hash it, and save the hash in binary format:
kf106@media-pc3:~/GIT/yubikey/openssl-test$ echo "some random thing to sign" > message.txt
kf106@media-pc3:~/GIT/yubikey/openssl-test$ openssl dgst -sha256 -binary message.txt > hash.bin
kf106@media-pc3:~/GIT/yubikey/openssl-test$ cat message.txt
some random thing to sign
kf106@media-pc3:~/GIT/yubikey/openssl-test$ cat hash.bin
R_??/!????"??z?K?x????^??tkf106@media-pc3:~/GIT/yubikey/openssl-test$
kf106@media-pc3:~/GIT/yubikey/openssl-test$ la -la hash.bin
-rw-rw-r-- 1 kf106 kf106 32 maalis 12 18:03 hash.bin
It looks like hash.bin is 32 bytes long, and it’s not in uft-8, so I’m convinced it's a 256 bit SHA256 hash output. Aren’t you?
Step 4: Sign the hash with the private key and save the signature.
kf106@media-pc3:~/GIT/yubikey/openssl-test$ openssl pkeyutl -sign -inkey key.pem -in hash.bin > sig.bin
kf106@media-pc3:~/GIT/yubikey/openssl-test$ ls -la sig.bin
-rw-rw-r-- 1 kf106 kf106 72 maalis 12 18:07 sig.bin
kf106@media-pc3:~/GIT/yubikey/openssl-test$ cat sig.bin
?V???-d?|?Y???!?`9??;w??
???s???7R,??-?
That’s interesting?—?the signature is a 72 byte binary file. ECDSA signatures consist of two numbers, r and s, that are each as long as the private key. So the signature should be 64 bytes long. The extra 8 bytes are some kind of data specification?—?probably DER.
Step 5: Verify the signature using the public key:
kf106@media-pc3:~/GIT/yubikey/openssl-test$ openssl pkeyutl -verify -in hash.bin -sigfile sig.bin -inkey pub.pem -pubin
Signature Verified Successfully
Hurrah! I can sign stuff and then verify that the signature is valid.
So for the FIDO2 assert verification I just have to make sure I am using the right public key, the right signature in the right format, and the right material that was signed.
Easy, right?
Check that I can fido2-assert verify an assertion
The next thing to check is that the tools provided by Yubikey work. The security key clearly works, because I’m using it to log in to my various Google accounts.
I can use the fido2-assert command with the -V flag to verify an assertion. To start with, let’s get the public key into a pub.pem file:
kf106@media-pc3:~/GIT/yubikey/assert-test$ fido2-token -L
/dev/hidraw6: vendor=0x1050, product=0x0402 (Yubico YubiKey FIDO)
kf106@media-pc3:~/GIT/yubikey/assert-test$ ykman fido credentials list
Enter your PIN:
Credential ID RP ID Username Display name
9e7a503a... google.com [email protected] Keir Finlow-Bates
f0aab06b... google.com [email protected] Keir Finlow-Bates
c8073c7a... google.com [email protected] Keir Finlow-Bates
e7ee320a... chainfrog kf106
kf106@media-pc3:~/GIT/yubikey/assert-test$ fido2-token -L -k chainfrog /dev/hidraw6
Enter PIN for /dev/hidraw6:
00: 5+4yCrpSHhtIAAlpSF/yvecroRyyh4FpjWyGaZH0Y3Ni9GOVjOyPYSyPI5NdNrPa (null) a2YxMDYK es256 uvopt
kf106@media-pc3:~/GIT/yubikey/assert-test$ fido2-token -I -k chainfrog -i 5+4yCrpSHhtIAAlpSF/yvecroRyyh4FpjWyGaZH0Y3Ni9GOVjOyPYSyPI5NdNrPa /dev/hidraw6 | tail -n +2 > pub.pem
Enter PIN for /dev/hidraw6:
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat pub.pem
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5+4yCrpSHhtIAAlpSOYVEM5UIinM
twTLuM5kAUFolfOtxTR2VvWdRlx0pxMKax1GGiXUd3HKYxqq5H3VEnzdbQ==
-----END PUBLIC KEY-----
Then we create the client data to sign, in text, base64, and binary format so we have everything that might be needed:
kf106@media-pc3:~/GIT/yubikey/assert-test$ echo "some random thing to sign" > clientData.txt
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat clientData.txt | openssl sha256 -binary | base64 > clientData.b64
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat clientData.b64 | base64 -d > clientData.bin
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat clientData.txt
some random thing to sign
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat clientData.b64
PQhSX/O9HYEvIbUZH/fWyCKg1nqXS5t41sztwV745nQ=
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat clientData.bin
R_??/!????"??z?K?x????^??tkf106@media-pc3:~/GIT/yubikey/assert-test$
Now to generate the assertion parameters:
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat clientData.b64 > assert_param
kf106@media-pc3:~/GIT/yubikey/assert-test$ echo "chainfrog" >> assert_param
kf106@media-pc3:~/GIT/yubikey/assert-test$ echo "5+4yCrpSHhtIAAlpSF/yvecroRyyh4FpjWyGaZH0Y3Ni9GOVjOyPYSyPI5NdNrPa" >> assert_param
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat assert_param
PQhSX/O9HYEvIbUZH/fWyCKg1nqXS5t41sztwV745nQ=
chainfrog
5+4yCrpSHhtIAAlpSF/yvecroRyyh4FpjWyGaZH0Y3Ni9GOVjOyPYSyPI5NdNrPa
And then store the output from feeding the assert parameters to the fido2-assert command with the -G flag to get back an assertion:
kf106@media-pc3:~/GIT/yubikey/assert-test$ fido2-assert -G -i assert_param /dev/hidraw6 > assert_output
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat assert_output
PQhSX/O9HYEvIbUZH/fWyCKg1nqXS5t41sztwV745nQ=
chainfrog
WCVnaHBXPn50f21/ute5h879H92zhpX0EtNrad3xx1KnPgEAAAAy
MEYCIQCmAFE4jOiHVIoxIn00ecjAhDb3uUL61CFDl/d5RokTFQIhANYpiBUnRADTVDC6TvtpiDzMdzw+UzGXGfR/YZizQgBe
The output has the following content:
line 1: the base64 SHA256 hash output of the clientData.txt, i.e. clientData.b64
line 2: the relying party name
line 3: the authenticator data?—?more about that in the next section.
line 4: the digital signature.
Let’s verify that this is a valid assertion by handing the assert_output to the fido2-assert command with the -V flag and the public key file pub.pem:
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat assert_output | fido2-assert -V pub.pem es256
kf106@media-pc3:~/GIT/yubikey/assert-test$ echo $?
0
The return value of 0 shows that the signature is valid for the thing that we’re trying to work out.
This proves that fido2-assert -G and fido2-assert -V work.
More about the authenticator data
I now run the get assertion command multiple times to see how the output changes:
kf106@media-pc3:~/GIT/yubikey/assert-test$ fido2-assert -G -i assert_param /dev/hidraw6
PQhSX/O9HYEvIbUZH/fWyCKg1nqXS5t41sztwV745nQ=
chainfrog
WCVnaHBXPn50f21/ute5h879H92zhpX0EtNrad3xx1KnPgEAAAAz
MEUCIQDNvxFy0KJGiRR0AfBDPKWkjLD1M9VBYVinl8Ax8qOHagIgTFGR1LkNQ94jFuxVp3fx1qDG567ZceXiit3PD2WJfT8=
kf106@media-pc3:~/GIT/yubikey/assert-test$ fido2-assert -G -i assert_param /dev/hidraw6
PQhSX/O9HYEvIbUZH/fWyCKg1nqXS5t41sztwV745nQ=
chainfrog
WCVnaHBXPn50f21/ute5h879H92zhpX0EtNrad3xx1KnPgEAAAA0
MEUCIEk7Q55pfKRyum+OwEhZ8ne5Dtw3TgULo6vw0+wpdM3bAiEA+1hkebFViJu6uuR2dKEOXrue8DxZfF+DwHPT8koexPE=
kf106@media-pc3:~/GIT/yubikey/assert-test$ fido2-assert -G -i assert_param /dev/hidraw6
PQhSX/O9HYEvIbUZH/fWyCKg1nqXS5t41sztwV745nQ=
chainfrog
WCVnaHBXPn50f21/ute5h879H92zhpX0EtNrad3xx1KnPgEAAAA4
MEUCIGP7BwfB6IYyCodFDQkDhsZTIKP9OPLnNgPqc1XTuJT+AiEAkXoN0uggM4NRB52WsBKjlBItGG88Nn3HAbY4TEPAoXQ=
The signature data changes (expected) and the authenticator data changes (requires investigating), and the challenge message and the relying party remain the same (expected).
Have a look at the authenticator data:
WCVnaHBXPn50f21/ute5h879H92zhpX0EtNrad3xx1KnPgEAAAAz
WCVnaHBXPn50f21/ute5h879H92zhpX0EtNrad3xx1KnPgEAAAA0
WCVnaHBXPn50f21/ute5h879H92zhpX0EtNrad3xx1KnPgEAAAA4
The beginning is the same, but the end changes. If we decode it into hexadecimal, we can see how it changes:
kf106@media-pc3:~/GIT/yubikey/assert-test$ echo "WCVnaHBXPn50f21/ute5h879H92zhpX0EtNrad3xx1KnPgEAAAA0" | base64 -d | xxd -p
5825676870573e7e747f6d7fbad7b987cefd1fddb38695f412d36b69ddf1
c752a73e0100000034
kf106@media-pc3:~/GIT/yubikey/assert-test$ echo "WCVnaHBXPn50f21/ute5h879H92zhpX0EtNrad3xx1KnPgEAAAA4" | base64 -d | xxd -p
5825676870573e7e747f6d7fbad7b987cefd1fddb38695f412d36b69ddf1
c752a73e0100000038
It looks like the last 8 octals might be a counter, which makes sense, because you don’t want to sign the same message over and over again, as that can make ECDSA vulnerable to key extraction attacks.
Messing with assert_output
Now I’m going to edit each of the first line, the second line, and the third line in turn, to see if that affects the fido2-assert -V response:
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat assert_output | fido2-assert -V pub.pem es256
kf106@media-pc3:~/GIT/yubikey/assert-test$ echo $?
0
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat assert_output1
AAASX/O9HYEvIbUZH/fWyCKg1nqXS5t41sztwV745nQ=
chainfrog
WCVnaHBXPn50f21/ute5h879H92zhpX0EtNrad3xx1KnPgEAAAAy
MEYCIQCmAFE4jOiHVIoxIn00ecjAhDb3uUL61CFDl/d5RokTFQIhANYpiBUnRADTVDC6TvtpiDzMdzw+UzGXGfR/YZizQgBe
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat assert_output1 | fido2-assert -V pub.pem es256
fido2-assert: fido_assert_verify: FIDO_ERR_INVALID_SIG
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat assert_output2
PQhSX/O9HYEvIbUZH/fWyCKg1nqXS5t41sztwV745nQ=
BBBinfrog
WCVnaHBXPn50f21/ute5h879H92zhpX0EtNrad3xx1KnPgEAAAAy
MEYCIQCmAFE4jOiHVIoxIn00ecjAhDb3uUL61CFDl/d5RokTFQIhANYpiBUnRADTVDC6TvtpiDzMdzw+UzGXGfR/YZizQgBe
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat assert_output2 | fido2-assert -V pub.pem es256
fido2-assert: fido_assert_verify: FIDO_ERR_INVALID_PARAM
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat assert_output3
PQhSX/O9HYEvIbUZH/fWyCKg1nqXS5t41sztwV745nQ=
chainfrog
CCCnaHBXPn50f21/ute5h879H92zhpX0EtNrad3xx1KnPgEAAAAy
MEYCIQCmAFE4jOiHVIoxIn00ecjAhDb3uUL61CFDl/d5RokTFQIhANYpiBUnRADTVDC6TvtpiDzMdzw+UzGXGfR/YZizQgBe
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat assert_output3 | fido2-assert -V pub.pem es256
fido2-assert: fido_assert_set: FIDO_ERR_INTERNAL
First I checked that the assertion verification still works.
Then I edited the client data and got back a FIDO_ERR_INVALID_SIG
Then I edited the relying party name and got back a FIDO_ERR_INVALID_PARAM
Finally, I edited just the authenticator data and got back a FIDO_ERR_INTERNAL
Clearly each parameter is important to fido2-assert -V, but they cause a failure in different ways each time.
Let’s see what happens if I use the public key from my OpenSSL experiment, i.e. the wrong public key, and try:
kf106@media-pc3:~/GIT/yubikey/assert-test$ cat assert_output | fido2-assert -V pub-openssl.pem es256
fido2-assert: fido_assert_verify: FIDO_ERR_INVALID_SIG
That gives us the same error we got when we changed the client data.
Interesting.
Yubikey implementation error?
Time to go back to the documentation, now that I understand the practicalities of what is going on.
Problem 1: Is the Yubikey signing the wrong?thing?
https://www.w3.org/TR/webauthn/#assertion-signature says that the assertion signature should sign a concatenation of the authenticator data and the client data hash.
However, despite trying various hashes and combinations of the returned authenticator data and the client data hash, I cannot get a successful signature with a concatenation of the client data hash and the authenticator data as returned by fido2-assert.
Problem 2: fido2-assert returns the wrong authenticator data
https://developers.yubico.com/libfido2/Manuals/fido2-assert.html says the third line of the returned line is authenticator data.
https://www.w3.org/TR/webauthn/#authenticator-data says authenticator data is 32 bytes of the relying party ID hashed with SHA256, 1 byte of flag data, and 4 bytes for a counter, unless there are further extensions. There should certainly not be more than 33 bytes before what must be the counter though.
What the Yubikey returns through fido2-assert is not correct according to the ww3 and yubico documentation. The authenticator has a 5825 prefix, followed by the hash of the relying party ID and the four-byte counter.
I asked for clarification on StackOverflow, and someone pointed out that 58 is something called a CBOR prefix, and 25 is the length of the remaining data (37 in decimal): https://stackoverflow.com/questions/79506595/yubikey-attestation-returns-39-byte-authenticator-data-instead-of-37-bytes
The authenticator data is presented in CBOR format, but OpenSSL expects the raw data that was signed.
Removing the 5825 prefix gives the correct authenticator data, which you can see from the image above.
Solved?
I tried signing the concatenated data again, just to be sure I hadn’t messed up. It still didn’t work.
Initially I wasn’t removing the length indicator, because https://www.w3.org/TR/webauthn/#authenticator-data says that the authenticator describes its own length:
However, using the authenticator data with the 5825 prefix removed and the client data hash as generated resulted in a valid signature. I guess “describes its own length” means “either describes its own length or has a default length of 37.”
Because I hadn’t managed my data well enough, I initially thought I’d verified a valid signature using just the authenticator data with the prefix removed. This would have been a serious bug in the Yubikey's hardware.
However, it is important to be thorough when checking cryptographic matters, so I tried again, working through the whole process above.
It turns out that the Yubikey does indeed sign a concatenation of the correct authenticator data with the client data hash, so no bug in the hardware.
Anyway?—?success! I have managed to generate an assertion with fido2-assert and then verify the resulting signature using openssl, as I wanted to.
And in the process I’ve learned a bit more about digital signing. A win!
Conclusion
On closer examination, I suspect it is just a problem with the output of fido2-assert not matching the implementation documentation or the documentation for the tool. Not as serious as I first thought, but definitely a waste of time for people trying to get to grips with what Yubikeys are doing under the hood.
As a good Netizen, I have opened a documentation bug for Yubico to deal with, which you can find here: https://github.com/Yubico/libfido2/issues/858
I am hopeful they will act on it, as it’s a small change (which might save other developers hours of messing around). Although hopefully, anyone else experiencing this problem may stumble across this article.
Update: Yubico fixed it!
?? Founder, Tokenomics.net. Free Tokenomics Template & Course in "Featured". Co-Founder of CONV3RT, A Creative Agency for Web3 Brands.
1 小时前Commenting for reach and support! Need more technical blockchain content on LI
i have to try this!
CEO & Founder, NEXTGEN POWER GROUP LTD.
1 天前Chris Horner ????????