Stage 5a: ECDSA on P-256, P-384 and P-521, and RSA

nist_curves.dart reads an uncompressed point as Go's
ecdsa.ParseUncompressedPublicKey does, and computes u1 G + u2 Q in
Jacobian coordinates with Shamir's trick, the infinity included.
ecdsa.dart is VerifyASN1: the encoding as cryptobyte reads it, r and s
in [1, n - 1] and never reduced, s above n/2 accepted, the hash cut to
the bits of the order. rsa.dart is VerifyPKCS1v15, which rebuilds the
encoding and compares it whole, and VerifyPSS with a salt of the length
of the hash, with the bytes before emLen that must be zero; and the
hashes of the table, SHA-256, SHA-384 and SHA-512 of package:crypto.

All on BigInt, exact on the VM and compiled to JavaScript, and not
constant-time: a verification sees public values only. The curve
constants are those that the vectors record from Go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.11
dev 2 days ago
parent 68eb96eed0
commit 1f1a69650a

@ -0,0 +1,114 @@
/// The verification of an ECDSA signature in ASN.1 (SEC 1 4.1.4, FIPS 186-5
/// 6.4.2) on P-256, P-384 and P-521, as Go's crypto/ecdsa.VerifyASN1, which
/// the CMS signatures and the time-stamp tokens of spec v0.12 §29.10 and
/// §29.11 use: the same signatures accepted, and only those.
///
/// It works on public values only, on the BigInt of nist_curves.dart, which
/// is not constant-time.
///
/// Internal: lib/datekeys.dart does not export it.
library;
import 'dart:typed_data';
import 'nist_curves.dart';
/// Whether [sig], an ECDSA-Sig-Value (a SEQUENCE of the INTEGERs r and s in
/// DER), is a signature of [hash] by [key], as Go's ecdsa.VerifyASN1:
///
/// - the encoding is read as Go's cryptobyte reads it: minimal lengths, of at
/// most four bytes, tags of one byte, minimal INTEGERs that are not
/// negative, and nothing after s or after the SEQUENCE;
/// - r and s are in [1, n − 1]: a value of n or more is refused, not reduced
/// modulo n, and an s above n/2 is accepted, as Go does;
/// - the hash is cut to the leftmost bits of n when it is longer, then taken
/// modulo n;
/// - u1·G + u2·Q is not the infinity and its x modulo n is r.
bool verifyEcdsaAsn1(NistPoint key, Uint8List hash, Uint8List sig) {
final rs = _parseSignature(sig);
if (rs == null) return false;
final (r, s) = rs;
final c = key.curve;
if (r == BigInt.zero || r >= c.n || s == BigInt.zero || s >= c.n) {
return false;
}
final e = _hashToInt(c, hash);
final w = s.modInverse(c.n);
final x = mulAddX(c, e * w % c.n, r * w % c.n, key);
return x != null && x % c.n == r;
}
// hashToNat of Go's crypto/internal/fips140/ecdsa: the leftmost bits of the
// hash, as many as the order has, as an integer modulo n.
BigInt _hashToInt(NistCurve c, Uint8List hash) {
final size = (c.n.bitLength + 7) ~/ 8;
var e = bigFromBytes(hash.length >= size ? hash.sublist(0, size) : hash);
if (hash.length >= size) {
final excess = size * 8 - c.n.bitLength;
if (excess > 0) e = e >> excess;
}
return e % c.n;
}
// parseSignature of Go's crypto/ecdsa: r and s, or null when sig is not
// exactly a SEQUENCE of two INTEGERs that cryptobyte reads.
(BigInt, BigInt)? _parseSignature(Uint8List sig) {
final seq = _read(sig, 0, sig.length, 0x30);
if (seq == null || seq.end != sig.length) return null;
final r = _readInteger(sig, seq.start, seq.end);
if (r == null) return null;
final s = _readInteger(sig, r.next, seq.end);
if (s == null || s.next != seq.end) return null;
return (r.value, s.value);
}
// readASN1Bytes of Go's cryptobyte: a minimal INTEGER that is not negative,
// at b[offset:end].
({BigInt value, int next})? _readInteger(Uint8List b, int offset, int end) {
final e = _read(b, offset, end, 0x02);
if (e == null) return null;
final n = e.end - e.start;
if (n == 0) return null;
if (n > 1 &&
((b[e.start] == 0 && b[e.start + 1] & 0x80 == 0) ||
(b[e.start] == 0xff && b[e.start + 1] & 0x80 != 0))) {
return null;
}
if (b[e.start] & 0x80 != 0) return null;
return (
value: bigFromBytes(Uint8List.sublistView(b, e.start, e.end)),
next: e.end,
);
}
// ReadASN1 of Go's cryptobyte at b[offset:end]: the element of identifier
// octet tag, with its content at [start, end), where the element ends. Null
// when the element does not fit, has a tag number of 31 or more,
// a length that is not in its shortest form or of more than four bytes, or
// another identifier octet.
({int start, int end})? _read(Uint8List b, int offset, int end, int tag) {
if (end - offset < 2) return null;
final id = b[offset];
if (id & 0x1f == 0x1f) return null;
final l = b[offset + 1];
int header;
int length;
if (l & 0x80 == 0) {
header = 2;
length = l;
} else {
final n = l & 0x7f;
if (n == 0 || n > 4 || end - offset < 2 + n) return null;
// At most four bytes, below 2^32: exact without a shift.
var v = 0;
for (var i = 0; i < n; i++) {
v = v * 256 + b[offset + 2 + i];
}
if (v < 128 || b[offset + 2] == 0) return null;
header = 2 + n;
length = v;
}
if (end - offset - header < length) return null;
if (id != tag) return null;
return (start: offset + header, end: offset + header + length);
}

@ -0,0 +1,234 @@
/// The NIST prime curves P-256, P-384 and P-521 of FIPS 186-5, which the
/// table of spec v0.12 §29.10 names for ECDSA: the reading of an
/// uncompressed point as Go's ecdsa.ParseUncompressedPublicKey reads it, and
/// u1·G + u2·Q, the multiplication of an ECDSA verification (ecdsa.dart).
///
/// The arithmetic is on BigInt, exact on the VM and compiled to JavaScript,
/// where an int is a double. It is not constant-time, and it does not need
/// to be: a verification works on public values only, the key of a
/// certificate and a signature. Never use it with a secret.
///
/// Internal: lib/datekeys.dart does not export it.
library;
import 'dart:typed_data';
import 'bytes.dart';
/// A curve y² = x³ − 3x + b over GF(p), with a base point G of prime order
/// n and cofactor 1, as Go's elliptic.CurveParams.
final class NistCurve {
NistCurve._(
this.name,
this.byteLength,
String p,
String n,
String b,
String gx,
String gy,
) : p = BigInt.parse(p, radix: 16),
n = BigInt.parse(n, radix: 16),
b = BigInt.parse(b, radix: 16),
gx = BigInt.parse(gx, radix: 16),
gy = BigInt.parse(gy, radix: 16);
/// The name of FIPS 186-5, such as `P-256`.
final String name;
/// The bytes of a coordinate, and of the order: 32, 48 or 66.
final int byteLength;
/// The prime of the field, the order of G, the constant b and G.
final BigInt p, n, b, gx, gy;
@override
String toString() => name;
}
// The parameters as Go's elliptic.P256, P384 and P521 give them; a test
// compares them with those that tool/cms_go_vectors_test.go records.
/// P-256, secp256r1 (1.2.840.10045.3.1.7).
final p256 = NistCurve._(
'P-256',
32,
'ffffffff00000001000000000000000000000000ffffffffffffffffffffffff',
'ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551',
'5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b',
'6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296',
'4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5',
);
/// P-384, secp384r1 (1.3.132.0.34).
final p384 = NistCurve._(
'P-384',
48,
'fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe'
'ffffffff0000000000000000ffffffff',
'ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf'
'581a0db248b0a77aecec196accc52973',
'b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875a'
'c656398d8a2ed19d2a85c8edd3ec2aef',
'aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a38'
'5502f25dbf55296c3a545e3872760ab7',
'3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c0'
'0a60b1ce1d7e819d7a431d7c90ea0e5f',
);
/// P-521, secp521r1 (1.3.132.0.35).
final p521 = NistCurve._(
'P-521',
66,
'01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
'ffff',
'01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
'fffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e9138'
'6409',
'51953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109'
'e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f'
'00',
'c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3d'
'baa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd'
'66',
'011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e'
'662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd1'
'6650',
);
/// A point of a curve in affine coordinates, never the point at infinity.
final class NistPoint {
const NistPoint._(this.curve, this.x, this.y);
/// The curve of the point.
final NistCurve curve;
/// The coordinates, each below p.
final BigInt x, y;
}
/// The unsigned big-endian integer of [bytes], zero when they are empty.
BigInt bigFromBytes(List<int> bytes) =>
bytes.isEmpty ? BigInt.zero : BigInt.parse(toHex(bytes), radix: 16);
/// The [length] bytes of the unsigned big-endian encoding of the
/// non-negative [v], which must fit in them.
Uint8List bytesFromBig(BigInt v, int length) {
final hex = v.toRadixString(16);
if (v.isNegative || hex.length > 2 * length) {
throw ArgumentError.value(v, 'v', 'not an integer of $length bytes');
}
return fromHex(hex.padLeft(2 * length, '0'));
}
/// The point of the uncompressed encoding [data] on [curve], as Go's
/// ecdsa.ParseUncompressedPublicKey reads it (SEC 1 2.3.3): 0x04 and the two
/// coordinates in exactly [NistCurve.byteLength] bytes each, both below p,
/// on the curve. Null for anything else: a compressed point, the encoding of
/// the infinity, another length, a coordinate of p or more, or a point off
/// the curve, none of them a key of the table of spec §29.10.
NistPoint? decodeUncompressed(NistCurve curve, List<int> data) {
final l = curve.byteLength;
if (data.length != 1 + 2 * l || data[0] != 4) return null;
final x = bigFromBytes(data.sublist(1, 1 + l));
final y = bigFromBytes(data.sublist(1 + l));
if (x >= curve.p || y >= curve.p) return null;
final p = curve.p;
final rhs = ((x * x - BigInt.from(3)) * x + curve.b) % p;
if (y * y % p != rhs) return null;
return NistPoint._(curve, x, y);
}
// A point in Jacobian coordinates, x = X/Z² and y = Y/Z³; Z = 0 is the point
// at infinity.
final class _Jacobian {
const _Jacobian(this.x, this.y, this.z);
final BigInt x, y, z;
bool get isInfinity => z == BigInt.zero;
}
final _infinity = _Jacobian(BigInt.one, BigInt.one, BigInt.zero);
final _three = BigInt.from(3);
final _four = BigInt.from(4);
final _eight = BigInt.from(8);
// 2·a, with the formulas dbl-2001-b of the Explicit-Formulas Database for
// a = −3.
_Jacobian _double(BigInt p, _Jacobian a) {
if (a.isInfinity || a.y == BigInt.zero) return _infinity;
final delta = a.z * a.z % p;
final gamma = a.y * a.y % p;
final beta = a.x * gamma % p;
final alpha = _three * ((a.x - delta) * (a.x + delta) % p) % p;
final x3 = (alpha * alpha - _eight * beta) % p;
final yz = a.y + a.z;
final z3 = (yz * yz - gamma - delta) % p;
final y3 = (alpha * (_four * beta - x3) - _eight * (gamma * gamma % p)) % p;
return _Jacobian(x3, y3, z3);
}
// a + (x2, y2), an affine point: the mixed addition of Jacobian coordinates,
// with the doubling when the two points are the same and the infinity when
// they are opposite.
_Jacobian _addAffine(BigInt p, _Jacobian a, BigInt x2, BigInt y2) {
if (a.isInfinity) return _Jacobian(x2, y2, BigInt.one);
final z1z1 = a.z * a.z % p;
final u2 = x2 * z1z1 % p;
final s2 = y2 * (a.z * z1z1 % p) % p;
final h = (u2 - a.x) % p;
final r = (s2 - a.y) % p;
if (h == BigInt.zero) {
return r == BigInt.zero ? _double(p, a) : _infinity;
}
final hh = h * h % p;
final hhh = h * hh % p;
final v = a.x * hh % p;
final x3 = (r * r - hhh - v - v) % p;
final y3 = (r * (v - x3) - a.y * hhh) % p;
final z3 = a.z * h % p;
return _Jacobian(x3, y3, z3);
}
// The affine coordinates of a, which is not the infinity.
(BigInt, BigInt) _affine(BigInt p, _Jacobian a) {
final zi = a.z.modInverse(p);
final zi2 = zi * zi % p;
return (a.x * zi2 % p, a.y * (zi2 * zi % p) % p);
}
// The bits of the non-negative v, the most significant first, in exactly
// width places.
List<bool> _bits(BigInt v, int width) {
final s = v.toRadixString(2).padLeft(width, '0');
return [for (var i = 0; i < s.length; i++) s.codeUnitAt(i) == 0x31];
}
/// The x coordinate of u1·G + u2·Q, both scalars in [0, n), or null when the
/// sum is the point at infinity: Shamir's trick, one doubling per bit and
/// one addition of G, Q or G + Q.
BigInt? mulAddX(NistCurve curve, BigInt u1, BigInt u2, NistPoint q) {
final p = curve.p;
final gq = _addAffine(p, _Jacobian(curve.gx, curve.gy, BigInt.one), q.x, q.y);
// G + Q is the infinity when Q = −G: adding it then adds nothing.
final (BigInt, BigInt)? gqAffine = gq.isInfinity ? null : _affine(p, gq);
final width = u1.bitLength > u2.bitLength ? u1.bitLength : u2.bitLength;
final b1 = _bits(u1, width);
final b2 = _bits(u2, width);
var acc = _infinity;
for (var i = 0; i < width; i++) {
acc = _double(p, acc);
if (b1[i] && b2[i]) {
if (gqAffine != null) {
acc = _addAffine(p, acc, gqAffine.$1, gqAffine.$2);
}
} else if (b1[i]) {
acc = _addAffine(p, acc, curve.gx, curve.gy);
} else if (b2[i]) {
acc = _addAffine(p, acc, q.x, q.y);
}
}
if (acc.isInfinity) return null;
return _affine(p, acc).$1;
}

@ -0,0 +1,179 @@
/// The verification of RSA signatures, RSASSA-PKCS1-v1_5 and RSASSA-PSS of
/// RFC 8017, as Go's crypto/rsa.VerifyPKCS1v15 and VerifyPSS verify them for
/// the CMS signatures and the time-stamp tokens of spec v0.12 §29.10 and
/// §29.11: the same signatures accepted, and only those. And the hashes of
/// the table of §29.10, SHA-256, SHA-384 and SHA-512, from package:crypto.
///
/// It works on public values only, on BigInt, exact on the VM and compiled
/// to JavaScript and not constant-time: a public key and a signature.
///
/// Internal: lib/datekeys.dart does not export it.
library;
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import 'bytes.dart';
import 'nist_curves.dart' show bigFromBytes, bytesFromBig;
/// A hash of the table of spec §29.10, as Go's crypto.Hash.
enum Sha2 {
/// SHA-256, 2.16.840.1.101.3.4.2.1.
sha256(32, '3031300d060960864801650304020105000420'),
/// SHA-384, 2.16.840.1.101.3.4.2.2.
sha384(48, '3041300d060960864801650304020205000430'),
/// SHA-512, 2.16.840.1.101.3.4.2.3.
sha512(64, '3051300d060960864801650304020305000440');
const Sha2(this.size, this._digestInfo);
/// The length of a hash, in bytes.
final int size;
final String _digestInfo;
/// The DER of the DigestInfo of RSASSA-PKCS1-v1_5 up to the hash, as the
/// hashPrefixes of Go's crypto/internal/fips140/rsa.
Uint8List get digestInfoPrefix => fromHex(_digestInfo);
/// The hash of [data].
Uint8List digest(List<int> data) {
final h = switch (this) {
Sha2.sha256 => crypto.sha256,
Sha2.sha384 => crypto.sha384,
Sha2.sha512 => crypto.sha512,
};
return Uint8List.fromList(h.convert(data).bytes);
}
}
/// An RSA public key, a modulus and an exponent, as the table of spec
/// §29.10 admits them: the caller checks that the modulus is odd and of 2048
/// to 4096 bits and the exponent odd from 3 to 2^31 − 1, so that Go's checks
/// of the key never refuse it.
final class RsaPublicKey {
/// The key of modulus [n] and exponent [e].
RsaPublicKey(this.n, this.e);
/// The modulus.
final BigInt n;
/// The public exponent.
final int e;
/// The length of the modulus in bytes, k of RFC 8017.
int get size => (n.bitLength + 7) ~/ 8;
// m^e mod n in size bytes, for the signature [sig] of exactly size bytes,
// or null when its integer is n or more: encrypt of Go, which refuses it.
Uint8List? _encrypt(Uint8List sig) {
final s = bigFromBytes(sig);
if (s >= n) return null;
return bytesFromBig(s.modPow(BigInt.from(e), n), size);
}
}
/// Whether [sig] is an RSASSA-PKCS1-v1_5 signature of [hashed], the [hash]
/// of the message, by [key], as Go's rsa.VerifyPKCS1v15: a signature of
/// exactly k bytes, whose integer is below n, and whose encrypted value is
/// byte for byte the encoding that Go builds: 0x00 0x01, 0xff bytes, 0x00,
/// the DigestInfo and the hash, in k bytes.
bool verifyPkcs1v15(
RsaPublicKey key,
Sha2 hash,
Uint8List hashed,
Uint8List sig,
) {
if (hashed.length != hash.size) return false;
final k = key.size;
if (sig.length != k) return false;
final em = key._encrypt(sig);
if (em == null) return false;
final prefix = hash.digestInfoPrefix;
if (k < prefix.length + hashed.length + 2 + 8 + 1) return false;
final want = Uint8List(k);
want[1] = 1;
for (var i = 2; i < k - prefix.length - hashed.length - 1; i++) {
want[i] = 0xff;
}
want.setRange(k - prefix.length - hashed.length, k - hashed.length, prefix);
want.setRange(k - hashed.length, k, hashed);
return equalBytes(em, want);
}
/// Whether [sig] is an RSASSA-PSS signature of [digest], the [hash] of the
/// message, by [key], with MGF1 of the same hash and a salt of exactly the
/// length of the hash, as Go's rsa.VerifyPSS with that SaltLength: a
/// signature of exactly k bytes whose integer is below n; the bytes of the
/// encrypted value before the last emLen, emLen = ⌈(bits of n − 1) / 8⌉,
/// all zero; and EMSA-PSS-VERIFY of RFC 8017 9.1.2 on the last emLen.
bool verifyPss(RsaPublicKey key, Sha2 hash, Uint8List digest, Uint8List sig) {
if (sig.length != key.size) return false;
final emBits = key.n.bitLength - 1;
final emLen = (emBits + 7) ~/ 8;
final full = key._encrypt(sig);
if (full == null) return false;
var start = 0;
while (full.length - start > emLen) {
if (full[start] != 0) return false;
start++;
}
return _emsaPssVerify(
digest,
Uint8List.sublistView(full, start),
emBits,
hash.size,
hash,
);
}
// emsaPSSVerify of Go's crypto/internal/fips140/rsa, with a known salt
// length.
bool _emsaPssVerify(
Uint8List mHash,
Uint8List em,
int emBits,
int sLen,
Sha2 hash,
) {
final hLen = hash.size;
final emLen = (emBits + 7) ~/ 8;
if (emLen != em.length) return false;
if (hLen != mHash.length) return false;
if (emLen < hLen + sLen + 2) return false;
if (em[emLen - 1] != 0xbc) return false;
final db = Uint8List.fromList(Uint8List.sublistView(em, 0, emLen - hLen - 1));
final h = Uint8List.sublistView(em, emLen - hLen - 1, emLen - 1);
final bitMask = 0xff >> (8 * emLen - emBits);
if (em[0] & (0xff ^ bitMask) != 0) return false;
_mgf1Xor(db, hash, h);
db[0] &= bitMask;
final psLen = emLen - hLen - sLen - 2;
for (var i = 0; i < psLen; i++) {
if (db[i] != 0) return false;
}
if (db[psLen] != 1) return false;
final salt = Uint8List.sublistView(db, db.length - sLen);
final h0 = hash.digest(concatBytes([Uint8List(8), mHash, salt]));
return equalBytes(h0, h);
}
// MGF1 of RFC 8017 B.2.1 with [hash] over [seed], XORed into [out].
void _mgf1Xor(Uint8List out, Sha2 hash, Uint8List seed) {
final counter = Uint8List(4);
var done = 0;
while (done < out.length) {
final d = hash.digest(concatBytes([seed, counter]));
for (var i = 0; i < d.length && done < out.length; i++) {
out[done++] ^= d[i];
}
// The counter as Go's incCounter: big-endian, by one.
for (var i = 3; i >= 0; i--) {
counter[i] = (counter[i] + 1) & 0xff;
if (counter[i] != 0) break;
}
}
}

@ -1,9 +1,19 @@
// The vectors of the CMS reader of stage 5a: the names of the files
// test/vectors/cms_*.json, which tool/cms_go_vectors_test.go writes with
// internal/cms of datekeys-go and the ECDSA and RSA of Go, and the reading
// of their cases.
// The differential of ECDSA and RSA against the Go reference: the cases
// of test/vectors/cms_ecdsa.json and cms_rsa.json, which
// tool/cms_go_vectors_test.go writes with the ECDSA and RSA of Go, run
// through ecdsa.dart, rsa.dart and nist_curves.dart, with the same results.
// It reads no file itself, so that the tests compiled to JavaScript run it
// too, on the part of the cases that cms_vectors.g.dart holds.
library;
import 'dart:typed_data';
import 'package:datekeys/src/bytes.dart';
import 'package:datekeys/src/ecdsa.dart';
import 'package:datekeys/src/nist_curves.dart';
import 'package:datekeys/src/rsa.dart';
import 'package:test/test.dart';
typedef Json = Map<String, Object?>;
/// The names of the vector files, cms_<name>.json.
@ -21,3 +31,98 @@ const cmsFiles = [
/// The cases of [section] of [file].
List<Json> cases(Json file, String section) =>
(file[section]! as List).cast<Json>();
Uint8List hexOf(Object? v) => fromHex(v! as String);
/// A short name of the case [c], for the reasons of the expectations.
String caseName(Json c) =>
(c['name'] ??
(c['names'] as List?)?.first ??
c['fixture'] ??
'${c['base']} ${c['path']} ${c['op']} ${c['arg'] ?? ''}')
.toString();
// ---------------------------------------------------------------------------
// The tests of each file
final _curves = {'P-256': p256, 'P-384': p384, 'P-521': p521};
final _hashes = {
'SHA-256': Sha2.sha256,
'SHA-384': Sha2.sha384,
'SHA-512': Sha2.sha512,
};
/// The tests of cms_ecdsa.json.
void ecdsaTests(Json file) {
test('the curves are those of Go', () {
for (final c in cases(file, 'curves')) {
final curve = _curves[c['name']]!;
expect(curve.p, BigInt.parse(c['p']! as String, radix: 16));
expect(curve.n, BigInt.parse(c['n']! as String, radix: 16));
expect(curve.b, BigInt.parse(c['b']! as String, radix: 16));
expect(curve.gx, BigInt.parse(c['gx']! as String, radix: 16));
expect(curve.gy, BigInt.parse(c['gy']! as String, radix: 16));
expect(curve.p.bitLength, c['bits']);
expect(curve.byteLength, ((c['bits']! as int) + 7) ~/ 8);
}
});
test('a point reads as ecdsa.ParseUncompressedPublicKey reads it', () {
for (final c in cases(file, 'points')) {
final p = decodeUncompressed(_curves[c['curve']]!, hexOf(c['point']));
expect(p != null, c['ok'], reason: caseName(c));
}
});
test('a signature verifies as ecdsa.VerifyASN1 verifies it', () {
final keys = [
for (final k in cases(file, 'keys'))
decodeUncompressed(_curves[k['curve']]!, hexOf(k['point']))!,
];
for (final c in cases(file, 'verify')) {
expect(
verifyEcdsaAsn1(
keys[c['key']! as int],
hexOf(c['hash']),
hexOf(c['sig']),
),
c['valid'],
reason: caseName(c),
);
}
});
}
/// The tests of cms_rsa.json.
void rsaTests(Json file) {
test('the DigestInfo of each hash is that of Go', () {
final info = file['digest_info']! as Json;
for (final MapEntry(:key, :value) in _hashes.entries) {
expect(toHex(value.digestInfoPrefix), info[key], reason: key);
}
});
test(
'a signature verifies as rsa.VerifyPKCS1v15 and VerifyPSS verify it',
() {
final keys = [
for (final k in cases(file, 'keys'))
RsaPublicKey(
BigInt.parse(k['n']! as String, radix: 16),
k['e']! as int,
),
];
for (final (i, k) in cases(file, 'keys').indexed) {
expect(keys[i].n.bitLength, k['bits'], reason: caseName(k));
}
for (final c in [...cases(file, 'valid'), ...cases(file, 'verify')]) {
final key = keys[c['key']! as int];
final hash = _hashes[c['hash']]!;
final digest = hexOf(c['digest']);
final sig = hexOf(c['sig']);
final valid = c['scheme'] == 'pss'
? verifyPss(key, hash, digest, sig)
: verifyPkcs1v15(key, hash, digest, sig);
expect(valid, c['valid'], reason: caseName(c));
}
},
);
}

@ -0,0 +1,20 @@
// The differential of ECDSA and RSA against the Go reference on the part of
// the cases that test/vectors/cms_vectors.g.dart holds: it reads no file,
// so it also runs compiled to JavaScript, where the BigInt of ECDSA and RSA
// must be exact too. The part is small: a signature of P-521 or RSA-4096
// takes long compiled to JavaScript.
library;
import 'dart:convert';
import 'package:test/test.dart';
import 'cms_support.dart';
import 'vectors/cms_vectors.g.dart';
void main() {
Json parse(String text) => jsonDecode(text) as Json;
group('ECDSA', () => ecdsaTests(parse(cmsEcdsaJson)));
group('RSA', () => rsaTests(parse(cmsRsaJson)));
}

@ -1,7 +1,8 @@
// The vectors of the CMS reader on the VM: test/vectors/cms_*.json are of
// the draft v0.12 and of their generator, and cms_vectors.g.dart holds the
// part of them for the tests compiled to JavaScript: for each section, the
// cases whose index is a multiple of the step that "every" gives.
// The differential of ECDSA and RSA against the Go reference on the VM,
// with every case of test/vectors/cms_ecdsa.json and cms_rsa.json. And the
// part of the files test/vectors/cms_*.json that cms_vectors.g.dart holds
// for the tests compiled to JavaScript: for each section, the cases whose
// index is a multiple of the step that "every" gives.
@TestOn('vm')
library;
@ -61,4 +62,7 @@ void main() {
});
}
});
group('ECDSA', () => ecdsaTests(files['ecdsa']!));
group('RSA', () => rsaTests(files['rsa']!));
}

Loading…
Cancel
Save

Powered by TurnKey Linux.