Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions lib/src/signed_xml.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1073,8 +1073,9 @@ class RSASHA1 implements SignatureAlgorithm {
String getSignature(String xml, Uint8List signingKey,
[CalculateSignatureCallback? callback]) {
final rsa = RSAPrivateKey.fromPEM(utf8.decode(signingKey));
final res =
final raw =
rsa.signSsaPkcs1v15ToBase64(utf8.encode(xml), hasher: EmsaHasher.sha1);
final res = normalizeRsaSignatureBase64(raw, rsa.n);
if (callback != null) callback(null, res);
return res;
}
Expand Down Expand Up @@ -1102,8 +1103,9 @@ class RSASHA256 implements SignatureAlgorithm {
String getSignature(String xml, Uint8List signingKey,
[CalculateSignatureCallback? callback]) {
final rsa = RSAPrivateKey.fromPEM(utf8.decode(signingKey));
final res = rsa.signSsaPkcs1v15ToBase64(utf8.encode(xml),
final raw = rsa.signSsaPkcs1v15ToBase64(utf8.encode(xml),
hasher: EmsaHasher.sha256);
final res = normalizeRsaSignatureBase64(raw, rsa.n);
if (callback != null) callback(null, res);
return res;
}
Expand Down Expand Up @@ -1131,8 +1133,9 @@ class RSASHA512 implements SignatureAlgorithm {
String getSignature(String xml, Uint8List signingKey,
[CalculateSignatureCallback? callback]) {
final rsa = RSAPrivateKey.fromPEM(utf8.decode(signingKey));
final res = rsa.signSsaPkcs1v15ToBase64(utf8.encode(xml),
final raw = rsa.signSsaPkcs1v15ToBase64(utf8.encode(xml),
hasher: EmsaHasher.sha512);
final res = normalizeRsaSignatureBase64(raw, rsa.n);
if (callback != null) callback(null, res);
return res;
}
Expand Down
35 changes: 35 additions & 0 deletions lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
//History: Wed Feb 09 10:44:40 CST 2022
// Author: rudyhuang

import 'dart:convert';
import 'dart:typed_data';

import 'package:xml/xml.dart';
import 'package:xpath_selector_xml_parser/xpath_selector_xml_parser.dart';

Expand Down Expand Up @@ -84,3 +87,35 @@ XmlDocument parseFromString(String xml) =>

String normalizeLinebreaks(String xml) =>
xml.replaceAll(RegExp(r'\r\n'), '\n').replaceAll(RegExp(r'\r'), '\n');

/// Normalizes a Base64-encoded RSA signature to the expected modulus width.
///
/// An RSA-PKCS#1 signature must be exactly `ceil(modulus.bitLength / 8)` bytes
/// long. Some implementations (e.g. the ninja library) may serialize the raw
/// RSA result [BigInt] without left-padding, dropping leading `0x00` bytes when
/// the integer value happens to be smaller than the modulus. This produces a
/// signature that is one (or more) bytes too short and fails XMLDSig
/// wire-format validation on the receiving end.
///
/// This function decodes [base64Sig], left-pads the byte array with `0x00`
/// bytes when its length is less than the expected modulus width, and returns
/// the corrected Base64 string. A correctly-sized signature is returned
/// unchanged. A signature that is *longer* than the modulus width indicates a
/// serious upstream error and causes a [StateError] to be thrown.
String normalizeRsaSignatureBase64(String base64Sig, BigInt modulus) {
final expectedLen = (modulus.bitLength + 7) ~/ 8;
final sigBytes = base64Decode(base64Sig);

if (sigBytes.length == expectedLen) return base64Sig;

if (sigBytes.length > expectedLen) {
throw StateError(
'RSA signature is ${sigBytes.length} bytes but modulus requires '
'$expectedLen bytes');
}

// Left-pad with 0x00 bytes to reach the expected modulus width.
final padded = Uint8List(expectedLen)
..setRange(expectedLen - sigBytes.length, expectedLen, sigBytes);
return base64Encode(padded);
}
75 changes: 75 additions & 0 deletions test/utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
//History: Wed Feb 09 11:30:56 CST 2022
// Author: rudyhuang

import 'dart:convert';
import 'dart:typed_data';

import 'package:test/test.dart';
import 'package:xml_crypto/src/utils.dart';

Expand All @@ -15,4 +18,76 @@ void main() {
final result = encodeSpecialCharactersInText('<D&D value="done\tdone\r\n">');
expect(result, '&lt;D&amp;D value="done\tdone&#xD;\n"&gt;');
});

group('normalizeRsaSignatureBase64', () {
// Modulus that represents a 2048-bit RSA key: expected signature = 256 bytes.
// BigInt.two.pow(2047) has bitLength == 2048, so expectedLen = (2048+7)~/8 = 256.
final modulus2048 = BigInt.two.pow(2047);

test('returns the signature unchanged when it is already the correct length', () {
final bytes = Uint8List(256);
for (var i = 0; i < 256; i++) bytes[i] = i & 0xff;
final b64 = base64Encode(bytes);
expect(normalizeRsaSignatureBase64(b64, modulus2048), b64);
});

test('left-pads a one-byte-short signature with a leading 0x00', () {
// Build a 256-byte "expected" signature whose first byte is 0x00.
final expected = Uint8List(256);
expected[0] = 0x00;
for (var i = 1; i < 256; i++) expected[i] = i & 0xff;
final expectedB64 = base64Encode(expected);

// Simulate the ninja bug: the leading 0x00 is dropped → 255-byte signature.
final short = expected.sublist(1);
final shortB64 = base64Encode(short);
expect(shortB64.length, 340); // 255 bytes → 340 Base64 chars (not 344)

final normalized = normalizeRsaSignatureBase64(shortB64, modulus2048);
expect(normalized, expectedB64);
expect(normalized.length, 344); // 256 bytes → 344 Base64 chars
});

test('left-pads a two-byte-short signature with two leading 0x00 bytes', () {
final expected = Uint8List(256);
expected[0] = 0x00;
expected[1] = 0x00;
for (var i = 2; i < 256; i++) expected[i] = i & 0xff;
final expectedB64 = base64Encode(expected);

final short = expected.sublist(2); // 254 bytes
final shortB64 = base64Encode(short);

final normalized = normalizeRsaSignatureBase64(shortB64, modulus2048);
expect(normalized, expectedB64);
});

test('throws StateError when the signature is longer than the modulus width', () {
final bytes = Uint8List(257); // one byte too many for a 2048-bit key
final b64 = base64Encode(bytes);
expect(
() => normalizeRsaSignatureBase64(b64, modulus2048),
throwsA(isA<StateError>().having(
(e) => e.message,
'message',
contains('257 bytes'),
)),
);
});

test('works for a 1024-bit key (128-byte expected length)', () {
final modulus1024 = BigInt.two.pow(1023); // bitLength == 1024

final expected = Uint8List(128);
expected[0] = 0x00;
for (var i = 1; i < 128; i++) expected[i] = i & 0xff;
final expectedB64 = base64Encode(expected);

final short = expected.sublist(1); // 127 bytes
final shortB64 = base64Encode(short);

final normalized = normalizeRsaSignatureBase64(shortB64, modulus1024);
expect(normalized, expectedB64);
});
});
}
Loading