Signature on Web3 - ECDSA Algorithm

By Charles LAZIOSI
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. Reusing k 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:

  1. Add pointycastle as a dependency in your pubspec.yaml file:
dependencies:
  flutter:
    sdk: flutter
  pointycastle: ^3.6.0
  convert: ^3.0.1
  asn1lib: ^1.0.2
  http: ^0.13.3
  1. Run flutter pub get to install the new dependency.

  2. 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);
  }
}
  1. 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.

  1. 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"] }
  1. 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")
    }

}

  1. 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.

Flutter App Check Signature
Flutter App Check Signature

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