diff --git a/src/main/java/org/jruby/ext/openssl/ASN1.java b/src/main/java/org/jruby/ext/openssl/ASN1.java
index 3e7417e4..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 ) {
@@ -1841,6 +1830,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 +2033,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 +2076,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 +2101,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 +2159,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/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/main/java/org/jruby/ext/openssl/PKey.java b/src/main/java/org/jruby/ext/openssl/PKey.java
index 51b99b48..51ff30d4 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);
@@ -278,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) );
}
@@ -294,6 +303,31 @@ 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();
+ return digest.asJavaString();
+ }
+
static RubyString convertToString(final Ruby runtime, final IRubyObject str, final String errorType, final CharSequence errorMsg) {
try {
return str.convertToString();
@@ -395,9 +429,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/PKeyDSA.java b/src/main/java/org/jruby/ext/openssl/PKeyDSA.java
index 83c6d1fd..4d9ea39f 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);
}
@@ -434,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!");
}
@@ -448,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/main/java/org/jruby/ext/openssl/PKeyEC.java b/src/main/java/org/jruby/ext/openssl/PKeyEC.java
index 77dd75ed..96a26964 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 {
@@ -555,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");
@@ -575,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;
@@ -784,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);
}
}
@@ -804,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);
}
}
@@ -860,6 +923,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 +946,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 +1093,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/main/java/org/jruby/ext/openssl/PKeyRSA.java b/src/main/java/org/jruby/ext/openssl/PKeyRSA.java
index 7e0e28bb..e13f7698 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;
@@ -51,7 +53,29 @@
import static javax.crypto.Cipher.*;
+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.DataLengthException;
+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;
+import org.bouncycastle.pkcs.jcajce.JcaPKCS8EncryptedPrivateKeyInfoBuilder;
+import org.bouncycastle.pkcs.jcajce.JcePKCSPBEOutputEncryptorBuilder;
import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.RubyBignum;
@@ -61,8 +85,10 @@
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;
import org.jruby.runtime.Arity;
import org.jruby.runtime.Block;
import org.jruby.runtime.ObjectAllocator;
@@ -70,6 +96,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.*;
@@ -308,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();
@@ -502,9 +530,92 @@ 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(), "");
+ throw newPKeyError(getRuntime(), "");
}
// BC accepts "/NONE/*" but SunJCE doesn't. use "/ECB/*"
String p = "/ECB/PKCS1Padding";
@@ -524,7 +635,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);
}
@@ -534,7 +645,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);
}
@@ -544,7 +655,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);
}
@@ -554,7 +665,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);
}
@@ -580,6 +691,449 @@ 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(hashBytes, digestAlg, mgf1Alg, saltLen));
+ } catch (IllegalArgumentException | CryptoException e) {
+ throw (RaiseException) newPKeyError(runtime, e.getMessage()).initCause(e);
+ }
+ }
+ }
+
+ // 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);
+ // verify_raw: input is already the hash → use PreHashedDigest (pass-through phase 1)
+ return verifyPSS(runtime, true, hashBytes, digestAlg, mgf1Alg, saltLen, sigBytes);
+ }
+ }
+
+ // 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();
+
+ // verify (non-raw): feed raw data; PSSSigner will hash it internally via SHA-NNN
+ return verifyPSS(runtime, false, dataBytes, digestAlg, mgf1Alg, saltLen, sigBytes);
+ }
+ }
+
+ // Fall back to standard PKey#verify (PKCS#1 v1.5)
+ 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");
+ }
+
+ 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(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(getRuntime())));
+ signer.update(hashBytes, 0, hashBytes.length);
+ return signer.generateSignature();
+ }
+
+ // 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 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);
+ verifier.init(false, new RSAKeyParameters(false, pubKey.getModulus(), pubKey.getPublicExponent()));
+ 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());
+ }
+
+ // 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 1afc2ffd..15ec8eee 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,32 @@ 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;
+ 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/main/java/org/jruby/ext/openssl/X509Cert.java b/src/main/java/org/jruby/ext/openssl/X509Cert.java
index 5d4fe48c..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;
@@ -303,6 +304,49 @@ 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);
+ }
+ }
+
+ @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/main/java/org/jruby/ext/openssl/X509Extension.java b/src/main/java/org/jruby/ext/openssl/X509Extension.java
index 48aafbd6..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);
@@ -796,6 +806,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/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/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/main/java/org/jruby/ext/openssl/impl/PKey.java b/src/main/java/org/jruby/ext/openssl/impl/PKey.java
index c50726ae..23d2fd15 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);
@@ -139,11 +153,23 @@ 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 instanceof SubjectPublicKeyInfo) {
+ return new JcaPEMKeyConverter().getPublicKey((SubjectPublicKeyInfo) pemObject);
+ }
+ }
+ // 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
@@ -273,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) {
@@ -322,7 +352,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/main/java/org/jruby/ext/openssl/x509store/PEMInputOutput.java b/src/main/java/org/jruby/ext/openssl/x509store/PEMInputOutput.java
index b515a9d8..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;
@@ -112,6 +113,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 +363,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 +381,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 +457,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);
@@ -1041,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 {
@@ -1053,6 +1123,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/dsa/test_dsa.rb b/src/test/ruby/dsa/test_dsa.rb
index 3235777d..b73103a8 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
@@ -98,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/ec/test_ec.rb b/src/test/ruby/ec/test_ec.rb
index d11f8958..c0b45b1a 100644
--- a/src/test/ruby/ec/test_ec.rb
+++ b/src/test/ruby/ec/test_ec.rb
@@ -432,6 +432,57 @@ 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)
+ 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]
@@ -488,6 +539,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"
@@ -497,10 +556,50 @@ 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_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
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-----
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/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/rsa/test_rsa.rb b/src/test/ruby/rsa/test_rsa.rb
index d9b24d1c..cb7bdf65 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,157 @@ 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
+
+ # 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_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_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))
@@ -277,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
@@ -339,11 +499,80 @@ 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")
- #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
@@ -368,7 +597,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
@@ -416,6 +645,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 +690,4 @@ def dup_public(key)
raise "unknown key type: #{key.class}"
end
end
-
end
diff --git a/src/test/ruby/test_asn1.rb b/src/test/ruby/test_asn1.rb
index 0cc59e27..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) {
@@ -620,61 +623,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
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
diff --git a/src/test/ruby/test_pkey.rb b/src/test/ruby/test_pkey.rb
index 2a0f0e12..f8fb301d 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
@@ -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
diff --git a/src/test/ruby/x509/test_x509cert.rb b/src/test/ruby/x509/test_x509cert.rb
index fac2a77d..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)
@@ -426,6 +486,39 @@ 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_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)
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)
diff --git a/src/test/ruby/x509/test_x509req.rb b/src/test/ruby/x509/test_x509req.rb
index 14a1786b..aac2ec04 100644
--- a/src/test/ruby/x509/test_x509req.rb
+++ b/src/test/ruby/x509/test_x509req.rb
@@ -109,6 +109,56 @@ 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
+ end
+
# from GH-150
def test_to_der_new_from_der; require 'base64'
# Build the CSR