From eee9cd3aa77bb44db548ad3ff2359c949c0181a7 Mon Sep 17 00:00:00 2001 From: kares Date: Thu, 5 Mar 2026 17:51:08 +0100 Subject: [PATCH 01/21] [compat] improve ASN.1 tagging behavior now passing OpenSSL imported tests --- src/main/java/org/jruby/ext/openssl/ASN1.java | 62 ++++++++++++-- src/test/ruby/test_asn1.rb | 83 ++++++++----------- 2 files changed, 91 insertions(+), 54 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/ASN1.java b/src/main/java/org/jruby/ext/openssl/ASN1.java index 3e7417e4..adfa2d83 100644 --- a/src/main/java/org/jruby/ext/openssl/ASN1.java +++ b/src/main/java/org/jruby/ext/openssl/ASN1.java @@ -1841,6 +1841,17 @@ byte[] toDER(final ThreadContext context) throws IOException { return toDERInternal(context, false, false, string); } + // Special behavior: Encoding universal types with non-default 'tag' + // attribute and nil tagging method - replace the tag byte with the custom tag. + if ( !isTagged() && isUniversal(context) ) { + final IRubyObject defTag = defaultTag(); + if ( !defTag.isNil() && getTag(context) != RubyNumeric.fix2int(defTag) ) { + final byte[] encoded = toASN1Primitive(context).toASN1Primitive().getEncoded(ASN1Encoding.DER); + encoded[0] = (byte) getTag(context); + return encoded; + } + } + return toASN1(context).toASN1Primitive().getEncoded(ASN1Encoding.DER); } @@ -2033,11 +2044,18 @@ ASN1Encodable toASN1(final ThreadContext context) { if ( isInfiniteLength() ) return super.toASN1(context); if ( isSequence() ) { - return new DERSequence( toASN1EncodableVector(context) ); + final ASN1Encodable seq = new DERSequence( toASN1EncodableVector(context) ); + if ( isTagged() ) { + return new DERTaggedObject(isExplicitTagging(), getTagClass(context), getTag(context), seq); + } + return seq; } if ( isSet() ) { - return new DLSet( toASN1EncodableVector(context) ); // return new BERSet(values); - //return ASN1Set.getInstance(toASN1TaggedObject(context), isExplicitTagging()); + final ASN1Encodable set = new DLSet( toASN1EncodableVector(context) ); + if ( isTagged() ) { + return new DERTaggedObject(isExplicitTagging(), getTagClass(context), getTag(context), set); + } + return set; } switch ( getTag(context) ) { // "raw" Constructive ?!? case OCTET_STRING: @@ -2069,10 +2087,10 @@ byte[] toDER(final ThreadContext context) throws IOException { if ( isIndefiniteLength ) { if ( isSequence() || tagNo == SEQUENCE ) { - return sequenceToDER(context); + return applyIndefiniteTagging(context, sequenceToDER(context)); } if ( isSet() || tagNo == SET) { - return setToDER(context); + return applyIndefiniteTagging(context, setToDER(context)); } // "raw" Constructive switch ( getTag(context) ) { @@ -2094,6 +2112,18 @@ byte[] toDER(final ThreadContext context) throws IOException { return toDERInternal(context, true, isIndefiniteLength, valueAsArray(context)); } + // Special behavior: Encoding universal types with non-default 'tag' + // attribute and nil tagging method - replace the tag byte with the custom tag, + // preserving the CONSTRUCTED bit. + if ( !isTagged() && isUniversal(context) ) { + final IRubyObject defTag = defaultTag(); + if ( !defTag.isNil() && tagNo != RubyNumeric.fix2int(defTag) ) { + final byte[] encoded = toASN1(context).toASN1Primitive().getEncoded(ASN1Encoding.DER); + encoded[0] = (byte) (BERTags.CONSTRUCTED | tagNo); + return encoded; + } + } + return super.toDER(context); } @@ -2140,6 +2170,28 @@ private byte[] setToDER(final ThreadContext context) throws IOException { return new BERSet(values).toASN1Primitive().getEncoded(); } + // Applies EXPLICIT or IMPLICIT tagging to an already-encoded indefinite-length + // Sequence or Set byte array. For IMPLICIT, replaces the tag byte in-place. + // For EXPLICIT, wraps the inner bytes with an outer tag + indefinite-length header + // and appends the required outer EOC (0x00 0x00). + private byte[] applyIndefiniteTagging(final ThreadContext context, final byte[] innerBytes) throws IOException { + if ( !isTagged() ) return innerBytes; + final int tag = getTag(context); + final int tagClass = getTagClass(context); + if ( isImplicitTagging() ) { + innerBytes[0] = (byte) (tagClass | BERTags.CONSTRUCTED | tag); + return innerBytes; + } else { // EXPLICIT + final ByteArrayOutputStream out = new ByteArrayOutputStream(innerBytes.length + 4); + writeDERIdentifier(tag, tagClass | BERTags.CONSTRUCTED, out); + out.write(0x80); // indefinite length + out.write(innerBytes); + out.write(0x00); // outer EOC + out.write(0x00); + return out.toByteArray(); + } + } + private ASN1EncodableVector toASN1EncodableVector(final ThreadContext context) { final ASN1EncodableVector vec = new ASN1EncodableVector(); final IRubyObject value = value(context); diff --git a/src/test/ruby/test_asn1.rb b/src/test/ruby/test_asn1.rb index 0cc59e27..302c00ef 100644 --- a/src/test/ruby/test_asn1.rb +++ b/src/test/ruby/test_asn1.rb @@ -620,61 +620,46 @@ def test_prim_implicit_tagging encode_test B(%w{ 80 01 01 }), int int2 = OpenSSL::ASN1::Integer.new(1, 1, :IMPLICIT, :APPLICATION) encode_test B(%w{ 41 01 01 }), int2 - #decoded = OpenSSL::ASN1.decode(int2.to_der) - # <:APPLICATION> expected but was <:UNIVERSAL> - #assert_equal :APPLICATION, decoded.tag_class - # <1> expected but was <2> - #assert_equal 1, decoded.tag - # <"\x01"> expected but was <#> - #assert_equal B(%w{ 01 }), decoded.value - - # Special behavior: Encoding universal types with non-default 'tag' - # attribute and nil tagging method. - #int3 = OpenSSL::ASN1::Integer.new(1, 1) - # <"\x01\x01\x01"> expected but was <"\x02\x01\x01"> - #encode_test B(%w{ 01 01 01 }), int3 + decoded = OpenSSL::ASN1.decode(int2.to_der) + assert_equal :APPLICATION, decoded.tag_class + assert_equal 1, decoded.tag + assert_equal B(%w{ 01 }), decoded.value + + # Special behavior: Encoding universal types with non-default 'tag' attribute and nil tagging method. + int3 = OpenSSL::ASN1::Integer.new(1, 1) + encode_test B(%w{ 01 01 01 }), int3 end def test_cons_explicit_tagging - #content = [ OpenSSL::ASN1::PrintableString.new('abc') ] - #seq = OpenSSL::ASN1::Sequence.new(content, 2, :EXPLICIT) - # TODO: Import Issue - # RuntimeError: No message available - #encode_test B(%w{ A2 07 30 05 13 03 61 62 63 }), seq - #seq2 = OpenSSL::ASN1::Sequence.new(content, 3, :EXPLICIT, :APPLICATION) - # RuntimeError: No message available - #encode_test B(%w{ 63 07 30 05 13 03 61 62 63 }), seq2 - - #content3 = [ OpenSSL::ASN1::PrintableString.new('abc'), - # OpenSSL::ASN1::EndOfContent.new() ] - #seq3 = OpenSSL::ASN1::Sequence.new(content3, 2, :EXPLICIT) - #seq3.indefinite_length = true - # RuntimeError: No message available - #encode_test B(%w{ A2 80 30 80 13 03 61 62 63 00 00 00 00 }), seq3 + content = [ OpenSSL::ASN1::PrintableString.new('abc') ] + seq = OpenSSL::ASN1::Sequence.new(content, 2, :EXPLICIT) + encode_test B(%w{ A2 07 30 05 13 03 61 62 63 }), seq + seq2 = OpenSSL::ASN1::Sequence.new(content, 3, :EXPLICIT, :APPLICATION) + encode_test B(%w{ 63 07 30 05 13 03 61 62 63 }), seq2 + + content3 = [ OpenSSL::ASN1::PrintableString.new('abc'), + OpenSSL::ASN1::EndOfContent.new() ] + seq3 = OpenSSL::ASN1::Sequence.new(content3, 2, :EXPLICIT) + seq3.indefinite_length = true + encode_test B(%w{ A2 80 30 80 13 03 61 62 63 00 00 00 00 }), seq3 end def test_cons_implicit_tagging - #content = [ OpenSSL::ASN1::Null.new(nil) ] - #seq = OpenSSL::ASN1::Sequence.new(content, 1, :IMPLICIT) - # TODO: Import Issue - # <"\xA1\x02\x05\x00"> expected but was <"0\x02\x05\x00"> - #encode_test B(%w{ A1 02 05 00 }), seq - #seq2 = OpenSSL::ASN1::Sequence.new(content, 1, :IMPLICIT, :APPLICATION) - # <"a\x02\x05\x00"> expected but was <"0\x02\x05\x00"> - #encode_test B(%w{ 61 02 05 00 }), seq2 - - #content3 = [ OpenSSL::ASN1::Null.new(nil), - # OpenSSL::ASN1::EndOfContent.new() ] - #seq3 = OpenSSL::ASN1::Sequence.new(content3, 1, :IMPLICIT) - #seq3.indefinite_length = true - # <"\xA1\x80\x05\x00\x00\x00"> expected but was <"0\x80\x05\x00\x00\x00"> - #encode_test B(%w{ A1 80 05 00 00 00 }), seq3 - - # Special behavior: Encoding universal types with non-default 'tag' - # attribute and nil tagging method. - #seq4 = OpenSSL::ASN1::Sequence.new([], 1) - # <"!\x00"> expected but was <"0\x00"> - #encode_test B(%w{ 21 00 }), seq4 + content = [ OpenSSL::ASN1::Null.new(nil) ] + seq = OpenSSL::ASN1::Sequence.new(content, 1, :IMPLICIT) + encode_test B(%w{ A1 02 05 00 }), seq + seq2 = OpenSSL::ASN1::Sequence.new(content, 1, :IMPLICIT, :APPLICATION) + encode_test B(%w{ 61 02 05 00 }), seq2 + + content3 = [ OpenSSL::ASN1::Null.new(nil), + OpenSSL::ASN1::EndOfContent.new() ] + seq3 = OpenSSL::ASN1::Sequence.new(content3, 1, :IMPLICIT) + seq3.indefinite_length = true + encode_test B(%w{ A1 80 05 00 00 00 }), seq3 + + # Special behavior: Encoding universal types with non-default 'tag' attribute and nil tagging method. + seq4 = OpenSSL::ASN1::Sequence.new([], 1) + encode_test B(%w{ 21 00 }), seq4 end def test_octet_string_constructed_tagging From 5651d85adfea07e8f143966873f5ebd358b05e6d Mon Sep 17 00:00:00 2001 From: kares Date: Thu, 5 Mar 2026 19:11:09 +0100 Subject: [PATCH 02/21] [compat] implement RSA#private_to_der/pem and improve compatibility parsing encrypted key info --- src/main/java/org/jruby/ext/openssl/PKey.java | 25 ++++- .../java/org/jruby/ext/openssl/PKeyRSA.java | 91 +++++++++++++++ .../ext/openssl/x509store/PEMInputOutput.java | 78 +++++++++++-- src/test/ruby/fixtures/pkey/rsa-1.pem | 51 +++++++++ src/test/ruby/rsa/test_rsa.rb | 106 +++++++++++++++++- 5 files changed, 337 insertions(+), 14 deletions(-) create mode 100644 src/test/ruby/fixtures/pkey/rsa-1.pem diff --git a/src/main/java/org/jruby/ext/openssl/PKey.java b/src/main/java/org/jruby/ext/openssl/PKey.java index 51b99b48..65656168 100644 --- a/src/main/java/org/jruby/ext/openssl/PKey.java +++ b/src/main/java/org/jruby/ext/openssl/PKey.java @@ -62,6 +62,7 @@ import org.jruby.ext.openssl.x509store.PEMInputOutput; import static org.jruby.ext.openssl.OpenSSL.*; +import static org.jruby.ext.openssl.Cipher._Cipher; /** * @author Ola Bini @@ -126,13 +127,22 @@ public static IRubyObject read(final ThreadContext context, IRubyObject recv, IR final RubyString str = readInitArg(context, data); KeyPair keyPair; - // d2i_PrivateKey_bio + // d2i_PrivateKey_bio (PEM formats: RSA PRIVATE KEY, DSA PRIVATE KEY, PRIVATE KEY, ENCRYPTED PRIVATE KEY) try { keyPair = readPrivateKey(str, pass); } catch (IOException e) { debugStackTrace(runtime, "PKey readPrivateKey", e); /* ignore */ keyPair = null; } + // DER-encoded PKCS#8 PrivateKeyInfo or EncryptedPrivateKeyInfo + if (keyPair == null) { + try { + final byte[] derInput = str.getBytes(); + keyPair = PEMInputOutput.readPrivateKeyFromDER(derInput, pass); + } catch (IOException e) { + debugStackTrace(runtime, "PKey readPrivateKeyFromDER", e); /* ignore */ + } + } // PEM_read_bio_PrivateKey if (keyPair != null) { final String alg = getAlgorithm(keyPair); @@ -395,9 +405,16 @@ static void addSplittedAndFormatted(StringBuilder result, CharSequence v) { } protected static CipherSpec cipherSpec(final IRubyObject cipher) { - if ( cipher != null && ! cipher.isNil() ) { - final Cipher c = (Cipher) cipher; - return new CipherSpec(c.getCipherInstance(), c.getName(), c.getKeyLength() * 8); + Cipher obj = null; + if (cipher instanceof RubyString) { + final Ruby runtime = cipher.getRuntime(); + obj = new Cipher(runtime, _Cipher(runtime)); + obj.initializeImpl(runtime, cipher.asString().toString()); + } else if (cipher instanceof Cipher) { + obj = (Cipher) cipher; + } + if (obj != null) { + return new CipherSpec(obj.getCipherInstance(), obj.getName(), obj.getKeyLength() * 8); } return null; } diff --git a/src/main/java/org/jruby/ext/openssl/PKeyRSA.java b/src/main/java/org/jruby/ext/openssl/PKeyRSA.java index 7e0e28bb..5bf8fd15 100644 --- a/src/main/java/org/jruby/ext/openssl/PKeyRSA.java +++ b/src/main/java/org/jruby/ext/openssl/PKeyRSA.java @@ -51,7 +51,15 @@ import static javax.crypto.Cipher.*; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.operator.OutputEncryptor; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; +import org.bouncycastle.pkcs.jcajce.JcaPKCS8EncryptedPrivateKeyInfoBuilder; +import org.bouncycastle.pkcs.jcajce.JcePKCSPBEOutputEncryptorBuilder; import org.jruby.Ruby; import org.jruby.RubyClass; import org.jruby.RubyBignum; @@ -502,6 +510,89 @@ public RubyString public_to_pem(ThreadContext context) { } } + @JRubyMethod(rest = true) + public RubyString private_to_der(ThreadContext context, final IRubyObject[] args) { + Arity.checkArgumentCount(context.runtime, args, 0, 2); + if (privateKey == null) { + throw newRSAError(context.runtime, "private key is not available"); + } + CipherSpec spec = null; char[] passwd = null; + if (args.length > 0) { + spec = cipherSpec(args[0]); + if (args.length > 1) passwd = password(context, args[1], null); + } + try { + if (spec != null && passwd != null) { + final ASN1ObjectIdentifier cipherOid = osslNameToCipherOid(spec.getOsslName()); + final OutputEncryptor encryptor = new JcePKCSPBEOutputEncryptorBuilder(cipherOid) + .setProvider(SecurityHelper.getSecurityProvider()).build(passwd); + final PKCS8EncryptedPrivateKeyInfo enc = new JcaPKCS8EncryptedPrivateKeyInfoBuilder(privateKey).build(encryptor); + return StringHelper.newString(context.runtime, enc.getEncoded()); + } + return StringHelper.newString(context.runtime, privateKey.getEncoded()); + } + catch (NoClassDefFoundError e) { + throw newRSAError(context.runtime, bcExceptionMessage(e)); + } + catch (OperatorCreationException | IOException e) { + throw newRSAError(context.runtime, e.getMessage(), e); + } + } + + @JRubyMethod(rest = true) + public RubyString private_to_pem(ThreadContext context, final IRubyObject[] args) { + Arity.checkArgumentCount(context.runtime, args, 0, 2); + if (privateKey == null) { + throw newRSAError(context.runtime, "private key is not available"); + } + CipherSpec spec = null; char[] passwd = null; + if (args.length > 0) { + spec = cipherSpec(args[0]); + if (args.length > 1) passwd = password(context, args[1], null); + } + try { + final StringWriter writer = new StringWriter(); + if (spec != null && passwd != null) { + final ASN1ObjectIdentifier cipherOid = osslNameToCipherOid(spec.getOsslName()); + final OutputEncryptor encryptor = new JcePKCSPBEOutputEncryptorBuilder(cipherOid) + .setProvider(SecurityHelper.getSecurityProvider()).build(passwd); + final PKCS8EncryptedPrivateKeyInfo enc = new JcaPKCS8EncryptedPrivateKeyInfoBuilder(privateKey).build(encryptor); + PEMInputOutput.writeEncryptedPKCS8PrivateKey(writer, enc.getEncoded()); + } else { + PEMInputOutput.writePKCS8PrivateKey(writer, privateKey.getEncoded()); + } + return RubyString.newString(context.runtime, writer.getBuffer()); + } + catch (NoClassDefFoundError e) { + throw newRSAError(context.runtime, bcExceptionMessage(e)); + } + catch (OperatorCreationException | IOException e) { + throw newRSAError(context.runtime, e.getMessage(), e); + } + } + + private static ASN1ObjectIdentifier osslNameToCipherOid(final String osslName) { + switch (osslName.toUpperCase()) { + case "AES-128-CBC": return NISTObjectIdentifiers.id_aes128_CBC; + case "AES-192-CBC": return NISTObjectIdentifiers.id_aes192_CBC; + case "AES-256-CBC": return NISTObjectIdentifiers.id_aes256_CBC; + case "AES-128-ECB": return NISTObjectIdentifiers.id_aes128_ECB; + case "AES-192-ECB": return NISTObjectIdentifiers.id_aes192_ECB; + case "AES-256-ECB": return NISTObjectIdentifiers.id_aes256_ECB; + case "AES-128-OFB": return NISTObjectIdentifiers.id_aes128_OFB; + case "AES-192-OFB": return NISTObjectIdentifiers.id_aes192_OFB; + case "AES-256-OFB": return NISTObjectIdentifiers.id_aes256_OFB; + case "AES-128-CFB": return NISTObjectIdentifiers.id_aes128_CFB; + case "AES-192-CFB": return NISTObjectIdentifiers.id_aes192_CFB; + case "AES-256-CFB": return NISTObjectIdentifiers.id_aes256_CFB; + case "DES-EDE3-CBC": + case "DES-EDE-CBC": + case "DES3": return PKCSObjectIdentifiers.des_EDE3_CBC; + default: + throw new IllegalArgumentException("Unsupported cipher for PKCS8 encryption: " + osslName); + } + } + private String getPadding(final int padding) { if ( padding < 1 || padding > 4 ) { throw newRSAError(getRuntime(), ""); diff --git a/src/main/java/org/jruby/ext/openssl/x509store/PEMInputOutput.java b/src/main/java/org/jruby/ext/openssl/x509store/PEMInputOutput.java index b515a9d8..9b8aec22 100644 --- a/src/main/java/org/jruby/ext/openssl/x509store/PEMInputOutput.java +++ b/src/main/java/org/jruby/ext/openssl/x509store/PEMInputOutput.java @@ -112,6 +112,8 @@ import org.bouncycastle.crypto.CipherParameters; import org.bouncycastle.crypto.InvalidCipherTextException; import org.bouncycastle.crypto.PBEParametersGenerator; +import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; +import org.bouncycastle.crypto.engines.AESEngine; import org.bouncycastle.crypto.engines.DESedeEngine; import org.bouncycastle.crypto.engines.RC2Engine; import org.bouncycastle.crypto.generators.OpenSSLPBEParametersGenerator; @@ -360,14 +362,15 @@ else if ( line.indexOf(BEG_STRING_PKCS8) != -1 ) { try { byte[] bytes = readBase64Bytes(reader, BEF_E + PEM_STRING_PKCS8); EncryptedPrivateKeyInfo eIn = EncryptedPrivateKeyInfo.getInstance(bytes); - AlgorithmIdentifier algId = eIn.getEncryptionAlgorithm(); - PrivateKey privKey; - if (algId.getAlgorithm().toString().equals("1.2.840.113549.1.5.13")) { // PBES2 - privKey = derivePrivateKeyPBES2(eIn, algId, passwd); - } else { - privKey = derivePrivateKeyPBES1(eIn, algId, passwd); - } - return new KeyPair(null, privKey); + org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo encInfo = + new org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo(eIn); + org.bouncycastle.operator.InputDecryptorProvider decryptor = + new org.bouncycastle.pkcs.jcajce.JcePKCSPBEInputDecryptorProviderBuilder() + .setProvider(SecurityHelper.getSecurityProvider()) + .build(passwd); + final PrivateKeyInfo keyInfo = encInfo.decryptPrivateKeyInfo(decryptor); + final Type type = getPrivateKeyType(keyInfo.getPrivateKeyAlgorithm()); + return org.jruby.ext.openssl.impl.PKey.readPrivateKey(type, keyInfo); } catch (Exception e) { throw mapReadException("problem creating private key: ", e); @@ -377,6 +380,43 @@ else if ( line.indexOf(BEG_STRING_PKCS8) != -1 ) { return null; } + /** + * Attempt to read a private key from DER-encoded PKCS#8 bytes (PrivateKeyInfo or + * EncryptedPrivateKeyInfo). Returns null if the input cannot be parsed as either format. + */ + public static KeyPair readPrivateKeyFromDER(final byte[] input, final char[] passwd) throws IOException { + // Try as unencrypted PKCS#8 PrivateKeyInfo + try { + final PrivateKeyInfo keyInfo = PrivateKeyInfo.getInstance(ASN1Primitive.fromByteArray(input)); + if (keyInfo != null) { + final Type type = getPrivateKeyType(keyInfo.getPrivateKeyAlgorithm()); + return org.jruby.ext.openssl.impl.PKey.readPrivateKey(type, keyInfo); + } + } catch (Exception e) { + // Not a PrivateKeyInfo - try as EncryptedPrivateKeyInfo + } + // Try as encrypted PKCS#8 EncryptedPrivateKeyInfo + if (passwd != null) { + try { + EncryptedPrivateKeyInfo eIn = EncryptedPrivateKeyInfo.getInstance(ASN1Primitive.fromByteArray(input)); + if (eIn != null) { + org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo encInfo = + new org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo(eIn); + org.bouncycastle.operator.InputDecryptorProvider decryptor = + new org.bouncycastle.pkcs.jcajce.JcePKCSPBEInputDecryptorProviderBuilder() + .setProvider(SecurityHelper.getSecurityProvider()) + .build(passwd); + final PrivateKeyInfo keyInfo = encInfo.decryptPrivateKeyInfo(decryptor); + final Type type = getPrivateKeyType(keyInfo.getPrivateKeyAlgorithm()); + return org.jruby.ext.openssl.impl.PKey.readPrivateKey(type, keyInfo); + } + } catch (Exception e) { + throw new IOException("Could not decrypt PKCS#8 key: " + e.getMessage(), e); + } + } + return null; + } + private static IOException mapReadException(final String message, final Exception ex) { if ( ex instanceof PasswordRequiredException ) { return (PasswordRequiredException) ex; @@ -416,12 +456,20 @@ private static PrivateKey derivePrivateKeyPBES2(EncryptedPrivateKeyInfo eIn, Alg EncryptionScheme scheme = pbeParams.getEncryptionScheme(); BufferedBlockCipher cipher; - if ( scheme.getAlgorithm().equals( PKCSObjectIdentifiers.RC2_CBC ) ) { + ASN1ObjectIdentifier encOid = scheme.getAlgorithm(); + if ( encOid.equals( PKCSObjectIdentifiers.RC2_CBC ) ) { RC2CBCParameter rc2Params = RC2CBCParameter.getInstance(scheme); byte[] iv = rc2Params.getIV(); CipherParameters param = new ParametersWithIV(cipherParams, iv); cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new RC2Engine())); cipher.init(false, param); + } else if ( encOid.equals( NISTObjectIdentifiers.id_aes128_CBC ) || + encOid.equals( NISTObjectIdentifiers.id_aes192_CBC ) || + encOid.equals( NISTObjectIdentifiers.id_aes256_CBC ) ) { + byte[] iv = ASN1OctetString.getInstance( scheme.getParameters() ).getOctets(); + CipherParameters param = new ParametersWithIV(cipherParams, iv); + cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine())); + cipher.init(false, param); } else { byte[] iv = ASN1OctetString.getInstance( scheme.getParameters() ).getOctets(); CipherParameters param = new ParametersWithIV(cipherParams, iv); @@ -1053,6 +1101,18 @@ public static void writeECPrivateKey(Writer _out, ECPrivateKey obj, CipherSpec c } } + /** Writes a PKCS#8 unencrypted private key in PEM format ("PRIVATE KEY"). */ + public static void writePKCS8PrivateKey(Writer _out, byte[] pkcs8Bytes) throws IOException { + BufferedWriter out = makeBuffered(_out); + writePemPlain(out, PEM_STRING_PKCS8INF, pkcs8Bytes); + } + + /** Writes a PKCS#8 encrypted private key in PEM format ("ENCRYPTED PRIVATE KEY"). */ + public static void writeEncryptedPKCS8PrivateKey(Writer _out, byte[] encryptedPKCS8Bytes) throws IOException { + BufferedWriter out = makeBuffered(_out); + writePemPlain(out, PEM_STRING_PKCS8, encryptedPKCS8Bytes); + } + public static void writeECParameters(Writer _out, ASN1ObjectIdentifier obj, CipherSpec cipher, char[] passwd) throws IOException { assert (obj != null); final String PEM_STRING_EC = "EC PARAMETERS"; diff --git a/src/test/ruby/fixtures/pkey/rsa-1.pem b/src/test/ruby/fixtures/pkey/rsa-1.pem new file mode 100644 index 00000000..bd5a624f --- /dev/null +++ b/src/test/ruby/fixtures/pkey/rsa-1.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/src/test/ruby/rsa/test_rsa.rb b/src/test/ruby/rsa/test_rsa.rb index d9b24d1c..fbe572fc 100644 --- a/src/test/ruby/rsa/test_rsa.rb +++ b/src/test/ruby/rsa/test_rsa.rb @@ -339,6 +339,76 @@ def test_RSAPublicKey assert_same_rsa rsa1024pub, key end + def test_private_encoding + pkey = Fixtures.pkey("rsa-1.pem") + asn1 = OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::Integer(0), + OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::ObjectId("rsaEncryption"), + OpenSSL::ASN1::Null(nil) + ]), + OpenSSL::ASN1::OctetString(pkey.to_der) + ]) + assert_equal asn1.to_der, pkey.private_to_der + assert_same_rsa pkey, OpenSSL::PKey.read(asn1.to_der) + + pem = der_to_pem(asn1.to_der, "PRIVATE KEY") + assert_equal pem, pkey.private_to_pem + assert_same_rsa pkey, OpenSSL::PKey.read(pem) + end + + def test_private_encoding_encrypted + rsa = Fixtures.pkey("rsa2048") + encoded = rsa.private_to_der("aes-128-cbc", "abcdef") + asn1 = OpenSSL::ASN1.decode(encoded) # PKCS #8 EncryptedPrivateKeyInfo + assert_kind_of OpenSSL::ASN1::Sequence, asn1 + assert_equal 2, asn1.value.size + assert_not_equal rsa.private_to_der, encoded + assert_same_rsa rsa, OpenSSL::PKey.read(encoded, "abcdef") + #assert_same_rsa rsa, OpenSSL::PKey.read(encoded) { "abcdef" } + #assert_raise(OpenSSL::PKey::PKeyError) { OpenSSL::PKey.read(encoded, "abcxyz") } + + encoded = rsa.private_to_pem("aes-128-cbc", "abcdef") + assert_match (/BEGIN ENCRYPTED PRIVATE KEY/), encoded.lines[0] + assert_same_rsa rsa, OpenSSL::PKey.read(encoded, "abcdef") + + # Use openssl instead of certtool due to https://gitlab.com/gnutls/gnutls/-/issues/1632 + # openssl pkcs8 -in test/openssl/fixtures/pkey/rsa2048.pem -topk8 -v2 aes-128-cbc -passout pass:abcdef + pem = <<~EOF + -----BEGIN ENCRYPTED PRIVATE KEY----- + MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIay5V8CDQi5oCAggA + MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAECBBB6eyagcbsvdQlM1kPcH7kiBIIE + 0Ng1apIyoPAZ4BfC4kMNeSmeAv3XspxqYi3uWzXiNyTcoE6390swrwM6WvdpXvLI + /n/V06krxPZ9X4fBG2kLUzXt5f09lEvmQU1HW1wJGU5Sq3bNeXBrlJF4DzJE4WWd + whVVvNMm44ghdzN/jGSw3z+6d717N+waa7vrpBDsHjhsPNwxpyzUvcFPFysTazxx + kN/dziIBF6SRKi6w8VaJEMQ8czGu5T3jOc2e/1p3/AYhHLPS4NHhLR5OUh0TKqLK + tANAqI9YqCAjhqcYCmN3mMQXY52VfOqG9hlX1x9ZQyqiH7l102EWbPqouk6bCBLQ + wHepPg4uK99Wsdh65qEryNnXQ5ZmO6aGb6T3TFENCaNKmi8Nh+/5dr7J7YfhIwpo + FqHvk0hrZ8r3EQlr8/td0Yb1/IKzeQ34638uXf9UxK7C6o+ilsmJDR4PHJUfZL23 + Yb9qWJ0GEzd5AMsI7x6KuUxSuH9nKniv5Tzyty3Xmb4FwXUyADWE19cVuaT+HrFz + GraKnA3UXbEgWAU48/l4K2HcAHyHDD2Kbp8k+o1zUkH0fWUdfE6OUGtx19Fv44Jh + B7xDngK8K48C6nrj06/DSYfXlb2X7WQiapeG4jt6U57tLH2XAjHCkvu0IBZ+//+P + yIWduEHQ3w8FBRcIsTNJo5CjkGk580TVQB/OBLWfX48Ay3oF9zgnomDIlVjl9D0n + lKxw/KMCLkvB78rUeGbr1Kwj36FhGpTBw3FgcYGa5oWFZTlcOgMTXLqlbb9JnDlA + Zs7Tu0WTyOTV/Dne9nEm39Dzu6wRojiIpmygTD4FI7rmOy3CYNvL3XPv7XQj0hny + Ee/fLxugYlQnwPZSqOVEQY2HsG7AmEHRsvy4bIWIGt+yzAPZixt9MUdJh91ttRt7 + QA/8J1pAsGqEuQpF6UUINZop3J7twfhO4zWYN/NNQ52eWNX2KLfjfGRhrvatzmZ0 + BuCsCI9hwEeE6PTlhbX1Rs177MrDc3vlqz2V3Po0OrFjXAyg9DR/OC4iK5wOG2ZD + 7StVSP8bzwQXsz3fJ0ardKXgnU2YDAP6Vykjgt+nFI09HV/S2faOc2g/UK4Y2khl + J93u/GHMz/Kr3bKWGY1/6nPdIdFheQjsiNhd5gI4tWik2B3QwU9mETToZ2LSvDHU + jYCys576xJLkdMM6nJdq72z4tCoES9IxyHVs4uLjHKIo/ZtKr+8xDo8IL4ax3U8+ + NMhs/lwReHmPGahm1fu9zLRbNCVL7e0zrOqbjvKcSEftObpV/LLcPYXtEm+lZcck + /PMw49HSE364anKEXCH1cyVWJwdZRpFUHvRpLIrpHru7/cthhiEMdLgK1/x8sLob + DiyieLxH1DPeXT4X+z94ER4IuPVOcV5AXc/omghispEX6DNUnn5jC4e3WyabjUbw + MuO9lVH9Wi2/ynExCqVmQkdbTXuLwjni1fJ27Q5zb0aCmhO8eq6P869NCjhJuiUj + NI9XtGLP50YVWE0kL8KEJqnyFudky8Khzk4/dyixQFqin5GfT4vetrLunGHy7lRB + 3LpnFrpMOr+0xr1RW1k9vlmjRsJSiojJfReYO7gH3B5swiww2azogoL+4jhF1Jxh + OYLWdkKhP2jSVGqtIDtny0O4lBm2+hLpWjiI0mJQ7wdA + -----END ENCRYPTED PRIVATE KEY----- + EOF + assert_same_rsa rsa, OpenSSL::PKey.read(pem, "abcdef") + end + def test_export rsa1024 = Fixtures.pkey("rsa1024") @@ -416,6 +486,41 @@ def check_component(base, test, keys) keys.each { |comp| assert_equal base.send(comp), test.send(comp) } end + # def assert_sign_verify_false_or_error + # ret = yield + # rescue => e + # assert_kind_of(OpenSSL::PKey::PKeyError, e) + # else + # assert_equal(false, ret) + # end + + def der_to_pem(der, pem_header) + # RFC 7468 + <<~EOS + -----BEGIN #{pem_header}----- + #{[der].pack("m0").scan(/.{1,64}/).join("\n")} + -----END #{pem_header}----- + EOS + end + + def der_to_encrypted_pem(der, pem_header, password) + # OpenSSL encryption, non-standard + iv = 16.times.to_a.pack("C*") + encrypted = OpenSSL::Cipher.new("aes-128-cbc").encrypt.then { |cipher| + cipher.key = OpenSSL::Digest.digest("MD5", password + iv[0, 8]) + cipher.iv = iv + cipher.update(der) << cipher.final + } + <<~EOS + -----BEGIN #{pem_header}----- + Proc-Type: 4,ENCRYPTED + DEK-Info: AES-128-CBC,#{iv.unpack1("H*").upcase} + + #{[encrypted].pack("m0").scan(/.{1,64}/).join("\n")} + -----END #{pem_header}----- + EOS + end + def dup_public(key) case key when OpenSSL::PKey::RSA @@ -426,5 +531,4 @@ def dup_public(key) raise "unknown key type: #{key.class}" end end - end From adc56658b27040d4b1182bf053debb77375a6d21 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Mar 2026 07:44:43 +0100 Subject: [PATCH 03/21] [fix] PKey.read to parse subject PKI --- .../java/org/jruby/ext/openssl/impl/PKey.java | 17 +++++++++++++++-- src/test/ruby/rsa/test_rsa.rb | 5 ++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/impl/PKey.java b/src/main/java/org/jruby/ext/openssl/impl/PKey.java index c50726ae..673fa908 100644 --- a/src/main/java/org/jruby/ext/openssl/impl/PKey.java +++ b/src/main/java/org/jruby/ext/openssl/impl/PKey.java @@ -139,11 +139,24 @@ public static KeyPair readPrivateKey(final Type type, final PrivateKeyInfo keyIn // d2i_PUBKEY_bio public static PublicKey readPublicKey(final byte[] input) throws IOException { + // Try PEM first try (Reader in = new InputStreamReader(new ByteArrayInputStream(input))) { Object pemObject = new PEMParser(in).readObject(); - SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(pemObject); - return new JcaPEMKeyConverter().getPublicKey(publicKeyInfo); + if (pemObject != null) { + SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(pemObject); + return new JcaPEMKeyConverter().getPublicKey(publicKeyInfo); + } } + // Fall back to DER-encoded SubjectPublicKeyInfo + try { + SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Primitive.fromByteArray(input)); + if (publicKeyInfo != null) { + return new JcaPEMKeyConverter().getPublicKey(publicKeyInfo); + } + } catch (Exception e) { + throw new IOException("Could not parse public key: " + e.getMessage(), e); + } + return null; } // d2i_RSAPrivateKey_bio diff --git a/src/test/ruby/rsa/test_rsa.rb b/src/test/ruby/rsa/test_rsa.rb index fbe572fc..1a4ee95f 100644 --- a/src/test/ruby/rsa/test_rsa.rb +++ b/src/test/ruby/rsa/test_rsa.rb @@ -412,8 +412,7 @@ def test_private_encoding_encrypted def test_export rsa1024 = Fixtures.pkey("rsa1024") - #pub = OpenSSL::PKey.read(rsa1024.public_to_der) # TODO not supported - pub = OpenSSL::PKey::RSA.new(rsa1024.public_to_der) + pub = OpenSSL::PKey.read(rsa1024.public_to_der) assert_not_equal rsa1024.export, pub.export assert_equal rsa1024.public_to_pem, pub.export @@ -438,7 +437,7 @@ def test_export def test_to_der rsa1024 = Fixtures.pkey("rsa1024") - pub = OpenSSL::PKey::RSA.new(rsa1024.public_to_der) + pub = OpenSSL::PKey.read(rsa1024.public_to_der) assert_not_equal rsa1024.to_der, pub.to_der assert_equal rsa1024.public_to_der, pub.to_der From 411aefb8ab3063244d543b51d7a686a44ee663e6 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Mar 2026 07:58:15 +0100 Subject: [PATCH 04/21] [compat] adjust error raised from PKey::RSA --- .../java/org/jruby/ext/openssl/PKeyRSA.java | 10 +++--- src/test/ruby/rsa/test_rsa.rb | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/PKeyRSA.java b/src/main/java/org/jruby/ext/openssl/PKeyRSA.java index 5bf8fd15..3c037b16 100644 --- a/src/main/java/org/jruby/ext/openssl/PKeyRSA.java +++ b/src/main/java/org/jruby/ext/openssl/PKeyRSA.java @@ -595,7 +595,7 @@ private static ASN1ObjectIdentifier osslNameToCipherOid(final String osslName) { private String getPadding(final int padding) { if ( padding < 1 || padding > 4 ) { - throw newRSAError(getRuntime(), ""); + throw newPKeyError(getRuntime(), ""); } // BC accepts "/NONE/*" but SunJCE doesn't. use "/ECB/*" String p = "/ECB/PKCS1Padding"; @@ -615,7 +615,7 @@ public IRubyObject private_encrypt(final ThreadContext context, final IRubyObjec if ( Arity.checkArgumentCount(context.runtime, args, 1, 2) == 2 && ! args[1].isNil() ) { padding = RubyNumeric.fix2int(args[1]); } - if ( privateKey == null ) throw newRSAError(context.runtime, "incomplete RSA"); + if ( privateKey == null ) throw newPKeyError(context.runtime, "incomplete RSA"); return doCipherRSA(context.runtime, args[0], padding, ENCRYPT_MODE, privateKey); } @@ -625,7 +625,7 @@ public IRubyObject private_decrypt(final ThreadContext context, final IRubyObjec if ( Arity.checkArgumentCount(context.runtime, args, 1, 2) == 2 && ! args[1].isNil()) { padding = RubyNumeric.fix2int(args[1]); } - if ( privateKey == null ) throw newRSAError(context.runtime, "incomplete RSA"); + if ( privateKey == null ) throw newPKeyError(context.runtime, "incomplete RSA"); return doCipherRSA(context.runtime, args[0], padding, DECRYPT_MODE, privateKey); } @@ -635,7 +635,7 @@ public IRubyObject public_encrypt(final ThreadContext context, final IRubyObject if ( Arity.checkArgumentCount(context.runtime, args, 1, 2) == 2 && ! args[1].isNil()) { padding = RubyNumeric.fix2int(args[1]); } - if ( publicKey == null ) throw newRSAError(context.runtime, "incomplete RSA"); + if ( publicKey == null ) throw newPKeyError(context.runtime, "incomplete RSA"); return doCipherRSA(context.runtime, args[0], padding, ENCRYPT_MODE, publicKey); } @@ -645,7 +645,7 @@ public IRubyObject public_decrypt(final ThreadContext context, final IRubyObject if ( Arity.checkArgumentCount(context.runtime, args, 1, 2) == 2 && ! args[1].isNil() ) { padding = RubyNumeric.fix2int(args[1]); } - if ( publicKey == null ) throw newRSAError(context.runtime, "incomplete RSA"); + if ( publicKey == null ) throw newPKeyError(context.runtime, "incomplete RSA"); return doCipherRSA(context.runtime, args[0], padding, DECRYPT_MODE, publicKey); } diff --git a/src/test/ruby/rsa/test_rsa.rb b/src/test/ruby/rsa/test_rsa.rb index 1a4ee95f..0745a64b 100644 --- a/src/test/ruby/rsa/test_rsa.rb +++ b/src/test/ruby/rsa/test_rsa.rb @@ -9,6 +9,15 @@ def setup require 'base64' end + def test_no_private_exp + key = OpenSSL::PKey::RSA.new + rsa = Fixtures.pkey("rsa-1.pem") + key.set_key(rsa.n, rsa.e, nil) + key.set_factors(rsa.p, rsa.q) + assert_raise(OpenSSL::PKey::PKeyError){ key.private_encrypt("foo") } + assert_raise(OpenSSL::PKey::PKeyError){ key.private_decrypt("foo") } + end + def test_private # Generated by key size and public exponent key = OpenSSL::PKey::RSA.new(512, 3) @@ -128,6 +137,33 @@ def test_rsa_public_encrypt # } end + def test_sign_verify_raw_legacy + key = Fixtures.pkey("rsa-1.pem") + bits = key.n.num_bits + + # Need right size for raw mode + plain0 = "x" * (bits/8) + cipher = key.private_encrypt(plain0, OpenSSL::PKey::RSA::NO_PADDING) + plain1 = key.public_decrypt(cipher, OpenSSL::PKey::RSA::NO_PADDING) + assert_equal(plain0, plain1) + + # Need smaller size for pkcs1 mode + plain0 = "x" * (bits/8 - 11) + cipher1 = key.private_encrypt(plain0, OpenSSL::PKey::RSA::PKCS1_PADDING) + plain1 = key.public_decrypt(cipher1, OpenSSL::PKey::RSA::PKCS1_PADDING) + assert_equal(plain0, plain1) + + cipherdef = key.private_encrypt(plain0) # PKCS1_PADDING is default + plain1 = key.public_decrypt(cipherdef) + assert_equal(plain0, plain1) + assert_equal(cipher1, cipherdef) + + # Failure cases + assert_raise(ArgumentError){ key.private_encrypt() } + assert_raise(ArgumentError){ key.private_encrypt("hi", 1, nil) } + assert_raise(OpenSSL::PKey::PKeyError){ key.private_encrypt(plain0, 666) } + end + def test_rsa_param_accessors key_file = File.join(File.dirname(__FILE__), 'private_key.pem') key = OpenSSL::PKey::RSA.new(File.read(key_file)) From dacede134576d1ef15fe981175f8d2f4ab16a9ce Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Mar 2026 08:30:03 +0100 Subject: [PATCH 05/21] [compat] improve DSA key parsing and errors --- .../java/org/jruby/ext/openssl/PKeyDSA.java | 6 +++ .../java/org/jruby/ext/openssl/impl/PKey.java | 38 +++++++++++++------ src/test/ruby/dsa/test_dsa.rb | 20 ++++++++++ 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/PKeyDSA.java b/src/main/java/org/jruby/ext/openssl/PKeyDSA.java index 83c6d1fd..e330fb36 100644 --- a/src/main/java/org/jruby/ext/openssl/PKeyDSA.java +++ b/src/main/java/org/jruby/ext/openssl/PKeyDSA.java @@ -302,6 +302,9 @@ public RubyBoolean private_p() { @JRubyMethod(name = "public_to_der") public RubyString public_to_der(ThreadContext context) { + if (publicKey == null) { + throw newPKeyError(context.runtime, "incompletely initialized DSA key"); + } final byte[] bytes; try { bytes = toDerDSAPublicKey(publicKey); @@ -325,6 +328,9 @@ public RubyString to_der() { catch (NoClassDefFoundError e) { throw newDSAError(getRuntime(), bcExceptionMessage(e)); } + catch (IllegalArgumentException e) { + throw newPKeyError(getRuntime(), e.getMessage()); + } catch (IOException e) { throw newDSAError(getRuntime(), e.getMessage(), e); } diff --git a/src/main/java/org/jruby/ext/openssl/impl/PKey.java b/src/main/java/org/jruby/ext/openssl/impl/PKey.java index 673fa908..fd926215 100644 --- a/src/main/java/org/jruby/ext/openssl/impl/PKey.java +++ b/src/main/java/org/jruby/ext/openssl/impl/PKey.java @@ -119,14 +119,28 @@ public static KeyPair readPrivateKey(final Type type, final PrivateKeyInfo keyIn exp1.getValue(), exp2.getValue(), crtCoef.getValue()); break; case DSA: - seq = (ASN1Sequence) keyInfo.parsePrivateKey(); - ASN1Integer p = (ASN1Integer) seq.getObjectAt(1); - ASN1Integer q = (ASN1Integer) seq.getObjectAt(2); - ASN1Integer g = (ASN1Integer) seq.getObjectAt(3); - ASN1Integer y = (ASN1Integer) seq.getObjectAt(4); - ASN1Integer x = (ASN1Integer) seq.getObjectAt(5); - privSpec = new DSAPrivateKeySpec(x.getValue(), p.getValue(), q.getValue(), g.getValue()); - pubSpec = new DSAPublicKeySpec(y.getValue(), p.getValue(), q.getValue(), g.getValue()); + final ASN1Encodable parsedDSAKey = keyInfo.parsePrivateKey(); + if (parsedDSAKey instanceof ASN1Integer) { + // PKCS#8 format: private key is just x (INTEGER), params in AlgorithmIdentifier + final BigInteger xVal = ((ASN1Integer) parsedDSAKey).getValue(); + final DSAParameter dsaParam = DSAParameter.getInstance(keyInfo.getPrivateKeyAlgorithm().getParameters()); + final BigInteger pVal = dsaParam.getP(); + final BigInteger qVal = dsaParam.getQ(); + final BigInteger gVal = dsaParam.getG(); + final BigInteger yVal = gVal.modPow(xVal, pVal); + privSpec = new DSAPrivateKeySpec(xVal, pVal, qVal, gVal); + pubSpec = new DSAPublicKeySpec(yVal, pVal, qVal, gVal); + } else { + // Traditional "DSA PRIVATE KEY" format: SEQUENCE { version, p, q, g, y, x } + seq = (ASN1Sequence) parsedDSAKey; + ASN1Integer p = (ASN1Integer) seq.getObjectAt(1); + ASN1Integer q = (ASN1Integer) seq.getObjectAt(2); + ASN1Integer g = (ASN1Integer) seq.getObjectAt(3); + ASN1Integer y = (ASN1Integer) seq.getObjectAt(4); + ASN1Integer x = (ASN1Integer) seq.getObjectAt(5); + privSpec = new DSAPrivateKeySpec(x.getValue(), p.getValue(), q.getValue(), g.getValue()); + pubSpec = new DSAPublicKeySpec(y.getValue(), p.getValue(), q.getValue(), g.getValue()); + } break; case EC: return readECPrivateKey(SecurityHelper.getKeyFactory("EC"), keyInfo); @@ -142,9 +156,8 @@ public static PublicKey readPublicKey(final byte[] input) throws IOException { // Try PEM first try (Reader in = new InputStreamReader(new ByteArrayInputStream(input))) { Object pemObject = new PEMParser(in).readObject(); - if (pemObject != null) { - SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(pemObject); - return new JcaPEMKeyConverter().getPublicKey(publicKeyInfo); + if (pemObject instanceof SubjectPublicKeyInfo) { + return new JcaPEMKeyConverter().getPublicKey((SubjectPublicKeyInfo) pemObject); } } // Fall back to DER-encoded SubjectPublicKeyInfo @@ -335,7 +348,8 @@ public static ASN1Sequence toASN1Primitive(final RSAPublicKey publicKey) { return new DERSequence(vec); } - public static byte[] toDerDSAKey(DSAPublicKey pubKey, DSAPrivateKey privKey) throws IOException { + public static byte[] toDerDSAKey(DSAPublicKey pubKey, DSAPrivateKey privKey) + throws IOException, IllegalArgumentException { if ( pubKey != null && privKey == null ) { return toDerDSAPublicKey(pubKey); } diff --git a/src/test/ruby/dsa/test_dsa.rb b/src/test/ruby/dsa/test_dsa.rb index 3235777d..65d8037d 100644 --- a/src/test/ruby/dsa/test_dsa.rb +++ b/src/test/ruby/dsa/test_dsa.rb @@ -8,6 +8,7 @@ def setup end def test_private + # Traditional "DSA PRIVATE KEY" (OpenSSL legacy format) key = Fixtures.pkey("dsa1024") assert_equal true, key.private? key2 = OpenSSL::PKey::DSA.new(key.to_der) @@ -18,12 +19,31 @@ def test_private assert_equal false, key4.private? end + def test_private_pkcs8 + # PKCS#8 "PRIVATE KEY" format: private key is bare INTEGER (x), + # params (p, q, g) come from AlgorithmIdentifier; y must be derived. + key = Fixtures.pkey("dsa2048") + assert_equal true, key.private? + key2 = OpenSSL::PKey::DSA.new(key.to_der) + assert_equal true, key2.private? + key3 = key.public_key + assert_equal false, key3.private? + key4 = OpenSSL::PKey::DSA.new(key3.to_der) + assert_equal false, key4.private? + end + def test_new key = OpenSSL::PKey::DSA.new(2048) pem = key.public_key.to_pem OpenSSL::PKey::DSA.new pem end + def test_new_empty + key = OpenSSL::PKey::DSA.new + assert_nil(key.p) + assert_raise(OpenSSL::PKey::PKeyError) { key.to_der } + end + def test_dup key = Fixtures.pkey("dsa1024") key2 = key.dup From 9b3de2bba232591eae80b9d1e437969f95724e37 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Mar 2026 09:56:27 +0100 Subject: [PATCH 06/21] [compat] support PKey::DSA#sign_raw and verify_raw --- .../java/org/jruby/ext/openssl/PKeyDSA.java | 48 +++++++++++++++++-- src/test/ruby/dsa/test_dsa.rb | 22 +++++++++ src/test/ruby/fixtures/pkey/dsa2048 | 15 ++++++ 3 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 src/test/ruby/fixtures/pkey/dsa2048 diff --git a/src/main/java/org/jruby/ext/openssl/PKeyDSA.java b/src/main/java/org/jruby/ext/openssl/PKeyDSA.java index e330fb36..4d9ea39f 100644 --- a/src/main/java/org/jruby/ext/openssl/PKeyDSA.java +++ b/src/main/java/org/jruby/ext/openssl/PKeyDSA.java @@ -440,8 +440,8 @@ public RubyString public_to_pem(ThreadContext context) { public IRubyObject syssign(IRubyObject data) { final Ruby runtime = getRuntime(); - DSAPrivateKey privateKey; - if ((privateKey = this.privateKey) == null) { + DSAPrivateKey privateKey = this.privateKey; + if (privateKey == null) { throw newDSAError(runtime, "Private DSA key needed!"); } @@ -454,13 +454,55 @@ public IRubyObject syssign(IRubyObject data) { } } + // In OpenSSL, sign_raw/verify_raw are base-class PKey methods that use EVP_PKEY_sign / + // EVP_PKEY_verify (low-level, no-hashing operations). + // When the digest argument is non-nil, EVP_PKEY_CTX_set_signature_md is called, but for DSA + // this only affects input-length validation inside OpenSSL - does not hash the data. + // + // In JCA, "NONEwithDSA" is the direct equivalent: it accepts already-hashed bytes and signs + // them without any further digest step. + // The digest argument therefore does not influence the JCA algorithm and is intentionally unused. + @JRubyMethod(name = "sign_raw") + public IRubyObject sign_raw(ThreadContext context, IRubyObject digest, IRubyObject data) { + DSAPrivateKey privateKey = this.privateKey; + if (privateKey == null) { + throw newDSAError(context.runtime, "Private DSA key needed!"); + } + try { + ByteList sign = sign("NONEwithDSA", privateKey, data.convertToString().getByteList()); + return RubyString.newString(context.runtime, sign); + } + catch (GeneralSecurityException ex) { + throw newDSAError(context.runtime, ex.getMessage()); + } + } + + @JRubyMethod(name = "verify_raw") + public IRubyObject verify_raw(IRubyObject digest, IRubyObject sign, IRubyObject data) { + final Ruby runtime = getRuntime(); + ByteList sigBytes = convertToString(runtime, sign, "OpenSSL::PKey::PKeyError", "invalid signature").getByteList(); + ByteList dataBytes = convertToString(runtime, data, "OpenSSL::PKey::PKeyError", "invalid data").getByteList(); + try { + return runtime.newBoolean(verify("NONEwithDSA", getPublicKey(), dataBytes, sigBytes)); + } + catch (NoSuchAlgorithmException e) { + throw newPKeyError(runtime, e.getMessage()); + } + catch (SignatureException e) { + throw newPKeyError(runtime, "invalid signature"); + } + catch (InvalidKeyException e) { + throw newPKeyError(runtime, "invalid key"); + } + } + @JRubyMethod // ossl_dsa_verify public IRubyObject sysverify(IRubyObject data, IRubyObject sign) { final Ruby runtime = getRuntime(); ByteList sigBytes = convertToString(runtime, sign, "OpenSSL::PKey::DSAError", "invalid signature").getByteList(); ByteList dataBytes = convertToString(runtime, data, "OpenSSL::PKey::DSAError", "invalid data").getByteList(); try { - return runtime.newBoolean( verify("NONEwithDSA", getPublicKey(), dataBytes, sigBytes) ); + return runtime.newBoolean(verify("NONEwithDSA", getPublicKey(), dataBytes, sigBytes)); } catch (NoSuchAlgorithmException e) { throw newDSAError(runtime, e.getMessage()); diff --git a/src/test/ruby/dsa/test_dsa.rb b/src/test/ruby/dsa/test_dsa.rb index 65d8037d..b73103a8 100644 --- a/src/test/ruby/dsa/test_dsa.rb +++ b/src/test/ruby/dsa/test_dsa.rb @@ -118,6 +118,28 @@ def test_dsa_sys_sign_verify assert dsa.sysverify(digest, sig).eql?(true) end + def test_sign_verify_raw + key = Fixtures.pkey("dsa2048") + data = 'Sign me!' + digest = OpenSSL::Digest.digest('SHA1', data) + + invalid_sig = key.sign_raw(nil, digest.succ) + + # Sign by #syssign + sig = key.syssign(digest) + assert_equal true, key.sysverify(digest, sig) + assert_equal false, key.sysverify(digest, invalid_sig) + assert_equal true, key.verify_raw(nil, sig, digest) + assert_equal false, key.verify_raw(nil, invalid_sig, digest) + + # Sign by #sign_raw + sig = key.sign_raw(nil, digest) + assert_equal true, key.sysverify(digest, sig) + assert_equal false, key.sysverify(digest, invalid_sig) + assert_equal true, key.verify_raw(nil, sig, digest) + assert_equal false, key.verify_raw(nil, invalid_sig, digest) + end + def test_DSAPrivateKey # OpenSSL DSAPrivateKey format; similar to RSAPrivateKey dsa512 = Fixtures.pkey("dsa512") diff --git a/src/test/ruby/fixtures/pkey/dsa2048 b/src/test/ruby/fixtures/pkey/dsa2048 new file mode 100644 index 00000000..3f22b22b --- /dev/null +++ b/src/test/ruby/fixtures/pkey/dsa2048 @@ -0,0 +1,15 @@ +-----BEGIN PRIVATE KEY----- +MIICXgIBADCCAjYGByqGSM44BAEwggIpAoIBAQDXZhJ/dQoWkQELzjzlx8FtIp96 +voCYe5NY0H8j0jz7GyHpXt41+MteqkZK3/Ah+cNR9uG8iEYArAZ71LcWotfee2Gz +xdxozr9bRt0POYhO2YIsfMpBrEskPsDH2g/2nFV8l4OJgxU2qZUrF4PN5ha+Mu6u +sVtN8hjvAvnbf4Pxn0b8NN9f4PJncroUa8acv5WsV85E1RW7NYCefggU4LytYIHg +euRF9eY9gVCX5MkUgW2xODHIYJhwk/+5lJxG7qUsSahD/nPHO/yoWgdVHq2DkdTq +KYXkAxx2PJcTBOHTglhE6mgCbEKp8vcfElnBWyCT6QykclZiPXXD2JV829J/Ah0A +vYa+/G/gUZiomyejVje6UsGoCc+vInxmovOL8QKCAQEAhnKEigYPw6u8JY7v5iGo +Ylz8qiMFYmaJCwevf3KCjWeEXuNO4OrKdfzkQl1tPuGLioYFfP1A2yGosjdUdLEB +0JqnzlKxUp+G6RfBj+WYzbgc5hr7t0M+reAJh09/hDzqfxjcgiHstq7mpRXBP8Y7 +iu27s7TRYJNSAYRvWcXNSBEUym3mHBBbZn7VszYooSrn60/iZ8I+VY1UF/fgqhbj +JfaaZNQCDO9K3Vb3rsXoYd8+bOZIen9uHB+pNjMqhpl4waysqrlpGFeeqdxivH6S +vkrHLs6/eWVMnS08RdcryoCrI3Bm8mMBKQglDwKLnWLfzG565qEhslzyCd/l9k9a +cwQfAh0Ao8/g72fSFmo04FizM7DZJSIPqDLjfZu9hLvUFA== +-----END PRIVATE KEY----- From 06612cab2468ece209c3245acd10023635f49166 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Mar 2026 11:23:13 +0100 Subject: [PATCH 07/21] [compat] support PKey::RSA#sign_raw and verify_raw --- src/main/java/org/jruby/ext/openssl/PKey.java | 9 +- .../java/org/jruby/ext/openssl/PKeyRSA.java | 280 ++++++++++++++++++ .../java/org/jruby/ext/openssl/Utils.java | 23 +- src/test/ruby/rsa/test_rsa.rb | 32 ++ 4 files changed, 341 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/PKey.java b/src/main/java/org/jruby/ext/openssl/PKey.java index 65656168..b44b9686 100644 --- a/src/main/java/org/jruby/ext/openssl/PKey.java +++ b/src/main/java/org/jruby/ext/openssl/PKey.java @@ -288,8 +288,7 @@ public IRubyObject verify(IRubyObject digest, IRubyObject sign, IRubyObject data final Ruby runtime = getRuntime(); ByteList sigBytes = convertToString(runtime, sign, "OpenSSL::PKey::PKeyError", "invalid signature").getByteList(); ByteList dataBytes = convertToString(runtime, data, "OpenSSL::PKey::PKeyError", "invalid data").getByteList(); - String digAlg = (digest instanceof Digest) ? ((Digest) digest).getShortAlgorithm() : digest.asJavaString(); - final String algorithm = digAlg + "WITH" + getAlgorithm(); + final String algorithm = getDigestAlgName(digest) + "WITH" + getAlgorithm(); try { return runtime.newBoolean( verify(algorithm, getPublicKey(), dataBytes, sigBytes) ); } @@ -304,6 +303,12 @@ public IRubyObject verify(IRubyObject digest, IRubyObject sign, IRubyObject data } } + static String getDigestAlgName(IRubyObject digest) { + if (digest.isNil()) return "SHA256"; + if (digest instanceof Digest) return ((Digest) digest).getShortAlgorithm(); + return digest.asJavaString(); + } + static RubyString convertToString(final Ruby runtime, final IRubyObject str, final String errorType, final CharSequence errorMsg) { try { return str.convertToString(); diff --git a/src/main/java/org/jruby/ext/openssl/PKeyRSA.java b/src/main/java/org/jruby/ext/openssl/PKeyRSA.java index 3c037b16..133ae9c2 100644 --- a/src/main/java/org/jruby/ext/openssl/PKeyRSA.java +++ b/src/main/java/org/jruby/ext/openssl/PKeyRSA.java @@ -33,6 +33,7 @@ import java.math.BigInteger; import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; import java.security.Key; import java.security.KeyFactory; import java.security.KeyPair; @@ -40,6 +41,7 @@ import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; +import java.security.SignatureException; import java.security.interfaces.RSAPrivateCrtKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; @@ -53,8 +55,21 @@ import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.DERNull; import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.DigestInfo; +import org.bouncycastle.crypto.CryptoException; +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.digests.SHA384Digest; +import org.bouncycastle.crypto.digests.SHA512Digest; +import org.bouncycastle.crypto.engines.RSABlindedEngine; +import org.bouncycastle.crypto.params.ParametersWithRandom; +import org.bouncycastle.crypto.params.RSAKeyParameters; +import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters; +import org.bouncycastle.crypto.signers.PSSSigner; import org.bouncycastle.operator.OutputEncryptor; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; @@ -71,6 +86,7 @@ import org.jruby.RubyString; import org.jruby.anno.JRubyMethod; import org.jruby.exceptions.RaiseException; +import org.jruby.ext.openssl.util.ByteArrayOutputStream; import org.jruby.runtime.Arity; import org.jruby.runtime.Block; import org.jruby.runtime.ObjectAllocator; @@ -78,6 +94,8 @@ import org.jruby.runtime.builtin.IRubyObject; import org.jruby.runtime.Visibility; +import org.jruby.util.ByteList; + import org.jruby.ext.openssl.impl.CipherSpec; import org.jruby.ext.openssl.x509store.PEMInputOutput; import static org.jruby.ext.openssl.OpenSSL.*; @@ -671,6 +689,268 @@ public IRubyObject oid() { return getRuntime().newString("rsaEncryption"); } + // sign_raw(digest, data [, opts]) -- signs already-hashed data with this RSA private key. + // With no opts (or opts without rsa_padding_mode: "pss"), uses PKCS#1 v1.5 padding: + // the hash is wrapped in a DigestInfo ASN.1 structure and signed with NONEwithRSA. + // With opts containing rsa_padding_mode: "pss", uses RSA-PSS via BC's PSSSigner with + // NullDigest (so the pre-hashed bytes are fed directly without re-hashing). + @JRubyMethod(name = "sign_raw", required = 2, optional = 1) + public IRubyObject sign_raw(ThreadContext context, IRubyObject[] args) { + final Ruby runtime = context.runtime; + if (privateKey == null) throw newPKeyError(runtime, "Private RSA key needed!"); + + final String digestAlg = getDigestAlgName(args[0]); + final byte[] hashBytes = args[1].convertToString().getBytes(); + final IRubyObject opts = args.length > 2 ? args[2] : context.nil; + + if (!opts.isNil()) { + String paddingMode = Utils.extractStringOpt(context, opts, "rsa_padding_mode", true); + if ("pss".equalsIgnoreCase(paddingMode)) { + int saltLen = Utils.extractIntOpt(context, opts, "rsa_pss_saltlen", -1, true); + String mgf1Alg = Utils.extractStringOpt(context, opts, "rsa_mgf1_md", true); + if (mgf1Alg == null) mgf1Alg = digestAlg; + if (saltLen < 0) saltLen = getDigestLength(digestAlg); + try { + return StringHelper.newString(runtime, signWithPSS(runtime, hashBytes, digestAlg, mgf1Alg, saltLen)); + } catch (CryptoException e) { + throw newPKeyError(runtime, e.getMessage()); + } + } + } + + // Default: PKCS#1 v1.5 — wrap hash in DigestInfo, then sign with NONEwithRSA + try { + byte[] digestInfoBytes = buildDigestInfo(digestAlg, hashBytes); + ByteList signed = sign("NONEwithRSA", privateKey, new ByteList(digestInfoBytes, false)); + return RubyString.newString(runtime, signed); + } catch (IOException e) { + throw newPKeyError(runtime, "failed to encode DigestInfo: " + e.getMessage()); + } catch (NoSuchAlgorithmException e) { + throw newPKeyError(runtime, "unsupported algorithm: NONEwithRSA"); + } catch (InvalidKeyException e) { + throw newPKeyError(runtime, "invalid key"); + } catch (SignatureException e) { + throw newPKeyError(runtime, e.getMessage()); + } + } + + // verify_raw(digest, signature, data [, opts]) -- verifies signature over already-hashed data. + @JRubyMethod(name = "verify_raw", required = 3, optional = 1) + public IRubyObject verify_raw(ThreadContext context, IRubyObject[] args) { + final Ruby runtime = context.runtime; + final String digestAlg = getDigestAlgName(args[0]); + byte[] sigBytes = args[1].convertToString().getBytes(); + byte[] hashBytes = args[2].convertToString().getBytes(); + IRubyObject opts = args.length > 3 ? args[3] : runtime.getNil(); + + if (!opts.isNil()) { + String paddingMode = Utils.extractStringOpt(context, opts, "rsa_padding_mode", true); + if ("pss".equalsIgnoreCase(paddingMode)) { + int saltLen = Utils.extractIntOpt(context, opts, "rsa_pss_saltlen", -1, true); + String mgf1Alg = Utils.extractStringOpt(context, opts, "rsa_mgf1_md", true); + if (mgf1Alg == null) mgf1Alg = digestAlg; + if (saltLen < 0) saltLen = getDigestLength(digestAlg); + try { // verify_raw: input is already the hash → use PreHashedDigest (pass-through phase 1) + return runtime.newBoolean(verifyWithPSS(publicKey, hashBytes, digestAlg, true, mgf1Alg, saltLen, sigBytes)); + } catch (Exception e) { + throw newPKeyError(runtime, e.getMessage()); + } + } + } + + // Default: PKCS#1 v1.5 — verify against DigestInfo-wrapped hash bytes + try { + byte[] digestInfoBytes = buildDigestInfo(digestAlg, hashBytes); + boolean ok = verify("NONEwithRSA", getPublicKey(), + new ByteList(digestInfoBytes, false), + new ByteList(sigBytes, false)); + return runtime.newBoolean(ok); + } catch (IOException e) { + throw newPKeyError(runtime, "failed to encode DigestInfo: " + e.getMessage()); + } catch (NoSuchAlgorithmException e) { + throw newPKeyError(runtime, "unsupported algorithm: NONEwithRSA"); + } catch (InvalidKeyException e) { + throw newPKeyError(runtime, "invalid key"); + } catch (SignatureException e) { + return runtime.getFalse(); + } + } + + // Override verify to support optional 4th opts argument for PSS. + // Without opts (or with non-PSS opts), delegates to the base PKey#verify logic. + @JRubyMethod(name = "verify", required = 3, optional = 1) + public IRubyObject verify(ThreadContext context, IRubyObject[] args) { + final Ruby runtime = context.runtime; + IRubyObject digest = args[0]; + IRubyObject sign = args[1]; + IRubyObject data = args[2]; + IRubyObject opts = args.length > 3 ? args[3] : runtime.getNil(); + + if (!opts.isNil()) { + String paddingMode = Utils.extractStringOpt(context, opts, "rsa_padding_mode", true); + if ("pss".equalsIgnoreCase(paddingMode)) { + final String digestAlg = getDigestAlgName(digest); + int saltLen = Utils.extractIntOpt(context, opts, "rsa_pss_saltlen", -1, true); + String mgf1Alg = Utils.extractStringOpt(context, opts, "rsa_mgf1_md", true); + if (mgf1Alg == null) mgf1Alg = digestAlg; + if (saltLen < 0) saltLen = getDigestLength(digestAlg); + byte[] sigBytes = sign.convertToString().getBytes(); + byte[] dataBytes = data.convertToString().getBytes(); + try { // verify (non-raw): feed raw data; PSSSigner will hash it internally via SHA-NNN + return runtime.newBoolean(verifyWithPSS(publicKey, dataBytes, digestAlg, false, mgf1Alg, saltLen, sigBytes)); + } catch (Exception e) { + throw newPKeyError(runtime, e.getMessage()); + } + } + } + + // Fall back to standard PKey#verify (PKCS#1 v1.5) + return super.verify(digest, sign, data); + } + + private static byte[] buildDigestInfo(String digestAlg, byte[] hashBytes) throws IOException { + AlgorithmIdentifier algId = getDigestAlgId(digestAlg); + return new DigestInfo(algId, hashBytes).getEncoded("DER"); + } + + private static AlgorithmIdentifier getDigestAlgId(String digestAlg) { + String upper = digestAlg.toUpperCase().replace("-", ""); + ASN1ObjectIdentifier oid; + switch (upper) { + case "SHA1": case "SHA": oid = new ASN1ObjectIdentifier("1.3.14.3.2.26"); break; + case "SHA224": oid = NISTObjectIdentifiers.id_sha224; break; + case "SHA256": oid = NISTObjectIdentifiers.id_sha256; break; + case "SHA384": oid = NISTObjectIdentifiers.id_sha384; break; + case "SHA512": oid = NISTObjectIdentifiers.id_sha512; break; + default: + throw new IllegalArgumentException("Unsupported digest for DigestInfo: " + digestAlg); + } + return new AlgorithmIdentifier(oid, DERNull.INSTANCE); + } + + private static org.bouncycastle.crypto.Digest createBCDigest(String digestAlg) { + String upper = digestAlg.toUpperCase().replace("-", ""); + switch (upper) { + case "SHA1": case "SHA": return new SHA1Digest(); + case "SHA256": return new SHA256Digest(); + case "SHA384": return new SHA384Digest(); + case "SHA512": return new SHA512Digest(); + default: + throw new IllegalArgumentException("Unsupported digest for PSS: " + digestAlg); + } + } + + private static int getDigestLength(String digestAlg) { + String upper = digestAlg.toUpperCase().replace("-", ""); + switch (upper) { + case "SHA1": case "SHA": return 20; + case "SHA224": return 28; + case "SHA256": return 32; + case "SHA384": return 48; + case "SHA512": return 64; + default: return 32; // fallback + } + } + + // Signs pre-hashed bytes using RSA-PSS. PSSSigner internally reuses the content digest for + // BOTH hashing the message (phase 1) and hashing mDash (phase 2), so we use PreHashedDigest + // which passes through pre-hashed bytes verbatim in phase 1 and runs a real SHA hash in phase 2. + private byte[] signWithPSS(Ruby runtime, byte[] hashBytes, String digestAlg, String mgf1Alg, int saltLen) + throws CryptoException { + org.bouncycastle.crypto.Digest contentDigest = new PreHashedDigest(getDigestLength(digestAlg), digestAlg); + org.bouncycastle.crypto.Digest mgf1Digest = createBCDigest(mgf1Alg); + PSSSigner signer = new PSSSigner(new RSABlindedEngine(), contentDigest, mgf1Digest, saltLen); + RSAKeyParameters bcKey = toBCPrivateKeyParams(privateKey); + signer.init(true, new ParametersWithRandom(bcKey, getSecureRandom(runtime))); + signer.update(hashBytes, 0, hashBytes.length); + return signer.generateSignature(); + } + + // Verifies an RSA-PSS signature. When isRaw=true the input is a pre-computed hash (verify_raw); + // PreHashedDigest passes it through in phase 1 then uses a real SHA for hashing mDash in phase 2. + // When isRaw=false the input is raw data (verify with opts); a real SHA digest is used throughout. + private static boolean verifyWithPSS(RSAPublicKey pubKey, byte[] inputBytes, + String digestAlg, boolean isRaw, + String mgf1Alg, int saltLen, byte[] sigBytes) { + org.bouncycastle.crypto.Digest contentDigest = isRaw + ? new PreHashedDigest(getDigestLength(digestAlg), digestAlg) + : createBCDigest(digestAlg); + org.bouncycastle.crypto.Digest mgf1Digest = createBCDigest(mgf1Alg); + PSSSigner verifier = new PSSSigner(new RSABlindedEngine(), contentDigest, mgf1Digest, saltLen); + RSAKeyParameters bcPubKey = new RSAKeyParameters(false, pubKey.getModulus(), pubKey.getPublicExponent()); + verifier.init(false, bcPubKey); + verifier.update(inputBytes, 0, inputBytes.length); + return verifier.verifySignature(sigBytes); + } + + /** + * Two-phase Digest for PSS raw-sign/verify. + * + * PSSSigner internally calls the content digest twice: + * Phase 1 - to hash the message content → we pass pre-computed hash bytes through verbatim. + * Phase 2 - to hash mDash (needs a real hash) → we switch to the actual BC digest algorithm. + * + * getDigestSize() always returns the fixed hash length so PSSSigner can allocate its internal + * buffers correctly even before any data has been accumulated. + */ + private static class PreHashedDigest implements org.bouncycastle.crypto.Digest { + private final int hashLen; + private final String digestAlg; // algorithm name for the real phase-2 digest + private final ByteArrayOutputStream buf = new ByteArrayOutputStream(); + private org.bouncycastle.crypto.Digest realDigest; // non-null during phase 2 + + PreHashedDigest(int hashLen, String digestAlg) { + this.hashLen = hashLen; + this.digestAlg = digestAlg; + } + + public String getAlgorithmName() { return "PRE-HASHED"; } + public int getDigestSize() { return hashLen; } + + public void update(byte in) { + if (realDigest != null) realDigest.update(in); + else buf.write(in); + } + + public void update(byte[] in, int off, int len) { + if (realDigest != null) realDigest.update(in, off, len); + else buf.write(in, off, len); + } + + public int doFinal(byte[] out, final int off) { + if (realDigest == null) { + // Phase 1: emit the pre-hashed bytes verbatim, then arm the real digest for phase 2 + final int len = buf.size(); + System.arraycopy(buf.buffer(), 0, out, off, len); + buf.reset(); + realDigest = createBCDigest(digestAlg); + return len; + } else { + // Phase 2: emit the real hash of the mDash bytes that PSSSigner fed us + final int len = realDigest.doFinal(out, off); + realDigest = null; // back to phase 1 for reuse + return len; + } + } + + public void reset() { + buf.reset(); + realDigest = null; + } + } + + private static RSAKeyParameters toBCPrivateKeyParams(RSAPrivateKey privKey) { + if (privKey instanceof RSAPrivateCrtKey) { + RSAPrivateCrtKey crtKey = (RSAPrivateCrtKey) privKey; + return new RSAPrivateCrtKeyParameters( + crtKey.getModulus(), crtKey.getPublicExponent(), crtKey.getPrivateExponent(), + crtKey.getPrimeP(), crtKey.getPrimeQ(), + crtKey.getPrimeExponentP(), crtKey.getPrimeExponentQ(), + crtKey.getCrtCoefficient()); + } + return new RSAKeyParameters(true, privKey.getModulus(), privKey.getPrivateExponent()); + } + @JRubyMethod(name="d=") public synchronized IRubyObject set_d(final ThreadContext context, IRubyObject value) { if ( privateKey != null ) { diff --git a/src/main/java/org/jruby/ext/openssl/Utils.java b/src/main/java/org/jruby/ext/openssl/Utils.java index 1afc2ffd..2cdfb2d5 100644 --- a/src/main/java/org/jruby/ext/openssl/Utils.java +++ b/src/main/java/org/jruby/ext/openssl/Utils.java @@ -39,7 +39,6 @@ import org.jruby.runtime.Block; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; -import org.jruby.util.TypeConverter; /** * @author Ola Bini @@ -192,6 +191,28 @@ public void visit(IRubyObject key, IRubyObject value) { return ret; } + static String extractStringOpt(ThreadContext context, IRubyObject opts, + String key, boolean tryStringKey) { + if (!(opts instanceof RubyHash)) return null; + RubyHash hash = (RubyHash) opts; + // OpenSSL option hashes may use string or symbol keys — try both. + IRubyObject val = hash.fastARef(context.runtime.newSymbol(key)); + if (val == null && tryStringKey) val = hash.fastARef(context.runtime.newString(key)); + if (val == null || val.isNil()) return null; + return val.convertToString().asJavaString(); + } + + static int extractIntOpt(ThreadContext context, IRubyObject opts, + String key, int defaultVal, boolean tryStringKey) { + if (!(opts instanceof RubyHash)) return defaultVal; + RubyHash hash = (RubyHash) opts; + // OpenSSL option hashes may use string or symbol keys — try both. + IRubyObject val = hash.fastARef(context.runtime.newSymbol(key)); + if (val == null && tryStringKey) val = hash.fastARef(context.runtime.newString(key)); + if (val == null || val.isNil()) return defaultVal; + return RubyNumeric.fix2int(val); + } + static ByteBuffer ensureCapacity(final ByteBuffer buffer, final int size) { if (size <= buffer.capacity()) return buffer; buffer.flip(); diff --git a/src/test/ruby/rsa/test_rsa.rb b/src/test/ruby/rsa/test_rsa.rb index 0745a64b..8803c5a1 100644 --- a/src/test/ruby/rsa/test_rsa.rb +++ b/src/test/ruby/rsa/test_rsa.rb @@ -164,6 +164,38 @@ def test_sign_verify_raw_legacy assert_raise(OpenSSL::PKey::PKeyError){ key.private_encrypt(plain0, 666) } end + def test_sign_verify_raw + key = Fixtures.pkey("rsa-1.pem") + data = "Sign me!" + hash = OpenSSL::Digest.digest("SHA256", data) + + # PKCS#1 v1.5: sign pre-hashed bytes; signature must be compatible with standard verify + signature = key.sign_raw("SHA256", hash) + assert_equal true, key.verify_raw("SHA256", signature, hash) + assert_equal true, key.verify("SHA256", signature, data) + + # verify_raw must return false for wrong hash (not raise) + wrong_hash = OpenSSL::Digest.digest("SHA256", data + "!") + assert_equal false, key.verify_raw("SHA256", signature, wrong_hash) + + # Data exceeding the key modulus must raise PKeyError + assert_raise(OpenSSL::PKey::PKeyError) { + key.sign_raw("SHA1", "x" * (key.n.num_bytes + 1)) + } + + # RSA-PSS: sign_raw with pss options, verify with both verify and verify_raw + pssopts = { + "rsa_padding_mode" => "pss", + "rsa_pss_saltlen" => 20, + "rsa_mgf1_md" => "SHA256" + } + sig_pss = key.sign_raw("SHA256", hash, pssopts) + assert_equal true, key.verify("SHA256", sig_pss, data, pssopts) + assert_equal true, key.verify_raw("SHA256", sig_pss, hash, pssopts) + # PSS signature must not verify as PKCS#1 v1.5 + assert_equal false, key.verify_raw("SHA256", sig_pss, hash) + end + def test_rsa_param_accessors key_file = File.join(File.dirname(__FILE__), 'private_key.pem') key = OpenSSL::PKey::RSA.new(File.read(key_file)) From 1dbb89ec970490f9be00d4bf58e2c752aba79a45 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Mar 2026 12:26:24 +0100 Subject: [PATCH 08/21] [compat] support PKey::RSA#sign_pss and verify_pss --- .../java/org/jruby/ext/openssl/PKeyRSA.java | 229 ++++++++++++++++-- .../java/org/jruby/ext/openssl/Utils.java | 4 + src/test/ruby/rsa/test_rsa.rb | 92 +++++++ 3 files changed, 302 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/PKeyRSA.java b/src/main/java/org/jruby/ext/openssl/PKeyRSA.java index 133ae9c2..51bb1a65 100644 --- a/src/main/java/org/jruby/ext/openssl/PKeyRSA.java +++ b/src/main/java/org/jruby/ext/openssl/PKeyRSA.java @@ -61,6 +61,7 @@ import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.asn1.x509.DigestInfo; import org.bouncycastle.crypto.CryptoException; +import org.bouncycastle.crypto.DataLengthException; import org.bouncycastle.crypto.digests.SHA1Digest; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.digests.SHA384Digest; @@ -84,6 +85,7 @@ import org.jruby.RubyModule; import org.jruby.RubyNumeric; import org.jruby.RubyString; +import org.jruby.RubySymbol; import org.jruby.anno.JRubyMethod; import org.jruby.exceptions.RaiseException; import org.jruby.ext.openssl.util.ByteArrayOutputStream; @@ -711,9 +713,9 @@ public IRubyObject sign_raw(ThreadContext context, IRubyObject[] args) { if (mgf1Alg == null) mgf1Alg = digestAlg; if (saltLen < 0) saltLen = getDigestLength(digestAlg); try { - return StringHelper.newString(runtime, signWithPSS(runtime, hashBytes, digestAlg, mgf1Alg, saltLen)); - } catch (CryptoException e) { - throw newPKeyError(runtime, e.getMessage()); + return StringHelper.newString(runtime, signWithPSS(hashBytes, digestAlg, mgf1Alg, saltLen)); + } catch (IllegalArgumentException | CryptoException e) { + throw (RaiseException) newPKeyError(runtime, e.getMessage()).initCause(e); } } } @@ -750,11 +752,8 @@ public IRubyObject verify_raw(ThreadContext context, IRubyObject[] args) { String mgf1Alg = Utils.extractStringOpt(context, opts, "rsa_mgf1_md", true); if (mgf1Alg == null) mgf1Alg = digestAlg; if (saltLen < 0) saltLen = getDigestLength(digestAlg); - try { // verify_raw: input is already the hash → use PreHashedDigest (pass-through phase 1) - return runtime.newBoolean(verifyWithPSS(publicKey, hashBytes, digestAlg, true, mgf1Alg, saltLen, sigBytes)); - } catch (Exception e) { - throw newPKeyError(runtime, e.getMessage()); - } + // verify_raw: input is already the hash → use PreHashedDigest (pass-through phase 1) + return verifyPSS(runtime, true, hashBytes, digestAlg, mgf1Alg, saltLen, sigBytes); } } @@ -796,11 +795,9 @@ public IRubyObject verify(ThreadContext context, IRubyObject[] args) { if (saltLen < 0) saltLen = getDigestLength(digestAlg); byte[] sigBytes = sign.convertToString().getBytes(); byte[] dataBytes = data.convertToString().getBytes(); - try { // verify (non-raw): feed raw data; PSSSigner will hash it internally via SHA-NNN - return runtime.newBoolean(verifyWithPSS(publicKey, dataBytes, digestAlg, false, mgf1Alg, saltLen, sigBytes)); - } catch (Exception e) { - throw newPKeyError(runtime, e.getMessage()); - } + + // verify (non-raw): feed raw data; PSSSigner will hash it internally via SHA-NNN + return verifyPSS(runtime, false, dataBytes, digestAlg, mgf1Alg, saltLen, sigBytes); } } @@ -808,6 +805,127 @@ public IRubyObject verify(ThreadContext context, IRubyObject[] args) { return super.verify(digest, sign, data); } + // Override sign to support an optional 3rd opts argument. + // When opts contains rsa_padding_mode: "pss", signs the raw data with RSA-PSS. + // Otherwise delegates to PKey#sign (PKCS#1 v1.5). Non-Hash opts raise TypeError. + @JRubyMethod(name = "sign", required = 2, optional = 1) + public IRubyObject sign(ThreadContext context, IRubyObject[] args) { + final Ruby runtime = context.runtime; + final IRubyObject digest = args[0]; + final IRubyObject data = args[1]; + final IRubyObject opts = args.length > 2 ? args[2] : context.nil; + + if (!opts.isNil()) { + if (!(opts instanceof RubyHash)) throw runtime.newTypeError("expected Hash"); + String paddingMode = Utils.extractStringOpt(context, opts, "rsa_padding_mode", true); + if ("pss".equalsIgnoreCase(paddingMode)) { + if (privateKey == null) throw newPKeyError(runtime, "Private RSA key needed!"); + final String digestAlg = getDigestAlgName(digest); + int saltLen = Utils.extractIntOpt(context, opts, "rsa_pss_saltlen", -1, true); + String mgf1Alg = Utils.extractStringOpt(context, opts, "rsa_mgf1_md", true); + if (mgf1Alg == null) mgf1Alg = digestAlg; + if (saltLen < 0) saltLen = maxPSSSaltLength(digestAlg, privateKey.getModulus().bitLength()); + + final byte[] signedData; + try { + signedData = signDataWithPSS(runtime, data.convertToString(), digestAlg, mgf1Alg, saltLen); + } catch (IllegalArgumentException | DataLengthException | CryptoException e) { + throw (RaiseException) newPKeyError(runtime, e.getMessage()).initCause(e); + } + return StringHelper.newString(runtime, signedData); + } + } + return super.sign(digest, data); // PKCS#1 v1.5 fallback + } + + // sign_pss(digest, data, salt_length:, mgf1_hash:) + // Signs data with RSA-PSS. salt_length accepts :digest, :max, :auto, or an integer. + @JRubyMethod(name = "sign_pss", required = 2, optional = 1) + public IRubyObject sign_pss(ThreadContext context, IRubyObject[] args) { + final Ruby runtime = context.runtime; + if (privateKey == null) throw newPKeyError(runtime, "Private RSA key needed!"); + final String digestAlg = getDigestAlgName(args[0]); + final IRubyObject opts = args.length > 2 ? args[2] : context.nil; + final int maxSalt = maxPSSSaltLength(digestAlg, privateKey.getModulus().bitLength()); + + String mgf1Alg = Utils.extractStringOpt(context, opts, "mgf1_hash"); + if (mgf1Alg == null) mgf1Alg = digestAlg; + + final IRubyObject saltLenArg = opts instanceof RubyHash ? + ((RubyHash) opts).fastARef(runtime.newSymbol("salt_length")) : null; + final int saltLen; + if (saltLenArg instanceof RubySymbol) { + String sym = saltLenArg.asJavaString(); + if ("digest".equals(sym)) saltLen = getDigestLength(digestAlg); + else if ("max".equals(sym) || "auto".equals(sym)) saltLen = maxSalt; + else throw runtime.newArgumentError("unknown salt_length: " + sym); + } else if (saltLenArg != null && !saltLenArg.isNil()) { + saltLen = RubyNumeric.fix2int(saltLenArg); + } else { + saltLen = maxSalt; + } + + final byte[] signedData; + try { + signedData = signDataWithPSS(runtime, args[1].convertToString(), digestAlg, mgf1Alg, saltLen); + } catch (IllegalArgumentException | DataLengthException | CryptoException e) { + throw (RaiseException) newPKeyError(runtime, e.getMessage()).initCause(e); + } + return StringHelper.newString(runtime, signedData); + } + + // verify_pss(digest, signature, data, salt_length:, mgf1_hash:) + // Verifies a PSS signature. salt_length accepts :auto, :max, :digest, or an integer. + @JRubyMethod(name = "verify_pss", required = 3, optional = 1) + public IRubyObject verify_pss(ThreadContext context, IRubyObject[] args) { + final Ruby runtime = context.runtime; + final String digestAlg = getDigestAlgName(args[0]); + final byte[] sigBytes = args[1].convertToString().getBytes(); + final byte[] dataBytes = args[2].convertToString().getBytes(); + final IRubyObject opts = args.length > 3 ? args[3] : context.nil; + + String mgf1Alg = Utils.extractStringOpt(context, opts, "mgf1_hash"); + if (mgf1Alg == null) mgf1Alg = digestAlg; + + IRubyObject saltLenArg = opts instanceof RubyHash + ? ((RubyHash) opts).fastARef(runtime.newSymbol("salt_length")) : null; + int saltLen; + if (saltLenArg instanceof RubySymbol) { + String sym = saltLenArg.asJavaString(); + if ("auto".equals(sym)) { + saltLen = pssAutoSaltLength(publicKey, sigBytes, digestAlg, mgf1Alg); + if (saltLen < 0) return runtime.getFalse(); + } else if ("max".equals(sym)) { + saltLen = maxPSSSaltLength(digestAlg, publicKey.getModulus().bitLength()); + } else if ("digest".equals(sym)) { + saltLen = getDigestLength(digestAlg); + } else { + throw runtime.newArgumentError("unknown salt_length: " + sym); + } + } else if (saltLenArg != null && !saltLenArg.isNil()) { + saltLen = RubyNumeric.fix2int(saltLenArg); + } else { + saltLen = getDigestLength(digestAlg); + } + + return verifyPSS(runtime, false, dataBytes, digestAlg, mgf1Alg, saltLen, sigBytes); + } + + private IRubyObject verifyPSS(final Ruby runtime, final boolean rawVerify, + final byte[] dataBytes, final String digestAlg, + final String mgf1Alg, final int saltLen, final byte[] sigBytes) { + boolean verified; + try { + verified = verifyWithPSS(rawVerify, publicKey, dataBytes, digestAlg, mgf1Alg, saltLen, sigBytes); + } catch (IllegalArgumentException|IllegalStateException e) { + verified = false; + } catch (Exception e) { + debugStackTrace(runtime, e); + return runtime.getNil(); + } + return runtime.newBoolean(verified); + } + private static byte[] buildDigestInfo(String digestAlg, byte[] hashBytes) throws IOException { AlgorithmIdentifier algId = getDigestAlgId(digestAlg); return new DigestInfo(algId, hashBytes).getEncoded("DER"); @@ -855,30 +973,28 @@ private static int getDigestLength(String digestAlg) { // Signs pre-hashed bytes using RSA-PSS. PSSSigner internally reuses the content digest for // BOTH hashing the message (phase 1) and hashing mDash (phase 2), so we use PreHashedDigest // which passes through pre-hashed bytes verbatim in phase 1 and runs a real SHA hash in phase 2. - private byte[] signWithPSS(Ruby runtime, byte[] hashBytes, String digestAlg, String mgf1Alg, int saltLen) + private byte[] signWithPSS(byte[] hashBytes, String digestAlg, String mgf1Alg, int saltLen) throws CryptoException { org.bouncycastle.crypto.Digest contentDigest = new PreHashedDigest(getDigestLength(digestAlg), digestAlg); org.bouncycastle.crypto.Digest mgf1Digest = createBCDigest(mgf1Alg); PSSSigner signer = new PSSSigner(new RSABlindedEngine(), contentDigest, mgf1Digest, saltLen); RSAKeyParameters bcKey = toBCPrivateKeyParams(privateKey); - signer.init(true, new ParametersWithRandom(bcKey, getSecureRandom(runtime))); + signer.init(true, new ParametersWithRandom(bcKey, getSecureRandom(getRuntime()))); signer.update(hashBytes, 0, hashBytes.length); return signer.generateSignature(); } - // Verifies an RSA-PSS signature. When isRaw=true the input is a pre-computed hash (verify_raw); + // Verifies an RSA-PSS signature. When rawVerify=true the input is a pre-computed hash (verify_raw); // PreHashedDigest passes it through in phase 1 then uses a real SHA for hashing mDash in phase 2. - // When isRaw=false the input is raw data (verify with opts); a real SHA digest is used throughout. - private static boolean verifyWithPSS(RSAPublicKey pubKey, byte[] inputBytes, - String digestAlg, boolean isRaw, - String mgf1Alg, int saltLen, byte[] sigBytes) { - org.bouncycastle.crypto.Digest contentDigest = isRaw + // When rawVerify=false the input is raw data (verify with opts); a real SHA digest is used throughout. + private static boolean verifyWithPSS(final boolean rawVerify, RSAPublicKey pubKey, byte[] inputBytes, + String digestAlg, String mgf1Alg, int saltLen, byte[] sigBytes) { + org.bouncycastle.crypto.Digest contentDigest = rawVerify ? new PreHashedDigest(getDigestLength(digestAlg), digestAlg) : createBCDigest(digestAlg); org.bouncycastle.crypto.Digest mgf1Digest = createBCDigest(mgf1Alg); PSSSigner verifier = new PSSSigner(new RSABlindedEngine(), contentDigest, mgf1Digest, saltLen); - RSAKeyParameters bcPubKey = new RSAKeyParameters(false, pubKey.getModulus(), pubKey.getPublicExponent()); - verifier.init(false, bcPubKey); + verifier.init(false, new RSAKeyParameters(false, pubKey.getModulus(), pubKey.getPublicExponent())); verifier.update(inputBytes, 0, inputBytes.length); return verifier.verifySignature(sigBytes); } @@ -951,6 +1067,73 @@ private static RSAKeyParameters toBCPrivateKeyParams(RSAPrivateKey privKey) { return new RSAKeyParameters(true, privKey.getModulus(), privKey.getPrivateExponent()); } + // Signs raw (unhashed) data with RSA-PSS; PSSSigner applies the hash internally. + private byte[] signDataWithPSS(Ruby runtime, RubyString data, String digestAlg, String mgf1Alg, int saltLen) + throws CryptoException { + org.bouncycastle.crypto.Digest contentDigest = createBCDigest(digestAlg); + org.bouncycastle.crypto.Digest mgf1Digest = createBCDigest(mgf1Alg); + PSSSigner signer = new PSSSigner(new RSABlindedEngine(), contentDigest, mgf1Digest, saltLen); + signer.init(true, new ParametersWithRandom(toBCPrivateKeyParams(privateKey), getSecureRandom(runtime))); + final ByteList dataBytes = data.getByteList(); + signer.update(dataBytes.unsafeBytes(), dataBytes.getBegin(), dataBytes.getRealSize()); + return signer.generateSignature(); + } + + // Maximum PSS salt length per RFC 8017 §9.1.1: + // emLen = ceil((keyBits - 1) / 8), maxSalt = emLen - 2 - hLen + private static int maxPSSSaltLength(String digestAlg, int keyBits) { + int emLen = (keyBits - 1 + 7) / 8; + return emLen - 2 - getDigestLength(digestAlg); + } + + // Extracts the actual PSS salt length from a signature by parsing the PSS-encoded message. + // Returns -1 if the encoding is invalid (not a well-formed PSS block). + // This is used to implement salt_length: :auto in verify_pss. + private static int pssAutoSaltLength(RSAPublicKey pubKey, byte[] sigBytes, String digestAlg, String mgf1Alg) { + // Step 1: RSA public-key operation → encoded message (EM) + RSAKeyParameters bcPubKey = new RSAKeyParameters(false, pubKey.getModulus(), pubKey.getPublicExponent()); + RSABlindedEngine rsa = new RSABlindedEngine(); + rsa.init(false, bcPubKey); + byte[] em = rsa.processBlock(sigBytes, 0, sigBytes.length); + + int hLen = getDigestLength(digestAlg); + int emLen = em.length; + if (emLen < hLen + 2 || em[emLen - 1] != (byte) 0xBC) return -1; + + int dbLen = emLen - hLen - 1; + byte[] H = new byte[hLen]; + System.arraycopy(em, dbLen, H, 0, hLen); + + // Step 2: Recover DB = MGF1(H, dbLen) XOR maskedDB + byte[] DB = new byte[dbLen]; + System.arraycopy(em, 0, DB, 0, dbLen); + org.bouncycastle.crypto.Digest mgfDigest = createBCDigest(mgf1Alg); + int mgfHLen = mgfDigest.getDigestSize(); + byte[] hBuf = new byte[mgfHLen]; + byte[] ctr = new byte[4]; + for (int pos = 0, c = 0; pos < dbLen; c++) { + ctr[0] = (byte)(c >> 24); ctr[1] = (byte)(c >> 16); + ctr[2] = (byte)(c >> 8); ctr[3] = (byte) c; + mgfDigest.update(H, 0, hLen); + mgfDigest.update(ctr, 0, 4); + mgfDigest.doFinal(hBuf, 0); + int n = Math.min(mgfHLen, dbLen - pos); + for (int i = 0; i < n; i++) DB[pos + i] ^= hBuf[i]; + pos += n; + } + + // Step 3: Clear top bits per RFC 8017 §9.1.2 + int topBits = 8 * emLen - (pubKey.getModulus().bitLength() - 1); + if (topBits > 0) DB[0] &= (byte)(0xFF >>> topBits); + + // Step 4: Find the 0x01 separator; salt follows it + for (int i = 0; i < dbLen; i++) { + if (DB[i] == 0x01) return dbLen - i - 1; + if (DB[i] != 0x00) return -1; + } + return -1; + } + @JRubyMethod(name="d=") public synchronized IRubyObject set_d(final ThreadContext context, IRubyObject value) { if ( privateKey != null ) { diff --git a/src/main/java/org/jruby/ext/openssl/Utils.java b/src/main/java/org/jruby/ext/openssl/Utils.java index 2cdfb2d5..15ec8eee 100644 --- a/src/main/java/org/jruby/ext/openssl/Utils.java +++ b/src/main/java/org/jruby/ext/openssl/Utils.java @@ -191,6 +191,10 @@ public void visit(IRubyObject key, IRubyObject value) { return ret; } + static String extractStringOpt(ThreadContext context, IRubyObject opts, String key) { + return extractStringOpt(context, opts, key, false); + } + static String extractStringOpt(ThreadContext context, IRubyObject opts, String key, boolean tryStringKey) { if (!(opts instanceof RubyHash)) return null; diff --git a/src/test/ruby/rsa/test_rsa.rb b/src/test/ruby/rsa/test_rsa.rb index 8803c5a1..dee892f8 100644 --- a/src/test/ruby/rsa/test_rsa.rb +++ b/src/test/ruby/rsa/test_rsa.rb @@ -137,6 +137,46 @@ def test_rsa_public_encrypt # } end + def test_sign_verify + rsa = Fixtures.pkey("rsa2048") + data = "Sign me!" + signature = rsa.sign("SHA256", data) + assert_equal true, rsa.verify("SHA256", signature, data) + + signature0 = (<<~'end;').unpack1("m") + ooy49i8aeFtkDYUU0RPDsEugGiNw4lZxpbQPnIwtdftEkka945IqKZ/MY3YSw7wKsvBZeaTy8GqL + lSWLThsRFDV+UUS9zUBbQ9ygNIT8OjdV+tNL63ZpKGprczSnw4F05MQIpajNRud/8jiI9rf+Wysi + WwXecjMl2FlXlLJHY4PFQZU5TiametB4VCQRMcjLo1uf26u/yRpiGaYyqn5vxs0SqNtUDM1UL6x4 + NHCAdqLjuFRQPjYp1vGLD3eSl4061pS8x1NVap3YGbYfGUyzZO4VfwFwf1jPdhp/OX/uZw4dGB2H + gSK+q1JiDFwEE6yym5tdKovL1g1NhFYHF6gkZg== + end; + assert_equal true, rsa.verify("SHA256", signature0, data) + signature1 = signature0.succ + assert_equal false, rsa.verify("SHA256", signature1, data) + end + + def test_sign_verify_options + key = Fixtures.pkey("rsa2048") + data = "Sign me!" + pssopts = { + "rsa_padding_mode" => "pss", + "rsa_pss_saltlen" => 20, + "rsa_mgf1_md" => "SHA256" + } + sig_pss = key.sign("SHA256", data, pssopts) + assert_equal 256, sig_pss.bytesize + assert_equal true, key.verify("SHA256", sig_pss, data, pssopts) + assert_equal true, key.verify_pss("SHA256", sig_pss, data, + salt_length: 20, mgf1_hash: "SHA256") + # Defaults to PKCS #1 v1.5 padding => verification failure + assert_equal false, key.verify("SHA256", sig_pss, data) + + # option type check + assert_raise_with_message(TypeError, /expected Hash/) { + key.sign("SHA256", data, ["x"]) + } + end + def test_sign_verify_raw_legacy key = Fixtures.pkey("rsa-1.pem") bits = key.n.num_bits @@ -196,6 +236,58 @@ def test_sign_verify_raw assert_equal false, key.verify_raw("SHA256", sig_pss, hash) end + # def test_verify_empty_rsa + # rsa = OpenSSL::PKey::RSA.new + # assert_raise(OpenSSL::PKey::PKeyError, "[Bug #12783]") { + # rsa.verify("SHA1", "a", "b") + # } + # end unless openssl?(3, 0, 0) # Empty RSA is not possible with OpenSSL >= 3.0 + + def test_sign_verify_pss + key = Fixtures.pkey("rsa2048") + data = "Sign me!" + invalid_data = "Sign me?" + + signature = key.sign_pss("SHA256", data, salt_length: 20, mgf1_hash: "SHA256") + assert_equal 256, signature.bytesize + assert_equal true, + key.verify_pss("SHA256", signature, data, salt_length: 20, mgf1_hash: "SHA256") + assert_equal true, + key.verify_pss("SHA256", signature, data, salt_length: :auto, mgf1_hash: "SHA256") + assert_equal false, + key.verify_pss("SHA256", signature, invalid_data, salt_length: 20, mgf1_hash: "SHA256") + + signature = key.sign_pss("SHA256", data, salt_length: :digest, mgf1_hash: "SHA256") + assert_equal true, + key.verify_pss("SHA256", signature, data, salt_length: 32, mgf1_hash: "SHA256") + assert_equal true, + key.verify_pss("SHA256", signature, data, salt_length: :auto, mgf1_hash: "SHA256") + assert_equal false, + key.verify_pss("SHA256", signature, data, salt_length: 20, mgf1_hash: "SHA256") + + # The sign_pss with `salt_length: :max` raises the "invalid salt length" + # error in FIPS. We need to skip the tests in FIPS. + # According to FIPS 186-5 section 5.4, the salt length shall be between zero + # and the output block length of the digest function (inclusive). + # + # FIPS 186-5 section 5.4 PKCS #1 + # https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf + unless OpenSSL.fips_mode + signature = key.sign_pss("SHA256", data, salt_length: :max, mgf1_hash: "SHA256") + # Should verify on the following salt_length (sLen). + # sLen <= emLen (octat) - 2 - hLen (octet) = 2048 / 8 - 2 - 256 / 8 = 222 + # https://datatracker.ietf.org/doc/html/rfc8017#section-9.1.1 + assert_equal true, + key.verify_pss("SHA256", signature, data, salt_length: 222, mgf1_hash: "SHA256") + assert_equal true, + key.verify_pss("SHA256", signature, data, salt_length: :auto, mgf1_hash: "SHA256") + end + + assert_raise(OpenSSL::PKey::PKeyError) { + key.sign_pss("SHA256", data, salt_length: 223, mgf1_hash: "SHA256") + } + end + def test_rsa_param_accessors key_file = File.join(File.dirname(__FILE__), 'private_key.pem') key = OpenSSL::PKey::RSA.new(File.read(key_file)) From add8620a9cc1d4644bd1e650b9056090cca186c6 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Mar 2026 12:59:06 +0100 Subject: [PATCH 09/21] [fix] raise PKeyError upon invalid RSA key --- src/main/java/org/jruby/ext/openssl/PKeyRSA.java | 2 +- src/test/ruby/rsa/test_rsa.rb | 2 +- src/test/ruby/test_pkey.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/PKeyRSA.java b/src/main/java/org/jruby/ext/openssl/PKeyRSA.java index 51bb1a65..e13f7698 100644 --- a/src/main/java/org/jruby/ext/openssl/PKeyRSA.java +++ b/src/main/java/org/jruby/ext/openssl/PKeyRSA.java @@ -336,7 +336,7 @@ public IRubyObject initialize(final ThreadContext context, final IRubyObject[] a if ( key == null ) key = tryPKCS8EncodedKey(runtime, rsaFactory, str.getBytes()); if ( key == null ) key = tryX509EncodedKey(runtime, rsaFactory, str.getBytes()); - if ( key == null ) throw newRSAError(runtime, "Neither PUB key nor PRIV key:"); + if ( key == null ) throw newPKeyError(runtime, "Neither PUB key nor PRIV key:"); if ( key instanceof KeyPair ) { PublicKey publicKey = ((KeyPair) key).getPublic(); diff --git a/src/test/ruby/rsa/test_rsa.rb b/src/test/ruby/rsa/test_rsa.rb index dee892f8..cb7bdf65 100644 --- a/src/test/ruby/rsa/test_rsa.rb +++ b/src/test/ruby/rsa/test_rsa.rb @@ -437,7 +437,7 @@ def test_RSAPrivateKey_encrypted cipher = OpenSSL::Cipher.new("aes-128-cbc") exported = rsa1024.to_pem(cipher, "abcdef\0\1") assert_same_rsa rsa1024, OpenSSL::PKey::RSA.new(exported, "abcdef\0\1") - assert_raise(OpenSSL::PKey::RSAError) { + assert_raise(OpenSSL::PKey::PKeyError) { OpenSSL::PKey::RSA.new(exported, "abcdef") } end diff --git a/src/test/ruby/test_pkey.rb b/src/test/ruby/test_pkey.rb index 2a0f0e12..c06cc86f 100644 --- a/src/test/ruby/test_pkey.rb +++ b/src/test/ruby/test_pkey.rb @@ -78,7 +78,7 @@ def test_pkey_pem_file_error begin ret = OpenSSL::PKey::RSA.new('not a PEM file') fail "expected OpenSSL::PKey::RSA.new to raise (got: #{ret.inspect})" - rescue OpenSSL::PKey::RSAError + rescue OpenSSL::PKey::PKeyError assert true end end From a56b251a0bc29297afdee7e18a8eb530cc689243 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Mar 2026 13:28:00 +0100 Subject: [PATCH 10/21] [compat] implement EC::Group#to_der + related bits --- .../java/org/jruby/ext/openssl/PKeyEC.java | 92 ++++++++++++++++++- src/test/ruby/ec/test_ec.rb | 33 +++++++ 2 files changed, 123 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/PKeyEC.java b/src/main/java/org/jruby/ext/openssl/PKeyEC.java index 77dd75ed..76abb85f 100644 --- a/src/main/java/org/jruby/ext/openssl/PKeyEC.java +++ b/src/main/java/org/jruby/ext/openssl/PKeyEC.java @@ -860,6 +860,8 @@ static RaiseException newError(final Ruby runtime, final String message) { private PointConversion conversionForm = PointConversion.UNCOMPRESSED; + private int asn1Flag = 1; // OPENSSL_EC_NAMED_CURVE + private String curveName; private RubyString impl_curve_name; @@ -881,11 +883,55 @@ public IRubyObject initialize(final ThreadContext context, final IRubyObject[] a IRubyObject arg = args[0]; if ( arg instanceof Group ) { - this.curveName = ((Group) arg).curveName; + final Group src = (Group) arg; + this.curveName = src.curveName; + this.impl_curve_name = src.impl_curve_name; + this.paramSpec = src.paramSpec; + this.asn1Flag = src.asn1Flag; + this.conversionForm = src.conversionForm; return this; } - this.impl_curve_name = arg.convertToString(); + final RubyString strArg = arg.convertToString(); + final byte[] bytes = strArg.getBytes(); + // Detect DER input: OID tag (0x06) for named curve, SEQUENCE tag (0x30) for explicit params + if (bytes.length > 0 && (bytes[0] == 0x06 || bytes[0] == 0x30)) { + try { + final ASN1Primitive primitive = ASN1Primitive.fromByteArray(bytes); + if (primitive instanceof ASN1ObjectIdentifier) { + // Named curve: DER-encoded OID -> look up curve name + setCurveName(runtime, PKeyEC.getCurveName((ASN1ObjectIdentifier) primitive)); + this.asn1Flag = 1; // NAMED_CURVE + return this; + } else if (primitive instanceof ASN1Sequence) { + // Explicit parameters: X9.62 ECParameters SEQUENCE + final X9ECParameters ecParams = X9ECParameters.getInstance(primitive); + final EllipticCurve curve = EC5Util.convertCurve(ecParams.getCurve(), ecParams.getSeed()); + this.paramSpec = new ECParameterSpec(curve, + EC5Util.convertPoint(ecParams.getG()), + ecParams.getN(), ecParams.getH().intValue()); + this.asn1Flag = 0; // explicit + return this; + } + } catch (IOException e) { + // fall through to treat as curve name string + } + } + + this.impl_curve_name = strArg; + } + return this; + } + + @JRubyMethod(name = "initialize_copy", visibility = Visibility.PRIVATE) + public IRubyObject initialize_copy(final IRubyObject original) { + if (original instanceof Group) { + final Group src = (Group) original; + this.curveName = src.curveName; + this.impl_curve_name = src.impl_curve_name; + this.paramSpec = src.paramSpec; + this.asn1Flag = src.asn1Flag; + this.conversionForm = src.conversionForm; } return this; } @@ -984,6 +1030,48 @@ public RubyString to_pem(final ThreadContext context, final IRubyObject[] args) } } + @JRubyMethod(name = "asn1_flag") + public IRubyObject asn1_flag(final ThreadContext context) { + return context.runtime.newFixnum(asn1Flag); + } + + @JRubyMethod(name = "asn1_flag=") + public IRubyObject set_asn1_flag(final ThreadContext context, final IRubyObject flag_v) { + this.asn1Flag = (int) RubyFixnum.num2long(flag_v); + return flag_v; + } + + /** + * Serializes the group as DER. For named curves (NAMED_CURVE flag set) this is the + * DER encoding of the curve OID. For explicit parameters it is the DER encoding of + * the X9.62 ECParameters SEQUENCE – matching OpenSSL's i2d_ECPKParameters(). + */ + @JRubyMethod(name = "to_der") + public RubyString to_der(final ThreadContext context) { + final Ruby runtime = context.runtime; + try { + final byte[] encoded; + if ((asn1Flag & 1) != 0) { // NAMED_CURVE: encode as DER OID + final ASN1ObjectIdentifier oid = getCurveOID(getCurveName()) + .orElseThrow(() -> newError(runtime, "invalid curve name: " + getCurveName())); + encoded = oid.getEncoded(ASN1Encoding.DER); + } else { // explicit parameters: encode as X9.62 ECParameters SEQUENCE + final ECParameterSpec ps = getParamSpec(); + final ECCurve bcCurve = EC5Util.convertCurve(ps.getCurve()); + final X9ECParameters ecParameters = new X9ECParameters( + bcCurve, + new X9ECPoint(EC5Util.convertPoint(bcCurve, ps.getGenerator()), false), + ps.getOrder(), + BigInteger.valueOf(ps.getCofactor()), + ps.getCurve().getSeed()); + encoded = ecParameters.getEncoded(ASN1Encoding.DER); + } + return StringHelper.newString(runtime, encoded); + } catch (IOException e) { + throw newError(runtime, e.getMessage()); + } + } + private ECParameterSpec getParamSpec() { if (paramSpec == null) { paramSpec = PKeyEC.getParamSpec(getCurveName()); diff --git a/src/test/ruby/ec/test_ec.rb b/src/test/ruby/ec/test_ec.rb index d11f8958..598baff0 100644 --- a/src/test/ruby/ec/test_ec.rb +++ b/src/test/ruby/ec/test_ec.rb @@ -432,6 +432,39 @@ def test_sign_verify assert_equal false, p256.verify("SHA256", signature1, data) end + def test_ec_group + group1 = OpenSSL::PKey::EC::Group.new("prime256v1") + key1 = OpenSSL::PKey::EC.new(group1) + assert_equal group1, key1.group + + group2 = OpenSSL::PKey::EC::Group.new(group1) + assert_equal group1.to_der, group2.to_der + assert_equal group1, group2 + group2.asn1_flag ^= OpenSSL::PKey::EC::NAMED_CURVE + # explicit parameters produce different (longer) DER than a named-curve OID + assert_not_equal group1.to_der, group2.to_der + assert_equal group1, group2 + + group3 = group1.dup + assert_equal group1.to_der, group3.to_der + + assert group1.asn1_flag & OpenSSL::PKey::EC::NAMED_CURVE # our default + + der = group1.to_der + group4 = OpenSSL::PKey::EC::Group.new(der) + group1.point_conversion_form = group4.point_conversion_form = :uncompressed + assert_equal :uncompressed, group1.point_conversion_form + assert_equal :uncompressed, group4.point_conversion_form + assert_equal group1, group4 + assert_equal group1.curve_name, group4.curve_name + assert_equal group1.generator.to_octet_string(:uncompressed), + group4.generator.to_octet_string(:uncompressed) + assert_equal group1.order, group4.order + assert_equal group1.cofactor, group4.cofactor + assert_equal group1.seed, group4.seed + assert_equal group1.degree, group4.degree + end + def test_group_encoding for group in @groups for meth in [:to_der, :to_pem] From 23b1cef846a2abed1c8bc409253c385de952e2e3 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Mar 2026 13:39:33 +0100 Subject: [PATCH 11/21] [compat] support PKey::EC#sign_raw and verify_raw --- .../java/org/jruby/ext/openssl/PKeyEC.java | 42 +++++++++++++++++++ src/test/ruby/ec/test_ec.rb | 21 +++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jruby/ext/openssl/PKeyEC.java b/src/main/java/org/jruby/ext/openssl/PKeyEC.java index 76abb85f..3c838001 100644 --- a/src/main/java/org/jruby/ext/openssl/PKeyEC.java +++ b/src/main/java/org/jruby/ext/openssl/PKeyEC.java @@ -527,6 +527,48 @@ public IRubyObject dsa_verify_asn1(final ThreadContext context, final IRubyObjec } } + // sign_raw(digest, data) -- signs pre-hashed (raw) bytes with the EC private key. + // Produces a DER-encoded ASN.1 SEQUENCE [r, s], identical to dsa_sign_asn1. + // The digest argument is accepted for API parity with RSA/DSA sign_raw but is unused; + // ECDSASigner operates directly on the supplied bytes without additional hashing. + @JRubyMethod(name = "sign_raw") + public IRubyObject sign_raw(final ThreadContext context, final IRubyObject digest, final IRubyObject data) { + return dsa_sign_asn1(context, data); + } + + // verify_raw(digest, signature, data) -- verifies a DER-encoded ECDSA signature over raw bytes. + // Returns true/false; returns false (rather than raising) for a malformed or invalid signature. + // Argument order matches PKey#verify_raw convention: (digest, signature, data), unlike + // dsa_verify_asn1 which takes (data, signature). + @JRubyMethod(name = "verify_raw") + public IRubyObject verify_raw(final ThreadContext context, final IRubyObject digest, + final IRubyObject sign, final IRubyObject data) { + final Ruby runtime = context.runtime; + try { + final ECNamedCurveParameterSpec params = getParameterSpec(); + + final ECDSASigner signer = new ECDSASigner(); + signer.init(false, new ECPublicKeyParameters( + EC5Util.convertPoint(publicKey.getParams(), publicKey.getW()), + new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()) + )); + + ASN1Primitive vec = new ASN1InputStream(sign.convertToString().getBytes()).readObject(); + if (!(vec instanceof ASN1Sequence)) return runtime.getFalse(); + + ASN1Sequence seq = (ASN1Sequence) vec; + ASN1Integer r = ASN1Integer.getInstance(seq.getObjectAt(0)); + ASN1Integer s = ASN1Integer.getInstance(seq.getObjectAt(1)); + + boolean verified = signer.verifySignature(data.convertToString().getBytes(), r.getPositiveValue(), s.getPositiveValue()); + return runtime.newBoolean(verified); + } + catch (IOException | IllegalArgumentException | IllegalStateException ex) { + debugStackTrace(runtime, ex); + return runtime.getFalse(); + } + } + @JRubyMethod(name = "dh_compute_key") public IRubyObject dh_compute_key(final ThreadContext context, final IRubyObject point) { try { diff --git a/src/test/ruby/ec/test_ec.rb b/src/test/ruby/ec/test_ec.rb index 598baff0..cb44426b 100644 --- a/src/test/ruby/ec/test_ec.rb +++ b/src/test/ruby/ec/test_ec.rb @@ -521,6 +521,14 @@ def test_dsa_sign_verify_all end end + def assert_sign_verify_false_or_error + ret = yield + rescue => e + assert_kind_of(OpenSSL::PKey::PKeyError, e) + else + assert_equal(false, ret) + end + def test_sign_verify_raw key = Fixtures.pkey("p256") data1 = "foo" @@ -530,10 +538,21 @@ def test_sign_verify_raw # Sign by #dsa_sign_asn1 sig = key.dsa_sign_asn1(data1) + assert_equal true, key.dsa_verify_asn1(data1, sig) + assert_equal false, key.dsa_verify_asn1(data2, sig) + assert_sign_verify_false_or_error { key.dsa_verify_asn1(data1, malformed_sig) } + assert_equal true, key.verify_raw(nil, sig, data1) + assert_equal false, key.verify_raw(nil, sig, data2) + assert_sign_verify_false_or_error { key.verify_raw(nil, malformed_sig, data1) } + # Sign by #sign_raw + sig = key.sign_raw(nil, data1) assert_equal true, key.dsa_verify_asn1(data1, sig) assert_equal false, key.dsa_verify_asn1(data2, sig) - assert_raise(OpenSSL::PKey::ECError) { key.dsa_verify_asn1(data1, malformed_sig) } + assert_sign_verify_false_or_error { key.dsa_verify_asn1(data1, malformed_sig) } + assert_equal true, key.verify_raw(nil, sig, data1) + assert_equal false, key.verify_raw(nil, sig, data2) + assert_sign_verify_false_or_error { key.verify_raw(nil, malformed_sig, data1) } end def test_new_from_der From f67c0afddf841b91f1afce3e6fb3a2a5fa0112a6 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Mar 2026 13:50:10 +0100 Subject: [PATCH 12/21] [compat] add PKey::EC#derive and fix nil group --- .../java/org/jruby/ext/openssl/PKeyEC.java | 18 ++++++++++++++++-- src/test/ruby/ec/test_ec.rb | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/PKeyEC.java b/src/main/java/org/jruby/ext/openssl/PKeyEC.java index 3c838001..29bcabab 100644 --- a/src/main/java/org/jruby/ext/openssl/PKeyEC.java +++ b/src/main/java/org/jruby/ext/openssl/PKeyEC.java @@ -597,6 +597,20 @@ public IRubyObject dh_compute_key(final ThreadContext context, final IRubyObject } } + // derive(peer_key) -- computes the ECDH shared secret with a peer EC public key. + // Equivalent to dh_compute_key(peer_key.public_key). + @JRubyMethod(name = "derive") + public IRubyObject derive(final ThreadContext context, final IRubyObject peer) { + if (!(peer instanceof PKeyEC)) { + throw context.runtime.newTypeError(peer, _EC(context.runtime)); + } + final IRubyObject peerPublicKey = ((PKeyEC) peer).public_key(context); + if (peerPublicKey.isNil()) { + throw newECError(context.runtime, "no public key"); + } + return dh_compute_key(context, peerPublicKey); + } + @JRubyMethod public IRubyObject oid() { return getRuntime().newString("id-ecPublicKey"); @@ -617,8 +631,8 @@ public IRubyObject group(ThreadContext context) { Group group = this.group; if (group != null) return group; - if (publicKey == null && publicKey == null) { - return context.nil; // PKey::EC.new + if (getCurveName() == null) { + return context.nil; // PKey::EC.new with no args / no curve configured } group = getGroup(false); return group == null ? context.nil : group; diff --git a/src/test/ruby/ec/test_ec.rb b/src/test/ruby/ec/test_ec.rb index cb44426b..6dc839d9 100644 --- a/src/test/ruby/ec/test_ec.rb +++ b/src/test/ruby/ec/test_ec.rb @@ -432,6 +432,24 @@ def test_sign_verify assert_equal false, p256.verify("SHA256", signature1, data) end + def test_derive_key + # NIST CAVP, KAS_ECC_CDH_PrimitiveTest.txt, P-256 COUNT = 0 + qCAVSx = "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287" + qCAVSy = "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac" + dIUT = "7d7dc5f71eb29ddaf80d6214632eeae03d9058af1fb6d22ed80badb62bc1a534" + zIUT = "46fc62106420ff012e54a434fbdd2d25ccc5852060561e68040dd7778997bd7b" + + a = OpenSSL::PKey::EC.new("prime256v1") + a.private_key = OpenSSL::BN.new(dIUT, 16) + + b = OpenSSL::PKey::EC.new("prime256v1") + uncompressed = OpenSSL::BN.new("04" + qCAVSx + qCAVSy, 16) + b.public_key = OpenSSL::PKey::EC::Point.new(b.group, uncompressed) + + assert_equal [zIUT].pack("H*"), a.derive(b) + assert_equal a.derive(b), a.dh_compute_key(b.public_key) + end + def test_ec_group group1 = OpenSSL::PKey::EC::Group.new("prime256v1") key1 = OpenSSL::PKey::EC.new(group1) From 29abc4205f2d8f9275fe2e325508877a05ae9db9 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Mar 2026 14:16:43 +0100 Subject: [PATCH 13/21] [fix] handle PKey::EC.new with encrypted PEM --- .../java/org/jruby/ext/openssl/PKeyEC.java | 19 ++++++++---- .../java/org/jruby/ext/openssl/impl/PKey.java | 8 +++-- .../ext/openssl/x509store/PEMInputOutput.java | 24 ++++++++++++++- src/test/ruby/ec/test_ec.rb | 29 +++++++++++++++++++ 4 files changed, 71 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/PKeyEC.java b/src/main/java/org/jruby/ext/openssl/PKeyEC.java index 29bcabab..96a26964 100644 --- a/src/main/java/org/jruby/ext/openssl/PKeyEC.java +++ b/src/main/java/org/jruby/ext/openssl/PKeyEC.java @@ -840,16 +840,23 @@ public RubyString to_pem(ThreadContext context, final IRubyObject[] args) { if ( args.length > 1 ) passwd = password(context, args[1], null); } - if (privateKey == null) { - return public_to_pem(context); - } + if (privateKey == null) return public_to_pem(context); try { final StringWriter writer = new StringWriter(); - PEMInputOutput.writeECPrivateKey(writer, (ECPrivateKey) privateKey, spec, passwd); + // Include curve OID and public-key point so the PEM can be decoded + // stand-alone (SEC1 optional fields parameters[0] and publicKey[1]). + final ASN1ObjectIdentifier curveOID = getCurveOID(getCurveName()).orElse(null); + byte[] pubKeyBytes = null; + if (publicKey != null) { + pubKeyBytes = EC5Util.convertPoint( + publicKey.getParams(), publicKey.getW()).getEncoded(false); + } + PEMInputOutput.writeECPrivateKey(writer, (ECPrivateKey) privateKey, + curveOID, pubKeyBytes, spec, passwd); return RubyString.newString(context.runtime, writer.getBuffer()); } catch (IOException ex) { - throw newECError(context.runtime, ex.getMessage()); + throw newECError(context.runtime, ex.getMessage(), ex); } } @@ -860,7 +867,7 @@ public RubyString public_to_pem(ThreadContext context) { PEMInputOutput.writeECPublicKey(writer, publicKey); return RubyString.newString(context.runtime, writer.getBuffer()); } catch (IOException ex) { - throw newECError(context.runtime, ex.getMessage()); + throw newECError(context.runtime, ex.getMessage(), ex); } } diff --git a/src/main/java/org/jruby/ext/openssl/impl/PKey.java b/src/main/java/org/jruby/ext/openssl/impl/PKey.java index fd926215..23d2fd15 100644 --- a/src/main/java/org/jruby/ext/openssl/impl/PKey.java +++ b/src/main/java/org/jruby/ext/openssl/impl/PKey.java @@ -299,13 +299,17 @@ public static KeyPair readECPrivateKey(final KeyFactory keyFactory, final Privat if (algId == null) { // mockPrivateKeyInfo algId = new AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, key.getParameters()); } - final SubjectPublicKeyInfo pubInfo = new SubjectPublicKeyInfo(algId, key.getPublicKey().getBytes()); final PrivateKeyInfo privInfo = new PrivateKeyInfo(algId, key); - ECPrivateKey privateKey = (ECPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privInfo.getEncoded())); if (algId.getParameters() instanceof ASN1ObjectIdentifier) { privateKey = ECPrivateKeyWithName.wrap(privateKey, (ASN1ObjectIdentifier) algId.getParameters()); } + + // The publicKey field in ECPrivateKey DER is optional (RFC 5915). + // Keys written by older JRuby-OpenSSL may omit it; handle gracefully. + final org.bouncycastle.asn1.ASN1BitString pubKeyBits = key.getPublicKey(); + if (pubKeyBits == null) return new KeyPair(null, privateKey); + final SubjectPublicKeyInfo pubInfo = new SubjectPublicKeyInfo(algId, pubKeyBits.getBytes()); return new KeyPair(keyFactory.generatePublic(new X509EncodedKeySpec(pubInfo.getEncoded())), privateKey); } catch (ClassCastException ex) { diff --git a/src/main/java/org/jruby/ext/openssl/x509store/PEMInputOutput.java b/src/main/java/org/jruby/ext/openssl/x509store/PEMInputOutput.java index 9b8aec22..6202a5c8 100644 --- a/src/main/java/org/jruby/ext/openssl/x509store/PEMInputOutput.java +++ b/src/main/java/org/jruby/ext/openssl/x509store/PEMInputOutput.java @@ -93,6 +93,7 @@ import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.DERUTF8String; +import org.bouncycastle.asn1.DERBitString; import org.bouncycastle.asn1.DLSequence; import org.bouncycastle.asn1.DERTaggedObject; import org.bouncycastle.asn1.ASN1ObjectIdentifier; @@ -1089,11 +1090,32 @@ public static void writeRSAPrivateKey(Writer _out, RSAPrivateCrtKey obj, CipherS } public static void writeECPrivateKey(Writer _out, ECPrivateKey obj, CipherSpec cipher, char[] passwd) throws IOException { + writeECPrivateKey(_out, obj, null, null, cipher, passwd); + } + + /** + * Writes an EC private key in SEC1 / "EC PRIVATE KEY" PEM format. + * When {@code curveOID} and {@code pubKeyBytes} are provided they are + * embedded as the optional {@code parameters[0]} and {@code publicKey[1]} + * fields so that the PEM can be decoded stand-alone (without external + * knowledge of the curve). + */ + public static void writeECPrivateKey(Writer _out, ECPrivateKey obj, + ASN1ObjectIdentifier curveOID, byte[] pubKeyBytes, + CipherSpec cipher, char[] passwd) throws IOException { assert (obj != null); final String PEM_STRING_EC = "EC PRIVATE KEY"; BufferedWriter out = makeBuffered(_out); final int bitLength = obj.getParams().getOrder().bitLength(); - org.bouncycastle.asn1.sec.ECPrivateKey keyStruct = new org.bouncycastle.asn1.sec.ECPrivateKey(bitLength, obj.getS()); + final org.bouncycastle.asn1.sec.ECPrivateKey keyStruct; + if (curveOID != null && pubKeyBytes != null) { + keyStruct = new org.bouncycastle.asn1.sec.ECPrivateKey( + bitLength, obj.getS(), + new DERBitString(pubKeyBytes), + curveOID); + } else { + keyStruct = new org.bouncycastle.asn1.sec.ECPrivateKey(bitLength, obj.getS()); + } if (cipher != null && passwd != null) { writePemEncrypted(out, PEM_STRING_EC, keyStruct.getEncoded(), cipher, passwd); } else { diff --git a/src/test/ruby/ec/test_ec.rb b/src/test/ruby/ec/test_ec.rb index 6dc839d9..c0b45b1a 100644 --- a/src/test/ruby/ec/test_ec.rb +++ b/src/test/ruby/ec/test_ec.rb @@ -573,6 +573,35 @@ def test_sign_verify_raw assert_sign_verify_false_or_error { key.verify_raw(nil, malformed_sig, data1) } end + def test_ECPrivateKey_encrypted + p256 = Fixtures.pkey("p256") + # key = abcdef (hardcoded encrypted PEM from MRI test suite) + pem = <<~EOF + -----BEGIN EC PRIVATE KEY----- + Proc-Type: 4,ENCRYPTED + DEK-Info: AES-128-CBC,85743EB6FAC9EA76BF99D9328AFD1A66 + + nhsP1NHxb53aeZdzUe9umKKyr+OIwQq67eP0ONM6E1vFTIcjkDcFLR6PhPFufF4m + y7E2HF+9uT1KPQhlE+D63i1m1Mvez6PWfNM34iOQp2vEhaoHHKlR3c43lLyzaZDI + 0/dGSU5SzFG+iT9iFXCwCvv+bxyegkBOyALFje1NAsM= + -----END EC PRIVATE KEY----- + EOF + key = OpenSSL::PKey::EC.new(pem, "abcdef") + assert_same_ec p256, key + key = OpenSSL::PKey::EC.new(pem) { "abcdef" } + assert_same_ec p256, key + + # Round-trip: to_pem with encryption and read back + cipher = OpenSSL::Cipher.new("aes-128-cbc") + exported = p256.to_pem(cipher, "abcdef\0\1") + assert_same_ec p256, OpenSSL::PKey::EC.new(exported, "abcdef\0\1") + # MRI raises OpenSSL::PKey::PKeyError; + # TODO JRuby raises more specific ECError + assert_raise(OpenSSL::PKey::ECError) { + OpenSSL::PKey::EC.new(exported, "abcdef") + } + end + def test_new_from_der priv_key_hex = '05768F097A19FFE5022D4A862CDBAE22019695D1C2F88FD41607417AD45E2F55' pub_key_hex = '04B827833DC1BC38CE0BBE36E0357B1D08AB0BFA05DBD211F0FC677FF9913FAF0EB3A3CC562EEAE8D841B112DBFDAD494E10CFBD4964DC2D175D06F17ACC5771CF' From 70f63077cdc64d2a8982460a7697ad55bc404f09 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Mar 2026 20:15:03 +0100 Subject: [PATCH 14/21] [compat] implement X.509 extension valut_der --- src/main/java/org/jruby/ext/openssl/X509Extension.java | 10 ++++++++++ src/test/ruby/x509/test_x509ext.rb | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/main/java/org/jruby/ext/openssl/X509Extension.java b/src/main/java/org/jruby/ext/openssl/X509Extension.java index 48aafbd6..118dbb9a 100644 --- a/src/main/java/org/jruby/ext/openssl/X509Extension.java +++ b/src/main/java/org/jruby/ext/openssl/X509Extension.java @@ -796,6 +796,16 @@ public IRubyObject set_critical(final ThreadContext context, IRubyObject arg) { return arg; } + @JRubyMethod + public RubyString value_der(final ThreadContext context) { + try { + return StringHelper.newString(context.runtime, getRealValueEncoded()); + } + catch (IOException e) { + throw newExtensionError(context.runtime, e); + } + } + @JRubyMethod public RubyString to_der() { try { diff --git a/src/test/ruby/x509/test_x509ext.rb b/src/test/ruby/x509/test_x509ext.rb index 25fbf010..a43933c4 100644 --- a/src/test/ruby/x509/test_x509ext.rb +++ b/src/test/ruby/x509/test_x509ext.rb @@ -107,6 +107,16 @@ def test_to_der # reproducing #389 assert_equal 'foo', value[1].value end + def test_value_der + value = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Boolean(true)]) + ext = OpenSSL::X509::Extension.new('basicConstraints', value) + + assert_equal value.to_der, ext.value_der + decoded = OpenSSL::ASN1.decode(ext.value_der) + assert_equal OpenSSL::ASN1::Sequence, decoded.class + assert_equal true, decoded.value.first.value + end + def test_to_der_is_the_same_for_non_critical ext1 = OpenSSL::X509::Extension.new('1.1.1.1.1.1', 'foo') ext2 = OpenSSL::X509::Extension.new('1.1.1.1.1.1', 'foo', false) From 351fdb70b6c73985011a5fb492f03a94c42bf997 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Mar 2026 20:24:32 +0100 Subject: [PATCH 15/21] [compat] add OpenSSL::X509::Certificate#tbs_bytes --- src/main/java/org/jruby/ext/openssl/X509Cert.java | 13 +++++++++++++ src/test/ruby/x509/test_x509cert.rb | 8 ++++++++ 2 files changed, 21 insertions(+) diff --git a/src/main/java/org/jruby/ext/openssl/X509Cert.java b/src/main/java/org/jruby/ext/openssl/X509Cert.java index 5d4fe48c..4620a636 100644 --- a/src/main/java/org/jruby/ext/openssl/X509Cert.java +++ b/src/main/java/org/jruby/ext/openssl/X509Cert.java @@ -303,6 +303,19 @@ public IRubyObject to_der() { } } + @JRubyMethod + public IRubyObject tbs_bytes() { + if ( cert == null ) { + throw newCertificateError(getRuntime(), "no certificate"); + } + try { + return StringHelper.newString(getRuntime(), cert.getTBSCertificate()); + } + catch (CertificateEncodingException ex) { + throw newCertificateError(getRuntime(), ex); + } + } + @JRubyMethod(name = {"to_pem", "to_s"}) public IRubyObject to_pem() { final StringWriter str = new StringWriter(); diff --git a/src/test/ruby/x509/test_x509cert.rb b/src/test/ruby/x509/test_x509cert.rb index fac2a77d..61f58dc9 100644 --- a/src/test/ruby/x509/test_x509cert.rb +++ b/src/test/ruby/x509/test_x509cert.rb @@ -426,6 +426,14 @@ def test_sign_cert_default_serial # jruby/jruby#1691 assert_equal 0, res.serial end + def test_tbs_precert_bytes + ca = OpenSSL::X509::Name.parse('/DC=org/DC=ruby-lang/CN=CA') + cert = issue_cert(ca, OpenSSL::PKey::RSA.new(TEST_KEY_RSA1024), 1, [], nil, nil) + seq = OpenSSL::ASN1.decode(cert.tbs_bytes) + + assert_equal 7, seq.value.size + end + def test_cert_loading_regression cert_text = "0\x82\x01\xAD0\x82\x01\xA1\xA0\x03\x02\x01\x02\x02\x01\x010\x03\x06\x01\x000g1\v0\t\x06\x03U\x04\x06\x13\x02US1\x130\x11\x06\x03U\x04\b\f\nCalifornia1\x150\x13\x06\x03U\x04\a\f\fSanta Monica1\x110\x0F\x06\x03U\x04\n\f\bOneLogin1\x190\x17\x06\x03U\x04\x03\f\x10app.onelogin.com0\x1E\x17\r100309095845Z\x17\r150309095845Z0g1\v0\t\x06\x03U\x04\x06\x13\x02US1\x130\x11\x06\x03U\x04\b\f\nCalifornia1\x150\x13\x06\x03U\x04\a\f\fSanta Monica1\x110\x0F\x06\x03U\x04\n\f\bOneLogin1\x190\x17\x06\x03U\x04\x03\f\x10app.onelogin.com0\x81\x9F0\r\x06\t*\x86H\x86\xF7\r\x01\x01\x01\x05\x00\x03\x81\x8D\x000\x81\x89\x02\x81\x81\x00\xE8\xD2\xBBW\xE3?/\x1D\xE7\x0E\x10\xC8\xBD~\xCD\xDE!#\rL\x92G\xDF\xE1f?L\xB1\xBC9\x99\x14\xE5\x84\xD2Zi\x87<>d\xBD\x81\xF9\xBA\x85\xD2\xFF\xAA\x90\xF3Z\x97\xA5\x1D\xB0W\xC0\x93\xA3\x06IP\xB84\xF5\xD7Qu\x19\xFCB\xCA\xA3\xD4\\\x8E\v\x9B%\x13|\xB6m\x9D\xA8\x16\xE6\xBB\xDA\x87\xFF\xE3\xD7\xE9\xBA9\xC5O\xA2\xA7C\xADB\x04\xCA\xA5\x0E\x84\xD0\xA8\xE4\xFA\xDA\xF1\x89\xF2s\xFA1\x95\xAF\x03\xAB1\xAA\xE7y\x02\x03\x01\x00\x010\x03\x06\x01\x00\x03\x01\x00" assert cert = OpenSSL::X509::Certificate.new(cert_text) From 57743074f190db8c3df06ee82a802c37f6b6f19c Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Mar 2026 20:43:35 +0100 Subject: [PATCH 16/21] [compat] implement X509::Certificate#== --- .../java/org/jruby/ext/openssl/X509Cert.java | 31 +++++++++++++++++++ src/test/ruby/x509/test_x509cert.rb | 25 +++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/main/java/org/jruby/ext/openssl/X509Cert.java b/src/main/java/org/jruby/ext/openssl/X509Cert.java index 4620a636..76f8749a 100644 --- a/src/main/java/org/jruby/ext/openssl/X509Cert.java +++ b/src/main/java/org/jruby/ext/openssl/X509Cert.java @@ -45,6 +45,7 @@ import java.security.interfaces.DSAPublicKey; import java.security.interfaces.RSAPublicKey; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; @@ -316,6 +317,36 @@ public IRubyObject tbs_bytes() { } } + @Override + @JRubyMethod(name = "==") + public IRubyObject op_equal(ThreadContext context, IRubyObject obj) { + return equalImpl(context.runtime, obj); + } + + private IRubyObject equalImpl(final Ruby runtime, IRubyObject obj) { + if ( this == obj ) return runtime.getTrue(); + if ( obj instanceof X509Cert ) { + final X509Certificate cert = this.cert; + final X509Certificate otherCert = ((X509Cert) obj).cert; + if ( cert == null || otherCert == null ) return runtime.getFalse(); + + final boolean equal; + try { + equal = Arrays.equals(cert.getEncoded(), otherCert.getEncoded()); + } + catch (CertificateEncodingException e) { + throw newCertificateError(runtime, e); + } + return runtime.newBoolean(equal); + } + return runtime.getFalse(); + } + + @Override + public IRubyObject eql_p(IRubyObject obj) { + return equalImpl(getRuntime(), obj); + } + @JRubyMethod(name = {"to_pem", "to_s"}) public IRubyObject to_pem() { final StringWriter str = new StringWriter(); diff --git a/src/test/ruby/x509/test_x509cert.rb b/src/test/ruby/x509/test_x509cert.rb index 61f58dc9..697dcca6 100644 --- a/src/test/ruby/x509/test_x509cert.rb +++ b/src/test/ruby/x509/test_x509cert.rb @@ -434,6 +434,31 @@ def test_tbs_precert_bytes assert_equal 7, seq.value.size end + def test_eq + now = Time.now + ca = OpenSSL::X509::Name.parse('/DC=org/DC=ruby-lang/CN=CA') + ee = OpenSSL::X509::Name.parse('/DC=org/DC=example/CN=ServerCert') + ca_key = OpenSSL::PKey::RSA.new(TEST_KEY_RSA1024) + ee_key = OpenSSL::PKey::RSA.new(TEST_KEY_RSA1024) + + cacert = issue_cert(ca, ca_key, 1, [], nil, nil, + not_before: now, not_after: now + 3600) + cert1 = issue_cert(ee, ee_key, 2, [], cacert, ca_key, + not_before: now, not_after: now + 3600) + cert2 = issue_cert(ee, ee_key, 2, [], cacert, ca_key, + not_before: now, not_after: now + 3600) + cert3 = issue_cert(ee, ee_key, 3, [], cacert, ca_key, + not_before: now, not_after: now + 3600) + cert4 = issue_cert(ee, ee_key, 2, [], cacert, ca_key, + digest: 'sha512', not_before: now, not_after: now + 3600) + + assert_equal false, cert1 == 12345 + assert_equal true, cert1 == cert2 + assert_equal false, cert1 == cert3 + assert_equal false, cert1 == cert4 + assert_equal false, cert3 == cert4 + end + def test_cert_loading_regression cert_text = "0\x82\x01\xAD0\x82\x01\xA1\xA0\x03\x02\x01\x02\x02\x01\x010\x03\x06\x01\x000g1\v0\t\x06\x03U\x04\x06\x13\x02US1\x130\x11\x06\x03U\x04\b\f\nCalifornia1\x150\x13\x06\x03U\x04\a\f\fSanta Monica1\x110\x0F\x06\x03U\x04\n\f\bOneLogin1\x190\x17\x06\x03U\x04\x03\f\x10app.onelogin.com0\x1E\x17\r100309095845Z\x17\r150309095845Z0g1\v0\t\x06\x03U\x04\x06\x13\x02US1\x130\x11\x06\x03U\x04\b\f\nCalifornia1\x150\x13\x06\x03U\x04\a\f\fSanta Monica1\x110\x0F\x06\x03U\x04\n\f\bOneLogin1\x190\x17\x06\x03U\x04\x03\f\x10app.onelogin.com0\x81\x9F0\r\x06\t*\x86H\x86\xF7\r\x01\x01\x01\x05\x00\x03\x81\x8D\x000\x81\x89\x02\x81\x81\x00\xE8\xD2\xBBW\xE3?/\x1D\xE7\x0E\x10\xC8\xBD~\xCD\xDE!#\rL\x92G\xDF\xE1f?L\xB1\xBC9\x99\x14\xE5\x84\xD2Zi\x87<>d\xBD\x81\xF9\xBA\x85\xD2\xFF\xAA\x90\xF3Z\x97\xA5\x1D\xB0W\xC0\x93\xA3\x06IP\xB84\xF5\xD7Qu\x19\xFCB\xCA\xA3\xD4\\\x8E\v\x9B%\x13|\xB6m\x9D\xA8\x16\xE6\xBB\xDA\x87\xFF\xE3\xD7\xE9\xBA9\xC5O\xA2\xA7C\xADB\x04\xCA\xA5\x0E\x84\xD0\xA8\xE4\xFA\xDA\xF1\x89\xF2s\xFA1\x95\xAF\x03\xAB1\xAA\xE7y\x02\x03\x01\x00\x010\x03\x06\x01\x00\x03\x01\x00" assert cert = OpenSSL::X509::Certificate.new(cert_text) From 00b4ebd4dbb4ccd8581c27e25b88c3191ae6641c Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Mar 2026 20:52:36 +0100 Subject: [PATCH 17/21] [fix] OpenSSL::X509::Request#dup behavior --- .../org/jruby/ext/openssl/X509Request.java | 27 ++++++++++++++++--- src/test/ruby/x509/test_x509req.rb | 5 ++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/X509Request.java b/src/main/java/org/jruby/ext/openssl/X509Request.java index 62938afc..0a20a0e3 100644 --- a/src/main/java/org/jruby/ext/openssl/X509Request.java +++ b/src/main/java/org/jruby/ext/openssl/X509Request.java @@ -40,6 +40,7 @@ import java.security.PrivateKey; import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.ASN1Set; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.pkcs.Attribute; @@ -151,13 +152,31 @@ private static IRubyObject newName(final ThreadContext context, X500Name name) { @Override @JRubyMethod(visibility = Visibility.PRIVATE) public IRubyObject initialize_copy(IRubyObject obj) { - final Ruby runtime = getRuntime(); - warn(runtime.getCurrentContext(), "WARNING: unimplemented method called: OpenSSL::X509::Request#initialize_copy"); - if ( this == obj ) return this; checkFrozen(); - // subject = public_key = null; + + final X509Request that = (X509Request) obj; + final ThreadContext context = getRuntime().getCurrentContext(); + + this.subject = that.subject == null ? null : newName(context, getX500Name(that.subject)); + this.public_key = that.public_key == null ? null : (PKey) that.public_key.dup(); + this.version = that.version; + + this.attributes.clear(); + for ( X509Attribute attribute : that.attributes ) { + this.attributes.add( (X509Attribute) attribute.dup() ); + } + + final PKCS10Request request = that.request; + if ( request != null ) { + final ASN1Sequence requestSequence = request.toASN1Structure(); + this.request = requestSequence.size() == 0 ? null : new PKCS10Request(requestSequence); + } + else { + this.request = null; + } + return this; } diff --git a/src/test/ruby/x509/test_x509req.rb b/src/test/ruby/x509/test_x509req.rb index 14a1786b..7d4482fd 100644 --- a/src/test/ruby/x509/test_x509req.rb +++ b/src/test/ruby/x509/test_x509req.rb @@ -109,6 +109,11 @@ def test_version assert_equal 1, req.version end + def test_dup; setup! + req = issue_csr(0, @dn, @rsa1024, OpenSSL::Digest.new('SHA256')) + assert_equal req.to_der, req.dup.to_der + end + # from GH-150 def test_to_der_new_from_der; require 'base64' # Build the CSR From 7be494650406282789388c9317cb946584bee2f7 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Mar 2026 20:59:55 +0100 Subject: [PATCH 18/21] [fix] request attributes internal reset --- .../jruby/ext/openssl/impl/PKCS10Request.java | 13 ++++-- src/test/ruby/x509/test_x509req.rb | 45 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/impl/PKCS10Request.java b/src/main/java/org/jruby/ext/openssl/impl/PKCS10Request.java index 1ddbb899..c5a6be3b 100644 --- a/src/main/java/org/jruby/ext/openssl/impl/PKCS10Request.java +++ b/src/main/java/org/jruby/ext/openssl/impl/PKCS10Request.java @@ -49,6 +49,7 @@ import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.ASN1Set; import org.bouncycastle.asn1.DERBitString; +import org.bouncycastle.asn1.DERSet; import org.bouncycastle.asn1.DLSequence; import org.bouncycastle.asn1.pkcs.CertificationRequestInfo; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; @@ -119,12 +120,17 @@ private void resetSignedRequest() { if ( signedRequest == null ) return; CertificationRequest req = signedRequest.toASN1Structure(); - CertificationRequestInfo reqInfo = new CertificationRequestInfo(subject, publicKeyInfo, req.getCertificationRequestInfo().getAttributes()); + CertificationRequestInfo reqInfo = new CertificationRequestInfo(subject, publicKeyInfo, toAttributesSet()); ASN1Sequence seq = (ASN1Sequence) req.toASN1Primitive(); req = new CertificationRequest(reqInfo, (AlgorithmIdentifier) seq.getObjectAt(1), (DERBitString) seq.getObjectAt(2)); signedRequest = new PKCS10CertificationRequest(req); // valid = true; } + private ASN1Set toAttributesSet() { + if ( attributes == null || attributes.isEmpty() ) return null; + return new DERSet( attributes.toArray(new Attribute[ attributes.size() ]) ); + } + // sign public PKCS10CertificationRequest sign(final PrivateKey privateKey, final AlgorithmIdentifier signatureAlg) @@ -279,12 +285,12 @@ else if ( keyParams instanceof ECPublicKeyParameters ) { } public Attribute[] getAttributes() { - return signedRequest != null ? signedRequest.getAttributes() : - attributes.toArray(new Attribute[ attributes.size() ]); + return attributes.toArray(new Attribute[ attributes.size() ]); } public void setAttributes(final List attrs) { this.attributes = attrs; + resetSignedRequest(); } private void setAttributes(final ASN1Set attrs) { @@ -297,6 +303,7 @@ private void setAttributes(final ASN1Set attrs) { public void addAttribute(final Attribute attribute) { this.attributes.add( attribute ); + resetSignedRequest(); } public BigInteger getVersion() { diff --git a/src/test/ruby/x509/test_x509req.rb b/src/test/ruby/x509/test_x509req.rb index 7d4482fd..aac2ec04 100644 --- a/src/test/ruby/x509/test_x509req.rb +++ b/src/test/ruby/x509/test_x509req.rb @@ -109,6 +109,51 @@ def test_version assert_equal 1, req.version end + def create_ext_req(exts) + ef = OpenSSL::X509::ExtensionFactory.new + exts = exts.collect { |e| ef.create_extension(*e) } + OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence(exts)]) + end + + def get_ext_req(ext_req_value) + set = OpenSSL::ASN1.decode(ext_req_value) + seq = set.value[0] + seq.value.collect { |asn1ext| OpenSSL::X509::Extension.new(asn1ext).to_a } + end + + def test_attr; setup! + exts = [ + ['keyUsage', 'Digital Signature, Key Encipherment', true], + ['subjectAltName', 'email:gotoyuzo@ruby-lang.org', false], + ] + attrval = create_ext_req(exts) + attrs = [ + OpenSSL::X509::Attribute.new('extReq', attrval), + OpenSSL::X509::Attribute.new('msExtReq', attrval), + ] + + req0 = issue_csr(0, @dn, @rsa1024, OpenSSL::Digest.new('SHA256')) + attrs.each { |attr| req0.add_attribute(attr) } + req1 = issue_csr(0, @dn, @rsa1024, OpenSSL::Digest.new('SHA256')) + req1.attributes = attrs + assert_equal req0.to_der, req1.to_der + + attrs = req0.attributes + assert_equal 2, attrs.size + assert_equal 'extReq', attrs[0].oid + assert_equal 'msExtReq', attrs[1].oid + assert_equal exts, get_ext_req(attrs[0].value) + assert_equal exts, get_ext_req(attrs[1].value) + + req = OpenSSL::X509::Request.new(req0.to_der) + attrs = req.attributes + assert_equal 2, attrs.size + assert_equal 'extReq', attrs[0].oid + assert_equal 'msExtReq', attrs[1].oid + assert_equal exts, get_ext_req(attrs[0].value) + assert_equal exts, get_ext_req(attrs[1].value) + end + def test_dup; setup! req = issue_csr(0, @dn, @rsa1024, OpenSSL::Digest.new('SHA256')) assert_equal req.to_der, req.dup.to_der From a29a273939765582bdcef6ea189336fe6dd2302e Mon Sep 17 00:00:00 2001 From: kares Date: Sat, 7 Mar 2026 20:36:58 +0100 Subject: [PATCH 19/21] [compat] implement OpenSSL::KDF.hkdf --- src/main/java/org/jruby/ext/openssl/HMAC.java | 7 +- src/main/java/org/jruby/ext/openssl/KDF.java | 57 +++++++++ src/test/ruby/test_kdf.rb | 116 ++++++++++++++++++ 3 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 src/test/ruby/test_kdf.rb diff --git a/src/main/java/org/jruby/ext/openssl/HMAC.java b/src/main/java/org/jruby/ext/openssl/HMAC.java index 03bc4e92..a710e263 100644 --- a/src/main/java/org/jruby/ext/openssl/HMAC.java +++ b/src/main/java/org/jruby/ext/openssl/HMAC.java @@ -55,10 +55,9 @@ static void createHMAC(final Ruby runtime, final RubyModule OpenSSL, final RubyC HMAC.defineAnnotatedMethods(HMAC.class); } - private static Mac getMacInstance(final String algorithmName) throws NoSuchAlgorithmException { - // final String algorithmSuffix = algorithmName.replaceAll("-", ""); + static Mac getMacInstance(final String algorithmName) throws NoSuchAlgorithmException { final StringBuilder algName = new StringBuilder(5 + algorithmName.length()); - algName.append("HMAC"); // .append(algorithmSuffix); + algName.append("HMAC"); for ( int i = 0; i < algorithmName.length(); i++ ) { char c = algorithmName.charAt(i); if ( c != '-' ) algName.append(c); @@ -199,7 +198,7 @@ private byte[] getSignatureBytes() { return mac.doFinal(); } - private static String getDigestAlgorithmName(final IRubyObject digest) { + static String getDigestAlgorithmName(final IRubyObject digest) { if ( digest instanceof Digest ) { return ((Digest) digest).getShortAlgorithm(); } diff --git a/src/main/java/org/jruby/ext/openssl/KDF.java b/src/main/java/org/jruby/ext/openssl/KDF.java index 6583e81d..95e3eaef 100644 --- a/src/main/java/org/jruby/ext/openssl/KDF.java +++ b/src/main/java/org/jruby/ext/openssl/KDF.java @@ -25,6 +25,7 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import javax.crypto.Mac; import org.jruby.*; import org.jruby.anno.JRubyMethod; @@ -50,6 +51,7 @@ static void createKDF(final Ruby runtime, final RubyModule OpenSSL, final RubyCl } private static final String[] PBKDF2_ARGS = new String[] { "salt", "iterations", "length", "hash" }; + private static final String[] HKDF_ARGS = new String[] { "salt", "info", "length", "hash" }; @JRubyMethod(module = true) // pbkdf2_hmac(pass, salt:, iterations:, length:, hash:) public static IRubyObject pbkdf2_hmac(ThreadContext context, IRubyObject self, IRubyObject pass, IRubyObject opts) { @@ -63,6 +65,61 @@ public static IRubyObject pbkdf2_hmac(ThreadContext context, IRubyObject self, I } } + @JRubyMethod(module = true) // hkdf(ikm, salt:, info:, length:, hash:) + public static IRubyObject hkdf(ThreadContext context, IRubyObject self, IRubyObject ikm, IRubyObject opts) { + IRubyObject[] args = extractKeywordArgs(context, (RubyHash) opts, HKDF_ARGS, 0); + try { + return hkdfImpl(context.runtime, ikm, args); + } + catch (NoSuchAlgorithmException|InvalidKeyException e) { + throw newKDFError(context.runtime, e.getMessage()); + } + } + + static RubyString hkdfImpl(final Ruby runtime, final IRubyObject ikmArg, final IRubyObject[] args) + throws NoSuchAlgorithmException, InvalidKeyException { + final byte[] ikm = ikmArg.convertToString().getBytes(); + final byte[] salt = args[0].convertToString().getBytes(); + final byte[] info = args[1].convertToString().getBytes(); + + final long length = RubyNumeric.num2long(args[2]); + if (length < 0) throw runtime.newArgumentError("length must be non-negative"); + + final Mac mac = getMac(args[3]); + final int macLength = mac.getMacLength(); + if (length > 255L * macLength) { + throw newKDFError(runtime, "length must be <= 255 * HashLen"); + } + + mac.init(new SimpleSecretKey(mac.getAlgorithm(), salt)); + final byte[] prk = mac.doFinal(ikm); + + mac.init(new SimpleSecretKey(mac.getAlgorithm(), prk)); + + final byte[] okm = new byte[(int) length]; + byte[] block = new byte[0]; + int offset = 0; + + for (int i = 1; offset < okm.length; i++) { + if (block.length > 0) mac.update(block); + if (info.length > 0) mac.update(info); + mac.update((byte) i); + + block = mac.doFinal(); + + final int copyLength = Math.min(block.length, okm.length - offset); + System.arraycopy(block, 0, okm, offset, copyLength); + offset += copyLength; + } + + return StringHelper.newString(runtime, okm); + } + + private static Mac getMac(final IRubyObject digest) throws NoSuchAlgorithmException { + final String digestAlg = HMAC.getDigestAlgorithmName(digest); + return HMAC.getMacInstance(digestAlg); + } + static RaiseException newKDFError(Ruby runtime, String message) { return Utils.newError(runtime, _KDF(runtime).getClass("KDFError"), message); } diff --git a/src/test/ruby/test_kdf.rb b/src/test/ruby/test_kdf.rb new file mode 100644 index 00000000..c4be6fd1 --- /dev/null +++ b/src/test/ruby/test_kdf.rb @@ -0,0 +1,116 @@ +require File.expand_path('test_helper', File.dirname(__FILE__)) + +class TestKDF < TestCase + + def test_pkcs5_pbkdf2_hmac_compatibility + expected = OpenSSL::KDF.pbkdf2_hmac('password', salt: 'salt', iterations: 1, length: 20, hash: 'sha1') + assert_equal expected, OpenSSL::PKCS5.pbkdf2_hmac('password', 'salt', 1, 20, 'sha1') + assert_equal expected, OpenSSL::PKCS5.pbkdf2_hmac_sha1('password', 'salt', 1, 20) + end + + def test_pbkdf2_hmac_sha1_rfc6070_c_1_len_20 + expected = b(%w[0c 60 c8 0f 96 1f 0e 71 f3 a9 b5 24 af 60 12 06 2f e0 37 a6]) + value = OpenSSL::KDF.pbkdf2_hmac('password', salt: 'salt', iterations: 1, length: 20, hash: 'sha1') + assert_equal expected, value + end + + def test_pbkdf2_hmac_sha1_rfc6070_c_2_len_20 + expected = b(%w[ea 6c 01 4d c7 2d 6f 8c cd 1e d9 2a ce 1d 41 f0 d8 de 89 57]) + value = OpenSSL::KDF.pbkdf2_hmac('password', salt: 'salt', iterations: 2, length: 20, hash: 'sha1') + assert_equal expected, value + end + + def test_pbkdf2_hmac_sha1_rfc6070_c_4096_len_20 + expected = b(%w[4b 00 79 01 b7 65 48 9a be ad 49 d9 26 f7 21 d0 65 a4 29 c1]) + value = OpenSSL::KDF.pbkdf2_hmac('password', salt: 'salt', iterations: 4096, length: 20, hash: 'sha1') + assert_equal expected, value + end + + def test_pbkdf2_hmac_sha1_rfc6070_c_4096_len_25 + expected = b(%w[3d 2e ec 4f e4 1c 84 9b 80 c8 d8 36 62 c0 e4 4a 8b 29 1a 96 4c f2 f0 70 38]) + value = OpenSSL::KDF.pbkdf2_hmac('passwordPASSWORDpassword', + salt: 'saltSALTsaltSALTsaltSALTsaltSALTsalt', + iterations: 4096, + length: 25, + hash: 'sha1') + assert_equal expected, value + end + + def test_pbkdf2_hmac_sha1_rfc6070_c_4096_len_16 + expected = b(%w[56 fa 6a a7 55 48 09 9d cc 37 d7 f0 34 25 e0 c3]) + value = OpenSSL::KDF.pbkdf2_hmac("pass\0word", salt: "sa\0lt", iterations: 4096, length: 16, hash: 'sha1') + assert_equal expected, value + end + + def test_pbkdf2_hmac_sha256_c_20000_len_32 + password = 'password' + salt = OpenSSL::Random.random_bytes(16) + value1 = OpenSSL::KDF.pbkdf2_hmac(password, salt: salt, iterations: 20_000, length: 32, hash: 'sha256') + value2 = OpenSSL::KDF.pbkdf2_hmac(password, salt: salt, iterations: 20_000, length: 32, hash: 'sha256') + assert_equal value1, value2 + end + + def test_scrypt_rfc7914_first + skip 'scrypt is not implemented' unless OpenSSL::KDF.respond_to?(:scrypt) + + expected = b(%w[ + 77 d6 57 62 38 65 7b 20 3b 19 ca 42 c1 8a 04 97 + f1 6b 48 44 e3 07 4a e8 df df fa 3f ed e2 14 42 + fc d0 06 9d ed 09 48 f8 32 6a 75 3a 0f c8 1f 17 + e8 d3 e0 fb 2e 0d 36 28 cf 35 e2 0c 38 d1 89 06 + ]) + assert_equal expected, OpenSSL::KDF.scrypt('', salt: '', N: 16, r: 1, p: 1, length: 64) + end + + def test_scrypt_rfc7914_second + skip 'scrypt is not implemented' unless OpenSSL::KDF.respond_to?(:scrypt) + + expected = b(%w[ + fd ba be 1c 9d 34 72 00 78 56 e7 19 0d 01 e9 fe + 7c 6a d7 cb c8 23 78 30 e7 73 76 63 4b 37 31 62 + 2e af 30 d9 2e 22 a3 88 6f f1 09 27 9d 98 30 da + c7 27 af b9 4a 83 ee 6d 83 60 cb df a2 cc 06 40 + ]) + assert_equal expected, OpenSSL::KDF.scrypt('password', salt: 'NaCl', N: 1024, r: 8, p: 16, length: 64) + end + + def test_hkdf_rfc5869_test_case_1 + assert_equal b('3cb25f25faacd57a90434f64d0362f2a' \ + '2d2d0a90cf1a5a4c5db02d56ecc4c5bf' \ + '34007208d5b887185865'), + OpenSSL::KDF.hkdf(b('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'), + salt: b('000102030405060708090a0b0c'), + info: b('f0f1f2f3f4f5f6f7f8f9'), + length: 42, + hash: 'sha256') + end + + def test_hkdf_rfc5869_test_case_3 + assert_equal b('8da4e775a563c18f715f802a063c5a31' \ + 'b8a11f5c5ee1879ec3454e5f3c738d2d' \ + '9d201395faa4b61a96c8'), + OpenSSL::KDF.hkdf(b('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'), + salt: b(''), + info: b(''), + length: 42, + hash: 'sha256') + end + + def test_hkdf_rfc5869_test_case_4 + assert_equal b('085a01ea1b10f36933068b56efa5ad81' \ + 'a4f14b822f5b091568a9cdd4f155fda2' \ + 'c22e422478d305f3f896'), + OpenSSL::KDF.hkdf(b('0b0b0b0b0b0b0b0b0b0b0b'), + salt: b('000102030405060708090a0b0c'), + info: b('f0f1f2f3f4f5f6f7f8f9'), + length: 42, + hash: 'sha1') + end + + private + + def b(ary) + [Array(ary).join].pack('H*') + end + +end \ No newline at end of file From 803970b3fb6273dc53be6bf992b209d75afa112b Mon Sep 17 00:00:00 2001 From: kares Date: Sat, 7 Mar 2026 20:09:24 +0100 Subject: [PATCH 20/21] [fix] fallback to raw bytes on unknown tag --- src/main/java/org/jruby/ext/openssl/ASN1.java | 25 +++----- .../org/jruby/ext/openssl/X509Extension.java | 12 +++- src/test/ruby/test_asn1.rb | 3 + src/test/ruby/x509/test_x509cert.rb | 60 +++++++++++++++++++ 4 files changed, 81 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/ASN1.java b/src/main/java/org/jruby/ext/openssl/ASN1.java index adfa2d83..f90c2512 100644 --- a/src/main/java/org/jruby/ext/openssl/ASN1.java +++ b/src/main/java/org/jruby/ext/openssl/ASN1.java @@ -421,7 +421,8 @@ static String ln2oid(final Ruby runtime, final String ln) { } static Integer oid2nid(final Ruby runtime, final ASN1ObjectIdentifier oid) { - return oidToNid(runtime).get(oid); + final Integer nid = oidToNid(runtime).get(oid); + return nid == null ? ASN1Registry.oid2nid(oid) : nid; } static String o2a(final Ruby runtime, final ASN1ObjectIdentifier oid) { @@ -429,35 +430,23 @@ static String o2a(final Ruby runtime, final ASN1ObjectIdentifier oid) { } static String o2a(final Ruby runtime, final ASN1ObjectIdentifier oid, final boolean silent) { - Integer nid = oidToNid(runtime).get(oid); - if ( nid != null ) { - final String name = nid2ln(runtime, nid, false); - return name == null ? nid2sn(runtime, nid, false) : name; - } - nid = ASN1Registry.oid2nid(oid); + final Integer nid = oid2nid(runtime, oid); if ( nid == null ) { if ( silent ) return null; throw new NullPointerException("nid not found for oid = '" + oid + "' (" + runtime + ")"); } - final String name = nid2ln(runtime, nid, false); - if ( name != null ) return name; - return nid2sn(runtime, nid, true); + final String name = nid2ln(runtime, nid); + return name == null ? nid2sn(runtime, nid) : name; } static String oid2name(final Ruby runtime, final ASN1ObjectIdentifier oid, final boolean silent) { - Integer nid = oidToNid(runtime).get(oid); - if ( nid != null ) { - final String name = nid2sn(runtime, nid, false); - return name == null ? nid2ln(runtime, nid, false) : name; - } - nid = ASN1Registry.oid2nid(oid); + final Integer nid = oid2nid(runtime, oid); if ( nid == null ) { if ( silent ) return null; throw new NullPointerException("nid not found for oid = '" + oid + "' (" + runtime + ")"); } final String name = nid2sn(runtime, nid, false); - if ( name != null ) return name; - return nid2ln(runtime, nid, true); + return name == null ? nid2ln(runtime, nid) : name; /* if ( nid == null ) nid = ASN1Registry.oid2nid(oid); if ( nid == null ) { diff --git a/src/main/java/org/jruby/ext/openssl/X509Extension.java b/src/main/java/org/jruby/ext/openssl/X509Extension.java index 118dbb9a..d5bb72d8 100644 --- a/src/main/java/org/jruby/ext/openssl/X509Extension.java +++ b/src/main/java/org/jruby/ext/openssl/X509Extension.java @@ -574,7 +574,17 @@ else if ( entry.respondsTo("value") ) { private RubyString rawValueAsString(final ThreadContext context) throws IOException { final Ruby runtime = context.runtime; - final IRubyObject value = getValue(runtime); // e.g. [ ASN1::UTF8String, ... ] + final IRubyObject value; + try { + value = getValue(runtime); // e.g. [ ASN1::UTF8String, ... ] + } + catch (IOException|IllegalArgumentException e) { + // a generic extension may be syntactically valid DER but still fail when we try + // to map its payload into ASN.1 object model (e.g. because of unsupported nested tags). + // MRI/OpenSSL still renders the raw extension bytes in to_text instead of raising. + debugStackTrace(runtime, "X509Extension.rawValueAsString", e); + return StringHelper.newString(runtime, getRealValueEncoded()); + } if ( value instanceof RubyArray ) { final RubyArray arr = (RubyArray) value; final ByteList strVal = new ByteList(64); diff --git a/src/test/ruby/test_asn1.rb b/src/test/ruby/test_asn1.rb index 302c00ef..86824d32 100644 --- a/src/test/ruby/test_asn1.rb +++ b/src/test/ruby/test_asn1.rb @@ -321,6 +321,9 @@ def test_object_identifier assert_equal "2.16.840.1.101.3.4.2.1", obj.oid assert_equal "SHA256", obj.sn assert_equal "sha256", obj.ln + scts = OpenSSL::ASN1::ObjectId.new("1.3.6.1.4.1.11129.2.4.2") + assert_equal "ct_precert_scts", scts.sn + assert_equal "CT Precertificate SCTs", scts.ln # TODO: Import Issue # Fails with: expected but was ) #assert_raise(OpenSSL::ASN1::ASN1Error) { diff --git a/src/test/ruby/x509/test_x509cert.rb b/src/test/ruby/x509/test_x509cert.rb index 697dcca6..027ff326 100644 --- a/src/test/ruby/x509/test_x509cert.rb +++ b/src/test/ruby/x509/test_x509cert.rb @@ -324,6 +324,66 @@ def test_to_text_regression assert cert.to_text.index('Signature Algorithm: sha256WithRSAEncryption') end + def test_to_text_issue_322_sigstore_certificate + # https://github.com/jruby/jruby-openssl/issues/322 + cert = OpenSSL::X509::Certificate.new <<-EOF +-----BEGIN CERTIFICATE----- +MIIIMDCCB7agAwIBAgIUaUHXj0S4ZNEEjDxaXlzPw/VYQQ4wCgYIKoZIzj0EAwMw +NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl +cm1lZGlhdGUwHhcNMjMwOTI3MTYwNDQwWhcNMjMwOTI3MTYxNDQwWjAAMFkwEwYH +KoZIzj0CAQYIKoZIzj0DAQcDQgAEad0Uh6twE3x8YAbfBme0T/G0V2xxIl0rw/uY +8GfamPrQk3AzW9b/TwQMtipyTY2GAPDC7SVbZTxGBd6BtTWUmqOCBtUwggbRMA4G +A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUOizU +dUPvmWDSB8LtOjpjyLNKgM0wHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y +ZD8wgaUGA1UdEQEB/wSBmjCBl4aBlGh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9y +ZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRjLWJl +YWNvbi8uZ2l0aHViL3dvcmtmbG93cy9leHRyZW1lbHktZGFuZ2Vyb3VzLW9pZGMt +YmVhY29uLnltbEByZWZzL2hlYWRzL21haW4wOQYKKwYBBAGDvzABAQQraHR0cHM6 +Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAfBgorBgEEAYO/ +MAECBBF3b3JrZmxvd19kaXNwYXRjaDA2BgorBgEEAYO/MAEDBChmZTdhZGU5MWY0 +YzRkNDZjZTc5ODg2ZmE4MGRmODAwNmEzZmFlOWUyMC0GCisGAQQBg78wAQQEH0V4 +dHJlbWVseSBkYW5nZXJvdXMgT0lEQyBiZWFjb24wSQYKKwYBBAGDvzABBQQ7c2ln +c3RvcmUtY29uZm9ybWFuY2UvZXh0cmVtZWx5LWRhbmdlcm91cy1wdWJsaWMtb2lk +Yy1iZWFjb24wHQYKKwYBBAGDvzABBgQPcmVmcy9oZWFkcy9tYWluMDsGCisGAQQB +g78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50 +LmNvbTCBpgYKKwYBBAGDvzABCQSBlwyBlGh0dHBzOi8vZ2l0aHViLmNvbS9zaWdz +dG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRj +LWJlYWNvbi8uZ2l0aHViL3dvcmtmbG93cy9leHRyZW1lbHktZGFuZ2Vyb3VzLW9p +ZGMtYmVhY29uLnltbEByZWZzL2hlYWRzL21haW4wOAYKKwYBBAGDvzABCgQqDChm +ZTdhZGU5MWY0YzRkNDZjZTc5ODg2ZmE4MGRmODAwNmEzZmFlOWUyMB0GCisGAQQB +g78wAQsEDwwNZ2l0aHViLWhvc3RlZDBeBgorBgEEAYO/MAEMBFAMTmh0dHBzOi8v +Z2l0aHViLmNvbS9zaWdzdG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vy +b3VzLXB1YmxpYy1vaWRjLWJlYWNvbjA4BgorBgEEAYO/MAENBCoMKGZlN2FkZTkx +ZjRjNGQ0NmNlNzk4ODZmYTgwZGY4MDA2YTNmYWU5ZTIwHwYKKwYBBAGDvzABDgQR +DA9yZWZzL2hlYWRzL21haW4wGQYKKwYBBAGDvzABDwQLDAk2MzI1OTY4OTcwNwYK +KwYBBAGDvzABEAQpDCdodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUtY29uZm9y +bWFuY2UwGQYKKwYBBAGDvzABEQQLDAkxMzE4MDQ1NjMwgaYGCisGAQQBg78wARIE +gZcMgZRodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUtY29uZm9ybWFuY2UvZXh0 +cmVtZWx5LWRhbmdlcm91cy1wdWJsaWMtb2lkYy1iZWFjb24vLmdpdGh1Yi93b3Jr +Zmxvd3MvZXh0cmVtZWx5LWRhbmdlcm91cy1vaWRjLWJlYWNvbi55bWxAcmVmcy9o +ZWFkcy9tYWluMDgGCisGAQQBg78wARMEKgwoZmU3YWRlOTFmNGM0ZDQ2Y2U3OTg4 +NmZhODBkZjgwMDZhM2ZhZTllMjAhBgorBgEEAYO/MAEUBBMMEXdvcmtmbG93X2Rp +c3BhdGNoMIGBBgorBgEEAYO/MAEVBHMMcWh0dHBzOi8vZ2l0aHViLmNvbS9zaWdz +dG9yZS1jb25mb3JtYW5jZS9leHRyZW1lbHktZGFuZ2Vyb3VzLXB1YmxpYy1vaWRj +LWJlYWNvbi9hY3Rpb25zL3J1bnMvNjMyODQ5OTI2My9hdHRlbXB0cy8xMBYGCisG +AQQBg78wARYECAwGcHVibGljMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUA3T0wasbH +ETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGK12KksgAABAMARjBEAiB/73GK +v9a3CdW4uBkWhNw1W0YCeLuBLRi/Pv6yrASVpwIgOrK8L2ubaLnXSWAiK76oDmmJ +1MaHKGanSuh13pxW4fgwCgYIKoZIzj0EAwMDaAAwZQIwaG18DfwChTX9hPA/WADa +i9Wh9i3hESo5Nixoff/71AtMwETfBDu2MVN3lqo8o73NAjEAxed8hLxiJdxmZ3ZA +XPOarzmFTZLPC794+i15i7RqInsZ49FtUVLjHuvccINZL63Y +-----END CERTIFICATE----- +EOF + + assert_equal 2, cert.version + assert cert.extensions.any? { |ext| ext.oid == '1.3.6.1.4.1.57264.1.13' } + + text = cert.to_text + assert text.index('X509v3 Subject Alternative Name: critical') + assert text.index('1.3.6.1.4.1.57264.1.13:') + assert text.index('CT Precertificate SCTs:') + end + def test_to_text_read_back crt = File.expand_path('ca.crt', File.dirname(__FILE__)) cert = OpenSSL::X509::Certificate.new File.read(crt) From 4e1cbbd89ed582309d0d3d0c4578f1553ad8f92b Mon Sep 17 00:00:00 2001 From: kares Date: Sun, 8 Mar 2026 11:01:57 +0100 Subject: [PATCH 21/21] [compat] implement OpenSSL::PKey#compare? --- src/main/java/org/jruby/ext/openssl/PKey.java | 19 +++++++ src/test/ruby/fixtures/pkey/rsa-2.pem | 51 +++++++++++++++++++ src/test/ruby/test_pkey.rb | 28 ++++++++++ 3 files changed, 98 insertions(+) create mode 100644 src/test/ruby/fixtures/pkey/rsa-2.pem diff --git a/src/main/java/org/jruby/ext/openssl/PKey.java b/src/main/java/org/jruby/ext/openssl/PKey.java index b44b9686..51ff30d4 100644 --- a/src/main/java/org/jruby/ext/openssl/PKey.java +++ b/src/main/java/org/jruby/ext/openssl/PKey.java @@ -303,6 +303,25 @@ public IRubyObject verify(IRubyObject digest, IRubyObject sign, IRubyObject data } } + // Used primarily to check if an OpenSSL::X509::Certificate#public_key compares to its private key. + @JRubyMethod(name = "compare?") + public IRubyObject compare_p(ThreadContext context, IRubyObject arg) { + final Ruby runtime = context.runtime; + if (!(arg instanceof PKey)) { + throw runtime.newTypeError("OpenSSL::PKey::PKey expected but got " + arg.getMetaClass().getRealClass().getName()); + } + final PKey other = (PKey) arg; + if (!getKeyType().equals(other.getKeyType())) { + throw runtime.newTypeError("Cannot compare different key types"); + } + final PublicKey myPub = getPublicKey(); + final PublicKey otherPub = other.getPublicKey(); + if (myPub == null || otherPub == null) { + return runtime.getFalse(); + } + return runtime.newBoolean(java.util.Arrays.equals(myPub.getEncoded(), otherPub.getEncoded())); + } + static String getDigestAlgName(IRubyObject digest) { if (digest.isNil()) return "SHA256"; if (digest instanceof Digest) return ((Digest) digest).getShortAlgorithm(); diff --git a/src/test/ruby/fixtures/pkey/rsa-2.pem b/src/test/ruby/fixtures/pkey/rsa-2.pem new file mode 100644 index 00000000..e4fd4f43 --- /dev/null +++ b/src/test/ruby/fixtures/pkey/rsa-2.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/src/test/ruby/test_pkey.rb b/src/test/ruby/test_pkey.rb index c06cc86f..f8fb301d 100644 --- a/src/test/ruby/test_pkey.rb +++ b/src/test/ruby/test_pkey.rb @@ -97,6 +97,34 @@ def test_pkey_dh assert_equal nil, dh.q end + def test_compare? + key1 = Fixtures.pkey("rsa-1.pem") + key2 = Fixtures.pkey("rsa-1.pem") + key3 = Fixtures.pkey("rsa-2.pem") + key4 = Fixtures.pkey("p256") + + assert_equal(true, key1.compare?(key2)) + assert_equal(true, key1.public_key.compare?(key2)) + assert_equal(true, key2.compare?(key1)) + assert_equal(true, key2.public_key.compare?(key1)) + + assert_equal(false, key1.compare?(key3)) + + assert_raise(TypeError) do + key1.compare?(key4) + end + end + + def test_compare_with_certificate_public_key + fixtures = File.dirname(__FILE__) + cert = OpenSSL::X509::Certificate.new(File.read(File.join(fixtures, 'pkey-cert.pem'))) + matching_key = OpenSSL::PKey.read(File.read(File.join(fixtures, 'pkey-pkcs8.pem'))) + other_key = Fixtures.pkey("rsa-1.pem") + + assert_equal true, matching_key.compare?(cert.public_key) + assert_equal false, other_key.compare?(cert.public_key) + end + def test_to_java pkey = OpenSSL::PKey.read(KEY) assert_kind_of java.security.PublicKey, pkey.to_java