Skip to content

feat(crypto): Crypt::OpenSSL::Random/RSA via Bouncy Castle; fix list-context ... range#535

Merged
fglock merged 2 commits intomasterfrom
feature/crypt-openssl-bouncy-castle
Apr 22, 2026
Merged

feat(crypto): Crypt::OpenSSL::Random/RSA via Bouncy Castle; fix list-context ... range#535
fglock merged 2 commits intomasterfrom
feature/crypt-openssl-bouncy-castle

Conversation

@fglock
Copy link
Copy Markdown
Owner

@fglock fglock commented Apr 22, 2026

Summary

Investigating jcpan -t OAuth::Lite surfaced two issues: two missing XS modules (Crypt::OpenSSL::Random, Crypt::OpenSSL::RSA) and a Perl-semantics bug in the list-context ... range operator. This PR addresses both.

Before: 3/14 test files passing (10 of 14 fail at use-time with "Can't load loadable object"), 43/197 subtests.
After: 14/14 test files, 197/197 subtests.

Changes

New: Crypt::OpenSSL::Random (CryptOpenSSLRandom.java)

Backed by java.security.SecureRandom:

  • random_bytes / random_pseudo_bytes -> SecureRandom.nextBytes
  • random_seed -> SecureRandom.setSeed
  • random_status -> always 1 (JDK SecureRandom is always seeded)
  • random_egd -> -1 (unsupported, matches LibreSSL)

New: Crypt::OpenSSL::RSA (CryptOpenSSLRSA.java)

Backed by Bouncy Castle (bcprov/bcpkix, already on the classpath) + java.security:

  • PEM parsing for PKCS#1 RSA PRIVATE/PUBLIC KEY and X.509 PUBLIC KEY via BC PEMParser + JcaPEMKeyConverter
  • Sign/verify via Signature.getInstance("<hash>withRSA") (PKCS#1 v1.5)
  • generate_key via KeyPairGenerator
  • get_{public,private}_key_string / get_public_key_x509_string via BC PemWriter, translating between PKCS#8 and PKCS#1 as needed
  • Full set of use_*_hash / use_*_padding selectors; default hash is SHA-1 (matches OAuth 1.0 RSA-SHA1 test vectors); use_pkcs1_padding is fatal per Crypt::OpenSSL::RSA >= 0.35
  • encrypt / decrypt stubbed (OAuth doesn't need them; they croak with "not implemented")

Bug fix: ... range operator in list context

t/07_signing_requests.t exposed that for (0...scalar(@$extra)/2-1) { ... } looped exactly once with $_ = "" instead of iterating 0..6, because ... was being unconditionally dispatched to the scalar-context flip-flop emitter. Real Perl treats ... in list context as identical to .. (range) — perl -MO=Deparse even normalizes it.

  • EmitBinaryOperatorNode: route both .. and ... through handleRangeOrFlipFlop, so list context -> range, scalar context -> flip-flop
  • EmitOperator.handleRangeOperator: always look up the JVM descriptor under .. (no separate ... handler is registered, and none is needed — the scalar flip-flop path still honors the three-dot variant via node.operator.equals("..."))

Scalar flip-flop with ... still matches system Perl (verified 1..10 with $_==3...$_==3 still matches 3..10).

Test plan

  • make (unit tests) passes
  • jcpan -t OAuth::Lite -> 14/14 test files, 197/197 subtests
  • perl vs jperl parity for for (0...6) and scalar ... flip-flop
  • One-shot sanity: ./jperl -e 'use Crypt::OpenSSL::Random; print unpack "H*", Crypt::OpenSSL::Random::random_bytes(10)'

Notes / follow-ups (not in this PR)

  • encrypt / decrypt / private_encrypt / public_decrypt are stubbed. Any module that does RSA encryption (rather than signing) will still fail — wire up when needed.
  • _new_key_from_parameters / _get_key_parameters (Crypt::OpenSSL::Bignum integration) are not implemented.
  • jcpan -t still silently continues past its own "Missing dependencies" warning at Makefile.PL time. Orthogonal to this PR.

Generated with Devin

@fglock fglock force-pushed the feature/crypt-openssl-bouncy-castle branch from 456e0bc to d9bca57 Compare April 22, 2026 09:43
fglock and others added 2 commits April 22, 2026 12:12
… `...` range in list context

Unblocks jcpan -t OAuth::Lite (was 3/14 test files passing, now 14/14,
197/197 subtests) by providing Java XS implementations for the two
CPAN modules it depends on, plus fixing a Perl-semantics bug in the
list-context range operator.

Crypt::OpenSSL::Random (new CryptOpenSSLRandom.java)
  - random_bytes / random_pseudo_bytes -> SecureRandom.nextBytes
  - random_seed -> SecureRandom.setSeed
  - random_status -> always 1 (SecureRandom is always seeded)
  - random_egd -> -1 (unsupported, matches LibreSSL)

Crypt::OpenSSL::RSA (new CryptOpenSSLRSA.java)
  - Key parsing: PKCS#1 RSA PRIVATE/PUBLIC KEY and X.509 PUBLIC KEY
    via Bouncy Castle PEMParser + JcaPEMKeyConverter
  - Sign/verify: java.security.Signature ("<hash>withRSA", PKCS#1 v1.5)
  - generate_key via KeyPairGenerator
  - get_{public,private}_key_string and get_public_key_x509_string via
    BC PemWriter, translating between PKCS#8 and PKCS#1 as needed
  - Full set of use_*_hash / use_*_padding selectors; default hash is
    SHA-1 to match OAuth 1.0 RSA-SHA1 test vectors; use_pkcs1_padding
    is fatal per Crypt::OpenSSL::RSA >= 0.35
  - encrypt/decrypt stubbed (not needed for OAuth; croak if called)

Range operator bug:
  for (0...6) { ... } looped exactly once with $_ = "" because `...`
  was always dispatched to the scalar-context flip-flop emitter. In
  real Perl, `...` in list context is identical to `..` (range). Fix:
    - EmitBinaryOperatorNode: route both `..` and `...` through
      handleRangeOrFlipFlop so scalar context -> flip-flop, list
      context -> range
    - EmitOperator.handleRangeOperator: always look up the JVM
      descriptor under `..` (no separate `...` handler exists, and
      none is needed since the scalar flip-flop path still honors
      the three-dot variant via node.operator.equals("..."))

Verified: scalar flip-flop with `...` still matches system Perl
(for 1..10 with $_==3...$_==3 still matches 3..10).

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…-trips

Builds on the initial Crypt::OpenSSL::{Random,RSA} scaffold to round
out the parts OAuth::Lite's follow-ups suggested.

New: Crypt::OpenSSL::Bignum (CryptOpenSSLBignum.java + Bignum.pm shim)
  - Backed by java.math.BigInteger
  - Constructors: new_from_bin / _decimal / _hex / _word, zero, one, rand
  - Conversions: to_bin (OpenSSL semantics: 0 -> 0 bytes), to_decimal, to_hex
  - Arithmetic: add, sub, mul, div (returns ($q,$r)), mod, exp, mod_exp,
    mod_inverse, gcd
  - Predicates / accessors: equals, cmp, is_zero, is_one, is_odd,
    num_bits, num_bytes, copy
  - pointer_copy / bless_pointer: marshal the BigInteger across the XS
    boundary as a JAVAOBJECT RuntimeScalar (no C pointer semantics needed)
  - Crypt::OpenSSL::Bignum::CTX stub (BigInteger is immutable so CTX has
    no state to carry)

RSA encrypt / decrypt / private_encrypt / public_decrypt:
  - Backed by javax.crypto.Cipher with the transformations
      NONE       -> RSA/ECB/NoPadding
      PKCS1      -> RSA/ECB/PKCS1Padding
      PKCS1_OAEP -> RSA/ECB/OAEPWithSHA-1AndMGF1Padding
    (PSS is signing-only, SSLv23 unsupported — both croak)
  - private_encrypt / public_decrypt plumb through to the same Cipher
    transforms; the JCE provider picks the correct PKCS#1 block type
    from the (mode, key type) combination.

RSA _new_key_from_parameters / _get_key_parameters:
  - Full round-trip wired via Bignum. Accepts any useful subset of
    (n, e, d, p, q): derives missing q from n/p (and vice versa),
    derives missing d from e and phi(n) when we have both primes,
    promotes to a CRT-accelerated RSAPrivateCrtKey whenever possible,
    falls back to (n, d) plain private, or stays public-only.
  - Rejects bogus "primes" with "OpenSSL error: p not prime" /
    "q not prime" so existing CPAN callers can pattern-match on the
    text (matches Crypt::OpenSSL::RSA's t/bignum.t expectations).
  - _get_key_parameters returns 8 slots (n, e, d, p, q, dmp1, dmq1,
    iqmp), undef for anything not known.

Bouncy Castle as the default provider:
  - Register BC as a JCE provider (static init) so RIPEMD160withRSA /
    WhirlpoolwithRSA / the full PSS family resolve through BC instead
    of being NoSuchAlgorithm from SunJCE alone.
  - Switch JcaPEMKeyConverter and KeyFactory.getInstance("RSA") to
    explicitly use BC so small / non-standard keys (e.g. the 77-bit
    canaries in Crypt::OpenSSL::RSA's t/format.t) parse instead of
    hitting Sun's "RSA keys must be at least 512 bits" floor.
  - sign/verify have a manual PKCS#1 v1.5 DigestInfo fallback for
    hashes that lack a bundled <Hash>withRSA Signature service
    (notably Whirlpool) — builds the DigestInfo DER with the matching
    OID and feeds it through RSA/ECB/PKCS1Padding so the JCE still
    applies type-1 padding.

Upstream CPAN test suite results (run against the installed
~/.perlonjava/cpan/build/Crypt-OpenSSL-RSA-0.37-0 tree):
  - t/rsa.t     92/92
  - t/bignum.t  64/64
  - t/sig_die.t  1/1
  - t/format.t  still fails on a deliberately-broken 77-bit key that
    triggers "RSA modulus has a small prime factor" from BC;
    acceptable since OpenSSL's own newer validators reject it too
OAuth::Lite end-to-end: 14/14 files, 197/197 subtests (unchanged).
make: green.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@fglock fglock force-pushed the feature/crypt-openssl-bouncy-castle branch from dc3d821 to 02869ec Compare April 22, 2026 10:13
@fglock fglock merged commit fd595db into master Apr 22, 2026
2 checks passed
@fglock fglock deleted the feature/crypt-openssl-bouncy-castle branch April 22, 2026 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant