OpenSSL을 빌드하려고 하는데, 아래와 같은 error가 발생한다.

Build error

 

기존에는 같은 환경에서 아래 방법으로 했을 때 잘 됐던 것 같은데 왜 에러가 발생하는지 참..

2024.06.18 - [OpenSSL] - Build OpenSSL on Windows

 

Build OpenSSL on Windows

OpenSSL 라이브러리를 빌드하려고 한다.자료는 Qt wiki에서 제공하는 "Compiling OpenSSL with MinGW" 을 참고하려한다.링크 -> https://wiki.qt.io/Compiling_OpenSSL_with_MinGW 빌드는 MSYS2에서 진행한다.MSYS2란?https://wiki

gunwooyun.tistory.com

 

이것저것 시도해보다 처음에 Configure 할 때 옵션을 잘 못 넣은 것 같다. 64bit로 빌드할 때는 마지막 옵션에 mingw64를 써야하는데 mingw만 넣고 빌드한 것 같다. (사실 확실치는 않다. 컴파일러도 삭제하고 다시 설치해보고, 뭐 이것저것 했기 때문이다)

 

./Configure --prefix=$PWD/dist no-idea no-mdc2 no-rc5 shared mingw64

 

다시 실행해보니 정상적으로 잘 빌드된다. 빌드가 다 되면 특별히 finish 됐다고 출력되진 않는다.

Finish building

 

굳.

Build 중 초반에 뜬금없이 에러가 발생한다.

cc1.exe: sorry, unimplemented: 64-bit mode not compiled in

 

뭔고 하니 Configure 할 때 64bit로 설정해놓고 gcc는 32bit로 사용해서 발생한 error다.

 

다시 환경변수에서 gcc 버전을 64bit용으로 설정하고 다시 build 한다.

Set gcc 64bit

 

build가 정상적으로 진행된다.

Block cipher의 AES를 FIPS문서대로 구현해보려고 한다. 사용언어는 C이고, 문서에 나와있는 pseudocode을 최대한 만족해서 작성할 것이다. 이렇게 해야 내가 나중에 이해하기 편할 듯 하다.

 

사실 문서의 내용들, 즉 이론을 리뷰하면서 글을 작성하려고 했는데, 너무 내용도 방대하거니와 생각보다 작성된 내용이 쉽게 해석이 되서 굳이 여기서 작성하지 않고 문서 자체를 보는게 이해하는데 훨씬 도움이 될 것 같다. 심지어 인터넷에 한글 또는 영문의 기가막힌 설명들이 너무 많다. 나도 해당 내용들을 참고해가며 구현했기에 굳이 여기서 허접한 영어 실력과 이해로 설명하고 싶진 않다.

 

먼저, FIPS 란

The Federal Information Processing Standards (FIPS) of the United States are a set of publicly announced standards that the National Institute of Standards and Technology (NIST) has developed for use in computer situs of non-military United States government agencies and contractors. -Wikipedia

미국 연방 정보 처리 표준 (FIPS)은 미국의 비군사 정부 기관 및 계약자가 사용하는 컴퓨터 시스템을 위해 국가 표준 기술 연구소 (NIST)가 개발한 공개 표준 세트입니다.

 

FIPS는 표준의 세트이다. 그러므로 여러 버전이 있다. 예를 들면 다음과 같다.

FIPS 140: Cryptographic Module Validation Program

FIPS 197: Advanced Encryption Standard

FIPS 180: Secure Hash Standard (SHS)

등등이 있다.

 

참고로 위 FIPS 140은 암호 모듈로서 개발된 소프트웨어나 하드웨어를 검증하는 프로그램이다. 흔히 FIPS 인증을 받았다고 한다면 위 FIPS 140으로 이해하면 된다. 현재 FIPS 140-3 버전이다.

 

AES는 FIPS 197 이므로 해당 문서를 참고해가며 구현해본다.

 

FIPS 197 -> https://csrc.nist.gov/pubs/fips/197/final

 

FIPS 197, Advanced Encryption Standard (AES) | CSRC

Publications Advanced Encryption Standard (AES)     Documentation     Topics Date Published: November 26, 2001; Updated May 9, 2023 Supersedes: FIPS 197 (11/26/2001) Planning Note (05/09/2023): This release updates the original publication of FIPS 19

csrc.nist.gov

 

먼저, 암호화 Cipher이다. 페이지 20이다.

암호화 의사코드

 

Cipher 함수의 입력 파라메터는 블록데이터 in, 연산되어 출력되는 블록데이터 out, 그리고 확장된 키 w가 있다. 함수 내부에서 작동되는 순서는 다음과 같다.

  1. 블록 데이터를 state 에 저장
  2. state와 round 0번 키 XOR
  3. round 1번부터 round n-1까지 아래 연산 반복
    1. 이미 정의되어 있는 substitution box를 이용해서 state 원소 값 치환
    2. 행의 원소들을 shift
    3. 열의 원소들을 정해진 행렬과 곱셈으로 변환
    4. 해당 round의 키와 XOR
  4. 마지막으로 3-1, 3-2, 3-4 순으로 수행 후 연산된 최종 state 값을 out으로 복사

C로 구현된 AES 함수들을 한번 보자.

 

Cipher

void Cipher(const byte *in, byte *out, word *w, int Nr)
{
    byte state[4 * Nb] = {0x00, };
    int round = 0;

    memcpy(state, in, 16);

    AddRoundKey(state, &w[round * Nb]);

    for(round = 1; round < Nr; round++)
    {
       SubBytes(state);
       ShiftRows(state);
       MixColumns(state);
       AddRoundKey(state, &w[round * Nb]);
    }

    SubBytes(state);
    ShiftRows(state);
    AddRoundKey(state, &w[Nr * Nb]);

    memcpy(out, state, 16);
}

 

나중에 다시 봐도 최대한 이해가 잘 되도록 Pseudo code 와 비슷하게 작성했다.

한가지 다른 점은 round 횟수를 매개변수로 받는다. 포인터로 받은 w로 확장된 키의 갯수를 알 수 없으니 이 매개변수를 따로 받을 수 밖에 없다.

 

음, 아니면 Context를 전역 변수로 운영하면서 Initialize 할 때, round 횟수를 Context에 저장하고, update 문에서 해당 값을 읽는 것도 방법일 듯 하긴 하다.

 

State

블록 데이터는 2x2 행렬의 State로 변환된다. State는 아래와 같이 배열이나 vector를 사용할 수 있다. 예를 들면 배열로 State를 구현한다면 다음과 같다.

State 2x2 행렬

0 | 4 | 8 | 12

1 | 5 | 9 | 13

2 | 6 | 10 | 14

3 | 7 | 11 | 15

위 행렬의 숫자는 배열의 index와 일치시킬 수 있다. 즉, 배열을 선언하면 0 ~ 3 index는 1열, 4 ~ 7은 2열, 8 ~ 11은 3열, 마지막으로 12 ~ 15는 4열이라고 할 수 있다.

 

State[16] = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 <- 숫자는 index다.

 

 

Cipher 함수 외부에서는 KeyExpansion 으로 키를 확장한다. 확장된 키는 각각 w 배열에 저장되고 이를 Cipher 함수에서 사용한다. 먼저 Pseudo code 부터 살펴보자.

KeyExpansion

Key Expansion

C 코드는 다음과 같다.

void KeyExpansion(byte *key, word* w, int Nk)
{
   int i = 0;
   word temp;

   while(i < Nk)
   {
      w[i] = ((key[4 * i] << 24) | (key[4 * i + 1] << 16) | (key[4 * i + 2] << 8) | key[4 * i + 3]);
      i = i + 1;
   }

   i = Nk;

   int Nr = 10;

   while(i < Nb * (Nr + 1)) /* Total round = preRound + round */
   {
      temp = w[i - 1];

      if((i % Nk) == 0)
      {
         temp = SubWord(RotWord(temp)) ^ Rcon[i / Nk];
      }
      else if((Nk > 6) && (i % Nk == 4))
      {
         temp = SubWord(temp);
      }
      else
      {
         // Do nothing
      }
      w[i] = w[i - Nk] ^ temp;
      i = i + 1;
   }
}

 

16, 24, 32bytes의 키를 각 키길이에 맞는 round 10, 12, 14의 횟수만큼 갯수를 늘리는게 KeyExpansion 역할이다. 단, round 전에 pre-round가 있기 때문에 실제 round 횟수는 각각 11, 13, 15이다.

 

RotWord와 SubWord는 각각 다음과 같다.

RotWord

RotWord()

아래 코드 주석에 나와있는대로, 1 word가 4바이트, 바이트 단위로 나눠봤을 때, [a0, a1, a2, a3] 각각 1바이트씩 총 4바이트다. 여기서 순서를 [a1, a2, a3, a0] 로 순환시키는게 RotWord다.

/* The function takes a RotWord() word [a0,a1,a2,a3] as input, performs a cyclic permutation,
 * and returns the word [a1,a2,a3,a0]. */
word RotWord(word w)
{
   w = (w >> 24) | (w << 8);
   return w;
}

 

SubWord

SubWord()

/* is a function that takes a four-byte input word and applies the S-box (Sec. 5.1.1,
SubWord() Fig. 7) to each of the four bytes to produce an output word.*/
word SubWord(word w)
{
   int idx = 0;
   for(int i = 0; i < 4; i++)
   {
      w = ((SBOX[(w >> (24 - (8 * i))) & 0xFF]) << (24 - (8 * i))) | w & ~(0xFF << (24 - (8 * i)));
   }
   return w;
}

 

Rcon은 미리 정의되어 있는 값이다.

/* The round constant word array, Rcon[i], contains the values given by [x^(i-1),{00},{00},{00}],
with x^(i-1) being powers of x (x is denoted as {02}) in the field GF(28), as discussed in Sec. 4.2 (note that i starts at 1, not 0).*/
static const word Rcon[] = {
    0x00000000, 0x01000000, 0x02000000, 0x04000000, 0x08000000,
    0x10000000, 0x20000000, 0x40000000, 0x80000000,
    0x1B000000, 0x36000000, /* for 128-bit blocks, Rijndael never uses more than 10 rcon values */
};

 

AddRoundKey

AddRoundKey

AddRoundKey는 각 round의 w와 state를 xor 하는 연산이다. 별거 없다. state의 첫번째 열부터 w의 첫번째 원소부터 각각 xor 을 반복하면 AddRoundKey가 된다.

/*
 * 5.1.4 AddRoundKey() Transformation
 * In the AddRoundKey() transformation, a Round Key is added to the State by a simple bitwise
XOR operation.
*/
void AddRoundKey(byte *state, const word *w)
{
   int i, j;

   for(i = 0; i < Nb; i++)
   {
      for(j = 0; j < Nb; j++)
      {
         state[(i * 4) + j] ^= (w[i] >> (24 - (8 * j))) & 0xFF;
      }
   }

}

 

ShiftRows

각 행의 원소들을 순환하며 섞는다. ShiftRows 의 연산 방법은 아래 그림 하나로 모든게 설명된다.

ShiftRows

첫번째 행은 연산되지 않는다. 두번째 행부터 shift 연산을 한다. 2행의 첫번째 원소가 2행의 가장 끝으로 shift 된다. 3행은 앞의 두개 원소가 뒤의 2개 원소 뒤로 shift 되고, 4행은 마지막 원소가 가장 앞으로 shift 된다. 코드로 구현하면 다음과 같다.

/*
 * 5.1.2 ShiftRows() Transformation 
 * In the ShiftRows() transformation, the bytes in the last three rows of the State are cyclically
shifted over different numbers of bytes (offsets). The first row, r = 0, is not shifted.
*/
void ShiftRows(byte *state)
{
   word temp;

   int i;
   // Row 0 not changed
   for(i = 1; i < 4; i++)
   {
      temp = (state[i] << 24) | (state[i + 4] << 16) | (state[i + 8] << 8) | (state[i + 12]);
      temp = (temp >> (32 - (i * 8))) | (temp << (i * 8));
      state[i] = (temp >> 24) & 0xFF;
      state[i + 4] = (temp >> 16) & 0xFF;
      state[i + 8] = (temp >> 8) & 0xFF;
      state[i + 12] = (temp) & 0xFF;
   }
}

MixColumns

이 함수가 구현하기 가장 까다롭지 않나 싶다. ShiftRows는 행을 섞었다면, MixColumns은 열의 값들을 섞는다. 이때, 사용되는 수학적 방법은 행렬의 곱셈이다. 아, 간만에 행렬의 곱, 역행렬, 단위행렬이란 단어를 보게 되네. 사실, 수학적 이론이 많이 가미되는 연산이라 구글링해서 찾아보는게 더 나을 듯 하다. 여기서는 구현된 코드만 작성한다.

MixColumns

byte Mul(byte state)
{
   return (state << 1) ^ (((state >> 7) & 0x01) * 0x1b);
}

/*
5.1.3 MixColumns() Transformation
The MixColumns() transformation operates on the State column-by-column, treating each 
column as a four-term polynomial as described in Sec. 4.3. The columns are considered as 
polynomials over GF(2^8) and multiplied modulo x^4 + 1 with a fixed polynomial a(x), given by
a(x) = {03}x^3 + {01}x^2 + {01}x + {02}.
*/
void MixColumns(byte *state)
{
   int i;
   byte temp[16];
   for(i = 0; i < 4; i++)
   {
      temp[(i * 4) + 0] =  Mul(state[(i * 4) + 0]) ^                          // ({02}*S0,c) ^
                           (Mul(state[(i * 4) + 1]) ^ state[(i * 4) + 1]) ^   // ({03}*S1,c) ^
                           state[(i * 4) + 2] ^                               // ({01}*S2,c) ^
                           state[(i * 4) + 3];                                // ({01}*S3,c) ^

      temp[(i * 4) + 1] =  state[(i * 4) + 0] ^                               // ({01}*S0,c) ^
                           Mul(state[(i * 4) + 1]) ^                          // ({02}*S1,c) ^
                           (Mul(state[(i * 4) + 2]) ^ state[(i * 4) + 2]) ^   // ({03}*S2,c) ^
                           state[(i * 4) + 3];                                // ({01}*S3,c) ^

      temp[(i * 4) + 2] =  state[(i * 4) + 0] ^                               // ({01}*S0,c) ^
                           state[(i * 4) + 1] ^                               // ({01}*S1,c) ^
                           Mul(state[(i * 4) + 2]) ^                          // ({02}*S2,c) ^
                           (Mul(state[(i * 4) + 3]) ^ state[(i * 4) + 3]);    // ({03}*S3,c) ^

      temp[(i * 4) + 3] =  (Mul(state[(i * 4) + 0]) ^ state[(i * 4) + 0]) ^   // ({03}*S0,c) ^
                           state[(i * 4) + 1] ^                               // ({01}*S1,c) ^
                           state[(i * 4) + 2] ^                               // ({01}*S2,c) ^
                           Mul(state[(i * 4) + 3]);                           // ({02}*S3,c) ^
   }
   memcpy(state, temp, 16);
}

 

지금까지 Cipher 함수를 구성하는 하위 함수들을 알아봤다. 이 함수들을 조합한 Cipher 함수로 블럭사이즈 128bits를 암호화할 수 있다. 간단한 벡터와 결과값은 해당 문서의 뒤쪽에 나와있다. 예를 들면, 아래와 같이 키 스케쥴부터 시작해서
ShiftRows, MixColumns 등의 단계별 출력값들이 작성되어 있다. 이 값들과 비교해가면서 Cipher와 하위 함수들을 구현해가면 된다.

 

 

InvCipher 는 문서에 나와있는 그대로 구현하면 어려울 건 없다. 다만, InvMixColumns 를 구현하는데 정말 애 먹었다. 일단, 구현 방법의 힌트는 다음과 같다.

 

𝑥 × 9 = (((𝑥 × 2) × 2) × 2) + 𝑥

𝑥 × 11 = ((((𝑥 × 2) × 2) + 𝑥) × 2) + 𝑥

𝑥 × 13=((((𝑥 × 2) + 𝑥) × 2) × 2) + 𝑥

x × 14 = ((((x × 2) + x) × 2) + x) × 2

 

여기서 𝑥 × 2 연산은 원소값을 2 곱한 값 (left shift 1) 과 원소의 msb가 1이라서 carry가 발생할 경우, 원소와 0x1b를 xor 한 값을 이전에 2 곱한 값과 xor 한다. 뭔가 장황한데, 수식으로 표현하면 이렇다.

(x << 1) ^ (((x >> 7) & 0x01) * 0x1b)

 

위의 연산을 9, 11, 13, 14를 곱할 때, 적용해서 하는거다.

처음에 저 연산을 macro 함수로 했다가, 값이 안나와서 몇일을 고생했다. 도대체 왜 안나오는거였는지 참...

 

InvColumns 만 잘 구현된다면 InvCipher도 쉽게 구현 가능하다.

 

쓰다 보니 너무 장황해지긴 했는데, 그래도 표준 문서에 최대한 근접하게 구현하려고 노력했다. 생각보다 표준문서에서 쉽게 구현할 수 있게 너무 자세하게 설명되어 있어서 놀랐다. 항상 이런 문서들을 보면서 느끼는거지만, 생각보다 표준문서에 설명이 잘 되어있고, 답이 있는 것 같다. 먼저 인터넷부터 뒤지고 학습하는 것보다 이런 표준문서를 보면서 기초부터 쌓아가는 것도 나이스한 것 간다.

OpenSSL 라이브러리를 빌드하려고 한다.

자료는 Qt wiki에서 제공하는 "Compiling OpenSSL with MinGW" 을 참고하려한다.

링크 -> https://wiki.qt.io/Compiling_OpenSSL_with_MinGW

 

빌드는 MSYS2에서 진행한다.

MSYS2란?

https://wikidocs.net/book/13025

 

MSYS2 Documentation

## [MSYS2](https://www.msys2.org/) ### MSYS2의 Documentation 부분을 번역한 자료입니다번역 : 브리티쉬 MSYS2는 기본 …

wikidocs.net

 

./Configure

No C compiler found

 

컴파일러가 없다. 컴파일러를 설치한다.

32bit

# pacman -S mingw-w64-i686-gcc

64bit

# pacman -S mingw-w64-x86_64-gcc

 

환경변수 설정

# export PATH="/c/Program Files/msys64/mingw32/bin:$PATH"

 

설정된 gcc 확인

# which gcc

32bit gcc

 

Configure 실행

./Configure --prefix=$PWD/dist no-idea no-mdc2 no-rc5 shared mingw

 

 

설치

make depend && make && make install

building

테스트 코드(정적 라이브러리 libcrypto.a)

#include <iostream>
#include "include/openssl/rand.h"
using namespace std;

int main(int argc, char const *argv[])
{
    /* code */
    unsigned char arr[32] = {0x00, };
    RAND_bytes(arr, 32);

    for (size_t i = 0; i < 32; i++)
    {
        printf("%02x ", arr[i]);
    }
    cout << endl;
    
    return 0;
}

 

실행

g++ main.cpp -L. -lcrypto

 

결과

 

에러#1

 

기존에 설치되어 있는 mingw-w64-x86_64-gettext로 인한 에러, 삭제 후 다시 재설치

+ Recent posts