有没有一种简单的方法可以在 Java 中使用 OpenSSL BIO 对象,或者有其他选择吗?

Is there a simple way to use OpenSSL BIO objects in Java or are there any alternatives?

谁能告诉我有什么方法可以使用 OpenSSL's BIO 来自 Java 的对象吗?

我正在做一个项目,旨在为 TinyRadius 处理 PEAP (https://en.wikipedia.org/wiki/Protected_Extensible_Authentication_Protocol) 数据包提供支持。

我试图在 Java 中搜索任何现有的 PEAP 实现,但似乎没有。

我成功地找到了一个用 Python 编写的实现,它使用 pyOpenSSL 来解密和加密 PEAP 会话中的数据。但问题是代码使用了几个 OpenSSL 功能,javax.net.ssl 没有提供这些功能,例如读取和写入 SSL 会话的 BIO 对象或获取主密钥和安全随机数,由客户端从会话中生成。

这是我正在尝试移植的代码示例:

def get_keys(self):
    self.master_key = self.ssl_connection.master_key()
    self.server_sec_random = self.ssl_connection.server_random()
    self.client_sec_random = self.ssl_connection.client_random()
...
def write(self, data):
    self.ssl_connection.bio_write(data)
...
def read(self):
    return self.ssl_connection.bio_read(4096)

我研究了 pyOpenSSL,发现所有这些调用都只是通过 libffi (http://sourceware.org/libffi) 对 OpenSSL 库函数的包装,但我不知道如何在 Java 中实现相同的功能。

据我所知,唯一的方法是使用 JNI(或 JNA)调用 OpenSSL 函数。此外,我需要实现用于管理对象生命周期的代码,该代码是在 OpenSSL 访问期间创建的,但我不知道该怎么做,因为我之前没有任何使用 Java 的本机代码的经验。

如果有人知道使用 Java 中的 OpenSSL 的其他方法或者 OpenSSL 的一些现成的实现或端口,请告诉我 - 所有答案都非常感谢.

谢谢!

在多次搜索 Java 中使用 OpenSSL 的方法后,我最终得到了 JNA 包装器实现,令人惊讶的是,它看起来非常简单。

幸运的是,OpenSSL 的设计方式使得在绝大多数用例中我们不需要确切知道从调用 OpenSSL 函数返回的值的类型(例如,OpenSSL 不需要调用任何来自结构的方法或直接使用结构字段),因此不需要对 OpenSSL 的数据类型进行庞大而复杂的包装。

OpenSSL的大部分函数都是用指针来操作的,可以将指针包装成com.sun.jna.Pointerclass的一个实例,相当于用C语言转换为void*,而被调用者将确定正确的类型并正确取消引用给定的指针。

这里是一个关于如何加载 ssleay32.dll、初始化库和创建上下文的小代码示例:

1) 定义 ssleay32.dll 库的接口(函数列表可以从 OpenSSL GitHub repo 或通过研究 dll 的导出部分获得):

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;

public interface OpenSSLLib extends Library {
  public OpenSSLLib INSTANCE = (OpenSSLLib) Native.loadLibrary("ssleay32",
                               OpenSSLLib.class);
  // Non-re-enterable! Should be called once per a thread (process).
  public void SSL_library_init();
  public void SSL_load_error_strings();

  // Supported context methods.
  public Pointer TLSv1_method();

  ...

  // Context-related methods.
  public Pointer SSL_CTX_new(Pointer method);
  public void SSL_CTX_free(Pointer context);
  public int SSL_CTX_use_PrivateKey_file(Pointer context, String filePath, int type);
  public int SSL_CTX_use_certificate_file(Pointer context, String filePath, int type);
  public int SSL_CTX_check_private_key(Pointer context);
  public int SSL_CTX_ctrl(Pointer context, int cmd, int larg, Pointer arg);
  public void SSL_CTX_set_verify(Pointer context, int mode, Pointer verifyCallback);
  public int SSL_CTX_set_cipher_list(Pointer context, String cipherList);

  public Pointer SSL_new(Pointer context);
  public void SSL_free(Pointer ssl);

  ...
}

2) 初始化库:

...
public static OpenSSLLib libSSL;
public static LibEayLib  libEay;
...

static {
    libSSL = OpenSSLLib.INSTANCE;
    libEay = LibEayLib.INSTANCE;
    libSSL.SSL_library_init();
    libEay.OPENSSL_add_all_algorithms_conf(); // This function is called from                                              
                                              // libeay32.dll via another JNA interface.
    libSSL.SSL_load_error_strings();
}

...

3) 创建并初始化 SSL_CTXSSL 对象:

public class SSLEndpoint {
  public Pointer context; // SSL_CTX*
  public Pointer ssl;     // SSL*
  ...
}

...

SSLEndpoint endpoint = new SSLEndpoint();     

...

// Use one of supported SSL/TLS methods; here is the example for TLSv1 Method
endpoint.context = libSSL.SSL_CTX_new(libSSL.TLSv1_method());

if(endpoint.context.equals(Pointer.NULL)) {
  throw new SSLGeneralException("Failed to create SSL Context!");
}

int res = libSSL.SSL_CTX_set_cipher_list(endpoint.context, OpenSSLLib.DEFAULT_CIPHER_LIST);
if(res != 1) {
  throw new SSLGeneralException("Failed to set the default cipher list!");
}

libSSL.SSL_CTX_set_verify(endpoint.context, OpenSSLLib.SSL_VERIFY_NONE, Pointer.NULL);

// pathToCert is a String object, which defines a path to a cerificate
// in PEM format. 
res = libSSL.SSL_CTX_use_certificate_file(endpoint.context, pathToCert, certKeyTypeToX509Const(certType));
if(res != 1) {
  throw new SSLGeneralException("Failed to load the cert file " + pathToCert);
}

// pathToKey is a String object, which defines a path to a priv. key
// in PEM format.
res = libSSL.SSL_CTX_use_PrivateKey_file(endpoint.context, pathToKey, certKeyTypeToX509Const(keyType));
if(res != 1) {
  throw new SSLGeneralException("Failed to load the private key file " + pathToKey);
}

res = libSSL.SSL_CTX_check_private_key(endpoint.context);
if(res != 1) {
  throw new SSLGeneralException("Given key " + pathToKey + " seems to be not valid.");
}

SSLGeneralException是自定义异常,简单继承自RuntimeException.

...
// Create and init SSL object with given SSL_CTX
endpoint.ssl = libSSL.SSL_new(endpoint.context);
...

下一步可能是创建 BIO 对象并将它们链接到 SSL 对象。

关于原题,client/server安全随机数和主密钥可以通过以下方法获得:

public int SSL_get_client_random(Pointer ssl, byte[] out, int outLen);
public int SSL_get_server_random(Pointer ssl, byte[] out, int outLen);
public int SSL_SESSION_get_master_key(Pointer session, byte[] out, int outLen);

请注意,JNA 将搜索 dll 以加载 jna.library.path 参数。因此,如果 dll 位于目录 D:\dlls 中,您必须指定这样的 VM 选项:

-Djna.library.path="D:/dll"

此外,JNA 需要 32 位 JRE 来实现 32 位 dll(可能 libffi 约束?)。如果您尝试使用 64 位 JRE 加载 32 位 dll,则会出现异常。