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

RSA key must be at least #{MINIMUM_RSA_KEY_BITS} bits

Error message

RSA key must be at least #{MINIMUM_RSA_KEY_BITS} bits

What it means

ActionPack::WebAuthn::CoseKey enforces a 2048-bit floor on RS256 keys: it raises InvalidKeyError when the modulus (label -1) is shorter than 256 bytes. A genuine RSA-2048 modulus always encodes as exactly 256 big-endian bytes with the top bit set, so a shorter modulus means the authenticator or your fixture produced a key below the WebAuthn RS256 profile's minimum strength. This is a deliberate security policy, not a parse error.

Source

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

      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

      n = OpenSSL::BN.new(n_bytes, 2)
      e = OpenSSL::BN.new(e_bytes, 2)

      asn1 = OpenSSL::ASN1::Sequence([
        OpenSSL::ASN1::Sequence([
          OpenSSL::ASN1::ObjectId("rsaEncryption"),
          OpenSSL::ASN1::Null.new(nil)
        ]),
        OpenSSL::ASN1::BitString(
          OpenSSL::ASN1::Sequence([
            OpenSSL::ASN1::Integer(n),
            OpenSSL::ASN1::Integer(e)
          ]).to_der
        )
      ])

      OpenSSL::PKey::RSA.new(asn1.to_der)

View on GitHub (pinned to 7aabe74580)

Solutions

  1. Check the modulus length: cose.parameters[-1].bytesize must be >= 256 for RSA-2048; log it in the error handler to confirm
  2. If this comes from a fixture, generate a real key with OpenSSL::PKey::RSA.new(2048) and export n/e via .to_s(2) instead of hand-written constants
  3. If a real authenticator emits keys under 2048 bits, register a different credential (ES256 or EdDSA) or use a conformant security key; do not lower MINIMUM_RSA_KEY_BITS, it is a security floor
  4. Verify n was not truncated or re-encoded upstream (base64url padding, hex) which can shorten the byte string

Example fix

# before: fixture modulus shorter than 256 bytes, fails the 2048-bit floor
n_bytes = (0x80..0xff).to_a.pack('C*') * 4 # 128 bytes

# after: derive from a real RSA-2048 key
rsa = OpenSSL::PKey::RSA.new(2048)
n_bytes = rsa.n.to_s(2) # exactly 256 bytes
e_bytes = rsa.e.to_s(2)
Defensive patterns

Strategy: validation

Validate before calling

n = cose.parameters[ActionPack::WebAuthn::CoseKey::RSA_N_LABEL]
return if n.nil? || n.bytesize * 8 < ActionPack::WebAuthn::CoseKey::MINIMUM_RSA_KEY_BITS # mirrors the 2048-bit floor
key = cose.to_openssl_key

Type guard

def rsa_modulus_at_least_2048_bits?(cose)
  n = cose.parameters[-1]
  n.is_a?(String) && n.bytesize * 8 >= 2048
end

Try / catch

begin
  openssl_key = cose_key.to_openssl_key
rescue ActionPack::WebAuthn::InvalidKeyError => e
  render json: { error: e.message }, status: :unprocessable_entity
end

Prevention

When it happens

Trigger: Calling to_openssl_key on an RS256 COSE key whose -1 modulus is under 256 bytes: a test fixture with a short dummy modulus, a sub-2048-bit key from a non-conformant authenticator, or modulus bytes truncated or incorrectly re-encoded upstream so the byte string shrank.

Common situations: Adopting real security keys that use RS256 after developing only against toy fixtures; CI fixtures with hand-written small constants for n; rare authenticators emitting RSA-1024 CTAP2 credentials; hex/base64 round-trips that drop leading bytes of the modulus.

Related errors


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