Search code examples
flutterdartdigital-signature

Dart / Flutter : How Do I Sign a String Using an ES 256 Algorithm and Private Key


Since Flutter doesn't support any map APIs across all platforms (mobile and desktop), I'm trying to fetch map snapshots with Apple's Web Snapshots API. This involves constructing a URL with various options then signing the URL. I append the signature to the end of my request URL so Apple can verify that it's from me.

Apple's instructions state:

To generate a signature, sign the string with your private key using a ES256 algorithm (also known as ECDSA using P-256 curve and SHA-256 hash algorithm). The signature must be Base64 URL-encoded.

I don't need to decrypt anything, I just need to sign the string and add it to the end of my request URL. So I don't think I need anything beyond the crypto library included with Flutter.

Here's what I've tried:

import 'package:crypto/crypto.dart';

//Private Key
var key = utf8.encode('''
-----BEGIN PRIVATE KEY-----
abcdef...
-----END PRIVATE KEY-----
''');

var bytes = utf8.encode('My URL String to Sign...');

var hmacSha256 = Hmac(sha256, key);
var sig = hmacSha256.convert(bytes);
    
var signature = base64UrlEncode(sig.bytes);

I get an unintelligible string as signature and add it to my request URL, but I still get a 401 Not Authorized error, so my signature must be incorrect.

How can I properly sign my URL string with my private key?


Solution

  • Using pointycastle, you need a suitable random number generator instance and a signer initialized with the relevant digest. Then just call generateSignature. That only gets you the r and s values which you need to encode.

    Here's an example:

    import 'dart:convert';
    import 'dart:math';
    import 'dart:typed_data';
    
    import 'package:pointycastle/asn1.dart';
    import 'package:pointycastle/export.dart';
    
      // the private key
      ECPrivateKey? privateKey;
      
      // some bytes to sign
      final bytes = Uint8List(0);
    
      // a suitable random number generator - create it just once and reuse
      final rand = Random.secure();
      final fortunaPrng = FortunaRandom()
        ..seed(KeyParameter(Uint8List.fromList(List<int>.generate(
          32,
          (_) => rand.nextInt(256),
        ))));
    
      // the ECDSA signer using SHA-256
      final signer = ECDSASigner(SHA256Digest())
        ..init(
          true,
          ParametersWithRandom(
            PrivateKeyParameter(privateKey!),
            fortunaPrng,
          ),
        );
    
      // sign the bytes
      final ecSignature = signer.generateSignature(bytes) as ECSignature;
    
      // encode the two signature values in a common format
      // hopefully this is what the server expects
      final encoded = ASN1Sequence(elements: [
        ASN1Integer(ecSignature.r),
        ASN1Integer(ecSignature.s),
      ]).encode();
    
      // and finally base 64 encode it
      final signature = base64UrlEncode(encoded);