flutter-institute / webauthn

A plugin to handle webauthn login
BSD 3-Clause "New" or "Revised" License
15 stars 7 forks source link

random OpenSSLError(ErrorStack[]) from webauthn_rs server #13

Closed xshadowlegendx closed 10 months ago

xshadowlegendx commented 10 months ago

hello, so I am implementing passkey server using webauthn_rs library from rust and currently integrating with flutter webauthn library, so far I am able to initialize and finish the registration except sometime it fails with the error stated in the title, sometime it success at first try, sometime have to try multiple time to get successful registration.

versions

dart: 3.0.5 flutter: 3.10.5 webauthn: ^0.2.0

sample attestations with success response

[{
  "type": "public-key",
  "id": "th0rhsQtff2QmdiKBKRe5Q7cXUEa0Pkpz7tqiQkAw6A",
  "rawId": "th0rhsQtff2QmdiKBKRe5Q7cXUEa0Pkpz7tqiQkAw6A",
  "authenticatorAttachment": "platform",
  "clientExtensionResults": {
    "credProps": {
      "rk": true
    }
  },
  "response": {
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJjaGFsbGVuZ2UiOiItVEl4M3pQblQzVVBSRTY1dGY1R3lXU2s0cFZ4azV5RVVtNDVSbjhHUjZzIn0=",
    "attestationObject": "o2hhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAAAAAAAAAAAAAAAAAAAAAAAILYdK4bELX39kJnYigSkXuUO3F1BGtD5Kc-7aokJAMOgpQECAyYgASFYIKA1Uf0pk02IsyJyb8i2NXLm9BU_RxZy_UeLFjNRtFbzIlgg6PfksVwM-JrOcIbyXWGmG_7Hi6s4XJXAbe8SFs62pmtjZm10ZnBhY2tlZGdhdHRTdG10omNhbGcmY3NpZ1hGMEQCIB-BmpdclMORspFlfdkYtPSn6tdRWYLB0g0akx8O_2BbAiBtzKp8KImiEvkbsB7JDL1NTjqQgWAHwJHd0gnxhARo2Q==",
    "transports": [
      "internal"
    ]
  }
}, {
  "type": "public-key",
  "id": "nwaYAxO9WKo3kplt1LFNm_bT6O7vFi2lX7qpMReG97E",
  "rawId": "nwaYAxO9WKo3kplt1LFNm_bT6O7vFi2lX7qpMReG97E",
  "authenticatorAttachment": "platform",
  "clientExtensionResults": {
    "credProps": {
      "rk": true
    }
  },
  "response": {
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJjaGFsbGVuZ2UiOiJ6NlIyQUxTUEIwU0hfNkFOT1ZTSFpqUlZHbV9lTC1JTUhCZHhDSjNqNmk4In0=",
    "attestationObject": "o2hhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAAAAAAAAAAAAAAAAAAAAAAAIJ8GmAMTvViqN5KZbdSxTZv20-ju7xYtpV-6qTEXhvexpQECAyYgASFYICPerAPyLHdkODGoX9CLNOhU9ypFkuzQpBmcfjGzh59yIlggyBY1OeDgVzouqvNmIxhk-PWtHpfJGMs7KUO8XeT2455jZm10ZnBhY2tlZGdhdHRTdG10omNhbGcmY3NpZ1hGMEQCIDGOLTD4aO84P8Yrt_FFR0dhvgAYbYbreKyfzd12Xs7SAiA09LwRLwWF_ic6GVWNS5jbqI82VZzXzwvZAJcC486Jbw==",
    "transports": [
      "internal"
    ]
  }
}]

sample attestations with the failure response

[{
  "type": "public-key",
  "id": "Lba4VzFH1h8Yw-yJBPGRCTDVEGnfUQbt517-JAFbhtQ",
  "rawId": "Lba4VzFH1h8Yw-yJBPGRCTDVEGnfUQbt517-JAFbhtQ",
  "authenticatorAttachment": "platform",
  "clientExtensionResults": {
    "credProps": {
      "rk": true
    }
  },
  "response": {
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJjaGFsbGVuZ2UiOiJZbk9qMGNNYTFaek51c2x6ZGlyd0hMb1VJMzVBMlZQaGZSRk5LZi1DQ0x3In0=",
    "attestationObject": "o2hhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAAAAAAAAAAAAAAAAAAAAAAAIC22uFcxR9YfGMPsiQTxkQkw1RBp31EG7ede_iQBW4bUpQECAyYgASFYIEFRxt-2aAfnOYmY-z184O0T2F2SBKuy0q5B3p5m2_W4IlggwciWfsy-tdtfmCLVVaxRrnaOoN3ivc-yqxTcmCaxBk1jZm10ZnBhY2tlZGdhdHRTdG10omNhbGcmY3NpZ1hGMEQCIABWNAJdsi-4RwKYr0-DNRUwpZsAsuwFh6cm4V0aaWWrAiDFsq0UCEgQbkspX9F1AJZecyGz4IA1EezA9YtANEDe2Q==",
    "transports": [
      "internal"
    ]
  }
}, {
  "type": "public-key",
  "id": "RvvODuU0zgTApIqQxZ8NioCaCkvtSclsx9XwkYAjmbg",
  "rawId": "RvvODuU0zgTApIqQxZ8NioCaCkvtSclsx9XwkYAjmbg",
  "authenticatorAttachment": "platform",
  "clientExtensionResults": {
    "credProps": {
      "rk": true
    }
  },
  "response": {
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJjaGFsbGVuZ2UiOiJzLS10QzQzWmZRazlSZklqV1VYeVRVV0lFeTVyQWhrWDZBZGlCd08xSWVrIn0=",
    "attestationObject": "o2hhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAAAAAAAAAAAAAAAAAAAAAAAAIEb7zg7lNM4EwKSKkMWfDYqAmgpL7UnJbMfV8JGAI5m4pQECAyYgASFYIJM0bclvnaoVa0lyT_-M6ostwmF_k5ls7W7DW7nRyC2AIlggfeZS6LKcrp3o_ZKS1jqWVLmbZS3k8jfxGicG8g_OKNFjZm10ZnBhY2tlZGdhdHRTdG10omNhbGcmY3NpZ1hGMEQCIJRw9Xnr6axogvBJ7kJsubryFM8g0CdX24FrVZUD7M8UAiArfVMwuvwZgitByPr8ORxfkRQMa8Gn7OiAZ4EiG0MvJQ==",
    "transports": [
      "internal"
    ]
  }
}]

flutter demo code

import 'dart:convert';

import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:flutter/material.dart';
import 'package:crypto/crypto.dart';
import 'package:webauthn/webauthn.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final dio = Dio();
  final cookieJar = CookieJar();

  final auth = Authenticator(true, true);

  final textController = TextEditingController();

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

    dio.interceptors.add(CookieManager(cookieJar));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: TextField(
          controller: textController,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          const baseUrl = "https://identity.example.com";

          final createUser = await dio.post(
            "$baseUrl/users",
            data: {'name': textController.value.text},
            options: Options(
              contentType: Headers.formUrlEncodedContentType,
            )
          );

          cookieJar.loadForRequest(Uri.parse("https://identity.example.com"));

          print('==== create user response ====');
          print(createUser.statusCode);

          final passkeyRegisterInit = await dio.post(
            "$baseUrl/authn/webauthn/register",
            data: {'display_name': 'iPhone 14 Pro Max - SonGoku'},
            options: Options(
              contentType: Headers.formUrlEncodedContentType,
            )
          );

         //////////// -------- starts here -------- //////////// 

          print('==== passkey register init response ====');
          print(passkeyRegisterInit.statusCode);
          print(passkeyRegisterInit.data);

          final makeCredOpts = passkeyRegisterInit.data['publicKey'];

          final clientDataJson = utf8.encode(json.encode({
            'type': 'webauthn.create',
            'origin': 'http://localhost:3000',
            'crossOrigin': false,
            'challenge': makeCredOpts['challenge'],
          }));

          makeCredOpts['clientDataHash'] = base64.encode(sha256.convert(clientDataJson).bytes);
          makeCredOpts['credTypesAndPubKeyAlgs'] = (makeCredOpts['pubKeyCredParams'] as List).map((d) => [d['type'], d['alg']]).toList();
          makeCredOpts['requireUserPresence'] = false;
          makeCredOpts['requireResidentKey'] = makeCredOpts['authenticatorSelection']['requireResidentKey'];
          makeCredOpts['requireUserVerification'] = makeCredOpts['authenticatorSelection']['userVerification'] == 'preferred';

          print('==== see makeCredOpts ====');
          print(makeCredOpts);

          final attes = await auth.makeCredential(MakeCredentialOptions.fromJson(makeCredOpts));

          final credentialId = attes.getCredentialIdBase64();

          final attesResp = {
            'type': 'public-key',
            'id': credentialId,
            'rawId': credentialId,
            'authenticatorAttachment': 'platform',
            'clientExtensionResults': {
              'credProps': {
                'rk': true,
              },
            },
            'response': {
              'clientDataJSON': base64Url.encode(clientDataJson),
              'attestationObject': base64Url.encode(attes.asCBOR()),
              'transports': ['internal'],
            },
          };

          print('==== see attesResp ====');
          print(attesResp);

          final resp = await dio.patch(
            '$baseUrl/authn/webauthn/register',
            data: attesResp,
            options: Options(
              contentType: Headers.jsonContentType
            )
          );

          print('==== status code ====');
          print(resp.statusCode);

          if (resp.statusCode != 200 && resp.statusCode != 204) {
            print('==== fail mannnn ====');
            print(resp.statusMessage);
          }

          print(resp.data);
        },
        tooltip: 'just do it',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
killermonk commented 10 months ago

@xshadowlegendx Thanks for your report and the details! I've been going over your data to test and verify things. Here's where I'm at.

I believe your OpenSSLError(ErrorStack[]) is being caused by OpenSSL saying the signature is invalid.

In the bug/13-invalid-signatures branch, I have created a dart script in ./scripts/test_signature.dart that takes the attestation base64 as the first argument, and the clientDataJson base64 as the second argument. This script parses everything out and attempts to verify the signature.

What I found is that my library says that each of your example signatures is valid. But OpenSSL says the ones throwing errors are not valid.

I'm going to have to do some more testing where I can dump the private key to try to figure out what is going wrong with how I'm generating the signature.

Thanks for your patience while I keep digging in.

xshadowlegendx commented 10 months ago

hello @killermonk, thanks you, if you need any help from my side please let me know

killermonk commented 10 months ago

I've been testing on this @xshadowlegendx and I'm not entirely sure what the issue is. Currently, it appears that there is something wrong with the underlying crypto_keys library that is being used. I have opened an issue in that library, and will see if I can work with them to figure anything out on this.

killermonk commented 10 months ago

OK, so I was attempting to debug the crypto_keys library and got deep deep into the depths of the data. This is something with how I'm serializing the signature and handling numbers with a MSB of 1.

In the valid signature below the MSB of r/s are both 0 from 0x22 and 0x32.

00000000  30 44 02 20 *22* b7 e0 26  0f 37 4f 01 00 5e a5 ca  |0D. "..&.7O..^..|
00000010  09 98 37 13 e5 b1 3d 70  24 27 b8 eb 63 53 bd f8    |..7...=p$'..cS..|
00000020  59 d9 8f 94 02 20 *32* 66  8a bc 6a 3b 2e dc 40 dd  |Y.... 2f..j;..@.|
00000030  52 ce 70 fb 44 93 0a bd  27 3e 23 a6 04 cf c8 d0    |R.p.D...'>#.....|
00000040  7f 81 00 01 36 42                                   |....6B|

In this invalid signature the MSB on both r/s is 1 from 0xe2 and 0xe8.

00000000  30 44 02 20 *e2* da 45 a4  d2 e0 db 5a 00 92 4f 61  |0D. ..E....Z..Oa|
00000010  cb 86 51 ee 40 b0 30 ae  11 ee 5f 5a f0 85 5a af    |..Q.@.0..._Z..Z.|
00000020  db d1 84 4d 02 20 *e8* 38  0d 28 11 4b 58 a8 f4 5f  |...M. .8.(.KX.._|
00000030  a3 b5 d5 86 f9 0e 8e 2a  0d 6d b0 41 e6 04 81 8d    |.......*.m.A....|
00000040  ab 3d 19 95 06 56                                   |.=...V|

In this OpenSSL signature using the same keys, the MSB of r is 0 from 0x26, but the MSB of s would be 1 from 0x9a so OpenSSL put a 0x00 in front of it to ensure the number is not treated as negative.

00000000  30 45 02 20 *26* dc 4e 4a  54 93 c3 7d 38 1b df 0a  |0E. &.NJT..}8...|
00000010  9c 61 08 e1 42 cb 94 eb  8f 73 73 a2 0d 4f b2 70    |.a..B....ss..O.p|
00000020  ef 03 f4 02 02 21 *00 9a*  2f 4f fd 59 2e 63 ac 71  |.....!../O.Y.c.q|
00000030  58 3f 0a 17 1d f9 99 4f  fb 5c 77 fa 3f a0 39 ec    |X?.....O.\w.?.9.|
00000040  25 fd 09 74 ef 5f 84                                |%..t._.|

I was naively assuming the 32 byte integers were treated as unsigned values instead of signed.

I'm working on getting a new version published to address this issue.

killermonk commented 10 months ago

🤦 turns out I even caught this earlier on. If you look at https://github.com/flutter-institute/webauthn/blob/master/test/util/webauthn_cryptography_test.dart#L39 you can see that I had a bad signature and the line above has the valid OpenSSL signature.

And I didn't dig in deep enough to figure out why there was a disparity.

killermonk commented 10 months ago

@xshadowlegendx I've just published version 0.2.2 that should address this issue. Please let me know if you continue running into errors.