dvVerifySiwe function
Verifies a Sign-In with Ethereum message and its signature.
A signature never expires, so the checks that make this a login rather than a permanent credential are the nonce, the domain and the time window -- not the cryptography.
Implementation
DVWeb3Verification dvVerifySiwe({
required String message,
required Uint8List signature,
required String expectedDomain,
required String expectedNonce,
DateTime? now,
}) {
final DVSiweMessage? parsed = DVSiweMessage.parse(message);
if (parsed == null) {
return const DVWeb3Verification.failed(
'The message was not a well-formed EIP-4361 request.',
);
}
// Bound to the site that issued it, or a signature collected by any dapp the
// user visits is a login here.
if (parsed.domain != expectedDomain) {
return DVWeb3Verification.failed(
'The message was issued for "${parsed.domain}", not $expectedDomain.',
);
}
// Bound to one attempt, or the signature is a bearer token forever.
if (parsed.nonce != expectedNonce) {
return const DVWeb3Verification.failed(
'The nonce did not match the one issued.',
);
}
final DateTime at = now ?? DateTime.now().toUtc();
if (parsed.expirationTime != null && !at.isBefore(parsed.expirationTime!)) {
return const DVWeb3Verification.failed('The message has expired.');
}
if (parsed.notBefore != null && at.isBefore(parsed.notBefore!)) {
return const DVWeb3Verification.failed('The message is not yet valid.');
}
final String? recovered = dvRecoverSigner(message, signature);
if (recovered == null) {
return const DVWeb3Verification.failed(
'The signature did not recover an address.',
);
}
// Compared case-insensitively: the message may carry a lower-case address
// while recovery produces the checksummed form, and rejecting on case would
// refuse a perfectly good signature.
if (recovered.toLowerCase() != parsed.address.toLowerCase()) {
return DVWeb3Verification.failed(
'The signature was produced by $recovered, not ${parsed.address}.',
);
}
return DVWeb3Verification.passed(recovered);
}