nami
nami copied to clipboard
Clarification on CIP-0030 signData
Hello, I'm building a dapp that uses signData for its authentication process with the backend. I'm having difficulty figuring out how to upgrade from the deprecated signData function to the new one. I am able to sign the data the same as before, but the difficulty lies in interpreting the { key, signature } result. Previously, it returned a COSESign1 hex string. With the hex, I could verify the signData like so:
import * as Serialization from '@emurgo/cardano-serialization-lib-nodejs';
import * as MessageSigning from './message_signing/rust/pkg/emurgo_message_signing';
/**
* Determines if a wallet address signed a message (payload)
* @param address Serialized wallet address
* @param payload Serialized payload (i.e. message with nonce)
* @param coseSign1Hex Hex string of signed payload (signed by user's wallet)
* @returns True if payload was signed by wallet address
*/
export const verifySignedMessage = (address: string, payload: string, coseSign1Hex: string) => {
const coseSign1 = MessageSigning.COSESign1.from_bytes(Buffer.from(coseSign1Hex, 'hex'));
const payloadCose: Uint8Array | undefined = coseSign1.payload();
if (!payloadCose || !verifyPayload(payload, payloadCose)) {
throw new Error('Payload does not match');
}
const protectedHeaders: MessageSigning.HeaderMap = coseSign1
.headers()
.protected()
.deserialized_headers();
const headerCBORBytes: Uint8Array | undefined = protectedHeaders.header(MessageSigning.Label.new_text('address'))?.as_bytes();
if (!headerCBORBytes) {
throw new Error('Failed to convert header to bytes');
}
const keyId: Uint8Array | undefined = protectedHeaders.key_id();
if (!keyId) {
throw new Error('Failed to get keyId from header');
}
const addressCose: Serialization.Address = Serialization.Address.from_bytes(headerCBORBytes);
const publicKeyCose = Serialization.PublicKey.from_bytes(keyId);
if (!verifyAddress(address, addressCose, publicKeyCose))
throw new Error('Could not verify because of address mismatch');
const signature = Serialization.Ed25519Signature.from_bytes(coseSign1.signature());
const data = coseSign1.signed_data().to_bytes();
return publicKeyCose.verify(data, signature);
};
const verifyPayload = (payload: string, payloadCose: Uint8Array) => {
return Buffer.from(payloadCose).compare(Buffer.from(payload, 'hex')) === 0;
};
const verifyAddress = (address: string, addressCose: Serialization.Address, publicKeyCose: Serialization.PublicKey) => {
const checkAddress = Serialization.Address.from_bytes(Buffer.from(address, 'hex'));
if (addressCose.to_bech32() !== checkAddress.to_bech32()) return false;
// check if BaseAddress
try {
const baseAddress: Serialization.BaseAddress | undefined = Serialization.BaseAddress.from_address(addressCose);
if (!baseAddress) {
throw new Error('Failed to get base address from addressCose');
}
//reconstruct address
const paymentKeyHash = publicKeyCose.hash();
const stakeKeyHash: Serialization.Ed25519KeyHash | undefined = baseAddress.stake_cred().to_keyhash();
if (!stakeKeyHash) {
throw new Error('Failed to find stake key hash');
}
const reconstructedAddress = Serialization.BaseAddress.new(
checkAddress.network_id(),
Serialization.StakeCredential.from_keyhash(paymentKeyHash),
Serialization.StakeCredential.from_keyhash(stakeKeyHash)
);
if (
checkAddress.to_bech32() !== reconstructedAddress.to_address().to_bech32()
)
return false;
return true;
} catch (error) {
console.error('Caught error verifying address', error);
}
// check if RewardAddress
try {
//reconstruct address
const stakeKeyHash = publicKeyCose.hash();
const reconstructedAddress = Serialization.RewardAddress.new(
checkAddress.network_id(),
Serialization.StakeCredential.from_keyhash(stakeKeyHash)
);
if (
checkAddress.to_bech32() !== reconstructedAddress.to_address().to_bech32()
)
return false;
return true;
} catch (error) {
console.error('Caught error verifying address', error)
}
return false;
};
From what I understand, the signature field of the new return object is also a COSESign1 hex. If I try using that field (and ignoring the key), I get an error when retrieving the keyid (makes sense). I tried using the key to calculate the keyid like so:
const newKey = MessageSigning.COSEKey.from_bytes(Buffer.from(keyHex, 'hex'));
const keyId = newKey.key_id();
, but that didn't work. Is there something I'm missing?
Client side workaround we're using for now:
if (walletName === 'nami') {
signature = await window.cardano.signData(publicKey, toHex(message));
} else {
signature = await window.cardano[walletName].signData(publicKey, toHex(message));
}
Fall back to the old implementation if a message has to besigned by nami. On other wallets, e.g. CCVault, use the CIP-0030 v0.1.0 standard.
I've looked at the current implementation for signData. Something like this seems to work for obtaining the public key.
if (key) {
const coseKey = MS.COSEKey.from_bytes(Buffer.from(key, 'hex'));
const publicKeyBytes = coseKey.header(
MS.Label.new_int(
MS.Int.new_negative(MS.BigNum.from_str('2'))
)
).as_bytes();
publicKeyCose = Cardano.PublicKey.from_bytes(publicKeyBytes);
} else {
publicKeyCose = Cardano.PublicKey.from_bytes(protectedHeaders.key_id());
}
To be honest I need to read up un this some more to get a more complete picture.
Any updates when Nami will be up to date with CIP?