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