LoginSignup
5
2

More than 5 years have passed since last update.

OpenSSL の BIO を自作するには

Last updated at Posted at 2019-03-06

OpenSSL の BIO_METHOD について書きます

BIO_METHOD で検索しても BIO_METHOD を解説している情報が見つからないので書いてみます。

大事なことを最初に書いておきます。

BIO は入出力を抽象化したもので、BIO_METHOD構造体の定義が、BIO の実体となります。
BIO_METHOD構造体の定義を行うということは、bwrite などの関数を実装するということです。もし、OpenSSL が内部に実装していない種類の入出力を TLS プロトコルを使ったセキュアのものにしたいと考えたとき、あなたは、独自の BIO_METHOD構造体 を作ることで、それを実現できます。

IBMの OpenSSL API の説明ページ https://www.ibm.com/developerworks/jp/linux/library/l-openssl/index.html はとても素晴らしいと思うので一部引用します。「OpenSSL API のマニュアルは、記述が少し不明瞭です。」「OpenSSL の実装方法を学ぶ上での問題として、マニュアルが完全ではないということがあります。」これらは全く正しく、BIO_METHOD が何かを説明する情報はあまり見つかりません。

この解説ページで
bio = BIO_new_connect("hostname:port");
というコードが出てきます。この関数 BIO_new_connect のソースコードを見てみます。
(ソースは openssl-1.0.2k のものを使っています。)

BIO *BIO_new_connect(const char *str)
{
    BIO *ret;

    ret = BIO_new(BIO_s_connect());
    if (ret == NULL)
        return (NULL);
    if (BIO_set_conn_hostname(ret, str))
        return (ret);
    else {
        BIO_free(ret);
        return (NULL);
    }
}

とてもシンプルなソースとなっていて助かります。

この関数が返しているのは BIOポインタですが、"BIO OpenSSL" を検索すると解説記事は見つかります。書籍「OpenSSL―暗号・PKI・SSL/TLSライブラリの詳細」から引用すると、「BIOパッケージは、入出力を処理するための強力な抽象化機能を備えています。」とあります。つまり、入出力を処理するものが BIO です。

BIOポインタは、BIO_new で返されるものなので、BIO_set_conn_hostname は後回しにして、BIO_new(BIO_s_connect()) の実装を調べてみます。

BIO_new は、BIO_METHOD のポインタを引数に取ります。この引数が解説したい BIO_METHOD です。

BIO_s_connect() は、BIO_METHOD のポインタを返すはずですが、ソースコードを見てみます。

BIO_METHOD *BIO_s_connect(void)
{
    return (&methods_connectp);
}

これまたシンプルなソースで、助かりますが、意味不明なので methods_connectp の定義も見てみます。

static BIO_METHOD methods_connectp = {
    BIO_TYPE_CONNECT,
    "socket connect",
    conn_write,
    conn_read,
    conn_puts,
    NULL,                       /* connect_gets, */
    conn_ctrl,
    conn_new,
    conn_free,
    conn_callback_ctrl,
};

BIO_METHOD 構造体をグローバルな領域に宣言しています。見ただけで、BIO_METHOD 構造体は、関数ポインタを集めた構造体だなというのが想像できます。
実際の構造体の宣言が以下です。

typedef struct bio_method_st {
    int type;
    const char *name;
    int (*bwrite) (BIO *, const char *, int);
    int (*bread) (BIO *, char *, int);
    int (*bputs) (BIO *, const char *);
    int (*bgets) (BIO *, char *, int);
    long (*ctrl) (BIO *, int, long, void *);
    int (*create) (BIO *);
    int (*destroy) (BIO *);
    long (*callback_ctrl) (BIO *, int, bio_info_cb *);
} BIO_METHOD;

methods_connectp は type に BIO_TYPE_CONNECT を使っていますが、それ以外の type は crypto/bio/bio.h に定義されています。どういう使い分けなのかわからないので、関数メンバーに注目します。
関数名は、bwrite や bread など、意味の取りやすい名前となっています。この BIO に対して書き込みを処理したい時には、bwrite が呼ばれるのだなというようなことが想像できます。分かりにくいのは ctrl の役割くらいでしょうか? create や destroy のタイミングも気になりますが、でも、まあ、気にしても仕方ない気もします。

これらの関数の中で最初に呼ばれるのは create だと思います。本当でしょうか?

BIO_new のソースは以下のようになっています。

BIO *BIO_new(BIO_METHOD *method)
{
    BIO *ret = NULL;

    ret = (BIO *)OPENSSL_malloc(sizeof(BIO));
    if (ret == NULL) {
        BIOerr(BIO_F_BIO_NEW, ERR_R_MALLOC_FAILURE);
        return (NULL);
    }
    if (!BIO_set(ret, method)) {
        OPENSSL_free(ret);
        ret = NULL;
    }
    return (ret);
}

この関数も大したことはしておらず、メモリを確保して BIO_set 関数を呼び出しています。BIO_set 関数の中で create が呼ばれていますので、やはり最初に呼ばれる関数は create で合っていました。

methods_connectp の create の実体は conn_new 関数です。
conn_new の実装を見れば、create の関数に求められていることがわかると思います。

今はたまたま BIO_new_connect から始めましたが、OpenSSL 内部に実装している BIOオブジェクト(=BIO_METHOD の定義)は他にもあります。"static BIO_METHOD" で grep すると 26個の定義が見つかりました。かなり多いと感じました。

以下はメモリバッファに対する入出力を処理するものの定義です。

static BIO_METHOD mem_method = {
    BIO_TYPE_MEM,
    "memory buffer",
    mem_write,
    mem_read,
    mem_puts,
    mem_gets,
    mem_ctrl,
    mem_new,
    mem_free,
    NULL,
};

conn_new, mem_new の両方に共通することは、create の関数 は、BIO オブジェクトの初期化も行っているということです。BIO構造体の method ポインタは BIO_METHOD構造体を指していますので BIO は BIO_METHOD を所有していると言えますが、所有者である BIO を制御しているのは BIO_METHOD なのです。

BIO構造体を記載していませんでしたので以下に記載します。
例えば init は初期化済みかどうかのフラグとして使われていますが、定義には何のコメントもなく、init をどういう用途で使おうと、それは BIO_METHOD の実装者にまかされていると思われます。だけど、名前は大事なので、このフラグを他の用途に使うことは、ばかげていると思います。なので、BIO_METHOD を実装する時には、OpenSSLにすでに実装してある他の BIO_METHOD を参考にすることをお勧めします。

struct bio_st {
    BIO_METHOD *method;
    /* bio, mode, argp, argi, argl, ret */
    long (*callback) (struct bio_st *, int, const char *, int, long, long);
    char *cb_arg;               /* first argument for the callback */
    int init;
    int shutdown;
    int flags;                  /* extra storage */
    int retry_reason;
    int num;
    void *ptr;
    struct bio_st *next_bio;    /* used by filter BIOs */
    struct bio_st *prev_bio;    /* used by filter BIOs */
    int references;
    unsigned long num_read;
    unsigned long num_write;
    CRYPTO_EX_DATA ex_data;
};

では、後回しにしていた BIO_set_conn_hostname を見ていきます。

# define BIO_set_conn_hostname(b,name) BIO_ctrl(b,BIO_C_SET_CONNECT,0,(char *)name)

BIO_ctrl 関数は、少し長い(25行)ので省略しますが、BIO の callback が定義されていれば、まずそれを呼び出します。次に BIO_METHOD の ctrl を呼び出します。
methods_connectp の場合、conn_new は BIO の callback に対し NULL を代入しますので、callback は呼ばれずに ctrl の実体である conn_ctrl が呼ばれます。

conn_ctrl がやっていることは、conn_new で確保していたメモリ領域にホスト名を保存することですが、そのあたりは、methods_connectp に固有の処理なので注目しません。ここで大事なことは、create 関数が、その BIO に固有な情報領域をメモリ確保し、確保したメモリアドレスを BIO の ptr に設定するという作法だと思います。

(書きかけ)

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2