From bc7008803cfb3c322351fceb8382a1d58aa7ef1c Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 22 Apr 2026 11:18:50 +0200 Subject: [PATCH 1/2] feat(crypto): back Crypt::OpenSSL::Random/RSA with Bouncy Castle; fix `...` 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 ("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> --- .../backend/jvm/EmitBinaryOperatorNode.java | 9 +- .../perlonjava/backend/jvm/EmitOperator.java | 5 +- .../org/perlonjava/core/Configuration.java | 4 +- .../runtime/perlmodule/CryptOpenSSLRSA.java | 428 ++++++++++++++++++ .../perlmodule/CryptOpenSSLRandom.java | 77 ++++ 5 files changed, 516 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/perlonjava/runtime/perlmodule/CryptOpenSSLRSA.java create mode 100644 src/main/java/org/perlonjava/runtime/perlmodule/CryptOpenSSLRandom.java diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitBinaryOperatorNode.java b/src/main/java/org/perlonjava/backend/jvm/EmitBinaryOperatorNode.java index 6a1043db3..de607b82f 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitBinaryOperatorNode.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitBinaryOperatorNode.java @@ -75,10 +75,11 @@ public static void emitBinaryOperatorNode(EmitterVisitor emitterVisitor, BinaryO "|=", "|.=", "binary|=", ">>=", ".=", "%=", "^=", "^.=", "binary^=", "x=", "^^=" -> EmitBinaryOperator.handleCompoundAssignment(emitterVisitor, node); - // Range and flip-flop operators - case "..." -> EmitLogicalOperator.emitFlipFlopOperator(emitterVisitor, node); - - case ".." -> EmitBinaryOperator.handleRangeOrFlipFlop(emitterVisitor, node); + // Range and flip-flop operators. In list context both `..` and `...` + // are the range operator; in scalar context they become flip-flops + // (with `...` being the variant that stays "true" for at least one + // iteration after transitioning). + case "..", "..." -> EmitBinaryOperator.handleRangeOrFlipFlop(emitterVisitor, node); // Comparison operators (chained) case "<", ">", "<=", ">=", "lt", "gt", "le", "ge", diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java index 15b300a65..5ed164e1a 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java @@ -713,11 +713,14 @@ static void handleGlobBuiltin(EmitterVisitor emitterVisitor, OperatorNode node) } // Handles the 'range' operator, which creates a range of values. + // The operator is always emitted as ".." — in list context both `..` and + // `...` act as the range operator; only the scalar-context flip-flop path + // (handled elsewhere) cares about the distinction. static void handleRangeOperator(EmitterVisitor emitterVisitor, BinaryOperatorNode node) { // Accept both left and right operands in SCALAR context. node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); node.right.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); - emitOperator(node, emitterVisitor); + emitOperatorWithKey("..", node, emitterVisitor); } // Handles the 'substr' operator, which extracts a substring from a string. diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index e02142e28..8552e569e 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "e228c2529"; + public static final String gitCommitId = "9ee5a06d4"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -48,7 +48,7 @@ public final class Configuration { * Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at" * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String buildTimestamp = "Apr 22 2026 11:59:22"; + public static final String buildTimestamp = "Apr 22 2026 11:42:16"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/CryptOpenSSLRSA.java b/src/main/java/org/perlonjava/runtime/perlmodule/CryptOpenSSLRSA.java new file mode 100644 index 000000000..c79e7738a --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/perlmodule/CryptOpenSSLRSA.java @@ -0,0 +1,428 @@ +package org.perlonjava.runtime.perlmodule; + +import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.pkcs.RSAPublicKey; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemWriter; +import org.perlonjava.runtime.runtimetypes.*; + +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.interfaces.RSAKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.spec.RSAPublicKeySpec; + +import static org.perlonjava.runtime.operators.WarnDie.die; +import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarFalse; +import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarTrue; +import static org.perlonjava.runtime.runtimetypes.RuntimeScalarType.JAVAOBJECT; + +/** + * Crypt::OpenSSL::RSA implementation for PerlOnJava, backed by Bouncy Castle + * and the JDK's {@code java.security} APIs. + *

+ * Implements the subset of the CPAN XS interface exercised by OAuth::Lite and + * similar consumers: PKCS#1 / X.509 PEM parsing, RSA-PKCS1v15 signing and + * verification, and the {@code use_shaN_hash} / {@code use_*_padding} mode + * switches. OAEP encrypt/decrypt and key-parameter introspection are not yet + * wired up — methods that would need them throw a Perl-level error. + */ +public class CryptOpenSSLRSA extends PerlModuleBase { + + private static final String CLASS_NAME = "Crypt::OpenSSL::RSA"; + private static final String STATE_KEY = "_rsa_state"; + + // Sign/verify hash algorithms. Default is SHA-1 to match OAuth 1.0 RSA-SHA1 + // test vectors; the underlying Java Signature algorithm is "withRSA" + // (PKCS#1 v1.5 padding). + enum Hash { + SHA1("SHA1"), SHA224("SHA224"), SHA256("SHA256"), SHA384("SHA384"), + SHA512("SHA512"), MD5("MD5"), RIPEMD160("RIPEMD160"), WHIRLPOOL("WHIRLPOOL"); + final String javaName; + Hash(String javaName) { this.javaName = javaName; } + } + + enum Padding { NONE, PKCS1, PKCS1_OAEP, PKCS1_PSS, SSLV23 } + + /** Mutable RSA key state kept under $self->{_rsa_state}. */ + public static final class State { + PrivateKey priv; // null for public-only keys + PublicKey pub; // always set + Hash hash = Hash.SHA1; + Padding padding = Padding.PKCS1_OAEP; + } + + public CryptOpenSSLRSA() { + super(CLASS_NAME, false); + } + + public static void initialize() { + CryptOpenSSLRSA mod = new CryptOpenSSLRSA(); + GlobalVariable.getGlobalVariable("Crypt::OpenSSL::RSA::VERSION").set(new RuntimeScalar("0.37")); + try { + // Class methods + mod.registerMethod("generate_key", null); + mod.registerMethod("_new_public_key_pkcs1", null); + mod.registerMethod("_new_public_key_x509", null); + mod.registerMethod("new_private_key", null); + mod.registerMethod("_random_seed", null); + mod.registerMethod("_random_status", null); + // Instance methods + mod.registerMethod("DESTROY", null); + mod.registerMethod("get_public_key_string", null); + mod.registerMethod("get_public_key_x509_string", null); + mod.registerMethod("get_private_key_string", null); + mod.registerMethod("sign", null); + mod.registerMethod("verify", null); + mod.registerMethod("size", null); + mod.registerMethod("check_key", null); + mod.registerMethod("is_private", null); + mod.registerMethod("encrypt", null); + mod.registerMethod("decrypt", null); + mod.registerMethod("private_encrypt", null); + mod.registerMethod("public_decrypt", null); + // Padding selectors + mod.registerMethod("use_no_padding", null); + mod.registerMethod("use_pkcs1_padding", null); + mod.registerMethod("use_pkcs1_oaep_padding", null); + mod.registerMethod("use_pkcs1_pss_padding", null); + mod.registerMethod("use_sslv23_padding", null); + // Hash selectors + mod.registerMethod("use_md5_hash", null); + mod.registerMethod("use_sha1_hash", null); + mod.registerMethod("use_sha224_hash", null); + mod.registerMethod("use_sha256_hash", null); + mod.registerMethod("use_sha384_hash", null); + mod.registerMethod("use_sha512_hash", null); + mod.registerMethod("use_ripemd160_hash", null); + mod.registerMethod("use_whirlpool_hash", null); + } catch (NoSuchMethodException e) { + System.err.println("Warning: Missing Crypt::OpenSSL::RSA method: " + e.getMessage()); + } + } + + // ---- helpers ---- + + private static RuntimeScalar bytesToScalar(byte[] bytes) { + return new RuntimeScalar(new String(bytes, StandardCharsets.ISO_8859_1)); + } + + private static byte[] scalarToBytes(RuntimeScalar s) { + return s.toString().getBytes(StandardCharsets.ISO_8859_1); + } + + private static State getState(RuntimeScalar self) { + RuntimeHash h = self.hashDeref(); + RuntimeScalar d = h.get(STATE_KEY); + if (d == null || d.type != JAVAOBJECT || !(d.value instanceof State s)) { + die(new RuntimeScalar("Crypt::OpenSSL::RSA: invalid object (no state)"), + new RuntimeScalar("\n")); + return null; + } + return s; + } + + private static RuntimeScalar newBlessedObject(String className, State st) { + RuntimeHash h = new RuntimeHash(); + h.blessId = NameNormalizer.getBlessId(className); + h.put(STATE_KEY, new RuntimeScalar(st)); + return h.createReference(); + } + + private static String writePem(String type, byte[] der) { + try { + StringWriter sw = new StringWriter(); + try (PemWriter pw = new PemWriter(sw)) { + pw.writeObject(new PemObject(type, der)); + } + return sw.toString(); + } catch (Exception e) { + throw new RuntimeException("PEM write failed: " + e.getMessage(), e); + } + } + + // ---- class methods ---- + + /** generate_key($class, $bits, $exp = 65537) */ + public static RuntimeList generate_key(RuntimeArray args, int ctx) { + if (args.size() < 2) { + die(new RuntimeScalar("Usage: Crypt::OpenSSL::RSA->generate_key($bits [, $exp])"), + new RuntimeScalar("\n")); + } + String cls = args.get(0).toString(); + int bits = args.get(1).getInt(); + try { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + // NB: Java's default RSA exponent is 65537; honouring a custom $exp + // would require RSAKeyGenParameterSpec with a BigInteger — not worth + // the dependency on args.get(2) being a valid public exponent for OAuth. + kpg.initialize(bits); + KeyPair kp = kpg.generateKeyPair(); + State st = new State(); + st.priv = kp.getPrivate(); + st.pub = kp.getPublic(); + return newBlessedObject(cls, st).getList(); + } catch (Exception e) { + die(new RuntimeScalar("generate_key failed: " + e.getMessage()), + new RuntimeScalar("\n")); + return scalarFalse.getList(); + } + } + + /** _new_public_key_pkcs1($class, $pem) — -----BEGIN RSA PUBLIC KEY----- */ + public static RuntimeList _new_public_key_pkcs1(RuntimeArray args, int ctx) { + if (args.size() < 2) { + die(new RuntimeScalar("Usage: Crypt::OpenSSL::RSA->new_public_key($pem)"), + new RuntimeScalar("\n")); + } + String cls = args.get(0).toString(); + String pem = args.get(1).toString(); + try (PEMParser p = new PEMParser(new StringReader(pem))) { + Object obj = p.readObject(); + PublicKey pk; + if (obj instanceof SubjectPublicKeyInfo spki) { + pk = new JcaPEMKeyConverter().getPublicKey(spki); + } else if (obj instanceof RSAPublicKey rsaPub) { + // Raw PKCS#1 RSAPublicKey (some BC versions expose it directly). + KeyFactory kf = KeyFactory.getInstance("RSA"); + pk = kf.generatePublic(new RSAPublicKeySpec( + rsaPub.getModulus(), rsaPub.getPublicExponent())); + } else { + die(new RuntimeScalar("unrecognized public key PEM"), + new RuntimeScalar("\n")); + return scalarFalse.getList(); + } + State st = new State(); + st.pub = pk; + return newBlessedObject(cls, st).getList(); + } catch (Exception e) { + die(new RuntimeScalar("new_public_key (pkcs1) failed: " + e.getMessage()), + new RuntimeScalar("\n")); + return scalarFalse.getList(); + } + } + + /** _new_public_key_x509($class, $pem) — -----BEGIN PUBLIC KEY----- */ + public static RuntimeList _new_public_key_x509(RuntimeArray args, int ctx) { + // BC's PEMParser handles both PKCS1 RSA PUBLIC KEY and X.509 PUBLIC KEY + // by emitting a SubjectPublicKeyInfo, so the x509 path shares the + // pkcs1 code path. + return _new_public_key_pkcs1(args, ctx); + } + + /** new_private_key($class, $pem [, $passphrase]) */ + public static RuntimeList new_private_key(RuntimeArray args, int ctx) { + if (args.size() < 2) { + die(new RuntimeScalar("Usage: Crypt::OpenSSL::RSA->new_private_key($pem)"), + new RuntimeScalar("\n")); + } + String cls = args.get(0).toString(); + String pem = args.get(1).toString(); + // Passphrase-protected keys not yet supported. + try (PEMParser p = new PEMParser(new StringReader(pem))) { + Object obj = p.readObject(); + KeyPair kp; + if (obj instanceof PEMKeyPair pkp) { + kp = new JcaPEMKeyConverter().getKeyPair(pkp); + } else if (obj instanceof PrivateKeyInfo pki) { + PrivateKey pk = new JcaPEMKeyConverter().getPrivateKey(pki); + // derive public key from CRT parameters + if (pk instanceof RSAPrivateCrtKey crt) { + KeyFactory kf = KeyFactory.getInstance("RSA"); + PublicKey pub = kf.generatePublic(new RSAPublicKeySpec( + crt.getModulus(), crt.getPublicExponent())); + State st = new State(); + st.priv = pk; + st.pub = pub; + return newBlessedObject(cls, st).getList(); + } + die(new RuntimeScalar("unsupported private key (not RSA CRT)"), + new RuntimeScalar("\n")); + return scalarFalse.getList(); + } else { + die(new RuntimeScalar("unrecognized private key format"), + new RuntimeScalar("\n")); + return scalarFalse.getList(); + } + State st = new State(); + st.priv = kp.getPrivate(); + st.pub = kp.getPublic(); + return newBlessedObject(cls, st).getList(); + } catch (Exception e) { + die(new RuntimeScalar("new_private_key failed: " + e.getMessage()), + new RuntimeScalar("\n")); + return scalarFalse.getList(); + } + } + + /** Class-level stubs used by import_random_seed in RSA.pm */ + public static RuntimeList _random_status(RuntimeArray args, int ctx) { + return new RuntimeScalar(1).getList(); + } + public static RuntimeList _random_seed(RuntimeArray args, int ctx) { + return scalarTrue.getList(); + } + + // ---- instance methods ---- + + public static RuntimeList DESTROY(RuntimeArray args, int ctx) { + return scalarTrue.getList(); + } + + public static RuntimeList is_private(RuntimeArray args, int ctx) { + State st = getState(args.get(0)); + return (st.priv != null ? scalarTrue : scalarFalse).getList(); + } + + public static RuntimeList check_key(RuntimeArray args, int ctx) { + State st = getState(args.get(0)); + if (st.priv == null) { + die(new RuntimeScalar("check_key called on public key"), new RuntimeScalar("\n")); + } + return scalarTrue.getList(); + } + + public static RuntimeList size(RuntimeArray args, int ctx) { + State st = getState(args.get(0)); + if (st.pub instanceof RSAKey rsa) { + return new RuntimeScalar((rsa.getModulus().bitLength() + 7) / 8).getList(); + } + return new RuntimeScalar(0).getList(); + } + + public static RuntimeList get_public_key_string(RuntimeArray args, int ctx) { + State st = getState(args.get(0)); + try { + // PKCS#1 RSAPublicKey DER (not the SPKI). + SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(st.pub.getEncoded()); + byte[] pkcs1 = spki.parsePublicKey().getEncoded(ASN1Encoding.DER); + return new RuntimeScalar(writePem("RSA PUBLIC KEY", pkcs1)).getList(); + } catch (Exception e) { + die(new RuntimeScalar("get_public_key_string failed: " + e.getMessage()), + new RuntimeScalar("\n")); + return scalarFalse.getList(); + } + } + + public static RuntimeList get_public_key_x509_string(RuntimeArray args, int ctx) { + State st = getState(args.get(0)); + return new RuntimeScalar(writePem("PUBLIC KEY", st.pub.getEncoded())).getList(); + } + + public static RuntimeList get_private_key_string(RuntimeArray args, int ctx) { + State st = getState(args.get(0)); + if (st.priv == null) { + die(new RuntimeScalar("get_private_key_string called on public-only key"), + new RuntimeScalar("\n")); + } + try { + // Convert PKCS#8 encoding to PKCS#1 RSAPrivateKey DER. + PrivateKeyInfo pki = PrivateKeyInfo.getInstance(st.priv.getEncoded()); + byte[] pkcs1 = pki.parsePrivateKey().toASN1Primitive().getEncoded(ASN1Encoding.DER); + return new RuntimeScalar(writePem("RSA PRIVATE KEY", pkcs1)).getList(); + } catch (Exception e) { + die(new RuntimeScalar("get_private_key_string failed: " + e.getMessage()), + new RuntimeScalar("\n")); + return scalarFalse.getList(); + } + } + + public static RuntimeList sign(RuntimeArray args, int ctx) { + State st = getState(args.get(0)); + if (st.priv == null) { + die(new RuntimeScalar("sign requires a private key"), new RuntimeScalar("\n")); + } + byte[] data = scalarToBytes(args.get(1)); + try { + Signature sig = Signature.getInstance(st.hash.javaName + "withRSA"); + sig.initSign(st.priv); + sig.update(data); + return bytesToScalar(sig.sign()).getList(); + } catch (Exception e) { + die(new RuntimeScalar("sign failed: " + e.getMessage()), new RuntimeScalar("\n")); + return scalarFalse.getList(); + } + } + + public static RuntimeList verify(RuntimeArray args, int ctx) { + State st = getState(args.get(0)); + byte[] data = scalarToBytes(args.get(1)); + byte[] sigBytes = scalarToBytes(args.get(2)); + try { + Signature sig = Signature.getInstance(st.hash.javaName + "withRSA"); + sig.initVerify(st.pub); + sig.update(data); + return (sig.verify(sigBytes) ? scalarTrue : scalarFalse).getList(); + } catch (Exception e) { + // Per Crypt::OpenSSL::RSA semantics, bad signatures return false, + // not die. Only programmer errors should croak. + return scalarFalse.getList(); + } + } + + // encrypt/decrypt not wired up yet — OAuth doesn't need them. + public static RuntimeList encrypt(RuntimeArray args, int ctx) { + die(new RuntimeScalar("Crypt::OpenSSL::RSA::encrypt: not implemented in PerlOnJava"), + new RuntimeScalar("\n")); + return scalarFalse.getList(); + } + public static RuntimeList decrypt(RuntimeArray args, int ctx) { + die(new RuntimeScalar("Crypt::OpenSSL::RSA::decrypt: not implemented in PerlOnJava"), + new RuntimeScalar("\n")); + return scalarFalse.getList(); + } + public static RuntimeList private_encrypt(RuntimeArray args, int ctx) { + die(new RuntimeScalar("Crypt::OpenSSL::RSA::private_encrypt: not implemented in PerlOnJava"), + new RuntimeScalar("\n")); + return scalarFalse.getList(); + } + public static RuntimeList public_decrypt(RuntimeArray args, int ctx) { + die(new RuntimeScalar("Crypt::OpenSSL::RSA::public_decrypt: not implemented in PerlOnJava"), + new RuntimeScalar("\n")); + return scalarFalse.getList(); + } + + // ---- padding selectors ---- + + private static RuntimeList setPadding(RuntimeArray args, Padding p) { + getState(args.get(0)).padding = p; + return scalarTrue.getList(); + } + public static RuntimeList use_no_padding(RuntimeArray args, int ctx) { return setPadding(args, Padding.NONE); } + public static RuntimeList use_pkcs1_padding(RuntimeArray args, int ctx) { + // Crypt::OpenSSL::RSA 0.35+ makes this fatal. We match that. + die(new RuntimeScalar("use_pkcs1_padding: PKCS#1 v1.5 padding is insecure and disabled"), + new RuntimeScalar("\n")); + return scalarFalse.getList(); + } + public static RuntimeList use_pkcs1_oaep_padding(RuntimeArray args, int ctx) { return setPadding(args, Padding.PKCS1_OAEP); } + public static RuntimeList use_pkcs1_pss_padding(RuntimeArray args, int ctx) { return setPadding(args, Padding.PKCS1_PSS); } + public static RuntimeList use_sslv23_padding(RuntimeArray args, int ctx) { return setPadding(args, Padding.SSLV23); } + + // ---- hash selectors ---- + + private static RuntimeList setHash(RuntimeArray args, Hash h) { + getState(args.get(0)).hash = h; + return scalarTrue.getList(); + } + public static RuntimeList use_md5_hash(RuntimeArray args, int ctx) { return setHash(args, Hash.MD5); } + public static RuntimeList use_sha1_hash(RuntimeArray args, int ctx) { return setHash(args, Hash.SHA1); } + public static RuntimeList use_sha224_hash(RuntimeArray args, int ctx) { return setHash(args, Hash.SHA224); } + public static RuntimeList use_sha256_hash(RuntimeArray args, int ctx) { return setHash(args, Hash.SHA256); } + public static RuntimeList use_sha384_hash(RuntimeArray args, int ctx) { return setHash(args, Hash.SHA384); } + public static RuntimeList use_sha512_hash(RuntimeArray args, int ctx) { return setHash(args, Hash.SHA512); } + public static RuntimeList use_ripemd160_hash(RuntimeArray args, int ctx) { return setHash(args, Hash.RIPEMD160); } + public static RuntimeList use_whirlpool_hash(RuntimeArray args, int ctx) { return setHash(args, Hash.WHIRLPOOL); } +} diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/CryptOpenSSLRandom.java b/src/main/java/org/perlonjava/runtime/perlmodule/CryptOpenSSLRandom.java new file mode 100644 index 000000000..3095ae5ff --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/perlmodule/CryptOpenSSLRandom.java @@ -0,0 +1,77 @@ +package org.perlonjava.runtime.perlmodule; + +import org.perlonjava.runtime.runtimetypes.*; + +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; + +import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarTrue; + +/** + * Crypt::OpenSSL::Random implementation for PerlOnJava. + *

+ * Backed by Java's {@link SecureRandom}. Unlike the XS module, we have no + * explicit seed buffer to query — SecureRandom is always considered seeded, + * so {@code random_status()} always returns 1. {@code random_seed()} feeds + * additional entropy via {@code setSeed}; {@code random_egd()} is unsupported. + */ +public class CryptOpenSSLRandom extends PerlModuleBase { + + private static final SecureRandom SECURE = new SecureRandom(); + + public CryptOpenSSLRandom() { + super("Crypt::OpenSSL::Random", false); + } + + public static void initialize() { + CryptOpenSSLRandom mod = new CryptOpenSSLRandom(); + GlobalVariable.getGlobalVariable("Crypt::OpenSSL::Random::VERSION").set(new RuntimeScalar("0.17")); + try { + mod.registerMethod("random_bytes", null); + mod.registerMethod("random_pseudo_bytes", null); + mod.registerMethod("random_seed", null); + mod.registerMethod("random_status", null); + mod.registerMethod("random_egd", null); + } catch (NoSuchMethodException e) { + System.err.println("Warning: Missing Crypt::OpenSSL::Random method: " + e.getMessage()); + } + } + + /** Binary bytes -> Perl byte-string (latin1-encoded Java String). */ + private static RuntimeScalar bytesToScalar(byte[] bytes) { + return new RuntimeScalar(new String(bytes, StandardCharsets.ISO_8859_1)); + } + + /** random_bytes(IV num_bytes) - cryptographically strong pseudo-random bytes. */ + public static RuntimeList random_bytes(RuntimeArray args, int ctx) { + int n = args.isEmpty() ? 0 : args.get(0).getInt(); + if (n < 0) n = 0; + byte[] out = new byte[n]; + if (n > 0) SECURE.nextBytes(out); + return bytesToScalar(out).getList(); + } + + /** random_pseudo_bytes(IV num_bytes) - non-cryptographic random bytes. */ + public static RuntimeList random_pseudo_bytes(RuntimeArray args, int ctx) { + return random_bytes(args, ctx); + } + + /** random_seed(PV seed_bytes) - feed entropy into the PRNG. Returns true. */ + public static RuntimeList random_seed(RuntimeArray args, int ctx) { + if (!args.isEmpty()) { + byte[] seed = args.get(0).toString().getBytes(StandardCharsets.ISO_8859_1); + SECURE.setSeed(seed); + } + return scalarTrue.getList(); + } + + /** random_status() - PRNG always considered seeded. */ + public static RuntimeList random_status(RuntimeArray args, int ctx) { + return new RuntimeScalar(1).getList(); + } + + /** random_egd(PV path) - entropy gathering daemon not supported. */ + public static RuntimeList random_egd(RuntimeArray args, int ctx) { + return new RuntimeScalar(-1).getList(); + } +} From 02869eccff9e7aea57bbccd0877d7f47dbb8684a Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 22 Apr 2026 12:05:24 +0200 Subject: [PATCH 2/2] feat(crypto): wire RSA encrypt/decrypt + Crypt::OpenSSL::Bignum round-trips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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> --- .../org/perlonjava/core/Configuration.java | 4 +- .../perlmodule/CryptOpenSSLBignum.java | 289 ++++++++++++++ .../runtime/perlmodule/CryptOpenSSLRSA.java | 374 ++++++++++++++++-- src/main/perl/lib/Crypt/OpenSSL/Bignum.pm | 45 +++ 4 files changed, 683 insertions(+), 29 deletions(-) create mode 100644 src/main/java/org/perlonjava/runtime/perlmodule/CryptOpenSSLBignum.java create mode 100644 src/main/perl/lib/Crypt/OpenSSL/Bignum.pm diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 8552e569e..198c5c878 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "9ee5a06d4"; + public static final String gitCommitId = "3c20a8408"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -48,7 +48,7 @@ public final class Configuration { * Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at" * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String buildTimestamp = "Apr 22 2026 11:42:16"; + public static final String buildTimestamp = "Apr 22 2026 12:12:34"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/CryptOpenSSLBignum.java b/src/main/java/org/perlonjava/runtime/perlmodule/CryptOpenSSLBignum.java new file mode 100644 index 000000000..a79517486 --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/perlmodule/CryptOpenSSLBignum.java @@ -0,0 +1,289 @@ +package org.perlonjava.runtime.perlmodule; + +import org.perlonjava.runtime.runtimetypes.*; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; + +import static org.perlonjava.runtime.operators.WarnDie.die; +import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarFalse; +import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarTrue; +import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarUndef; +import static org.perlonjava.runtime.runtimetypes.RuntimeScalarType.JAVAOBJECT; + +/** + * Minimal Crypt::OpenSSL::Bignum implementation for PerlOnJava, backed by + * {@link BigInteger}. Covers the API surface needed by Crypt::OpenSSL::RSA's + * {@code new_key_from_parameters} / {@code get_key_parameters} round-trips + * plus the common constructors and conversions used by callers. + *

+ * The "pointer" that the Perl-side wrapper round-trips via + * {@code pointer_copy} / {@code bless_pointer} is a {@link RuntimeScalar} + * carrying the BigInteger as a JAVAOBJECT; there is no C pointer semantics + * to emulate. + */ +public class CryptOpenSSLBignum extends PerlModuleBase { + + private static final String CLASS_NAME = "Crypt::OpenSSL::Bignum"; + private static final String VALUE_KEY = "_bn_value"; + private static final SecureRandom RNG = new SecureRandom(); + + public CryptOpenSSLBignum() { + super(CLASS_NAME, false); + } + + public static void initialize() { + CryptOpenSSLBignum mod = new CryptOpenSSLBignum(); + GlobalVariable.getGlobalVariable("Crypt::OpenSSL::Bignum::VERSION").set(new RuntimeScalar("0.09")); + try { + // Constructors (class methods) + mod.registerMethod("new_from_bin", null); + mod.registerMethod("new_from_decimal", null); + mod.registerMethod("new_from_hex", null); + mod.registerMethod("new_from_word", null); + mod.registerMethod("zero", null); + mod.registerMethod("one", null); + mod.registerMethod("rand", null); + mod.registerMethod("pseudo_rand", null); + mod.registerMethod("bless_pointer", null); + // Instance methods + mod.registerMethod("pointer_copy", null); + mod.registerMethod("to_bin", null); + mod.registerMethod("to_decimal", null); + mod.registerMethod("to_hex", null); + mod.registerMethod("equals", null); + mod.registerMethod("cmp", null); + mod.registerMethod("is_zero", null); + mod.registerMethod("is_one", null); + mod.registerMethod("is_odd", null); + mod.registerMethod("num_bits", null); + mod.registerMethod("num_bytes", null); + mod.registerMethod("copy", null); + mod.registerMethod("DESTROY", null); + // Arithmetic (static-style: take context-free args, return new Bignum) + mod.registerMethod("add", null); + mod.registerMethod("sub", null); + mod.registerMethod("mul", null); + mod.registerMethod("div", null); + mod.registerMethod("mod", null); + mod.registerMethod("exp", null); + mod.registerMethod("mod_exp", null); + mod.registerMethod("mod_inverse", null); + mod.registerMethod("gcd", null); + } catch (NoSuchMethodException e) { + System.err.println("Warning: Missing Crypt::OpenSSL::Bignum method: " + e.getMessage()); + } + } + + // ---- helpers ---- + + /** Build a blessed hashref holding {@code $bn->{_bn_value} = }. */ + public static RuntimeScalar wrap(BigInteger v) { + RuntimeHash h = new RuntimeHash(); + h.blessId = NameNormalizer.getBlessId(CLASS_NAME); + h.put(VALUE_KEY, new RuntimeScalar(v)); + return h.createReference(); + } + + /** Extract the BigInteger from a blessed Crypt::OpenSSL::Bignum hashref. */ + public static BigInteger unwrap(RuntimeScalar self) { + RuntimeHash h = self.hashDeref(); + RuntimeScalar s = h.get(VALUE_KEY); + if (s == null || s.type != JAVAOBJECT || !(s.value instanceof BigInteger bi)) { + die(new RuntimeScalar("Crypt::OpenSSL::Bignum: invalid object (no value)"), + new RuntimeScalar("\n")); + return null; + } + return bi; + } + + /** + * Unwrap a "pointer" handed to {@code bless_pointer} or produced by + * {@code pointer_copy}. Accepts a scalar carrying a BigInteger JAVAOBJECT; + * falls back to treating the scalar as a decimal string for robustness. + */ + private static BigInteger unwrapPointer(RuntimeScalar ptr) { + if (ptr.type == JAVAOBJECT && ptr.value instanceof BigInteger bi) return bi; + try { + return new BigInteger(ptr.toString()); + } catch (NumberFormatException e) { + die(new RuntimeScalar("Crypt::OpenSSL::Bignum: bad pointer value"), + new RuntimeScalar("\n")); + return null; + } + } + + // ---- constructors ---- + + /** new_from_bin($class, $raw_bytes) — big-endian unsigned. */ + public static RuntimeList new_from_bin(RuntimeArray args, int ctx) { + if (args.size() < 2) return scalarUndef.getList(); + byte[] bytes = args.get(1).toString().getBytes(StandardCharsets.ISO_8859_1); + if (bytes.length == 0) return wrap(BigInteger.ZERO).getList(); + return wrap(new BigInteger(1, bytes)).getList(); + } + + public static RuntimeList new_from_decimal(RuntimeArray args, int ctx) { + if (args.size() < 2) return scalarUndef.getList(); + try { + return wrap(new BigInteger(args.get(1).toString(), 10)).getList(); + } catch (NumberFormatException e) { + die(new RuntimeScalar("new_from_decimal: " + e.getMessage()), new RuntimeScalar("\n")); + return scalarFalse.getList(); + } + } + + public static RuntimeList new_from_hex(RuntimeArray args, int ctx) { + if (args.size() < 2) return scalarUndef.getList(); + String s = args.get(1).toString(); + if (s.startsWith("0x") || s.startsWith("0X")) s = s.substring(2); + try { + return wrap(new BigInteger(s, 16)).getList(); + } catch (NumberFormatException e) { + die(new RuntimeScalar("new_from_hex: " + e.getMessage()), new RuntimeScalar("\n")); + return scalarFalse.getList(); + } + } + + public static RuntimeList new_from_word(RuntimeArray args, int ctx) { + if (args.size() < 2) return scalarUndef.getList(); + return wrap(BigInteger.valueOf(args.get(1).getLong())).getList(); + } + + public static RuntimeList zero(RuntimeArray args, int ctx) { return wrap(BigInteger.ZERO).getList(); } + public static RuntimeList one(RuntimeArray args, int ctx) { return wrap(BigInteger.ONE).getList(); } + + /** rand($class, $bits) — uniformly random integer of exactly $bits bits (top bit set). */ + public static RuntimeList rand(RuntimeArray args, int ctx) { + int bits = args.size() >= 2 ? args.get(1).getInt() : 0; + if (bits <= 0) return wrap(BigInteger.ZERO).getList(); + BigInteger v = new BigInteger(bits, RNG); + v = v.setBit(bits - 1); // force top bit + return wrap(v).getList(); + } + + public static RuntimeList pseudo_rand(RuntimeArray args, int ctx) { return rand(args, ctx); } + + /** bless_pointer($class, $ptr) — wrap a scalar carrying a BigInteger back into a Bignum. */ + public static RuntimeList bless_pointer(RuntimeArray args, int ctx) { + if (args.size() < 2) return scalarUndef.getList(); + BigInteger v = unwrapPointer(args.get(1)); + return wrap(v).getList(); + } + + // ---- instance accessors ---- + + /** pointer_copy($self) — returns a scalar carrying the BigInteger value. */ + public static RuntimeList pointer_copy(RuntimeArray args, int ctx) { + return new RuntimeScalar(unwrap(args.get(0))).getList(); + } + + public static RuntimeList to_bin(RuntimeArray args, int ctx) { + BigInteger v = unwrap(args.get(0)); + if (v.signum() < 0) { + die(new RuntimeScalar("to_bin: negative value"), new RuntimeScalar("\n")); + } + if (v.signum() == 0) { + // OpenSSL's BN_bn2bin returns 0 bytes for zero. + return new RuntimeScalar("").getList(); + } + // BigInteger.toByteArray() returns two's-complement; strip any leading zero + // byte that was added to keep the value non-negative. + byte[] raw = v.toByteArray(); + if (raw.length > 1 && raw[0] == 0) { + byte[] trimmed = new byte[raw.length - 1]; + System.arraycopy(raw, 1, trimmed, 0, trimmed.length); + raw = trimmed; + } + return new RuntimeScalar(new String(raw, StandardCharsets.ISO_8859_1)).getList(); + } + + public static RuntimeList to_decimal(RuntimeArray args, int ctx) { + return new RuntimeScalar(unwrap(args.get(0)).toString(10)).getList(); + } + + public static RuntimeList to_hex(RuntimeArray args, int ctx) { + return new RuntimeScalar(unwrap(args.get(0)).toString(16).toUpperCase()).getList(); + } + + public static RuntimeList equals(RuntimeArray args, int ctx) { + BigInteger a = unwrap(args.get(0)); + BigInteger b = unwrap(args.get(1)); + return (a.equals(b) ? scalarTrue : scalarFalse).getList(); + } + + public static RuntimeList cmp(RuntimeArray args, int ctx) { + return new RuntimeScalar(unwrap(args.get(0)).compareTo(unwrap(args.get(1)))).getList(); + } + + public static RuntimeList is_zero(RuntimeArray args, int ctx) { + return (unwrap(args.get(0)).signum() == 0 ? scalarTrue : scalarFalse).getList(); + } + public static RuntimeList is_one(RuntimeArray args, int ctx) { + return (unwrap(args.get(0)).equals(BigInteger.ONE) ? scalarTrue : scalarFalse).getList(); + } + public static RuntimeList is_odd(RuntimeArray args, int ctx) { + return (unwrap(args.get(0)).testBit(0) ? scalarTrue : scalarFalse).getList(); + } + + public static RuntimeList num_bits(RuntimeArray args, int ctx) { + return new RuntimeScalar(unwrap(args.get(0)).bitLength()).getList(); + } + public static RuntimeList num_bytes(RuntimeArray args, int ctx) { + return new RuntimeScalar((unwrap(args.get(0)).bitLength() + 7) / 8).getList(); + } + public static RuntimeList copy(RuntimeArray args, int ctx) { + return wrap(unwrap(args.get(0))).getList(); + } + public static RuntimeList DESTROY(RuntimeArray args, int ctx) { + return scalarTrue.getList(); + } + + // ---- arithmetic ---- + // OpenSSL's Bignum API threads a third "context" argument through most ops; + // we ignore it and return a fresh Bignum. + + public static RuntimeList add(RuntimeArray args, int ctx) { return wrap(unwrap(args.get(0)).add(unwrap(args.get(1)))).getList(); } + public static RuntimeList sub(RuntimeArray args, int ctx) { return wrap(unwrap(args.get(0)).subtract(unwrap(args.get(1)))).getList(); } + public static RuntimeList mul(RuntimeArray args, int ctx) { return wrap(unwrap(args.get(0)).multiply(unwrap(args.get(1)))).getList(); } + + /** div($a, $b) in list context returns ($quotient, $remainder). */ + public static RuntimeList div(RuntimeArray args, int ctx) { + BigInteger[] qr = unwrap(args.get(0)).divideAndRemainder(unwrap(args.get(1))); + RuntimeList rl = new RuntimeList(); + rl.add(wrap(qr[0])); + rl.add(wrap(qr[1])); + return rl; + } + + public static RuntimeList mod(RuntimeArray args, int ctx) { + // mod(a, m) — always a non-negative remainder, to match OpenSSL. + return wrap(unwrap(args.get(0)).mod(unwrap(args.get(1)).abs())).getList(); + } + + public static RuntimeList exp(RuntimeArray args, int ctx) { + // exp(a, b) — a ** b, integer exponent. + BigInteger b = unwrap(args.get(1)); + if (b.bitLength() > 31) { + die(new RuntimeScalar("exp: exponent too large"), new RuntimeScalar("\n")); + } + return wrap(unwrap(args.get(0)).pow(b.intValueExact())).getList(); + } + + public static RuntimeList mod_exp(RuntimeArray args, int ctx) { + return wrap(unwrap(args.get(0)).modPow(unwrap(args.get(1)), unwrap(args.get(2)))).getList(); + } + + public static RuntimeList mod_inverse(RuntimeArray args, int ctx) { + try { + return wrap(unwrap(args.get(0)).modInverse(unwrap(args.get(1)))).getList(); + } catch (ArithmeticException e) { + return scalarUndef.getList(); // no inverse exists + } + } + + public static RuntimeList gcd(RuntimeArray args, int ctx) { + return wrap(unwrap(args.get(0)).gcd(unwrap(args.get(1)))).getList(); + } +} diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/CryptOpenSSLRSA.java b/src/main/java/org/perlonjava/runtime/perlmodule/CryptOpenSSLRSA.java index c79e7738a..caa8c85a1 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/CryptOpenSSLRSA.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/CryptOpenSSLRSA.java @@ -13,6 +13,7 @@ import java.io.StringReader; import java.io.StringWriter; +import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.KeyFactory; import java.security.KeyPair; @@ -22,11 +23,13 @@ import java.security.Signature; import java.security.interfaces.RSAKey; import java.security.interfaces.RSAPrivateCrtKey; +import java.security.spec.RSAPrivateCrtKeySpec; import java.security.spec.RSAPublicKeySpec; import static org.perlonjava.runtime.operators.WarnDie.die; import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarFalse; import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarTrue; +import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarUndef; import static org.perlonjava.runtime.runtimetypes.RuntimeScalarType.JAVAOBJECT; /** @@ -35,15 +38,26 @@ *

* Implements the subset of the CPAN XS interface exercised by OAuth::Lite and * similar consumers: PKCS#1 / X.509 PEM parsing, RSA-PKCS1v15 signing and - * verification, and the {@code use_shaN_hash} / {@code use_*_padding} mode - * switches. OAEP encrypt/decrypt and key-parameter introspection are not yet - * wired up — methods that would need them throw a Perl-level error. + * verification, OAEP / PKCS#1 v1.5 encryption and decryption (including the + * legacy {@code private_encrypt} / {@code public_decrypt} primitives), the + * {@code use_shaN_hash} / {@code use_*_padding} mode switches, and the + * {@code new_key_from_parameters} / {@code get_key_parameters} round-trips + * (backed by {@link CryptOpenSSLBignum}). */ public class CryptOpenSSLRSA extends PerlModuleBase { private static final String CLASS_NAME = "Crypt::OpenSSL::RSA"; private static final String STATE_KEY = "_rsa_state"; + // Register Bouncy Castle as a JCE provider once, so that signature + // algorithms the JDK doesn't ship (e.g. RIPEMD160withRSA, WhirlpoolwithRSA, + // PSS with non-SHA-1 digests) resolve through BC transparently. + static { + if (java.security.Security.getProvider("BC") == null) { + java.security.Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); + } + } + // Sign/verify hash algorithms. Default is SHA-1 to match OAuth 1.0 RSA-SHA1 // test vectors; the underlying Java Signature algorithm is "withRSA" // (PKCS#1 v1.5 padding). @@ -77,6 +91,8 @@ public static void initialize() { mod.registerMethod("_new_public_key_pkcs1", null); mod.registerMethod("_new_public_key_x509", null); mod.registerMethod("new_private_key", null); + mod.registerMethod("_new_key_from_parameters", null); + mod.registerMethod("_get_key_parameters", null); mod.registerMethod("_random_seed", null); mod.registerMethod("_random_status", null); // Instance methods @@ -141,6 +157,22 @@ private static RuntimeScalar newBlessedObject(String className, State st) { return h.createReference(); } + // Use Bouncy Castle as the backing provider for key parsing + construction. + // BC is permissive about key sizes (Sun's RSA provider rejects keys + // smaller than 512 bits, which breaks Crypt::OpenSSL::RSA's t/format.t + // canary keys) and exposes a superset of the JDK's algorithms. + private static JcaPEMKeyConverter pemConverter() { + return new JcaPEMKeyConverter().setProvider("BC"); + } + + private static KeyFactory rsaKeyFactory() throws java.security.NoSuchAlgorithmException { + try { + return KeyFactory.getInstance("RSA", "BC"); + } catch (java.security.NoSuchProviderException e) { + return KeyFactory.getInstance("RSA"); + } + } + private static String writePem(String type, byte[] der) { try { StringWriter sw = new StringWriter(); @@ -193,11 +225,10 @@ public static RuntimeList _new_public_key_pkcs1(RuntimeArray args, int ctx) { Object obj = p.readObject(); PublicKey pk; if (obj instanceof SubjectPublicKeyInfo spki) { - pk = new JcaPEMKeyConverter().getPublicKey(spki); + pk = pemConverter().getPublicKey(spki); } else if (obj instanceof RSAPublicKey rsaPub) { // Raw PKCS#1 RSAPublicKey (some BC versions expose it directly). - KeyFactory kf = KeyFactory.getInstance("RSA"); - pk = kf.generatePublic(new RSAPublicKeySpec( + pk = rsaKeyFactory().generatePublic(new RSAPublicKeySpec( rsaPub.getModulus(), rsaPub.getPublicExponent())); } else { die(new RuntimeScalar("unrecognized public key PEM"), @@ -235,13 +266,12 @@ public static RuntimeList new_private_key(RuntimeArray args, int ctx) { Object obj = p.readObject(); KeyPair kp; if (obj instanceof PEMKeyPair pkp) { - kp = new JcaPEMKeyConverter().getKeyPair(pkp); + kp = pemConverter().getKeyPair(pkp); } else if (obj instanceof PrivateKeyInfo pki) { - PrivateKey pk = new JcaPEMKeyConverter().getPrivateKey(pki); + PrivateKey pk = pemConverter().getPrivateKey(pki); // derive public key from CRT parameters if (pk instanceof RSAPrivateCrtKey crt) { - KeyFactory kf = KeyFactory.getInstance("RSA"); - PublicKey pub = kf.generatePublic(new RSAPublicKeySpec( + PublicKey pub = rsaKeyFactory().generatePublic(new RSAPublicKeySpec( crt.getModulus(), crt.getPublicExponent())); State st = new State(); st.priv = pk; @@ -275,6 +305,165 @@ public static RuntimeList _random_seed(RuntimeArray args, int ctx) { return scalarTrue.getList(); } + // ---- Bignum-backed parameter round-trips ---- + // + // The Perl-side Crypt::OpenSSL::RSA wrapper passes BIGNUM values across XS + // as "pointers" (opaque scalars) produced by Crypt::OpenSSL::Bignum's + // pointer_copy(); here those scalars carry a java.math.BigInteger JAVAOBJECT. + // We do the BigInteger -> java.security.Key translation here. + + /** + * _new_key_from_parameters($class, $n, $e, $d, $p, $q) + *

+ * The public key requires ($n, $e). If ($p, $q) are present we derive the + * full CRT private key; otherwise if $d is present we build a plain + * (n,d) private key via PKCS#8. With just (n, e) we return a public-only + * RSA object, matching the upstream XS. + */ + public static RuntimeList _new_key_from_parameters(RuntimeArray args, int ctx) { + if (args.size() < 3) { + die(new RuntimeScalar("Usage: Crypt::OpenSSL::RSA->new_key_from_parameters($n, $e [, $d, $p, $q])"), + new RuntimeScalar("\n")); + } + String cls = args.get(0).toString(); + BigInteger n = scalarToBigInt(args.get(1)); + BigInteger e = scalarToBigInt(args.get(2)); + BigInteger d = args.size() > 3 ? scalarToBigIntOrNull(args.get(3)) : null; + BigInteger p = args.size() > 4 ? scalarToBigIntOrNull(args.get(4)) : null; + BigInteger q = args.size() > 5 ? scalarToBigIntOrNull(args.get(5)) : null; + + if (n == null || e == null) { + die(new RuntimeScalar("new_key_from_parameters: n and e are required"), + new RuntimeScalar("\n")); + } + + // Do the Bignum-level sanity / derivation work BEFORE asking Java's + // KeyFactory to build the public key: BC rejects even moduli outright + // with "RSA modulus is even", but we want to surface the semantically + // correct "p not prime" / "q not prime" error the caller is looking for. + if (d != null || p != null || q != null) { + // If we have one prime factor but not the other, derive it from + // the modulus (q = n / p when n % p == 0, and vice versa). If the + // division isn't exact the caller lied about the supposed prime. + if (p != null && q == null) { + BigInteger[] dr = n.divideAndRemainder(p); + if (dr[1].signum() != 0) { + die(new RuntimeScalar("OpenSSL error: q not prime"), new RuntimeScalar("\n")); + } + q = dr[0]; + } + if (q != null && p == null) { + BigInteger[] dr = n.divideAndRemainder(q); + if (dr[1].signum() != 0) { + die(new RuntimeScalar("OpenSSL error: p not prime"), new RuntimeScalar("\n")); + } + p = dr[0]; + } + if (p != null && !p.isProbablePrime(20)) { + die(new RuntimeScalar("OpenSSL error: p not prime"), new RuntimeScalar("\n")); + } + if (q != null && !q.isProbablePrime(20)) { + die(new RuntimeScalar("OpenSSL error: q not prime"), new RuntimeScalar("\n")); + } + // If d was omitted but we have both primes, derive it from e and + // the Euler totient phi(n) = (p-1)(q-1). + if (d == null && p != null && q != null) { + BigInteger phi = p.subtract(BigInteger.ONE).multiply(q.subtract(BigInteger.ONE)); + d = e.modInverse(phi); + } + } + + try { + KeyFactory kf = rsaKeyFactory(); + State st = new State(); + st.pub = kf.generatePublic(new RSAPublicKeySpec(n, e)); + + if (d != null) { + if (p != null && q != null) { + // Full CRT parameters: fastest private key. + BigInteger dP = d.mod(p.subtract(BigInteger.ONE)); + BigInteger dQ = d.mod(q.subtract(BigInteger.ONE)); + BigInteger qInv = q.modInverse(p); + st.priv = kf.generatePrivate(new RSAPrivateCrtKeySpec(n, e, d, p, q, dP, dQ, qInv)); + } else { + // (n, d) only — no CRT acceleration. + st.priv = kf.generatePrivate(new java.security.spec.RSAPrivateKeySpec(n, d)); + } + } + + return newBlessedObject(cls, st).getList(); + } catch (org.perlonjava.runtime.runtimetypes.PerlDieException pde) { + throw pde; + } catch (Exception ex) { + die(new RuntimeScalar("new_key_from_parameters failed: " + ex.getMessage()), + new RuntimeScalar("\n")); + return scalarFalse.getList(); + } + } + + /** + * _get_key_parameters($self) + *

+ * Returns up to 8 "pointers" (scalars carrying BigInteger JAVAOBJECTs): + * n, e, d, p, q, d mod (p-1), d mod (q-1), 1/q mod p. Missing values + * (e.g. d/p/q on a public-only key) come back as undef, which the Perl + * wrapper maps to undef in the Bignum list. + */ + public static RuntimeList _get_key_parameters(RuntimeArray args, int ctx) { + State st = getState(args.get(0)); + RuntimeList out = new RuntimeList(); + + BigInteger n = null, e = null; + if (st.pub instanceof java.security.interfaces.RSAPublicKey pk) { + n = pk.getModulus(); + e = pk.getPublicExponent(); + } else if (st.priv instanceof RSAPrivateCrtKey crt) { + n = crt.getModulus(); + e = crt.getPublicExponent(); + } + out.add(asPtr(n)); + out.add(asPtr(e)); + + if (st.priv instanceof RSAPrivateCrtKey crt) { + out.add(asPtr(crt.getPrivateExponent())); + out.add(asPtr(crt.getPrimeP())); + out.add(asPtr(crt.getPrimeQ())); + out.add(asPtr(crt.getPrimeExponentP())); + out.add(asPtr(crt.getPrimeExponentQ())); + out.add(asPtr(crt.getCrtCoefficient())); + } else if (st.priv instanceof java.security.interfaces.RSAPrivateKey pk) { + out.add(asPtr(pk.getPrivateExponent())); + for (int i = 0; i < 5; i++) out.add(scalarUndef); + } else { + // public-only key + for (int i = 0; i < 6; i++) out.add(scalarUndef); + } + return out; + } + + // ---- Bignum-pointer marshaling helpers ---- + + /** Decode a "pointer" scalar produced by Crypt::OpenSSL::Bignum::pointer_copy. */ + private static BigInteger scalarToBigInt(RuntimeScalar s) { + if (s.type == JAVAOBJECT && s.value instanceof BigInteger bi) return bi; + try { return new BigInteger(s.toString()); } + catch (NumberFormatException nfe) { return null; } + } + + private static BigInteger scalarToBigIntOrNull(RuntimeScalar s) { + if (s == null) return null; + // The Perl wrapper maps missing Bignums to 0, which we treat as "absent". + if (s.type == JAVAOBJECT && s.value instanceof BigInteger bi) return bi; + String str = s.toString(); + if (str.isEmpty() || str.equals("0")) return null; + try { return new BigInteger(str); } + catch (NumberFormatException nfe) { return null; } + } + + private static RuntimeScalar asPtr(BigInteger v) { + return v == null ? scalarUndef : new RuntimeScalar(v); + } + // ---- instance methods ---- public static RuntimeList DESTROY(RuntimeArray args, int ctx) { @@ -346,25 +535,54 @@ public static RuntimeList sign(RuntimeArray args, int ctx) { } byte[] data = scalarToBytes(args.get(1)); try { - Signature sig = Signature.getInstance(st.hash.javaName + "withRSA"); - sig.initSign(st.priv); - sig.update(data); - return bytesToScalar(sig.sign()).getList(); + return bytesToScalar(signImpl(st, data)).getList(); } catch (Exception e) { die(new RuntimeScalar("sign failed: " + e.getMessage()), new RuntimeScalar("\n")); return scalarFalse.getList(); } } + private static byte[] signImpl(State st, byte[] data) throws Exception { + // Fast path: whatever "withRSA" Signature algorithm the JDK + + // Bouncy Castle collectively expose. + try { + Signature sig = Signature.getInstance(st.hash.javaName + "withRSA"); + sig.initSign(st.priv); + sig.update(data); + return sig.sign(); + } catch (java.security.NoSuchAlgorithmException nsa) { + // Fallback for hashes with no bundled withRSA provider + // (e.g. Whirlpool). Build the DigestInfo ourselves and let + // Cipher("RSA/ECB/PKCS1Padding") apply PKCS#1 v1.5 type-1 padding. + byte[] digestInfo = buildDigestInfo(st.hash, data); + javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("RSA/ECB/PKCS1Padding"); + c.init(javax.crypto.Cipher.ENCRYPT_MODE, st.priv); + return c.doFinal(digestInfo); + } + } + public static RuntimeList verify(RuntimeArray args, int ctx) { State st = getState(args.get(0)); byte[] data = scalarToBytes(args.get(1)); byte[] sigBytes = scalarToBytes(args.get(2)); try { + // Fast path — symmetric with sign(). Signature sig = Signature.getInstance(st.hash.javaName + "withRSA"); sig.initVerify(st.pub); sig.update(data); return (sig.verify(sigBytes) ? scalarTrue : scalarFalse).getList(); + } catch (java.security.NoSuchAlgorithmException nsa) { + // Fallback: recover DigestInfo via Cipher("RSA/ECB/PKCS1Padding") + // and compare against the locally-computed DigestInfo. + try { + javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("RSA/ECB/PKCS1Padding"); + c.init(javax.crypto.Cipher.DECRYPT_MODE, st.pub); + byte[] recovered = c.doFinal(sigBytes); + byte[] expected = buildDigestInfo(st.hash, data); + return (java.util.Arrays.equals(recovered, expected) ? scalarTrue : scalarFalse).getList(); + } catch (Exception e) { + return scalarFalse.getList(); + } } catch (Exception e) { // Per Crypt::OpenSSL::RSA semantics, bad signatures return false, // not die. Only programmer errors should croak. @@ -372,26 +590,128 @@ public static RuntimeList verify(RuntimeArray args, int ctx) { } } - // encrypt/decrypt not wired up yet — OAuth doesn't need them. + /** + * Build the PKCS#1 v1.5 DigestInfo DER for the given hash algorithm over {@code data}: + * {@code SEQUENCE { SEQUENCE { OID algorithm, NULL }, OCTET STRING digest }}. + */ + private static byte[] buildDigestInfo(Hash h, byte[] data) throws Exception { + String jdkName = switch (h) { + case SHA1 -> "SHA-1"; + case SHA224 -> "SHA-224"; + case SHA256 -> "SHA-256"; + case SHA384 -> "SHA-384"; + case SHA512 -> "SHA-512"; + case MD5 -> "MD5"; + case RIPEMD160 -> "RIPEMD160"; + case WHIRLPOOL -> "WHIRLPOOL"; + }; + java.security.MessageDigest md; + try { + md = java.security.MessageDigest.getInstance(jdkName); + } catch (java.security.NoSuchAlgorithmException nsa) { + md = java.security.MessageDigest.getInstance(jdkName, "BC"); + } + byte[] digest = md.digest(data); + + org.bouncycastle.asn1.ASN1ObjectIdentifier oid = switch (h) { + case SHA1 -> new org.bouncycastle.asn1.ASN1ObjectIdentifier("1.3.14.3.2.26"); + case SHA224 -> new org.bouncycastle.asn1.ASN1ObjectIdentifier("2.16.840.1.101.3.4.2.4"); + case SHA256 -> new org.bouncycastle.asn1.ASN1ObjectIdentifier("2.16.840.1.101.3.4.2.1"); + case SHA384 -> new org.bouncycastle.asn1.ASN1ObjectIdentifier("2.16.840.1.101.3.4.2.2"); + case SHA512 -> new org.bouncycastle.asn1.ASN1ObjectIdentifier("2.16.840.1.101.3.4.2.3"); + case MD5 -> new org.bouncycastle.asn1.ASN1ObjectIdentifier("1.2.840.113549.2.5"); + case RIPEMD160 -> new org.bouncycastle.asn1.ASN1ObjectIdentifier("1.3.36.3.2.1"); + case WHIRLPOOL -> new org.bouncycastle.asn1.ASN1ObjectIdentifier("1.0.10118.3.0.55"); + }; + org.bouncycastle.asn1.x509.AlgorithmIdentifier ai = + new org.bouncycastle.asn1.x509.AlgorithmIdentifier(oid, org.bouncycastle.asn1.DERNull.INSTANCE); + return new org.bouncycastle.asn1.x509.DigestInfo(ai, digest).getEncoded(ASN1Encoding.DER); + } + + // ---- encrypt / decrypt ---- + // + // Padding → Java Cipher transformation mapping: + // NONE → RSA/ECB/NoPadding (raw modular exponentiation) + // PKCS1 → RSA/ECB/PKCS1Padding (PKCS#1 v1.5 type 2 for enc, type 1 for sign; + // Java chooses based on cipher mode + key type) + // PKCS1_OAEP → RSA/ECB/OAEPWithSHA-1AndMGF1Padding (SHA-1 per Crypt::OpenSSL::RSA docs) + // PKCS1_PSS → signing-only; encryption methods croak + // SSLV23 → not supported by the JDK; encryption methods croak + // + // {encrypt, decrypt} use the public/private key respectively in the usual + // encryption direction. {private_encrypt, public_decrypt} are the legacy + // low-level "sign raw block" primitives OpenSSL exposes; Java's SunJCE + // RSA/PKCS1Padding Cipher selects the correct PKCS#1 block type (1 vs 2) + // based on the (mode, key type) combination, so we just plumb through. + + private static String cipherTransform(Padding p) { + return switch (p) { + case NONE -> "RSA/ECB/NoPadding"; + case PKCS1 -> "RSA/ECB/PKCS1Padding"; + case PKCS1_OAEP -> "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; + case PKCS1_PSS -> throw new IllegalStateException("PSS padding is for signing only"); + case SSLV23 -> throw new IllegalStateException("SSLv23 padding is not supported"); + }; + } + + private static byte[] rsaCipher(State st, int mode, java.security.Key key, byte[] data) throws Exception { + javax.crypto.Cipher c = javax.crypto.Cipher.getInstance(cipherTransform(st.padding)); + c.init(mode, key); + return c.doFinal(data); + } + + /** encrypt($data) — encrypt with the public key. */ public static RuntimeList encrypt(RuntimeArray args, int ctx) { - die(new RuntimeScalar("Crypt::OpenSSL::RSA::encrypt: not implemented in PerlOnJava"), - new RuntimeScalar("\n")); - return scalarFalse.getList(); + State st = getState(args.get(0)); + byte[] data = scalarToBytes(args.get(1)); + try { + return bytesToScalar(rsaCipher(st, javax.crypto.Cipher.ENCRYPT_MODE, st.pub, data)).getList(); + } catch (Exception e) { + die(new RuntimeScalar("encrypt failed: " + e.getMessage()), new RuntimeScalar("\n")); + return scalarFalse.getList(); + } } + + /** decrypt($ciphertext) — decrypt with the private key. */ public static RuntimeList decrypt(RuntimeArray args, int ctx) { - die(new RuntimeScalar("Crypt::OpenSSL::RSA::decrypt: not implemented in PerlOnJava"), - new RuntimeScalar("\n")); - return scalarFalse.getList(); + State st = getState(args.get(0)); + if (st.priv == null) { + die(new RuntimeScalar("decrypt requires a private key"), new RuntimeScalar("\n")); + } + byte[] data = scalarToBytes(args.get(1)); + try { + return bytesToScalar(rsaCipher(st, javax.crypto.Cipher.DECRYPT_MODE, st.priv, data)).getList(); + } catch (Exception e) { + die(new RuntimeScalar("decrypt failed: " + e.getMessage()), new RuntimeScalar("\n")); + return scalarFalse.getList(); + } } + + /** private_encrypt($data) — legacy "sign raw block" using the private key. */ public static RuntimeList private_encrypt(RuntimeArray args, int ctx) { - die(new RuntimeScalar("Crypt::OpenSSL::RSA::private_encrypt: not implemented in PerlOnJava"), - new RuntimeScalar("\n")); - return scalarFalse.getList(); + State st = getState(args.get(0)); + if (st.priv == null) { + die(new RuntimeScalar("private_encrypt requires a private key"), new RuntimeScalar("\n")); + } + byte[] data = scalarToBytes(args.get(1)); + try { + return bytesToScalar(rsaCipher(st, javax.crypto.Cipher.ENCRYPT_MODE, st.priv, data)).getList(); + } catch (Exception e) { + die(new RuntimeScalar("private_encrypt failed: " + e.getMessage()), new RuntimeScalar("\n")); + return scalarFalse.getList(); + } } + + /** public_decrypt($ciphertext) — legacy "verify raw block" using the public key. */ public static RuntimeList public_decrypt(RuntimeArray args, int ctx) { - die(new RuntimeScalar("Crypt::OpenSSL::RSA::public_decrypt: not implemented in PerlOnJava"), - new RuntimeScalar("\n")); - return scalarFalse.getList(); + State st = getState(args.get(0)); + byte[] data = scalarToBytes(args.get(1)); + try { + return bytesToScalar(rsaCipher(st, javax.crypto.Cipher.DECRYPT_MODE, st.pub, data)).getList(); + } catch (Exception e) { + die(new RuntimeScalar("public_decrypt failed: " + e.getMessage()), new RuntimeScalar("\n")); + return scalarFalse.getList(); + } } // ---- padding selectors ---- diff --git a/src/main/perl/lib/Crypt/OpenSSL/Bignum.pm b/src/main/perl/lib/Crypt/OpenSSL/Bignum.pm new file mode 100644 index 000000000..c91c0b4ec --- /dev/null +++ b/src/main/perl/lib/Crypt/OpenSSL/Bignum.pm @@ -0,0 +1,45 @@ +package Crypt::OpenSSL::Bignum; + +use strict; +use warnings; + +our $VERSION = '0.09'; + +use Exporter; +our @ISA = qw(Exporter); + +# Delegate to the Java-backed implementation in +# src/main/java/org/perlonjava/runtime/perlmodule/CryptOpenSSLBignum.java. +use XSLoader; +XSLoader::load('Crypt::OpenSSL::Bignum', $VERSION); + +# Crypt::OpenSSL::Bignum::CTX is a trivial wrapper around OpenSSL's BN_CTX +# (a reusable scratch space for BIGNUM operations). In PerlOnJava it has no +# state to track — BigInteger is immutable — so we expose a tiny stub that +# just satisfies callers that thread a CTX through their arithmetic calls. +package Crypt::OpenSSL::Bignum::CTX; + +sub new { return bless {}, shift } +sub DESTROY { } + +1; + +__END__ + +=head1 NAME + +Crypt::OpenSSL::Bignum - Arbitrary-precision integers, OpenSSL API flavour + +=head1 DESCRIPTION + +Minimal subset of the CPAN C API, backed by +C on PerlOnJava. Provides the constructors, conversions +and arithmetic primitives used by C and similar modules +for shuttling BIGNUM values across the XS boundary. + +C is provided as an empty stub for API +compatibility; it has no state because C is immutable +and does not need a reusable scratch context. + +=cut +