bcgit / pc-dart

Pointy Castle - Dart Derived Bouncy Castle APIs
MIT License
230 stars 120 forks source link

RSA PKCS#1 v1.5 SHA-256 Signature Verification Failure in PointyCastle #239

Open chamodanethra opened 1 month ago

chamodanethra commented 1 month ago

Description

I'm facing an issue with the PointyCastle plugin in a Flutter application where RSA PKCS#1 v1.5 signatures, generated with SHA-256, fail to verify despite being valid. The same signatures are successfully verified using OpenSSL and other online RSA signature verification tools. This inconsistency suggests a potential problem with how PointyCastle handles RSA signature verification.

Context

We have implemented a Flutter plugin that utilizes RSA PKCS#1 v1.5 for signing and verifying data. The signing and verification processes are expected to use the SHA-256 hash algorithm.

Steps to Reproduce

  1. Generate RSA Key Pair: Use the RSA algorithm to generate a 2048-bit key pair.
  2. Sign Payload: Use the RSA private key to sign a payload ("Biometric payload") with PKCS#1 v1.5 padding and SHA-256 hashing.
  3. Verify Signature in PointyCastle:
    • Decode the base64 encoded signature.
    • Hash the payload using SHA-256.
    • Use PointyCastle's Signer initialized with SHA-256/RSA to verify the signature.
  4. Compare Verification Results:

Observed Behavior

Example Code

import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:biometric_signature/biometric_signature.dart';
import 'package:pointycastle/pointycastle.dart';
import 'package:pointycastle/digests/sha256.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _biometricSignature = BiometricSignature();
  String? _publicKey;

  @override
  void initState() {
    super.initState();
    asyncInit();
  }

  Future<void> asyncInit() async {
    try {
      final String? biometricsType =
          await _biometricSignature.biometricAuthAvailable();
      debugPrint("biometricsType : $biometricsType");

      final bool doExist =
          await _biometricSignature.biometricKeyExists() ?? false;
      debugPrint("doExist : $doExist");

      _publicKey = await _biometricSignature.createKeys();
      debugPrint("Public key generated: $_publicKey");

      const String payload = "Biometric payload";
      debugPrint("Payload: $payload");

      final String? signature = await _biometricSignature.createSignature(
          options: {"payload": payload, "promptMessage": "You are Welcome!"});
      debugPrint("Signature generated: $signature");

      if (signature != null && _publicKey != null) {
        final bool isValid = await verifySignatureWithPublicKey(
            payload: payload, signature: signature, publicKeyPem: _publicKey!);
        debugPrint("Is the signature valid? : $isValid");
      }
    } on PlatformException catch (e) {
      debugPrint("PlatformException: ${e.message}");
      debugPrint("Code: ${e.code}");
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: const Center(
          child: Text('Running'),
        ),
      ),
    );
  }

  Future<bool> verifySignatureWithPublicKey({
    required String payload,
    required String signature,
    required String publicKeyPem,
  }) async {
    try {
      // Decode the PEM public key to extract the modulus and exponent
      final publicKeyComponents = decodePublicKeyFromPem(publicKeyPem);
      final modulus = publicKeyComponents.modulus;
      final exponent = publicKeyComponents.exponent;

      debugPrint("Modulus: $modulus");
      debugPrint("Exponent: $exponent");

      // Create the RSA public key object
      final rsaPublicKey = RSAPublicKey(modulus, exponent);

      debugPrint(encodeRSAPublicKeyToPem(rsaPublicKey));

      // Convert payload and signature to bytes
      final messageBytes = utf8.encode(payload);
      final signatureBytes = base64.decode(signature);

      debugPrint("Message bytes: $messageBytes");
      debugPrint("Signature bytes: $signatureBytes");

      // Hash the payload using SHA-256
      final sha256Digest = SHA256Digest();
      final hashedMessage =
          sha256Digest.process(Uint8List.fromList(messageBytes));

      debugPrint("Hashed message: $hashedMessage");

      // Use PointyCastle to verify the signature using PKCS#1 v1.5 with SHA-256
      final verifier = Signer('SHA-256/RSA');
      verifier.init(false, PublicKeyParameter<RSAPublicKey>(rsaPublicKey));

      final rsaSignature = RSASignature(signatureBytes);

      // Verify the signature
      final verificationResult = verifier.verifySignature(
          Uint8List.fromList(hashedMessage), rsaSignature);
      debugPrint("Verification result from PointyCastle: $verificationResult");

      return verificationResult;
    } catch (e) {
      debugPrint("Verification failed: $e");
      return false;
    }
  }

  RsaKeyComponents decodePublicKeyFromPem(String pem) {
    final lines = pem
        .split('\n')
        .where((line) => line.isNotEmpty && !line.startsWith('---'))
        .toList();
    final base64String = lines.join('');
    final asn1Parser = ASN1Parser(base64.decode(base64String));
    final asn1Sequence = asn1Parser.nextObject() as ASN1Sequence;

    // Navigate through the ASN.1 structure
    final algorithmIdentifier = asn1Sequence.elements?[0] as ASN1Sequence;
    final subjectPublicKeyBitString =
        asn1Sequence.elements?[1] as ASN1BitString;

    // Parse the public key
    final publicKeyBytes = subjectPublicKeyBitString.stringValues!;
    final publicKeyParser = ASN1Parser(Uint8List.fromList(publicKeyBytes));
    final publicKeySequence = publicKeyParser.nextObject() as ASN1Sequence;

    // Extract modulus and exponent
    final modulus = publicKeySequence.elements?[0] as ASN1Integer;
    final exponent = publicKeySequence.elements?[1] as ASN1Integer;

    return RsaKeyComponents(
      modulus: _decodeBigInt(modulus.valueBytes!),
      exponent: _decodeBigInt(exponent.valueBytes!),
    );
  }

  String encodeRSAPublicKeyToPem(RSAPublicKey publicKey) {
    var algorithmSeq = ASN1Sequence();
    var paramsAsn1Obj = ASN1Object.fromBytes(Uint8List.fromList([0x5, 0x0]));
    algorithmSeq.add(ASN1ObjectIdentifier.fromName('rsaEncryption'));
    algorithmSeq.add(paramsAsn1Obj);

    var publicKeySeq = ASN1Sequence();
    publicKeySeq.add(ASN1Integer(publicKey.modulus));
    publicKeySeq.add(ASN1Integer(publicKey.exponent));
    var publicKeySeqBitString =
        ASN1BitString(stringValues: Uint8List.fromList(publicKeySeq.encode()));

    var topLevelSeq = ASN1Sequence();
    topLevelSeq.add(algorithmSeq);
    topLevelSeq.add(publicKeySeqBitString);
    var dataBase64 = base64.encode(topLevelSeq.encode());
    var chunks = chunk(dataBase64, 64);

    return '-----BEGIN PUBLIC KEY-----\n${chunks.join('\n')}\n-----END PUBLIC KEY-----';
  }

  List<String> chunk(String s, int chunkSize) {
    var chunked = <String>[];
    for (var i = 0; i < s.length; i += chunkSize) {
      var end = (i + chunkSize < s.length) ? i + chunkSize : s.length;
      chunked.add(s.substring(i, end));
    }
    return chunked;
  }

  BigInt _decodeBigInt(List<int> bytes) {
    var negative = bytes.isNotEmpty && bytes[0] & 0x80 == 0x80;
    var unsignedBytes = negative ? [0] + bytes : bytes;
    var result = BigInt.parse(
        unsignedBytes
            .map((byte) => byte.toRadixString(16).padLeft(2, '0'))
            .join(),
        radix: 16);
    return negative ? result.toUnsigned(8 * bytes.length) : result;
  }
}

class RsaKeyComponents {
  final BigInt modulus;
  final BigInt exponent;

  RsaKeyComponents({required this.modulus, required this.exponent});
}

Output

Restarted application in 779ms.
I/flutter (24086): biometricsType : fingerprint
I/flutter (24086): doExist : true
D/EGL_emulation(24086): app_time_stats: avg=1580024.12ms min=1580024.12ms max=1580024.12ms count=1
I/flutter (24086): Public key generated: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsLqX79Ts4BIphMGnn6Bq/Ozs1pP/FshiiMz5HGWfQYrkV4E0U8FSzDeOuxrz8jpAxL7OBsBjANKNHgDVj7CPrQlgBc7NM4td0n0JKPkUmyUM9v+cyolT+tU1RW5fojLq37DWihWjoGVZrnH+bniPcjDyAtaRrQCxHkdXO748B3FGCISoI+CNwCy4/ONjef760TJIE+SrUylJcDGqxaULmnO/eVx8kv+E1SQy0jZh8RMPwJW+wbikwiE5U47VQW444gV22TfjmhjkQKGtvyOM6LQY83XFaM8sRCrMDCvFxAAazSIGqpWHSruEeNXri7cgPzGdAaVGpK8MvmhUURGOkQIDAQAB
I/flutter (24086): Payload: Biometric payload
I/flutter (24086): Signature generated: hdOQNRoNpYjUHERpkhv5R/hQH8Ip/QVex1JET5fRrRsrnYeU0b/k9QaC49cwCmnKIrwmUEdiDyZ2l81spqEetiIHQhRWiMXpCBT51HwtIZT8O6lpQ8XbgWI2EXRxjGX1tnxUA6rHOV7/JSkR2fxhL2NFnLv46gK47L7SKxgcUnE/1VCLXOWWsFfLv5w/nPYVYrgx7zlKpFd58f30C3HruP4kf70ntyt5omBfIeEUZIzwjbAWbbMpVnVzKhPz7Z0sMAvZvNa9Bu2wKHU3UD3ZpRdzrVdpJltRO9zIqcliweTkeZr2EqMTausd/NGtTcbhciiBZRI5JQqgn+uh7oaT3Q==
I/flutter (24086): Modulus: 22309954359859329121179930388900541711115245118790342773149323158468033793502777805928933769458950939680792288496008015161813611290667687314911596271566701779546864209817828289468730866000266211271945242607572166756096617656961677631092347128721676224433915494332399668572155673649009706409562172893228409810175870741055420557785753492853985700796586502400051925456129658284927459816217421431252445994608511589723687089603562911661749376313961800748478282271040359205195968502232614302888027085137391081505579397725859302240582618309173566087675629333761984767678108698536365905213799756652646852553974671053144821393
I/flutter (24086): Exponent: 65537
I/flutter (24086): -----BEGIN PUBLIC KEY-----
I/flutter (24086): MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsLqX79Ts4BIphMGnn6Bq
I/flutter (24086): /Ozs1pP/FshiiMz5HGWfQYrkV4E0U8FSzDeOuxrz8jpAxL7OBsBjANKNHgDVj7CP
I/flutter (24086): rQlgBc7NM4td0n0JKPkUmyUM9v+cyolT+tU1RW5fojLq37DWihWjoGVZrnH+bniP
I/flutter (24086): cjDyAtaRrQCxHkdXO748B3FGCISoI+CNwCy4/ONjef760TJIE+SrUylJcDGqxaUL
I/flutter (24086): mnO/eVx8kv+E1SQy0jZh8RMPwJW+wbikwiE5U47VQW444gV22TfjmhjkQKGtvyOM
I/flutter (24086): 6LQY83XFaM8sRCrMDCvFxAAazSIGqpWHSruEeNXri7cgPzGdAaVGpK8MvmhUURGO
I/flutter (24086): kQIDAQAB
I/flutter (24086): -----END PUBLIC KEY-----
I/flutter (24086): Message bytes: [66, 105, 111, 109, 101, 116, 114, 105, 99, 32, 112, 97, 121, 108, 111, 97, 100]
I/flutter (24086): Signature bytes: [133, 211, 144, 53, 26, 13, 165, 136, 212, 28, 68, 105, 146, 27, 249, 71, 248, 80, 31, 194, 41, 253, 5, 94, 199, 82, 68, 79, 151, 209, 173, 27, 43, 157, 135, 148, 209, 191, 228, 245, 6, 130, 227, 215, 48, 10, 105, 202, 34, 188, 38, 80, 71, 98, 15, 38, 118, 151, 205, 108, 166, 161, 30, 182, 34, 7, 66, 20, 86, 136, 197, 233, 8, 20, 249, 212, 124, 45, 33, 148, 252, 59, 169, 105, 67, 197, 219, 129, 98, 54, 17, 116, 113, 140, 101, 245, 182, 124, 84, 3, 170, 199, 57, 94, 255, 37, 41, 17, 217, 252, 97, 47, 99, 69, 156, 187, 248, 234, 2, 184, 236, 190, 210, 43, 24, 28, 82, 113, 63, 213, 80, 139, 92, 229, 150, 176, 87, 203, 191, 156, 63, 156, 246, 21, 98, 184, 49, 239, 57, 74, 164, 87, 121, 241, 253, 244, 11, 113, 235, 184, 254, 36, 127, 189, 39, 183, 43, 121, 162, 96, 95, 33, 225, 20, 100, 140, 240, 141, 176, 22, 109, 179, 41, 86, 117, 115, 42, 19, 243, 237, 157, 44, 48, 11, 217, 188, 214, 189, 6, 237, 176, 40, 117, 55, 80, 61, 217, 165, 23, 115, 173, 87, 105, 38, 91, 81, 59, 220, 200, 169, 201, 98,
I/flutter (24086): Hashed message: [253, 59, 98, 240, 35, 254, 230, 185, 153, 77, 190, 237, 86, 49, 68, 128, 206, 54, 84, 41, 253, 107, 224, 201, 147, 220, 180, 198, 91, 70, 108, 75]
I/flutter (24086): Verification result from PointyCastle: false
I/flutter (24086): Is the signature valid? : false