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

Viewed 41

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?

1 Answers

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);
Related