basecamp/fizzy · error · ActionPack::WebAuthn::InvalidResponseError

Authenticator data is too short for attested credential data

Error message

Authenticator data is too short for attested credential data

What it means

During registration the flags byte carries the attested-credential-data bit (0x40). When that bit is set the parser needs at least 18 more bytes — a 16-byte AAGUID plus a 2-byte credential ID length field (AAGUID_LENGTH + CREDENTIAL_ID_LENGTH_BYTES). If the buffer ends first, the attested credential block is truncated and InvalidResponseError is raised.

Source

Thrown at lib/action_pack/web_authn/authenticator/data.rb:103

      position = 0

      relying_party_id_hash = bytes[position, RELYING_PARTY_ID_HASH_LENGTH].pack("C*")
      position += RELYING_PARTY_ID_HASH_LENGTH

      flags = bytes[position]
      position += FLAGS_LENGTH

      sign_count = bytes[position, SIGN_COUNT_LENGTH].pack("C*").unpack1("N")
      position += SIGN_COUNT_LENGTH

      aaguid = nil
      credential_id = nil
      public_key_bytes = nil

      if flags & ATTESTED_CREDENTIAL_DATA_FLAG != 0
        if bytes.length < position + AAGUID_LENGTH + CREDENTIAL_ID_LENGTH_BYTES
          raise ActionPack::WebAuthn::InvalidResponseError, "Authenticator data is too short for attested credential data"
        end

        aaguid_bytes = bytes[position, AAGUID_LENGTH].pack("C*")
        aaguid = aaguid_bytes.unpack("H8H4H4H4H12").join("-")
        position += AAGUID_LENGTH

        credential_id_length = bytes[position, CREDENTIAL_ID_LENGTH_BYTES].pack("C*").unpack1("n")
        position += CREDENTIAL_ID_LENGTH_BYTES

        if bytes.length < position + credential_id_length + 1
          raise ActionPack::WebAuthn::InvalidResponseError, "Authenticator data is too short for credential ID and public key"
        end

        credential_id = Base64.urlsafe_encode64(bytes[position, credential_id_length].pack("C*"), padding: false)
        position += credential_id_length

        public_key_bytes = bytes[position..].pack("C*")
      end

View on GitHub (pinned to 7aabe74580)

Solutions

  1. Confirm the total decoded length: registration data must exceed 37 + 18 + credential_id_length + COSE key bytes; anything close to 55 bytes with flag 0x40 set is truncated.
  2. Send the authenticatorData buffer from the browser untouched (base64url of the whole ArrayBuffer), never a reassembled copy.
  3. Rebuild fixtures from a real navigator.credentials.create() ceremony.
  4. Rescue InvalidResponseError and surface a clear 'registration payload incomplete' message rather than a stack trace.

Example fix

# before
# fixture with attested flag but no attested block
flags = 0x45 # UP | UV | AT (0x40)
data = ActionPack::WebAuthn::Authenticator::Data.decode(header_37_bytes_with(flags)) # raises

# after — include a minimal well-formed attested block (16-byte AAGUID + 2-byte length + id + key)
buf = rp_id_hash_32 + [0x45].pack('C') + [0].pack('N') + aaguid_16 + [credential_id.bytesize].pack('n') + credential_id + cose_key
data = ActionPack::WebAuthn::Authenticator::Data.decode(buf)
Defensive patterns

Strategy: try-catch

Validate before calling

raw = Base64.urlsafe_decode64(params[:authenticator_data].to_s)
flags = raw.getbyte(32)
# registration needs 37 + 18 bytes minimum when the attested flag (0x40) is set
min = (flags & 0x40).zero? ? 37 : 55
return render(json: { error: 'attestation data truncated' }, status: :bad_request) if raw.bytesize < min

Type guard

def plausible_attestation?(raw)
  flags = raw.getbyte(32)
  raw.bytesize >= ((flags & 0x40).zero? ? 37 : 55)
end

Try / catch

begin
  attestation = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(**webauthn_params)
  attestation.validate!
rescue ActionPack::WebAuthn::InvalidResponseError => e
  render json: { error: "registration failed: #{e.message}" }, status: :bad_request
end

Prevention

When it happens

Trigger: A registration (attestation) response whose authenticator data is exactly the 37-byte header plus a flags byte claiming attested data, but with the AAGUID/credential-ID block cut off; fabricated fixtures that set flag 0x40 without appending an attested credential payload.

Common situations: Test fixtures built by hand that copy flags from a real registration but strip the credential body; bugs in custom client code that reconstructs authenticatorData from parts; transport truncation of large registration payloads (credential IDs can be 100+ bytes).

Understand the failure class

Related errors


AI-assisted analysis of basecamp/fizzy@7aabe74580 (2026-08-21). Data as JSON: /api/errors/4decd3126c93cd70. Report an issue: GitHub.