{"record":{"id":"65c3ca1180b4d0d6","repo":"basecamp/fizzy","slug":"rsa-key-must-be-at-least-minimum-rsa-key-bits-b","errorCode":null,"errorMessage":"RSA key must be at least #{MINIMUM_RSA_KEY_BITS} bits","messagePattern":"RSA key must be at least #(.+?) bits","errorType":"exception","errorClass":"ActionPack::WebAuthn::InvalidKeyError","httpStatus":null,"severity":"error","filePath":"lib/action_pack/web_authn/cose_key.rb","lineNumber":161,"sourceCode":"      raise ActionPack::WebAuthn::InvalidKeyError, \"Missing OKP key coordinate\" if x.nil?\n\n      asn1 = OpenSSL::ASN1::Sequence([\n        OpenSSL::ASN1::Sequence([\n          OpenSSL::ASN1::ObjectId(\"ED25519\")\n        ]),\n        OpenSSL::ASN1::BitString(x)\n      ])\n\n      OpenSSL::PKey.read(asn1.to_der)\n    rescue OpenSSL::PKey::PKeyError => error\n      raise ActionPack::WebAuthn::InvalidKeyError, \"Invalid OKP key: #{error.message}\"\n    end\n\n    def build_rsa_rs256_key\n      n_bytes = parameters[RSA_N_LABEL]\n      e_bytes = parameters[RSA_E_LABEL]\n      raise ActionPack::WebAuthn::InvalidKeyError, \"Missing RSA key parameters\" if n_bytes.nil? || e_bytes.nil?\n      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\n      n = OpenSSL::BN.new(n_bytes, 2)\n      e = OpenSSL::BN.new(e_bytes, 2)\n\n      asn1 = OpenSSL::ASN1::Sequence([\n        OpenSSL::ASN1::Sequence([\n          OpenSSL::ASN1::ObjectId(\"rsaEncryption\"),\n          OpenSSL::ASN1::Null.new(nil)\n        ]),\n        OpenSSL::ASN1::BitString(\n          OpenSSL::ASN1::Sequence([\n            OpenSSL::ASN1::Integer(n),\n            OpenSSL::ASN1::Integer(e)\n          ]).to_der\n        )\n      ])\n\n      OpenSSL::PKey::RSA.new(asn1.to_der)","sourceCodeStart":143,"sourceCodeEnd":179,"githubUrl":"https://github.com/basecamp/fizzy/blob/7aabe7458060d8a1759a53b7ede39e74e6c0b20d/lib/action_pack/web_authn/cose_key.rb#L143-L179","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check the modulus length: cose.parameters[-1].bytesize must be >= 256 for RSA-2048; log it in the error handler to confirm","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","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","Verify n was not truncated or re-encoded upstream (base64url padding, hex) which can shorten the byte string"],"exampleFix":"# before: fixture modulus shorter than 256 bytes, fails the 2048-bit floor\nn_bytes = (0x80..0xff).to_a.pack('C*') * 4 # 128 bytes\n\n# after: derive from a real RSA-2048 key\nrsa = OpenSSL::PKey::RSA.new(2048)\nn_bytes = rsa.n.to_s(2) # exactly 256 bytes\ne_bytes = rsa.e.to_s(2)","handlingStrategy":"validation","validationCode":"n = cose.parameters[ActionPack::WebAuthn::CoseKey::RSA_N_LABEL]\nreturn if n.nil? || n.bytesize * 8 < ActionPack::WebAuthn::CoseKey::MINIMUM_RSA_KEY_BITS # mirrors the 2048-bit floor\nkey = cose.to_openssl_key","typeGuard":"def rsa_modulus_at_least_2048_bits?(cose)\n  n = cose.parameters[-1]\n  n.is_a?(String) && n.bytesize * 8 >= 2048\nend","tryCatchPattern":"begin\n  openssl_key = cose_key.to_openssl_key\nrescue ActionPack::WebAuthn::InvalidKeyError => e\n  render json: { error: e.message }, status: :unprocessable_entity\nend","preventionTips":["Generate fixture RSA keys with OpenSSL::PKey::RSA.new(2048) and export n/e via .to_s(2)","Treat MINIMUM_RSA_KEY_BITS as a security contract: never lower it to make a fixture pass","Log modulus bytesize on registration failure to spot truncation quickly","Prefer ES256 or EdDSA authenticators in CI to avoid RSA fixture pitfalls"],"tags":["webauthn","cose","rsa","key-size","security-policy"],"backgroundTag":"weak-rsa-key-rejected","analyzedSha":"7aabe7458060d8a1759a53b7ede39e74e6c0b20d","analyzedAt":"2026-08-21T18:33:25.349Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}