LoginSignup
2
3

More than 5 years have passed since last update.

RSAの暗号化、復号を計算してみる。

Last updated at Posted at 2017-11-30

といいつつ、署名と検証です。
愛のメッセージに署名を付与して改ざんを防ぎます。

.Net の RSACryptServiceProvider が 512bit 以上の鍵しか利用できないので、自前で計算する羽目になった

opensslにて手順の確認

以下のようにして生成した message.bin, sign.bin, public-key.pem を渡すことで、私がしたためたメッセージだということを保証します。

$ openssl genrsa 32 > private-key.pem
Generating RSA private key, 32 bit long modulus
.+++++++++++++++++++++++++++
.+++++++++++++++++++++++++++
e is 65537 (0x10001)
-----BEGIN RSA PRIVATE KEY-----
MC0CAQACBQC/R561AgMBAAECBQCH+mbVAgMA+G8CAwDFGwICKYMCAmr9AgMA3A4=
-----END RSA PRIVATE KEY-----

$ openssl rsa -in private-key.pem -pubout -out public-key.pem
writing RSA key
-----BEGIN PUBLIC KEY-----
MCAwDQYJKoZIhvcNAQEBBQADDwAwDAIFAL9HnrUCAwEAAQ==
-----END PUBLIC KEY-----

$ echo -en "\x6C\x6F\x76\x65" > message.bin

$ hexdump message.bin
0000000 6f6c 6576
0000004

$ openssl rsautl -raw -sign -inkey private-key.pem -in message.bin > sign.bin

$ hexdump sign.bin
0000000 d152 de16
0000004

$ openssl rsautl -raw -verify -pubin -inkey public-key.pem -in sign.sig > sign.verified.bin

$ hexdump sign.verified.bin
0000000 6f6c 6576
0000004

PEMをデコードする

Roslyn の C# Interactive (VS2017) で確認しながら実行していったのでメモとして掲載。

  1. s/-.*-//g でヘッダとフッダを削除し、Base64部分のみを取り出す。

    var privKey = "-----BEGIN RSA PRIVATE KEY-----\nMC0CAQACBQC/R561AgMBAAECBQCH+mbVAgMA+G8CAwDFGwICKYMCAmr9AgMA3A4=\n-----END RSA PRIVATE KEY-----";
    var regexHeader = new System.Text.RegularExpressions.Regex(@"-.*-");
    var regexReturn = new System.Text.RegularExpressions.Regex(@"\n");
    var privKeyBase64 = regexReturn.Replace(regexHeader.Replace(privKey, ""), "");
    WriteLine(priveKeyBase64);
    // MC0CAQACBQC/R561AgMBAAECBQCH+mbVAgMA+G8CAwDFGwICKYMCAmr9AgMA3A4=
    
  2. base64をdecodeする。

    var privKeyBuf = Convert.FromBase64String(privKeyBase64);
    WriteLine(BitConverter.ToString(privKeyBuf));
    // 30-2D-02-01-00-02-05-00-BF-47-9E-B5-02-03-01-00-01-02-05-00-87-FA-66-D5-02-03-00-F8-6F-02-03-00-C5-1B-02-02-29-83-02-02-6A-FD-02-03-00-DC-0E
    
  3. RSAのDERは1Byte目が 0x30 と決まっているのでそれ以外であれば弾く。

    var index = 0;
    if (privKeyBuf[index++] != 0x30) { throw new ArgumentException("The Format Private Key is not PEM."); }
    
  4. DER内の最初のコンテンツの長さを読み、コンテンツを取得する。(それ以外は捨てる)

    int contentLength = 0;
    //次のByteの最上位Bitが立っていれば、その下位7bitでコンテンツの長さの長さ(タイポじゃないよ)を表す。
    if ((privKeyBuf[index] & 0x80) != 0 )
    { 
      // コンテンツの長さを取得する。
      byte[] emptyBytes = {0, 0, 0, 0};
      byte[] lengthBytes = new byte[privKeyBuf[index] & 0x7F];
      Array.Copy(privKeyBuf, ++index, lengthBytes, 0, privKeyBuf[index] & 0x7F);
      index += privKeyBuf[index] & 0x7F;
      if (lengtsBytes >= 4)
      {
        // Intにしてるので4byte以上の場合はパースできない。
        throw new ArgumentException("PEM file is too large.");
      }
      lengthBytes = emptyBytes.Concat(lengthBytes).ToArray();
      // エンディアンによって処理が違う
      if (BitConverter.IsLittleEndian)
      {
        contentLength = (int)BitConverter.ToUInt32(emptyBytes, emptyBytes.Length - 4);
      }
      else
      {
        contentLength = (int)BitConverter.ToUInt32(emptyBytes.Reverse().ToArray(), 0);
      }
      if (contentLength == 0) {
        // Intにキャスト失敗した場合はパースできない。
        throw new ArgumentException("PEM file is too large.");
      }
    }
    else
    {
      // 最上位Bitが立っていなければ、そのbyte自体がコンテンツの長さを表す。
      contentLength = privKeyBuf[index++];
    }
    byte[] privKeyContents = new byte[contentLength];
    Array.Copy(privKeyBuf, index, privKeyContents, 0, contentLength);
    // byte[45] { 2, 1, 0, 2, 5, 0, 191, 71, 158, 181, 2, 3, 1, 0, 1, 2, 5, 0, 135, 250, 102, 213, 2, 3, 0, 248, 111, 2, 3, 0, 197, 27, 2, 2, 41, 131, 2, 2, 106, 253, 2, 3, 0, 220, 14 }
    
  5. コンテンツをパースする

    構造は次のように定義されている。

    RSAPrivateKey ::= SEQUENCE {
        version           Version,
        modulus           INTEGER, -- n
        publicExponent    INTEGER, -- e
        privateExponent   INTEGER, -- d
        prime1            INTEGER, -- p
        prime2            INTEGER, -- q
        exponent1         INTEGER, -- d mod (p-1)
        exponent2         INTEGER, -- d mod (q-1)
        coefficient       INTEGER, -- (inverse of q) mod p
        otherPrimeInfos   OtherPrimeInfos OPTIONAL
    }
    

    この辺は手計算。

    1. ID: 2(Int), Length: 1, Contents: byte[1] { 0 } = version: 0
      • したがって、 otherPrimeInfos は存在しない。
    2. ID: 2(Int), Length: 5, Contents: byte[5] { 0, 191, 71, 158, 181 } = modulus: 3,209,141,941
    3. ID: 2(Int), Length: 3, Contents: byte[3] { 1, 0, 1 } = publicExponent: 65,537
    4. ID: 2(Int), Length: 5, Contents: byte[5] { 0, 135, 250, 102, 213 } = privateExponent: 2,281,334,485
    5. ID: 2(Int), Length: 3, Contents: byte[3] { 0, 248, 111 } = prime1: 63,599
    6. ID: 2(Int), Length: 3, Contents: byte[3] { 0, 197, 27 } = prime2: 50,459
    7. ID: 2(Int), Length: 2, Contents: byte[2] { 41, 131 } = exponent1: 10,627
    8. ID: 2(Int), Length: 2, Contents: byte[2] { 106, 253 } = exponent2: 27,389
    9. ID: 2(Int), Length: 3, Contents: byte[3] { 0, 220, 14 } = coefficient: 56,334

署名する

今回の鍵は32bitなので平文は4文字まで。平文は "love" とします。
これをbyte列にすると byte[4] { 0x6C, 0x6F, 0x76, 0x65 } となります。
整数に直すと 1,819,244,133 です。

署名は秘密鍵での暗号化なので 平文 ^ privateExponent mod modulus の式で行います。
今回は 1,819,244,133 ^ 2,281,334,485 mod 3,209,141,941 です。

long plain = 1819244133;
long modulus = 3209141941;
long encrypt = 2281334485;

long result = plain;

WriteLine("{0} ^ {1} mod {2} =", plain, encrypt, modulus);
encrypt--;

while(encrypt > 0) {
    result = result * plain;
    encrypt--;
    if (result > modulus) {
        result = result % modulus;
    }
    WriteLine("( {0} x ( {1} ^ {2} mod {3} ) ) mod {3} =", result, plain, encrypt, modulus);
}

WriteLine("{0}", result);
// 1389434590

よって署名は byte[4] { 0x52, 0xD1, 0x16, 0xDE }
これは openssl にて手順の確認 の署名と一致します。
素晴らしい。

検証する

署名と一緒。

long encrypted = 1389434590;
long modulus = 3209141941;
long decrypt = 65537;

long result = encrypted;
Console.WriteLine("{0} ^ {1} mod {2} =", encrypted, decrypt, modulus);
decrypt--;

while (decrypt > 0)
{
    result = result * encrypted;
    decrypt--;
    if (result > modulus)
    {
        result = result % modulus;
    }
    Console.WriteLine("( {0} x ( {1} ^ {2} mod {3} ) ) mod {3} =", result, encrypted, decrypt, modulus);
}

Console.WriteLine("result = {0}", result);
// 1819244133

平文に戻りました。
今回はハッシュをしていないので、平文と一致すればOKですね。

まとめ

  • RSAの仕組みを理解できた。
  • 高速化が必要。32bitの署名に50秒かかった。

    • 平文 ^ E mod M((平文 ^ 2 mod M) ^ E/2 ) mod M と等価であることに気づいた。これを利用すれば処理回数が N から logN になる。 50 秒が 0.02秒になった。
            long plain = 1819244133;
            long modulus = 3209141941;
            long encrypt = 2281334485;
    
            long result = plain;
            long outer = 1;
            var sw = new Stopwatch();
            sw.Start();
            Console.WriteLine("{0} ^ {1} mod {2} =", plain, encrypt, modulus);
    
            while (encrypt > 0)
            {
                if (encrypt % 2 != 0)
                {
                    outer = (outer * result) % modulus;
                    encrypt--;
                    Console.WriteLine("( {0} x ( {1} ^ {2} mod {3} ) ) mod {3} =", outer, result, encrypt, modulus);
                }
                else
                {
                    Console.WriteLine("( {0} x ( {1} ^ 2 mod {3}) ^ {2}/2 ) mod {3} =", outer ,result, encrypt, modulus);
                    result = (result * result) % modulus;
                    encrypt = encrypt / 2;
                    Console.WriteLine("( {0} x ( {1} ^ {2} mod {3} ) ) mod {3} =", outer, result, encrypt, modulus);
                }
            }
    
            sw.Stop();
            Console.WriteLine("result = {0}", outer);
            Console.WriteLine("time spent = {0}", sw.Elapsed.ToString());
            Console.ReadLine();
    
  • bit数が大きくなると整数型で扱えなくなる。バイト列で計算する必要がある。

  • publicExponentmodulus から privateExponent を導いてみたい。

2
3
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
2
3