unit Webauthn.Cbor;
{
  Minimal CBOR (RFC 7049) decoder for WebAuthn attestationObject and authData parsing.
  Supports only what is needed: unsigned/negative integers, byte strings, text strings,
  arrays, and maps. Indefinite-length items are not supported.
}

interface

uses
  System.SysUtils, System.Classes;

// Extract the authData byte string from a CBOR-encoded attestationObject map.
function CborGetAuthData(const AAttestationObject: TBytes; out AAuthData: TBytes): Boolean;

// Parse a COSE EC2 public key from CBOR bytes starting at APos.
// Advances APos past the COSE map on success.
// Returns True if a valid ES256 (alg=-7) P-256 key with 32-byte x and y is found.
function CborParseCoseKey(const AData: TBytes; var APos: Integer;
  out AX, AY: TBytes; out AAlg: Integer): Boolean;

// Parse the fixed-layout authData structure.
// AT flag (bit 6) must be set; otherwise ACredentialId/APublicKey fields are empty.
function ParseAuthData(const AAuthData: TBytes;
  out ARpIdHash: TBytes;
  out AFlags: Byte;
  out ASignCount: Cardinal;
  out ACredentialId: TBytes;
  out APubKeyX, APubKeyY: TBytes;
  out APubKeyAlg: Integer): Boolean;

implementation

// ---------------------------------------------------------------------------
// Internal CBOR reader primitives
// ---------------------------------------------------------------------------

// Read the initial byte and additional length/value bytes.
// For major type 1 (negative int), AValue is returned as the negative result: -1 - raw.
// Returns False if data is truncated or an unsupported additional-info is encountered.
function CborReadHead(const AData: TBytes; var APos: Integer;
  out AMajorType: Byte; out AValue: Int64): Boolean;
var
  b, addInfo: Byte;
begin
  Result := False;
  if APos >= Length(AData) then Exit;

  b         := AData[APos]; Inc(APos);
  AMajorType := b shr 5;
  addInfo    := b and $1F;

  case addInfo of
    0..23: AValue := addInfo;
    24:
    begin
      if APos >= Length(AData) then Exit;
      AValue := AData[APos]; Inc(APos);
    end;
    25:
    begin
      if APos + 1 > Length(AData) then Exit;
      AValue := (Int64(AData[APos]) shl 8) or AData[APos + 1];
      Inc(APos, 2);
    end;
    26:
    begin
      if APos + 3 > Length(AData) then Exit;
      AValue := (Int64(AData[APos])     shl 24) or
                (Int64(AData[APos + 1]) shl 16) or
                (Int64(AData[APos + 2]) shl 8)  or
                AData[APos + 3];
      Inc(APos, 4);
    end;
    27:
    begin
      if APos + 7 > Length(AData) then Exit;
      AValue := (Int64(AData[APos])     shl 56) or
                (Int64(AData[APos + 1]) shl 48) or
                (Int64(AData[APos + 2]) shl 40) or
                (Int64(AData[APos + 3]) shl 32) or
                (Int64(AData[APos + 4]) shl 24) or
                (Int64(AData[APos + 5]) shl 16) or
                (Int64(AData[APos + 6]) shl 8)  or
                AData[APos + 7];
      Inc(APos, 8);
    end;
  else
    Exit; // indefinite-length or reserved — not supported
  end;

  if AMajorType = 1 then
    AValue := -1 - AValue;

  Result := True;
end;

function CborReadBytes(const AData: TBytes; var APos: Integer;
  out AResult: TBytes): Boolean;
var
  mt: Byte;
  count: Int64;
begin
  Result := False;
  if not CborReadHead(AData, APos, mt, count) then Exit;
  if mt <> 2 then Exit;
  if (count < 0) or (APos + count > Length(AData)) then Exit;
  SetLength(AResult, count);
  if count > 0 then
    Move(AData[APos], AResult[0], count);
  Inc(APos, Integer(count));
  Result := True;
end;

function CborReadText(const AData: TBytes; var APos: Integer;
  out AResult: string): Boolean;
var
  mt: Byte;
  count: Int64;
  raw: TBytes;
begin
  Result := False;
  if not CborReadHead(AData, APos, mt, count) then Exit;
  if mt <> 3 then Exit;
  if (count < 0) or (APos + count > Length(AData)) then Exit;
  SetLength(raw, count);
  if count > 0 then
    Move(AData[APos], raw[0], count);
  Inc(APos, Integer(count));
  AResult := TEncoding.UTF8.GetString(raw);
  Result := True;
end;

function CborReadInt(const AData: TBytes; var APos: Integer;
  out AResult: Int64): Boolean;
var
  mt: Byte;
begin
  Result := False;
  if not CborReadHead(AData, APos, mt, AResult) then Exit;
  Result := (mt = 0) or (mt = 1);
end;

// Skip any CBOR item at APos, advancing APos past it.
function CborSkipItem(const AData: TBytes; var APos: Integer): Boolean;
var
  mt: Byte;
  count, i: Int64;
begin
  Result := False;
  if not CborReadHead(AData, APos, mt, count) then Exit;

  case mt of
    0, 1: Result := True; // integer — head already consumed

    2, 3: // byte string or text string
    begin
      if (count < 0) or (APos + count > Length(AData)) then Exit;
      Inc(APos, Integer(count));
      Result := True;
    end;

    4: // array
    begin
      for i := 0 to count - 1 do
        if not CborSkipItem(AData, APos) then Exit;
      Result := True;
    end;

    5: // map
    begin
      for i := 0 to count - 1 do
      begin
        if not CborSkipItem(AData, APos) then Exit; // key
        if not CborSkipItem(AData, APos) then Exit; // value
      end;
      Result := True;
    end;

    6: // tag — skip tagged item
      Result := CborSkipItem(AData, APos);

    7: // simple / float — additional bytes already consumed by CborReadHead
      Result := True;
  end;
end;

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

function CborGetAuthData(const AAttestationObject: TBytes;
  out AAuthData: TBytes): Boolean;
var
  pos: Integer;
  mt: Byte;
  mapCount, i: Int64;
  key: string;
begin
  Result := False;
  pos := 0;

  if not CborReadHead(AAttestationObject, pos, mt, mapCount) then Exit;
  if mt <> 5 then Exit; // must be a CBOR map

  for i := 0 to mapCount - 1 do
  begin
    if not CborReadText(AAttestationObject, pos, key) then Exit;
    if key = 'authData' then
    begin
      Result := CborReadBytes(AAttestationObject, pos, AAuthData);
      Exit;
    end;
    // skip the value for any other key
    if not CborSkipItem(AAttestationObject, pos) then Exit;
  end;
end;

function CborParseCoseKey(const AData: TBytes; var APos: Integer;
  out AX, AY: TBytes; out AAlg: Integer): Boolean;
var
  mt: Byte;
  mapCount, i, keyInt, valInt: Int64;
begin
  Result := False;
  AAlg   := 0;
  SetLength(AX, 0);
  SetLength(AY, 0);

  if not CborReadHead(AData, APos, mt, mapCount) then Exit;
  if mt <> 5 then Exit;

  for i := 0 to mapCount - 1 do
  begin
    if not CborReadInt(AData, APos, keyInt) then Exit;
    case keyInt of
      3:   // alg
      begin
        if not CborReadInt(AData, APos, valInt) then Exit;
        AAlg := Integer(valInt);
      end;
      -2:  // x coordinate
      begin
        if not CborReadBytes(AData, APos, AX) then Exit;
      end;
      -3:  // y coordinate
      begin
        if not CborReadBytes(AData, APos, AY) then Exit;
      end;
    else
      if not CborSkipItem(AData, APos) then Exit;
    end;
  end;

  Result := (Length(AX) = 32) and (Length(AY) = 32);
end;

function ParseAuthData(const AAuthData: TBytes;
  out ARpIdHash: TBytes;
  out AFlags: Byte;
  out ASignCount: Cardinal;
  out ACredentialId: TBytes;
  out APubKeyX, APubKeyY: TBytes;
  out APubKeyAlg: Integer): Boolean;
var
  pos: Integer;
  credIdLen: Word;
begin
  Result := False;
  pos := 0;

  // Minimum length for fixed header: 32 (rpIdHash) + 1 (flags) + 4 (signCount) = 37
  if Length(AAuthData) < 37 then Exit;

  SetLength(ARpIdHash, 32);
  Move(AAuthData[0], ARpIdHash[0], 32);
  pos := 32;

  AFlags := AAuthData[pos]; Inc(pos);

  ASignCount := (Cardinal(AAuthData[pos])     shl 24) or
                (Cardinal(AAuthData[pos + 1]) shl 16) or
                (Cardinal(AAuthData[pos + 2]) shl 8)  or
                AAuthData[pos + 3];
  Inc(pos, 4);

  // AT flag = bit 6 ($40) — attested credential data present
  if (AFlags and $40) = 0 then
  begin
    Result := True; // no credential data, valid for authentication assertions
    Exit;
  end;

  // Skip AAGUID (16 bytes)
  if pos + 16 > Length(AAuthData) then Exit;
  Inc(pos, 16);

  // Credential ID length (2 bytes big-endian)
  if pos + 2 > Length(AAuthData) then Exit;
  credIdLen := (Word(AAuthData[pos]) shl 8) or AAuthData[pos + 1];
  Inc(pos, 2);

  // Credential ID
  if pos + Integer(credIdLen) > Length(AAuthData) then Exit;
  SetLength(ACredentialId, credIdLen);
  if credIdLen > 0 then
    Move(AAuthData[pos], ACredentialId[0], credIdLen);
  Inc(pos, credIdLen);

  // COSE public key
  Result := CborParseCoseKey(AAuthData, pos, APubKeyX, APubKeyY, APubKeyAlg);
end;

end.
