Verifying PassKey Signature - Structure

Hi, I'm reading different structures on how to construct my signature for verification with PassKeys.

I have my key with:

publicKeyU2F = b"".join([
		(0x04).to_bytes(1, byteorder='big'),
		key_from_dict.x,
		key_from_dict.y
	])

but when it comes to building the data to verify, I can see two choices...what's the correct format

https://medium.com/webauthnworks/verifying-fido2-responses-4691288c8770

signature_base = b"".join(
	[
		authenticator_data_bytes,
		client_data_hash_bytes,
	]
	)
	
	signature_base_hash = hashlib.sha256()
	signature_base_hash.update(signature_base)
	signature_base_hash_bytes = signature_base_hash.digest()

or https://www.w3.org/TR/webauthn-2/#sctn-fido-u2f-attestation

	signature_base = b"".join([
		(0x00).to_bytes(1, byteorder='big'),
		rpidhash,
		client_data_hash_bytes,
		credentialId,
		publicKeyU2F
	])
	signature_base_hash = hashlib.sha256()
	signature_base_hash.update(signature_base)
	signature_base_hash_bytes = signature_base_hash.digest()

Looks like a few different concepts got crossed here 🙂

  • U2F is an older spec. WebAuthn has backwards-compatibility support for U2F devices, but passkeys only use modern WebAuthn.
  • Public keys can be encoded in different forms for different algorithms. While your decoding may work for Apple's passkey implementation, it's recommended that you use a proper COSE_Key-aware decoder.
  • The verification steps are different depending on whether you're verifying a passkey attestation (i.e. registration) or assertion (i.e. sign in). The WebAuthn spec has exact algorithms for verifying both attestations and assertions.

Thanks, I'm trying to get it working with a very specific use-case to begin with, reading the spec, it reads like this should work, unfortunately, I always get a bad signature error :-(

  client_data_hash = hashlib.sha256()
	client_data_hash.update(client_data_bytes) #credentialAssertion.rawClientDataJSON
	client_data_hash_bytes = client_data_hash.digest()

  publicKey = b"".join([
		(0x04).to_bytes(1, byteorder='big'),
		key_from_dict.x,
		key_from_dict.y
	])
	
	signature_base = b"".join(
	[
		authenticator_data_bytes, #credentialAssertion.rawAuthenticatorData
		client_data_hash_bytes,
	]
	)
	
	signature_base_hash = hashlib.sha256()
	signature_base_hash.update(signature_base)
	signature_base_hash_bytes = signature_base_hash.digest()

	vk = ecdsa.VerifyingKey.from_string(publicKey, curve=ecdsa.NIST256p, hashfunc=sha256) # the default is sha1

	sig = decodedSignature #credentialAssertion.signature decoded from base64 to bytes
	msg = signature_base_hash_bytes

	try:
		vk.verify(sig, msg)
		print("good signature")
	except BadSignatureError:
		print("BAD SIGNATURE")

Update, this is slightly better, at least now I can see there is an issue with the signature, the authenticator is 37 bytes long, which breaks my generated signature:

32 bytes authenticator_data_bytes 32 bytes client_data_hash_bytes signature = 64 bytes

I'm foolishly taking the first 32 bytes of the authenticator...seems very wrong

client_data_hash = hashlib.sha256()
	client_data_hash.update(client_data_bytes)
	client_data_hash_bytes = client_data_hash.digest()

	key_from_dict = CoseKey.from_dict(key)

	publicKeyU2F = b"".join([
		#bytearray.fromhex('3059301306072a8648ce3d020106082a8648ce3d030107034200'),
		(0x04).to_bytes(1, byteorder='big'),
		key_from_dict.x,
		key_from_dict.y
	])
	print("publicKeyU2F: {0}".format(publicKeyU2F))
	
	signature_base = b"".join(
	[
		authenticator_data_bytes[0:32],
		client_data_hash_bytes,
	]
	)
	
	print("authenticator_data_bytes len: {}".format(len(authenticator_data_bytes)))
	print("client_data_hash_bytes len: {}".format(len(client_data_hash_bytes)))
	

	# vk = ecdsa.VerifyingKey.from_string(publicKeyU2F, curve=ecdsa.NIST256p, hashfunc=sha256) # the default is sha1
	verifyingKey = ecdsa.VerifyingKey.from_string(publicKeyU2F, curve=ecdsa.NIST256p, hashfunc=sha256)
	#verifyingKey = ecdsa.VerifyingKey.from_string(bytes.fromhex(keyasHex), curve=ecdsa.SECP256k1, hashfunc=sha256, valid_encodings=['raw'])

	verified = verifyingKey.verify(signature_base, decodedSignature)

Gives a

ecdsa.keys.BadSignatureError: Signature verification failed

aha, Signature is ASN.1, this is now valid

verified = verifyingKey.verify(decodedSignature, signature_base, hashfunc=hashlib.sha256, sigdecode=sigdecode_der)

Hey! I'm going nuts about the data construction. What data format are you using for clientData and assertion? Do you use the raw bytes converted from base64?

I hash clientData. Concatenate assertion and clientDataHash. And finally hash it to nonce, before verifying it.

Would appreciate your feedback worlds!

Hey!

I'm going nuts about the data construction. What data format are you using for rawClientDataJSON and rawAuthenticatorData? Do you use the raw bytes converted from base64?

I hash rawClientDataJSON to clientDataHash. Concatenate rawAuthenticatorData and clientDataHash. And finally hash it to nonce, before verifying it.

Would appreciate your feedback worlds!

Verifying PassKey Signature - Structure
 
 
Q