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()

Replies

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)