Signature on Web3 - ECDSA Algorithm
- Published on

Web3 definition
Web3, often referred to as Web 3.0, is an idea for a new iteration of the World Wide Web based on blockchain technology, which incorporates concepts such as decentralization, blockchain technologies, and token-based economics. Unlike the current web (Web 2.0), where data is primarily centralized in a small group of companies (like Google, Facebook, and Amazon), Web3 envisions a decentralized internet where users have control over their own data, identity, and transactions.
ECDSA Algorithm
The Elliptic Curve Digital Signature Algorithm (ECDSA) is a cryptographic algorithm used by various systems, including cryptocurrencies like Bitcoin and Ethereum, for digital signatures. It's an asymmetric algorithm that creates a pair of keys: a private key for signing and a public key for verification.
ECDSA is based on elliptic curve cryptography, which provides the same level of security as traditional public-key cryptography standards but with shorter key lengths. This results in faster computations and reduced storage and transmission requirements.
Security Aspects:
ECDSA relies on the difficulty of solving certain mathematical problems related to elliptic curves, specifically the elliptic curve discrete logarithm problem (ECDLP). As long as these problems remain computationally infeasible to solve within a reasonable time frame, ECDSA remains secure.
However, like all cryptographic algorithms, ECDSA's security depends on proper implementation and secure handling of keys. If private keys are compromised or if poor random number generators are used (particularly in generating nonce 'k'), this could lead to vulnerabilities that attackers can exploit.
Usage in Web3:
In Web3 applications and cryptocurrencies, ECDSA provides secure digital signatures that ensure transactions are authenticated and tamper-proof. For instance:
- In Bitcoin and Ethereum, when users make transactions, they sign them with their private keys using ECDSA. The network then verifies these transactions using their public keys.
- In smart contracts interacting with blockchain-based applications (dApps), ECDSA can be used to verify that actions taken are authorized by the rightful owner of the assets or identity involved.
Importance for Blockchain and Web3:
The role of ECDSA in blockchain technology and Web3 cannot be overstated. It is a cornerstone for ensuring the integrity and security of transactions across decentralized networks. Here's why it's so important:
- Non-repudiation: ECDSA provides a digital signature that is almost impossible to forge, which means that once a transaction is signed, the signer cannot deny having authorized it.
- Integrity: The signature ensures that the transaction has not been altered in transit. If any part of the transaction is changed after it's been signed, the signature will no longer be valid when checked against the public key.
- Authentication: By verifying the digital signature against a public key, anyone on the network can confirm that a transaction was indeed created by the holder of the corresponding private key.
Limitations and Considerations:
Despite its strengths, there are challenges associated with ECDSA:
- Key Security: The security of ECDSA is only as strong as how securely private keys are stored. If an attacker gains access to a user’s private key, they can sign transactions as if they were that user.
- Randomness Requirement: The random number (nonce)
k
used in signing must be truly random and never reused. Reusingk
across different signatures can lead to private key exposure. - Quantum Computing Threats: While current classical computers cannot feasibly break ECDSA under normal conditions, advancements in quantum computing could potentially pose a threat to this algorithm in the future.
In summary, while ECDSA plays an essential role in securing transactions within Web3 and ensuring trust within decentralized systems, it also requires careful handling and awareness of its limitations. As Web3 technologies mature, there may be advancements or new cryptographic methods adopted to address some of these challenges while maintaining or enhancing security levels.
ECDSA Implementation with Flutter and Rust
We want to create a system where a Flutter application (acting as the client) signs a message, and then a Rust-based server (acting as the backend) verifies that signature. The difficulty lies in the fact that most available examples demonstrate this process within a single language or on the same side of the communication channel. Our goal is to successfully execute the signing process on the Flutter side and validate it on the Rust side, bridging between two different languages and sides of an application. In this implementation we will use secp256k1 the parameters of the elliptic curve used in Bitcoin's public-key cryptography, and defined in Standards for Efficient Cryptography (SEC)
Flutter implementation : sign the message
To implement the ECDSA secp256k1 curve in a Flutter client, you'll need to use a cryptographic library that supports this algorithm. The pointycastle
package is a well-known cryptographic library in Dart that can be used for this purpose. Below are the steps to create a Flutter client that uses the secp256k1 curve:
- Add
pointycastle
as a dependency in yourpubspec.yaml
file:
dependencies:
flutter:
sdk: flutter
pointycastle: ^3.6.0
convert: ^3.0.1
asn1lib: ^1.0.2
http: ^0.13.3
Run
flutter pub get
to install the new dependency.Create a new Dart file (e.g.,
crypto_helper.dart
) and implement the ECDSA secp256k1 functionality:
import 'dart:math';
import 'dart:typed_data';
import 'package:asn1lib/asn1lib.dart';
import 'package:convert/convert.dart';
import 'package:pointycastle/export.dart';
class CryptoHelper {
final ECDomainParameters _domainParams = ECDomainParameters('secp256k1');
// Generate Key Pair
AsymmetricKeyPair<PublicKey, PrivateKey> generateKeyPair() {
var keyParams = ECKeyGeneratorParameters(_domainParams);
var secureRandom = FortunaRandom();
var random = Random.secure();
var seed = List<int>.generate(32, (_) => random.nextInt(256));
secureRandom.seed(KeyParameter(Uint8List.fromList(seed)));
var rngParams = ParametersWithRandom(keyParams, secureRandom);
var keyGenerator = ECKeyGenerator();
keyGenerator.init(rngParams);
return keyGenerator.generateKeyPair();
}
// Sign message hash
ECSignature sign(Uint8List messageHash, PrivateKey privateKey) {
var signer = ECDSASigner(null, HMac(SHA256Digest(), 64));
signer.init(true, PrivateKeyParameter<ECPrivateKey>(privateKey));
return signer.generateSignature(messageHash) as ECSignature;
}
// Verify signature
bool verify(
Uint8List messageHash, ECSignature signature, PublicKey publicKey) {
var verifier = ECDSASigner(null, HMac(SHA256Digest(), 64));
verifier.init(false, PublicKeyParameter<ECPublicKey>(publicKey));
return verifier.verifySignature(messageHash, signature);
}
// Hash message
hashMessage(String message) {
return SHA256Digest().process(Uint8List.fromList(message.codeUnits));
}
// Convert public key to hex
String publicKeyToHex(ECPublicKey publicKey) {
return hex.encode(publicKey.Q!.getEncoded(false));
}
// Convert DER encoded signature to hex
String encodeECSignatureToDER(ECSignature signature) {
var r = ASN1Integer(signature.r);
var s = ASN1Integer(signature.s);
var sequence = ASN1Sequence();
sequence.add(r);
sequence.add(s);
return hex.encode(sequence.encodedBytes);
}
// Convert hex encoded signature to DER
String privateKeyToHex(ECPrivateKey privateKey) {
return privateKey.d!.toRadixString(16);
}
// Convert hex encoded signature to DER
String messageHashToHex(Uint8List messageHash) {
return hex.encode(messageHash);
}
// Convert signature to hex
String signatureToHex(ECSignature signature) {
return signature.r.toRadixString(16) + signature.s.toRadixString(16);
}
}
- Now you can use this helper class in your Flutter app to generate key pairs, sign messages and verify signatures using the secp256k1 curve.
Here's an example of how you might use it within a Flutter widget:
Future<void> _generateAndSign() async {
// Nonce to be synchronized between client and server
var nonce = "30450221009137c8489f844822843868d77f93c288ea64427005";
// Generate Key Pair
AsymmetricKeyPair<PublicKey, PrivateKey> keyPair =
cryptoHelper.generateKeyPair();
// Update Private Key
setState(() {
privateKeyStr =
cryptoHelper.privateKeyToHex(keyPair.privateKey as ECPrivateKey);
});
// Update Public Key
setState(() {
publicKeyStr =
cryptoHelper.publicKeyToHex(keyPair.publicKey as ECPublicKey);
});
// Generate a random message string
var random = Random.secure();
var messageInt = List<int>.generate(32, (_) => random.nextInt(256));
var messageStr = String.fromCharCodes(messageInt);
// messagehash from the message string
var message =
cryptoHelper.messageHashToHex(cryptoHelper.hashMessage(messageStr));
// Convert string variable into UInt8List
var messageHash = cryptoHelper.hashMessage(message + nonce);
// Update Message Hash
setState(() {
messageHashStr = message;
});
// Sign the hash with private key
ECSignature signature = cryptoHelper.sign(messageHash, keyPair.privateKey);
// Convert signature to DER format for display purposes
String derEncodedSignature = cryptoHelper.encodeECSignatureToDER(signature);
setState(() {
signatureStr = derEncodedSignature;
});
// Verify Signature with public key
bool isVerified =
cryptoHelper.verify(messageHash, signature, keyPair.publicKey);
// Send signature to backend for verification
bool isValidateCompactBackend = await validateSignatureOnBackend(
messageHashStr, signatureStr, publicKeyStr);
setState(() {
validateCompactBackend = isValidateCompactBackend ? 'Valid' : 'Invalid';
});
// Update Verification Result
setState(() {
verificationResult = isVerified ? 'Valid' : 'Invalid';
});
}
As you can see, we call the validateSignatureOnBackend
function with the message, the signature and the public key as the parameters. Basically, we sign with the private key, and we validate the signature with the public key on backend. This API call source code is here:
Future<bool> validateSignatureOnBackend(
String messageHashHex,
String signatureHex,
String publicKeyHex,
) async {
final url = Uri.parse('http://127.0.0.1:8080/validate_signature');
try {
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: json.encode({
'message': messageHashHex,
'signature': signatureHex,
'public_key': publicKeyHex,
}),
);
if (response.statusCode == 200) {
print('Request successful. Response body: ${response.body}');
// Handle response here if necessary (e.g., parse JSON)
return response.body == 'true';
} else {
print(
'Request failed with status: ${response.statusCode}. Response body: ${response.body}');
return false;
}
} catch (e) {
print("Error sending request to backend: $e");
return false;
}
}
Finally, let's see what is happening on backend side with Rust
Rust Implementation : verify the signature
To implement the ECDSA signature verification, let's take the most famous library OpenSSL.
- Configure the dependencies
[package]
name = "secp256k1-server"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rand = "0.8"
actix-web = "4.4.1"
actix-rt = "2.3.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
hex = "0.4"
sha2 = { version = "0.10", default-features = false }
actix-cors ="0.7.0"
openssl = "0.10.32"
chrono = { version = "0.4", features = ["serde"] }
- Validate the signature receive in the POST API call
// Handler for validating signatures
async fn validate_signature(signed_message: web::Json<SignedMessage>) -> impl Responder {
// Nonce to prevent replay attacks (to be sent to the client on a regular basis)
// Change the nonce will prevent the reuse of the same signature twice
let nonce = "30450221009137c8489f844822843868d77f93c288ea64427005".as_bytes().to_vec();
// Convert hexadecimal strings to byte slices
let public_key_bytes = hex::decode(&signed_message.public_key).unwrap();
let signature_bytes = hex::decode(&signed_message.signature).unwrap();
println!("--------------------");
println!("Public key: {}", hex::encode(&public_key_bytes[..]));
println!("Message: {}", signed_message.message);
println!("Signature: {}", hex::encode(&signature_bytes[..]));
print!("--------------------");
// Initialize a BigNumContext
let mut ctx = BigNumContext::new().unwrap();
// Create an EC key directly from the public key bytes
let group = EcGroup::from_curve_name(Nid::SECP256K1).unwrap();
let ec_point = EcPoint::from_bytes(&group, &public_key_bytes, &mut ctx).unwrap();
let ec_key = EcKey::from_public_key(&group, &ec_point).unwrap();
let pkey = PKey::from_ec_key(ec_key).unwrap();
// Initialize a verifier
let mut verifier = Verifier::new_without_digest(&pkey).unwrap();
// Convert the message to a byte slice
let mut msg = signed_message.message.bytes().collect::<Vec<u8>>();
// Concatenate msg and once
msg.extend_from_slice(&nonce);
let result = verifier.verify_oneshot(&signature_bytes, &msg).unwrap();
if result == true {
HttpResponse::Ok().body("true")
} else {
HttpResponse::BadRequest().body("Signature is invalid")
}
}
- We can use this signature verification in a middleware for an API. In this case, we will force the execution of the signature middleware before any action on an API. Below you can find an example of middleware implementing the signature verification:
///
/// Middleware to require a Bearer token
///
/// This middleware will check if the request has a valid Bearer token
/// If the token is valid, the request will be passed to the next middleware
/// If the token is invalid, the request will be rejected
///
struct AuthToken;
impl<S> Transform<S, ServiceRequest> for AuthToken
where
S: Service<
ServiceRequest,
Response = ServiceResponse<actix_web::body::BoxBody>,
Error = actix_web::Error,
>,
S::Future: 'static,
{
type Response = ServiceResponse<actix_web::body::BoxBody>;
type Error = actix_web::Error;
type Transform = AuthMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
future::ready(Ok(AuthMiddleware {
service,
}))
}
}
struct AuthMiddleware<S> {
service: S,
}
impl<S> Service<ServiceRequest> for AuthMiddleware<S>
where
S: Service<
ServiceRequest,
Response = ServiceResponse<actix_web::body::BoxBody>,
Error = actix_web::Error,
>,
S::Future: 'static,
{
type Response = ServiceResponse<actix_web::body::BoxBody>;
type Error = actix_web::Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, actix_web::Error>>;
fn poll_ready(
&self,
ctx: &mut core::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
self.service.poll_ready(ctx)
}
fn call(&self, req: ServiceRequest) -> Self::Future {
let response = |req: ServiceRequest, response: HttpResponse| -> Self::Future {
Box::pin(async { Ok(req.into_response(response)) })
};
println!("Request: {:?}", req);
// Check if the signature is valid
// Get the token from the request Authorization base64 header and decode it
// manage if the token is empty or invalid
let authorization_header = match req.headers().get("Authorization") {
Some(header) => header,
None => {
return response(
req,
HttpResponse::Unauthorized()
.json(ErrorResponse::Unauthorized(String::from("Missing Token"))),
);
}
};
//replace "Bearer " with "" to get the token
let authorization_header = authorization_header.to_str().unwrap().replace("Bearer ", "");
// Decode the base64token to a string
let base64_token = authorization_header;
let token_bytes = match general_purpose::STANDARD.decode(base64_token) {
Ok(token) => token,
Err(_) => {
return response(
req,
HttpResponse::Unauthorized()
.json(ErrorResponse::Unauthorized(String::from("Malformed/Invalid Token"))),
);
}
};
let token = String::from_utf8(token_bytes).unwrap();
let public_key = token.split(":").collect::<Vec<&str>>()[0];
let signature = token.split(":").collect::<Vec<&str>>()[1];
let message = token.split(":").collect::<Vec<&str>>()[2];
// Nonce to prevent replay attacks (to be sent to the client on a regular basis)
// Change the nonce will prevent the reuse of the same signature twice
let nonce = "30450221009137c8489f844822843868d77f93c288ea64427005".as_bytes().to_vec();
// Convert hexadecimal strings to byte slices
let public_key_bytes = hex::decode(&public_key).unwrap();
let signature_bytes = hex::decode(&signature).unwrap();
// Initialize a BigNumContext
let mut ctx = BigNumContext::new().unwrap();
// Create an EC key directly from the public key bytes
let group = EcGroup::from_curve_name(Nid::SECP256K1).unwrap();
let ec_point = EcPoint::from_bytes(&group, &public_key_bytes, &mut ctx).unwrap();
let ec_key = EcKey::from_public_key(&group, &ec_point).unwrap();
let pkey = PKey::from_ec_key(ec_key).unwrap();
// Initialize a verifier
let mut verifier = Verifier::new_without_digest(&pkey).unwrap();
// Convert the message to a byte slice
let mut msg = message.bytes().collect::<Vec<u8>>();
// Concatenate msg and once
msg.extend_from_slice(&nonce);
let result = verifier.verify_oneshot(&signature_bytes, &msg).unwrap();
if result {
// If the signature is valid, call the next middleware
let future = self.service.call(req);
Box::pin(async move {
let response = future.await?;
Ok(response)
})
} else {
// If the signature is invalid, return an error
return response(
req,
HttpResponse::Unauthorized()
.json(ErrorResponse::Unauthorized(String::from("Invalid Token"))),
);
}
}
}
Final result
Once launched, the application shows all the parameters and the result of the signature verification.

The full source code can be retrieved here: https://github.com/claziosi/secp256k1