07
11月
2007

OpenSSLでRSAを試してみる

OpenSSLにはRSAのキーペアを作成し,これを使用して暗号化,復号する機能や,キーをPEM形式で入出力する機能があるのでためしてみた.
概要
キーペアの作成
キーのダンプ出力
キーのPEM形式出力
エラーメッセージの出力
PEM形式キーファイルの読み込み
暗号化と復号
サンプルの実行

概要

今回は,OpenSSLで以下の作業をする.
RSAキーペアの作成
RSAは公開鍵方式なので,その公開鍵と秘密鍵を生成する.
RSAキーをPEM形式で入出力
生成したRSAの公開鍵と秘密鍵をPEM形式でファイルに出力したり,PEM形式のキーデータを読み込んだりする.
RSA暗号化と復号
RSAキーペアで暗号化と復号を行う.
具体的には,キーペアを作成してPEM形式出力するサンプルと,PEM形式キーファイルを読み込んでRSA暗号化,復号を行うサンプルを作成してみた.

キーペアの作成

では,キーペアを作成してPEM形式出力するサンプルを以下に示す.

#include <stdio.h>
#include <string.h>
#include <openssl/rsa.h>
#include <openssl/engine.h>
#include <openssl/pem.h>

static void printError(char *msg, unsigned long err);

int
main(int argc, char *argv[])
{
  int size = 1024;
  unsigned long expornent = 65537;
  FILE *privateKeyFile;
  FILE *publicKeyFile;

  if(argc != 3)
    {
      fprintf(stderr,
              "Usage : %s privateKeyFile publicKeyFile\n", argv[0]);
      exit(-1);
    }
  else
    {
      privateKeyFile = fopen(argv[1], "w");
      if (privateKeyFile == NULL)
        {
          perror("failed to fopen");
          exit(-1);
        }

      publicKeyFile = fopen(argv[2], "w");
      if (publicKeyFile == NULL)
        {
          perror("failed to fopen");
          exit(-1);
        }
    }

  // キーペアの作成
  RSA *rsaKey = RSA_generate_key(size, expornent, NULL, NULL);
  if (rsaKey == NULL)
    {
      printError("failed to RSA_generate_key",
                 ERR_get_error());
      exit(-1);
    }

  if(RSA_print_fp(stdout, rsaKey, 0) != 1)
    {
      printError("failed to RSA_print_fp",
                 ERR_get_error());
      exit(-1);
    }

  // 公開鍵をPEM形式で書き出し
  if(PEM_write_RSAPublicKey(publicKeyFile, rsaKey) != 1)
    {
      printError("failed to PEM_write_RSAPublicKey",
                 ERR_get_error());
      exit(-1);
    }

  // 秘密鍵をPEM形式で書き出し
  if(PEM_write_RSAPrivateKey(privateKeyFile, rsaKey,
                             NULL,
                             NULL, 0,
                             NULL, NULL) != 1)
    {
      printError("failed to PEM_write_RSAPrivateKey",
                 ERR_get_error());
      exit(-1);
    }

  // 領域の開放
  RSA_free(rsaKey);

  fclose(privateKeyFile);
  fclose(publicKeyFile);

  return 0;
}

static void
printError(char *msg, unsigned long err)
{
  char *errmsg = ERR_error_string(err, NULL);
  fprintf(stderr, "%s(%s)\n",
          msg,
          errmsg);
}

OpenSSLのキーペア作成関数は以下.
RSA *
RSA_generate_key(int num, unsigned long e,
                 void (*callback)(int,int,void *),
		 void *cb_arg);
引数の意味は以下.
モジュラスサイズ num
1024より小さいと脆弱なキーとなるので,1024より大きくする.
対数サイズ e
奇数であり,3,17,65537が良く使用されるらしい.
コールバック関数 callback
キー生成の過程で失敗した場合に,RSA_generate_key関数からコールバックする関数を指定できる. たぶんデバッグ用で,manにキー生成の段階に応じてどんな引数が設定されてコールバックされるかが書かれている.
コールバック関数の引数 cb_arg
コールバック関数がコールバックされる際の引数.
返り値はRSA構造体へのポインタであり,公開鍵と秘密鍵のためのデータが格納されている. 失敗した場合は,返り値はNULLである. 今回はコールバック関数とその引数はNULLにした.
  RSA *rsaKey = RSA_generate_key(size, expornent, NULL, NULL);
  if (rsaKey == NULL)
    {
      printError("failed to RSA_generate_key",
		 ERR_get_error());
      exit(-1);
    }
RSA_generate_keyは内部でRSA構造体の領域を確保してそのポインタを返して来る. これは以下の関数で忘れずにfreeする.
void
RSA_free(RSA *rsa);

キーのダンプ出力

生成したキーはRSA構造体に格納されている. 通常は必要ない処理だが,これをダンプ出力する関数がある.
int
RSA_print_fp(FILE *fp, RSA *x, int offset);
fpは出力ファイルストリーム,xはダンプ対象のRSA構造体,offsetは出力時のインデント数. 返り値は1なら成功,0なら失敗である. サンプルではstdoutに出力している.
  if(RSA_print_fp(stdout, rsaKey, 0) != 1)
    {
      printError("failed to RSA_print_fp",
		 ERR_get_error());
      exit(-1);
    }

キーのPEM形式出力

RSA構造体に格納された公開鍵をPEM形式で出力する関数は以下.
int
PEM_write_RSAPublicKey(FILE *fp, RSA *x);
第一引数は出力先ファイルストリーム,第二引数は鍵データが格納されたRSA構造体. 返り値は1なら成功,0なら失敗.
  // 公開鍵をPEM形式で書き出し
  if(PEM_write_RSAPublicKey(publicKeyFile, rsaKey) != 1)
    {
      printError("failed to PEM_write_RSAPublicKey",
		 ERR_get_error());
      exit(-1);
    }
RSA構造体に格納された秘密鍵をPEM形式で出力する関数は以下.
int
PEM_write_RSAPrivateKey(FILE *fp, RSA *x,
                        const EVP_CIPHER *enc,
                        unsigned char *kstr,
                        int klen,
                        pem_password_cb *cb,
                        void *u);
引数は以下のとおり.
fp
出力ファイルストリーム.
x
秘密鍵データが格納されたRSA構造体.
他の引数はPEMを暗号化して出力する際に使用する. 詳細は別頁にて記述する. 今回は暗号化無しで出力してみた.
  // 秘密鍵をPEM形式で書き出し
  if(PEM_write_RSAPrivateKey(privateKeyFile, rsaKey,
			     NULL,
			     NULL, 0,
			     NULL, NULL) != 1)
    {
      printError("failed to PEM_write_RSAPrivateKey",
		 ERR_get_error());
      exit(-1);
    }

エラーメッセージの出力

サンプル中の関数
static void
printError(char *msg, unsigned long err)
{
  char *errmsg = ERR_error_string(err, NULL);
  fprintf(stderr, "%s(%s)\n",
	  msg,
	  errmsg);
}
は,OpenSSLでエラーが生じた際に,エラーメッセージを取得する処理である. errはprintError呼び出し元にて関数
unsigned long
ERR_get_error(void);
にて取得した値を使用する. そして関数
char *
ERR_error_string(unsigned long e, char *buf);
は,上記のerrの値エラーメッセージに変換する. この関数を使用するには,あらかじめ
void
ERR_load_crypto_strings(void);
にてエラーメッセージ文字列をロードし,暗号化処理が終ったら,
void
ERR_free_strings(void);
にて領域を開放する.

PEM形式キーファイルの読み込み

では,PEM形式キーファイルを読み込んでRSA暗号化,復号を行うサンプルを以下に示す.

#include <stdio.h>
#include <string.h>
#include <getopt.h>
#include <openssl/rsa.h>
#include <openssl/engine.h>
#include <openssl/pem.h>

#define PROC_TYPE_UNDEF   (-1)
#define PROC_TYPE_DECRYPT (1)
#define PROC_TYPE_ENCRYPT   (2)

#define KEY_TYPE_UNDEF    (-1)
#define KEY_TYPE_PRIVATE  (1)
#define KEY_TYPE_PUBLIC   (2)

static void printError(char *msg, unsigned long err);

int
main(int argc, char *argv[])
{
  int procType = PROC_TYPE_UNDEF;
  int keyType = KEY_TYPE_UNDEF;
  int ret;
  FILE *keyFile;
  FILE *inFile;
  FILE *outFile;
  RSA *key;

  ERR_load_crypto_strings();

  struct option options[] = {
    {"dec", 0, &procType, PROC_TYPE_DECRYPT},
    {"enc", 0, &procType, PROC_TYPE_ENCRYPT},
    {"pri", 0, &keyType, KEY_TYPE_PRIVATE},
    {"pub", 0, &keyType, KEY_TYPE_PUBLIC},
    {NULL, 0, NULL, 0}};

  while((ret = getopt_long(argc, argv, "", options, NULL))
        != -1);

  if (procType == PROC_TYPE_UNDEF
      || keyType == KEY_TYPE_UNDEF
      || (argc - optind) < 3)
    {
      fprintf(stderr, "Usage : %s (--dec|--enc) (--pri|--pub) keyFile inFile outFile\n", argv[0]);
      exit(-1);
    }

  // キーファイル
  keyFile = fopen(argv[optind], "r");
  if (keyFile == NULL)
    {
      perror(argv[optind]);
      exit(-1);
    }

  // 入力データファイル
  inFile = fopen(argv[optind + 1], "r");
  if (inFile == NULL)
    {
      perror(argv[optind + 1]);
      exit(-1);
    }

  // 出力ファイル
  outFile = fopen(argv[optind + 2], "w");
  if (outFile == NULL)
    {
      perror(argv[optind + 2]);
      exit(-1);
    }

  if (keyType == KEY_TYPE_PUBLIC)
    {
      // 公開鍵の読み込み
      key = PEM_read_RSAPublicKey(keyFile, NULL, NULL, NULL);
    }
  else if (keyType == KEY_TYPE_PRIVATE)
    {
      // 秘密鍵の読み込み
      key = PEM_read_RSAPrivateKey(keyFile, NULL, NULL, NULL);
    }

  if (key == NULL)
    {
      fprintf(stderr, "failed to read keyfile\n");
      exit(-1);
    }
  else
    {
      RSA_print_fp(stdout, key, 0);
      if(0 && RSA_check_key(key) != 1)
        {
          printError("failed to RSA_check_key",
                     ERR_get_error());
          exit(-1);
        }
    }

  int inlen;
  int outlen;
  long rsaSize = RSA_size(key);
  unsigned char *inbuf = malloc(rsaSize);
  unsigned char *outbuf = malloc(rsaSize);
  int readSize;

  if (procType == PROC_TYPE_ENCRYPT)
    {
      readSize = rsaSize - 11;
    }
  else if (procType == PROC_TYPE_DECRYPT)
    {
      readSize = rsaSize;
    }

  fprintf(stdout, "RSA_size = %ld\n", rsaSize);

  memset(inbuf, 0, rsaSize);
  while((inlen = fread(inbuf, 1, readSize, inFile)) > 0)
    {
      fprintf(stdout, "inlen = %d\n", inlen);
      memset(outbuf, 0, rsaSize);
      if (procType == PROC_TYPE_ENCRYPT)
        {
          if (keyType == KEY_TYPE_PUBLIC)
            {
              // 公開鍵で暗号化
              if((outlen = RSA_public_encrypt(inlen, inbuf, outbuf,
                                              key, RSA_PKCS1_PADDING)) == -1)
                {
                  printError("failed to RSA_public_encrypt",
                             ERR_get_error());
                  exit(-1);
                }
            }
          else if (keyType == KEY_TYPE_PRIVATE)
            {
              // 秘密鍵で暗号化
              if((outlen = RSA_private_encrypt(inlen, inbuf, outbuf,
                                               key, RSA_PKCS1_PADDING)) == -1)
                {
                  printError("failed to RSA_private_encrypt",
                             ERR_get_error());
                  exit(-1);
                }
            }
        }
      else if (procType == PROC_TYPE_DECRYPT)
        {
          if (keyType == KEY_TYPE_PUBLIC)
            {
              // 公開鍵で復号
              if((outlen = RSA_public_decrypt(inlen, inbuf, outbuf,
                                              key, RSA_PKCS1_PADDING)) == -1)
                {
                  printError("failed to RSA_public_decrypt",
                             ERR_get_error());
                  exit(-1);
                }
            }
          else if (keyType == KEY_TYPE_PRIVATE)
            {
              // 秘密鍵で復号
              if((outlen = RSA_private_decrypt(inlen, inbuf, outbuf,
                                               key, RSA_PKCS1_PADDING)) == -1)
                {
                  printError("failed to RSA_private_decrypt",
                             ERR_get_error());
                  exit(-1);
                }
            }
        }

      fwrite(outbuf, 1, outlen, outFile);
      memset(inbuf, 0, rsaSize);
    }
  free(inbuf);
  free(outbuf);

  fclose(inFile);
  fclose(outFile);

  ERR_free_strings();

  return 0;  
}

static void
printError(char *msg, unsigned long err)
{
  char *errmsg = ERR_error_string(err, NULL);
  fprintf(stderr, "%s(%s)\n",
          msg,
          errmsg);
}
このサンプルは先のサンプルで作成したキーファイルを使用して,データファイルを暗号化/復号するものである. サンプルはコマンドライン引数にてキーファイルに格納されているのが公開鍵なのか,秘密鍵なのかを判定している. そして,その判定結果に応じて,PEM形式キーファイルの読み込み関数を呼び分けている.
  if (keyType == KEY_TYPE_PUBLIC)
    {
      // 公開鍵の読み込み
      key = PEM_read_RSAPublicKey(keyFile, NULL, NULL, NULL);
    }
  else if (keyType == KEY_TYPE_PRIVATE)
    {
      // 秘密鍵の読み込み
      key = PEM_read_RSAPrivateKey(keyFile, NULL, NULL, NULL);
    }
関数プロトタイプは以下.
RSA *
PEM_read_RSAPrivateKey(FILE *fp, RSA **x,
                       pem_password_cb *cb,
                       void *u)

RSA *
PEM_read_RSAPublicKey(FILE *fp, RSA **x,
                      pem_password_cb *cb,
                      void *u);
第一引数はPEM形式キーファイルのファイルストリーム. 第二引数はRSAのポインタのポインタであるが,用途がよくわからない. サンプルではキーファイルの作成の際に暗号化等を行わなかったので,NULLとしているが,第三,第四引数移行はそれぞれ復号のためのパスワード取得用のコールバック関数やその引数データである. 返り値は読み込んだキー情報を格納したRSA構造体である. 読み込み失敗時は返り値はNULLとなる.

暗号化と復号

RSAは公開鍵暗号なので,暗号化,復号用の関数にはそれぞれ公開鍵を使用するものと秘密鍵を使用するものがあり,つまり4つの関数がある. サンプルでは,コマンドラインオプションにより,4つの関数を呼び分けている. プロトタイプは以下.
// 公開鍵で暗号化
int
RSA_public_encrypt(int flen,
                   unsigned char *from,
                   unsigned char *to,
                   RSA *rsa,
                   int padding);

// 秘密鍵で復号
int
RSA_private_decrypt(int flen,
                   unsigned char *from,
                   unsigned char *to,
                   RSA *rsa,
                   int padding);

// 秘密鍵で暗号化
int
RSA_private_encrypt(int flen,
                   unsigned char *from,
                   unsigned char *to,
                   RSA *rsa,
                   int padding);

// 公開鍵で復号
int
RSA_public_decrypt(int flen,
                   unsigned char *from,
                   unsigned char *to,
                   RSA *rsa,
                   int padding);

引数の意味はいずれも同じで,以下の通り.
flen
fromの長さ.
from
暗号化,復号対象データへのポインタ.
to
暗号化,復号結果データ格納先へのポインタ.
int
RSA_size(const RSA *rsa);
で返される値分のサイズが無くてはならない.
rsa
公開鍵,あるいは秘密鍵が格納されたRSA構造体へのポインタ.
padding
暗号データはRSA_size(rsa)の値単位で処理される. で,キリが悪い場合にパディングする方法を指定する.
関数の返り値は,暗号化あるいは復号結果のデータサイズである. 失敗時は-1となる. 暗号化の場合は,平文データのサイズをRSA_size(rsa)にしてしまうと,暗号文がRSA_size(rsa)を越えてしまってエラーになる. よくわからないのだが順に試すと,
RSA_size(rsa) - 11
だとうまくいく.

サンプルの実行

では,サンプルを実行してみる. まず,二組のキーペアを作成してみる.
$ ./myrsa_keygen pri1.pem pub1.pem
Private-Key: (1024 bit)
modulus:
    00:b9:a2:92:24:4d:24:4a:43:ee:88:8b:a9:09:20:
    17:ba:6c:fa:83:5c:17:66:8a:37:c7:60:5e:5b:be:
...省略...
$ cat pub1.pem 
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBALmikiRNJEpD7oiLqQkgF7ps+oNcF2aKN8dgXlu+H/LhfoFyY2EIk7ZS
fX0j52POToHm5wOiNXxXifOsw2D8VnUhS57ExAFUXlI+RMZ3f8ZHws2B+eLMLU03
y+whbIG/cyThMgemNnAlgARZwXMoqQqU4AeIcM/bymsM/sNRv9s5AgMBAAE=
-----END RSA PUBLIC KEY-----

$ cat pri1.pem 
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQC5opIkTSRKQ+6Ii6kJIBe6bPqDXBdmijfHYF5bvh/y4X6BcmNh
CJO2Un19I+djzk6B5ucDojV8V4nzrMNg/FZ1IUuexMQBVF5SPkTGd3/GR8LNgfni
zC1NN8vsIWyBv3Mk4TIHpjZwJYAEWcFzKKkKlOAHiHDP28prDP7DUb/bOQIDAQAB
AoGALJNOFmtzsGWZjK5Em81aBUkP6qUddWDxPe31GoCP+WmOUBUgqfc+SQSL35XU
hVHP5j+E74lu4HnAxYORsuNE+xh4TW/e/MVQi51O2Bx2YuvBjn+xBoIpwnpmEZh7
thqTsXtXYyKTQwUGoiDPzhz1v/DQKTXO043lG3LtKk7vL9ECQQDmByYd34wLmWb+
4Kb7P6QrbqdKNY5uGrMTqe2ETTyZLssVgCg3GUke7xnlCUct5kd/gdjxShq9dFM6
PJkwdVjdAkEAzphEI1CX5XfHtKvrotx5EubxLp25SY4cKfKXa7JiBPiPXRhNAwbf
aXHoXdDmQuSEvNbN6e91IIm0M4HGPyI4DQJBAIjasfvN91UjVFrJr68FoxfQAqmt
CWtKaUaGr/Apv6bnZx4InGSDn7ROztosVfPh8KHU0AxmaVhUL/wS95UUC+UCQClx
nEGlWkNbKwHQdz29ksA6+ekLFO9vhbDYjI27RDSRFFltlY1k5I6HnkCrtnV22DX1
tWsXcVysoyyt/PIPUEUCQF4c6xQ+vK2CBP2tgM+ycS35rSR1apbHItCLeor3HbLw
RBn9INhaCGfK3POCCgUivKIUQ3lStch3FUNddcJe9as=
-----END RSA PRIVATE KEY-----

$ ./myrsa_keygen pri2.pem pub2.pem
Private-Key: (1024 bit)
modulus:
    00:ca:7a:24:c6:de:63:f2:3e:5a:06:4c:a6:34:79:
    9c:ce:6b:40:3f:9e:14:2b:06:74:24:47:45:fa:e8:
...省略...
次に,pri1.pemで暗号化し,pub1.pemで復号する.
$ ./myrsa --enc --pri pri1.pem myrsa.c encedByPri1
Private-Key: (1024 bit)
modulus:
    00:b9:a2:92:24:4d:24:4a:43:ee:88:8b:a9:09:20:
...省略...
$ ./myrsa --dec --pub pub1.pem encedByPri1 decedByPub1
Modulus (1024 bit):
    00:b9:a2:92:24:4d:24:4a:43:ee:88:8b:a9:09:20:
...省略...
$ diff myrsa.c decedByPub1 
上の暗号データは当然pri1.pemやpub2.pemでは復号できない.
$ ./myrsa --dec --pri pri1.pem encedByPri1 decedByPub1
Private-Key: (1024 bit)
modulus:
    00:b9:a2:92:24:4d:24:4a:43:ee:88:8b:a9:09:20:
...省略...
failed to RSA_private_decrypt(error:0407106B:rsa\
routines:RSA_padding_check_PKCS1_type_2:block type is not 02)
$ ./myrsa --dec --pub pub2.pem encedByPri1 decedByPub1
Modulus (1024 bit):
    00:ca:7a:24:c6:de:63:f2:3e:5a:06:4c:a6:34:79:
...省略...
failed to RSA_public_decrypt(error:0407006A:rsa\
routines:RSA_padding_check_PKCS1_type_1:block type is not 01)
次にpub1.pemで暗号化し,pri1.pemで復号する.
$ ./myrsa --enc --pub pub1.pem myrsa.c encedByPub1
Modulus (1024 bit):
    00:b9:a2:92:24:4d:24:4a:43:ee:88:8b:a9:09:20:
...省略...
$ ./myrsa --dec --pri pri1.pem encedByPub1 decedByPri1 
Private-Key: (1024 bit)
modulus:
    00:b9:a2:92:24:4d:24:4a:43:ee:88:8b:a9:09:20:
...省略...
$ diff myrsa.c decedByPri1 
暗号データはやはりpub1.pemやpri2.pemでは復号できない.
$ ./myrsa --dec --pub pub1.pem encedByPub1 decedByPri1 
Modulus (1024 bit):
    00:b9:a2:92:24:4d:24:4a:43:ee:88:8b:a9:09:20:
...省略...
failed to RSA_public_decrypt(error:0407006A:rsa\
routines:RSA_padding_check_PKCS1_type_1:block type is not 01)
$ ./myrsa --dec --pri pri2.pem encedByPub1 decedByPri1 
Private-Key: (1024 bit)
modulus:
    00:ca:7a:24:c6:de:63:f2:3e:5a:06:4c:a6:34:79:
...省略...
failed to RSA_private_decrypt(error:0407106B:rsa\
routines:RSA_padding_check_PKCS1_type_2:block type is not 02)

You may also like...