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

Missing OKP key coordinate

Error message

Missing OKP key coordinate

What it means

ActionPack::WebAuthn::CoseKey raised InvalidKeyError because a COSE OKP key (key type 1, algorithm -8 EdDSA, curve 6 Ed25519) had no entry under label -2, the x-coordinate. WebAuthn authenticators deliver the credential public key as a CBOR-encoded COSE map, and RFC 9053 requires every OKP key to carry its 32-byte x-coordinate under label -2. The library validates the map's shape before handing anything to OpenSSL, so this error means the decoded CBOR was structurally incomplete, not that OpenSSL rejected the key.

Source

Thrown at lib/action_pack/web_authn/cose_key.rb:143

      asn1 = OpenSSL::ASN1::Sequence([
        OpenSSL::ASN1::Sequence([
          OpenSSL::ASN1::ObjectId("id-ecPublicKey"),
          OpenSSL::ASN1::ObjectId("prime256v1")
        ]),
        OpenSSL::ASN1::BitString(public_key_bytes)
      ])

      OpenSSL::PKey::EC.new(asn1.to_der)
    rescue OpenSSL::PKey::PKeyError => error
      raise ActionPack::WebAuthn::InvalidKeyError, "Invalid EC2 key: #{error.message}"
    end

    def build_okp_eddsa_key
      curve = parameters[OKP_CURVE_LABEL]
      raise ActionPack::WebAuthn::UnsupportedKeyTypeError, "Unsupported OKP curve: #{curve}" unless curve == ED25519

      x = parameters[OKP_X_LABEL]
      raise ActionPack::WebAuthn::InvalidKeyError, "Missing OKP key coordinate" if x.nil?

      asn1 = OpenSSL::ASN1::Sequence([
        OpenSSL::ASN1::Sequence([
          OpenSSL::ASN1::ObjectId("ED25519")
        ]),
        OpenSSL::ASN1::BitString(x)
      ])

      OpenSSL::PKey.read(asn1.to_der)
    rescue OpenSSL::PKey::PKeyError => error
      raise ActionPack::WebAuthn::InvalidKeyError, "Invalid OKP key: #{error.message}"
    end

    def build_rsa_rs256_key
      n_bytes = parameters[RSA_N_LABEL]
      e_bytes = parameters[RSA_E_LABEL]
      raise ActionPack::WebAuthn::InvalidKeyError, "Missing RSA key parameters" if n_bytes.nil? || e_bytes.nil?
      raise ActionPack::WebAuthn::InvalidKeyError, "RSA key must be at least #{MINIMUM_RSA_KEY_BITS} bits" if n_bytes.bytesize * 8 < MINIMUM_RSA_KEY_BITS

View on GitHub (pinned to 7aabe74580)

Solutions

  1. Inspect the decoded map (CoseKey.decode(bytes).parameters) and confirm label -2 is actually absent, to distinguish a malformed authenticator payload from your own slicing offset being wrong
  2. Re-check how you slice the credential public key out of attestedCredentialData (16-byte AAGUID + 2-byte big-endian credentialIdLength + credentialId + CBOR map); off-by-one slicing can decode to a map that lost trailing parameters
  3. If you build the COSE map yourself (tests or firmware), include the Ed25519 coordinate: { 1 => 1, 3 => -8, -1 => 6, -2 => x }
  4. Rescue ActionPack::WebAuthn::InvalidKeyError around to_openssl_key at registration and fail the ceremony with a client-visible error instead of a 500

Example fix

# before (test fixture): OKP map without the x-coordinate
params = { 1 => 1, 3 => -8, -1 => 6 }

# after: include the 32-byte Ed25519 x-coordinate under label -2
params = { 1 => 1, 3 => -8, -1 => 6, -2 => ed25519_public_bytes }
Defensive patterns

Strategy: validation

Validate before calling

cose = ActionPack::WebAuthn::CoseKey.decode(public_key_bytes)
x = cose.parameters[ActionPack::WebAuthn::CoseKey::OKP_X_LABEL]
return if x.nil? || !x.is_a?(String) || x.bytesize != 32 # reject before conversion
key = cose.to_openssl_key

Type guard

def ed25519_cose_key?(cose)
  [cose.key_type, cose.algorithm] == [1, -8] &&
    cose.parameters[-1] == 6 &&
    cose.parameters[-2].is_a?(String) && cose.parameters[-2].bytesize == 32
end

Try / catch

begin
  openssl_key = cose_key.to_openssl_key
rescue ActionPack::WebAuthn::InvalidKeyError, ActionPack::WebAuthn::UnsupportedKeyTypeError => e
  # client-supplied key is unusable: fail the registration ceremony, not a 500
  render json: { error: e.message }, status: :unprocessable_entity
end

Prevention

When it happens

Trigger: Calling ActionPack::WebAuthn::CoseKey.decode(cbor).to_openssl_key where the decoded map contains {1=>1, 3=>-8, -1=>6} but no -2 entry. Typical sources: a hand-built test COSE key, a truncated or wrongly sliced publicKey field of attestedCredentialData, or a buggy CTAP implementation that omits the coordinate.

Common situations: Writing WebAuthn registration tests with manually constructed COSE maps; parsing authenticatorData with home-grown offset math (misreading the 2-byte credentialIdLength) so the key slice starts mid-CBOR; firmware or emulator authenticators that emit incomplete OKP maps.

Related errors


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