How to decode the 65 byte signature produced by trezorctl?

I’m trying to understand the output format for Trezor signatures so I can verify it from another library programmatically. And I’m trying to use trezorctl to sign 32 byte messages and passing them in as base64 to the cli.

trezorctl get_public_node -n “m/44’/0’/0’/0/0”

Gives me a proper hex encoded public key that I can decode properly.

trezorctl sign-message -n “m/44’/0’/0’/0/0” “my_32_byte_b64encoded_message_here”

Gives me an output with an address, the message in encoded format, and the signature also in b64 format.

When I decode the b64 signature, it’s 65 bytes long, and I’m expecting 64 bytes to be the signature digest, and 1 byte at the front to be the recovery id (this was the only information I could find online and seems to match up.)

Recovery ids only can be 0,1,2,3 as a value, and what I got back was 31 or 32 on a different attempt.

I tried using all the possible recovery ids and using a Secp256k1 recovery function with the same message, and it yields a public key that does NOT match the output of get_public_node.

What am I not understanding here? I’ve gone through the trezorctl python code but I can’t find the encoder / decoders for this. Most other libraries for recoveryIds just represent them as a value with 4 possibilities.

Can anyone point me to any reference for how to decode this signature in compact format? I actually don’t even care about recovering the public key, the problem is that the signature fails to verify in the library I’m using (rust bitcoin lib,) so I was trying to double check that I’m decoding the signature properly. If I could just have a sanity check from anyone who knows what is going on here and knows how to decode the recoverable id and/or get the public key to match get_public_node it would help a lot. Maybe I am making some trivial mistake here?

I also tried using the GUI verify_message with the outputs it produced, and it worked entirely, I don’t think trezor is the issue here, I just can’t figure out how to manually decode the signature for use in another library.

Technically, the first byte is the recovery id, the next 32 bytes are r and s components of the signature. But 64 byte r + s is most likely what your signature library expects.

Kind of but not really. The first byte also specifies the address type.
See BIP-137 for details.

In any case if you tried all options you should have found the right one, so maybe something went wrong in that step?

Thank you for the link!

I have been able to verify the signatures now. I was missing the signed message header in my code. I found the solution here:

if not utils.BITCOIN_ONLY and coin.decred:
    h = utils.HashWriter(blake256())
else:
    h = utils.HashWriter(sha256())
if not coin.signed_message_header:
    raise wire.DataError("Empty message header not allowed.")
write_compact_size(h, len(coin.signed_message_header))
h.extend(coin.signed_message_header.encode())
write_compact_size(h, len(message))
h.extend(message)
ret = h.get_digest()
if coin.sign_hash_double:
    ret = sha256(ret).digest()
return ret

I couldn’t figure out the recovery part yet, but saw this code fragment here:

seckey = node.private_key()

digest = message_digest(coin, message)
signature = secp256k1.sign(seckey, digest)

if script_type == InputScriptType.SPENDADDRESS:
    script_type_info = 0
elif script_type == InputScriptType.SPENDP2SHWITNESS:
    script_type_info = 4
elif script_type == InputScriptType.SPENDWITNESS:
    script_type_info = 8
else:
    raise wire.ProcessError("Unsupported script type")

# Add script type information to the recovery byte.
if script_type_info != 0 and not msg.no_script_type:
    signature = bytes([signature[0] + script_type_info]) + signature[1:]

return MessageSignature(address=address, signature=signature)

Based on your information, I think the earlier test I did was invalid. The other libraries I am using in rust bitcoin only accept 0-3 for recovery Ids, but I am guessing this is because they must be operating on a step AFTER dealing with address format (since they’re dealing with the signature classes directly, not caring about address information.)

Thank you very much for the link. My validation function is working now, and I presume that would make one of the recovery Ids actually work as well, which I’ll verify later. I was using P2PKH keys compressed so the offset of 31 I was seeing actually makes sense.

Either way, the signature validation part is working now which is what I mostly concerned with, appreciate the link.

Also, in case anyone else encounters a similar issue, this was the piece I was missing (seen in rust bitcoin lib,)

/// Hash message for signature using Bitcoin's message signing format
pub fn signed_msg_hash(msg: &str) -> sha256d::Hash {
    sha256d::Hash::hash(
        &[
            MSG_SIGN_PREFIX,
            &encode::serialize(&encode::VarInt(msg.len() as u64)),
            msg.as_bytes(),
        ]
        .concat(),
    )
}

It’s required for validating the signature to make sure what you are signing in the message matches what the trezor firmware is signing. This was what I was missing during validation.