delphiwebauthnyubicocbor

Webauthn credential verifiation with fido2.dll fro Yubico


I started to interface yubicos fido2.dll in Delphi and was able to interface it according to the provided examples. Now I want to go a step further and use the dll on an e.g. apache server to handle credential creation and assertion.

So.. for this I basically use the javascripts found on THE testing site https://webauthn.io/

Basically I wanted to mimic some server functions for credential creation. On the website one can setup some properties - in my environment they come from the server.

Currently I have made the communication from Client to issue a credential initialization - server responses with a challange. The Key is queried and the browser creates credentials and sends it back to the server. This though where I have a problem with the data coming from the server aka I have a problem decoding the attestationObject part.

So here is the credential init json from my server:

{"publicKey":{"challenge":"LFJYIdXJfYpB1GZS+PzEOD8DNcYmdia4mZp2z0J4QcE=","pubKeyCredParams":[{"alg":-7,"type":"public-key"},{"alg":-257,"type":"public-key"},{"alg":-8,"type":"public-key"}],"authenticatorSelection":{"authenticatorAttachment":"cross-platform","userVerification":"required","requireResidentKey":false},"rp":{"id":"fidotest.com","name":"fidotest.com"},"user":{"id":"zVOUjBCxJNIbSSWNiGOv2\/kZP2UU8pPguVylFeiw4HE=","displayName":"test","name":"test"},"Timeout":60000,"attestation":"direct"}}

And the result from the server:

{"id":"MfcgyBxDxpq5S71fB45FFjecCGtvCepvb6IZexJpgaHyTPPsaz0srQyZc26HkE92eda7a2PmPIzvSpLbipktmw","rawId":"MfcgyBxDxpq5S71fB45FFjecCGtvCepvb6IZexJpgaHyTPPsaz0srQyZc26HkE92eda7a2PmPIzvSpLbipktmw","type":"public-key","response":{"attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIhAO8fbE8iQcMFYE4KBwL6HK6OxSReRKriXZDWhcfGRMFxAiB7mIPZ7n-fWas7aWkEkWd-9CWvd8ncRVCh3BBFIzMuRmN4NWOBWQLAMIICvDCCAaSgAwIBAgIEBMX-_DANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbTELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEmMCQGA1UEAwwdWXViaWNvIFUyRiBFRSBTZXJpYWwgODAwODQ3MzIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQc2Np2EaP17x-IXpULpl2A4zSFU5FYS9R_W3GcUyNcJCHk45m9tXNngkGQk1dmYUk8kUwuZyTfk5T8-n3qixgEo2wwajAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuMTATBgsrBgEEAYLlHAIBAQQEAwIFIDAhBgsrBgEEAYLlHAEBBAQSBBD4oBHzjApNFYAGFxEfntx9MAwGA1UdEwEB_wQCMAAwDQYJKoZIhvcNAQELBQADggEBAHcYTO91LRoF8wpThdwthvj6wGNxcLAiYqUZXPX-0Db-AGVODSkVvEVSmj-JXmrBzNQel3FW4AupOgbgrJmmcWWEBZyXSpRQtYcl2LTNU0-Iz9WbyHNN1wQJ9ybFwj608xBuoNRC0rG8wgYbMC4usyRadt3dYOVdQi0cfaksVB2VNKnw-ttQUWKoZsPHtuzFx8NlazLQBep1W2T0FCONFEG7x_l-ZcfNhT13azAbaurJ2J0_ff6H0PXJP6h-Obne4xfz0-8ujftWDUSh9oaiVRYf-tgam_tzOKyEU38V2liV11zMyHKWrXiK0AfyDgb58ky2HSrn_AgE5MW_oXg_CXdoYXV0aERhdGFYxNNxx2kdwL5GE4VmZm0_PerRaSEQdriBtCqmPBobyJXTRQAAAA34oBHzjApNFYAGFxEfntx9AEAx9yDIHEPGmrlLvV8HjkUWN5wIa28J6m9vohl7EmmBofJM8-xrPSytDJlzboeQT3Z51rtrY-Y8jO9KktuKmS2bpQECAyYgASFYIPVUDt7LCfuPyhdowBAhHCaRp-4acTmevkowvQhYxQluIlggCOL0rfuXgGze8yGX38sBXzsSMqxQxiskjsXia6UQvtQ","clientDataJSON":"eyJjaGFsbGVuZ2UiOiJMRkpZSWRYSmZZcEIxR1pTLVB6RU9EOEROY1ltZGlhNG1acDJ6MEo0UWNFIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cHM6Ly9maWRvdGVzdC5jb20iLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0"}}

Here is a console program that should verify the credentials: (note you need to paste the above content to a textfile and load it in the console).

program VerifyCred;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  Fido2 in '..\Fido2.pas',
  Fido2dll in '..\Fido2dll.pas',
  Fido2Json in '..\Fido2Json.pas',
  windows,
  classes,
  cbor,
  superobject;

function DoVerifyCred( credential : ISuperObject ) : string;
var clientData : ISuperObject;
    s : string;
    rawS : RawByteString;
    credentialId : string;
    rawId : TBytes;
    credVerify : TFidoCredVerify;
    cborItem : TCborMap;
    sig : TBytes;
    x5c : TBytes;
    authData : TBytes;
    fmt : string;
    alg : integer;
    i : integer;
    aName : string;
    res : boolean;
    rawChallange : RawByteString;
    credFMT : TFidoCredentialFmt;
    challenge : TFidoChallenge;
    authDataObj : TAuthData;
    attStmt : TCborMap;
    j : integer;
    restBuf : TBytes;
begin
     Result := '{"error":0,"msg":"Error parsing content"}';

     s := credential.S['response.clientDataJSON'];

     if s = '' then
        exit;

     ClientData := So( String(Base64URLDecode( s )) );
     if clientData = nil then
        exit;

     rawChallange := Base64URLDecode(ClientData.S['challenge']);

     if Length(rawChallange) <> sizeof(challenge) then
        exit;
     Move( rawChallange[1], challenge, sizeof(challenge));

     clientData := SO( String( Base64URLDecode( credential.S['response.clientDataJSON'] ) ) );

     s := credential.S['response.attestationObject'];
     if s = '' then
        exit;

     // attestation object is a cbor encoded raw base64ulr encoded string
     cborItem := TCborDecoding.DecodeBase64UrlEx(s, restBuf) as TCborMap;

     if not Assigned(cborItem) then
        exit;
     try
        alg := 0;
        fmt := '';
        sig := nil;
        authData := nil;
        x5c := nil;

        for i := 0 to cborItem.Count - 1 do
        begin
             assert( cborItem.Names[i] is TCborUtf8String, 'CBOR type error');

             aName := String((cborItem.Names[i] as TCborUtf8String).Value);
             if SameText(aName, 'attStmt') then
             begin
                  attStmt := cborItem.Values[i] as TCborMap;
                  for j := 0 to attStmt.Count - 1 do
                  begin
                       aName := String((attStmt.Names[j] as TCborUtf8String).Value);

                       if SameText(aName, 'alg')
                       then
                           alg := (attStmt.Values[j] as TCborNegIntItem).value
                       else if SameText(aName, 'sig')
                       then
                           sig := (attStmt.Values[j] as TCborByteString).ToBytes
                       else if SameText(aName, 'x5c')
                       then
                           x5c := ((attStmt.Values[j] as TCborArr)[0] as TCborByteString).ToBytes
                  end;
             end
             else if SameText(aName, 'authData')
             then
                 authData := (cborItem.Values[i] as TCborByteString).ToBytes
             else if SameText(aName, 'fmt')
             then
                 fmt := String( (cborItem.Values[i] as TCborUtf8String).Value );
        end;
     finally
            cborItem.Free;
     end;

     // check if anyhing is in place
     if not (( alg = COSE_ES256 ) or (alg = COSE_EDDSA) or (alg= COSE_RS256)) then
        raise Exception.Create('Unknown algorithm');
     if Length(sig) = 0 then
        raise Exception.Create('No sig field provided');
     if Length(x5c) = 0 then
        raise Exception.Create('No certificate');
     if Length(authData) = 0 then
        raise Exception.Create('Missing authdata');

     credentialId := credential.S['id'];

     s := credential.S['rawId'];
     if s = '' then
        raise Exception.Create('No Credential id found');
     raws := Base64URLDecode( s );
     SetLength( rawId, Length(rawS));
     Move( rawS[1], rawId[0], Length(rawId));

     authDataObj := nil;
     if Length(restBuf) > 0 then
        raise Exception.Create('Damend there is a rest buffer that should not be');

     if Length(authDAta) > 0 then
        authDataObj := TAuthData.Create( authData );

     try
        if fmt = 'packed'
        then
            credFmt := fmFido2
        else if fmt = 'fido-u2f'
        then
            credFmt := fmU2F
        else
            credFmt := fmDef;

        // now... verify the credentials
        credVerify := TFidoCredVerify.Create( TFidoCredentialType(alg), credFmt,
                                              authData, x5c, sig, FidoServer.RequireResidentKey,
                                              authDataObj.UserVerified, 0)  ;
        try
           // auth data seems bad!
           res := credVerify.Verify(challenge);

           if res then
           begin
                // -> save EVERYTHING to a database
           end;
        finally
               credVerify.Free;
        end;
     finally
            authDataObj.Free;
     end;

     // build result and generate a session
     if res then
     begin
          // yeeeha we got a
          Result := '{"success":true}';
     end
     else
         Result := '{"success":false}';
end;


var credential: ISuperObject;
begin
  try
     with TStringList.Create do
     try
        LoadFromFile('D:\CredVerify_delphi.json');
        credential := SO( Text );
     finally
            Free;
     end;
     Writeln( doVerifyCred(credential) );
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

The cbor and fido2 projects can be found on github.

I actually have a problem with the CBOR encoded attestationObject returned. If the resident key property is set the attestation object is only 63 bytes long - and there are bytes left that are actually not encoded. So... The cbor decoding there either fails or I get data back that does not conform the webauthn attestation object which should at these positions return the credential id and the public key (which is then also cbor encoded). If the resident key property is false as this is the case in the above statement the fido dll returns bad auth data. So... anyone has a clue what I'm doing wrong?

It should basically look like in the diagram but it ends either after 63 bytes which is in the mids of the credential id or it fails in the dll.


Solution

  • Turned out I misused the indy base64 routines. The base64 decoder did not work properly for a Unicodstring (I assumed that the string was converted to an ansistring...) So instead I use the following decoder:

    function Base64Decode( s : string ) : RawByteString;
    var aWrapStream : TWrapMemoryStream;
        sconvStr : UTF8String;
        lStream : TMemoryStream;
    begin
         sConvStr := UTF8String( s );
         aWrapStream := TWrapMemoryStream.Create( @sConvStr[1], Length(sConvStr) );
         lStream := TMemoryStream.Create;
    
         try
            with TIdDecoderMIME.Create(nil) do
            try
               DecodeBegin(lStream);
               Decode( aWrapStream );
               DecodeEnd;
    
               SetLength(Result, lStream.Size );
               if lStream.Size > 0 then
                    Move( PByte(lStream.Memory)^, Result[1], lStream.Size);
            finally
                   Free;
            end;
         finally
                lStream.Free;
         end;
         aWrapStream.Free;
    end;