본문 바로가기

勉強/Network

암호프로토콜 - TCP/IP 응용계층 보안 프로토콜 분석 (TLS, AES, RSA 이론 + KMS 프로젝트 구현 코드 분석)

보고서 개요

TCP/IP 4계층 모델에서 응용계층(Application Layer)의 보안 프로토콜을 이론적으로 분석하고, 실제 구현한 KMS(Key Management Server) 프로젝트 코드를 통해 TLS/RSA/AES가 어떻게 적용되는지 구체적으로 분석한 보고서입니다.

1. TCP/IP 계층별 보안 기술

계층 프로토콜 예시 보안 기술
응용 계층 HTTP, FTP, SMTP, DNS TLS/SSL, SSH, PGP ← 본 보고서 대상
전송 계층 TCP, UDP DTLS
인터넷 계층 IP, ICMP IPsec
네트워크 인터페이스 Ethernet, Wi-Fi WPA3, MACsec

응용계층은 사용자와 가장 가까운 계층으로, 암호화 키·개인정보·인증 정보가 모두 이 계층에서 처리됩니다. 주요 위협: 도청(Eavesdropping), 위·변조(Tampering), 재전송 공격(Replay Attack)

2. TLS/SSL 프로토콜

TLS(Transport Layer Security)는 인터넷에서 가장 널리 사용되는 보안 프로토콜입니다. 명칭은 '전송 계층 보안'이지만 실제로는 응용계층 바로 아래서 동작하며, 응용계층 데이터를 암호화하여 전송합니다.

2.1 TLS 핸드셰이크 과정

1. ClientHello : 클라이언트가 지원하는 TLS 버전 + 암호 스위트 목록 전송
2. ServerHello : 서버가 선택한 TLS 버전, 암호 스위트, 서버 인증서(X.509) 전송
3. 키 교환   : RSA 또는 ECDHE 방식으로 Pre-Master Secret 안전하게 교환
4. 세션 키 생성: Pre-Master Secret → 대칭키(세션 키) 파생
5. Finished  : 양측이 핸드셰이크 완료 확인 → 암호화 통신 시작

2.2 X.509 인증서와 PKI

TLS 서버 인증은 X.509 형식의 디지털 인증서로 이루어집니다. X.509 인증서 포함 정보: 서버의 공개키, 발급 기관(CA) 정보, 유효 기간, 디지털 서명.

KMS 프로젝트의 인증서 구조:

cert/
  ca.crt     ← 자체 CA 인증서
  ca.key     ← CA 개인키
  server.csr ← 인증서 서명 요청 (Certificate Signing Request)
  server.crt ← CA 서명을 받은 서버 인증서
  server.key ← 서버 개인키

3. AES 대칭키 암호화

AES(Advanced Encryption Standard)는 NIST가 2001년 지정한 블록 암호 알고리즘입니다. 블록 크기: 128비트 고정 / 키 길이: 128·192·256비트 선택 가능

3.1 AES-256-CBC 모드 (KMS 클라이언트 적용)

CBC(Cipher Block Chaining) 모드는 각 블록의 암호화 결과를 다음 블록에 XOR로 연결합니다. 동일한 평문 블록도 다른 암호문으로 변환되어 패턴 분석 공격에 강합니다.

// AesCrypto.cs
public static byte[] Encrypt(byte[] plaintext, byte[] key, byte[] iv)
{
    using (var aes = Aes.Create())
    {
        aes.KeySize = 256;           // AES-256
        aes.Mode    = CipherMode.CBC;
        aes.Padding = PaddingMode.PKCS7;
        aes.Key = key;  // 32 bytes
        aes.IV  = iv;   // 16 bytes (암호화마다 무작위 생성)
        // ...
    }
}

PKCS7 패딩: 블록 크기(16바이트)에 맞게 평문 길이를 조정하는 표준 방식

4. RSA 비대칭키 암호화

RSA는 1977년 개발된 공개키 암호 시스템으로, 큰 수의 소인수분해 난제에 기반합니다.

  • 공개키: (n, e) / 개인키: (n, d)
  • n = 두 큰 소수 p × q
  • 4096비트 RSA → 키 크기 512바이트, 높은 보안 수준

4.1 하이브리드 암호화 패턴

RSA는 느려서 대량 데이터 직접 암호화에 부적합합니다. 실제 시스템은 하이브리드 방식을 사용합니다:

1. AES 세션 키(32 bytes) 랜덤 생성
2. AES 세션 키를 RSA 공개키로 암호화 → 512 bytes (안전한 키 전달)
3. 실제 파일/데이터는 AES-256-CBC로 암호화 (빠른 속도)
구분 AES (대칭키) RSA (비대칭키)
속도 매우 빠름 (하드웨어 가속) 느림 (계산 집약적)
키 종류 암복호화에 동일한 키 공개키 암호화 / 개인키 복호화
주용도 대량 데이터 암호화 키 교환, 디지털 서명
KMS 적용 AesCrypto.cs - 파일 암호화 RsaCrypto.cs - AES 키 암호화 전달

5. KMS 프로젝트 구현 분석

5.1 전체 시스템 아키텍처

C# 클라이언트 (KmsClient)
    │ 1. AES 키를 조직 RSA 공개키로 암호화
    │ 2. TLS 소켓 연결 (port 8443)
    │ 3. requesterOrg + fileOrg + keyId + encKey 전송
    ▼
C KMS 서버 (kms-server)
    ├─ tls.c     → TLS 리스너, SSL 컨텍스트, 핸드셰이크
    ├─ protocol.c → 요청 파싱, 조직 권한 검사, 응답
    ├─ keymgr.c  → RSA 개인키 로드 (PEM)
    └─ policy.c  → 키 상태 검사 (ACTIVE/REVOKED/EXPIRED)

5.2 TLS 레이어 구현 (tls.c)

// 1. TCP 소켓 생성 및 바인딩
int create_tls_listener(int port) {
    int s = socket(AF_INET, SOCK_STREAM, 0);
    bind(s, (struct sockaddr*)&addr, sizeof(addr));
    listen(s, 10);
    return s;
}

// 2. SSL 컨텍스트 초기화 + 인증서 로드
SSL_CTX* setup_ssl_ctx(void) {
    SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
    SSL_CTX_use_certificate_file(ctx, "cert/server.crt", SSL_FILETYPE_PEM);
    SSL_CTX_use_PrivateKey_file(ctx, "cert/server.key", SSL_FILETYPE_PEM);
    SSL_CTX_check_private_key(ctx); // 인증서-개인키 매칭 검증
    return ctx;
}

// 3. TLS 핸드셰이크 수행
SSL* tls_accept_client(int client_fd, SSL_CTX *ctx) {
    SSL *ssl = SSL_new(ctx);
    SSL_set_fd(ssl, client_fd);
    SSL_accept(ssl); // TLS 핸드셰이크
    return ssl;
}

5.3 다계층 접근 제어 (protocol.c)

클라이언트로부터 세 가지 문자열을 수신하여 2단계 접근 제어를 수행합니다.

// 수신: requesterOrg(요청 조직) / fileOrg(파일 소유 조직) / keyId
read_string(ssl, requesterOrg, sizeof(requesterOrg));
read_string(ssl, fileOrg,     sizeof(fileOrg));
read_string(ssl, keyId,       sizeof(keyId));

// 1단계: 조직 권한 검사
static int can_decrypt(const char *requesterOrg, const char *fileOrg) {
    if (strcmp(requesterOrg, fileOrg) == 0) return 1;  // 동일 조직

    // 상위 조직도 허용: "teamA"는 "teamA/teamA1" 파일 접근 가능
    size_t rlen = strlen(requesterOrg);
    if (strncmp(fileOrg, requesterOrg, rlen) == 0 && fileOrg[rlen] == '/')
        return 1;

    return 0; // 거부 → -403 반환
}

// 2단계: 키 정책 검사
int pol = policy_check_key(fileOrg, keyId, nowUtc);
// pol == 0: 허용 / -451: 만료 / -452: 폐기(REVOKED) / -453: 미등록

5.4 동적 정책 리로딩 (policy.c + policy.conf)

policy.conf 파일의 mtime을 모니터링하여 변경 시 자동 리로딩합니다. 서버 재시작 없이 실시간 키 폐기 가능.

# policy.conf 형식: orgPath  keyId  state  expiresEpoch
teamA           key_v1   ACTIVE  0
teamA           key_v2   ACTIVE  0
teamA/teamA1    key_v1   ACTIVE  0
teamB           key_v1   ACTIVE  0

# 즉시 폐기:
# teamB key_v1 REVOKED 0

# 만료 설정 (Unix timestamp):
# teamA key_v1 ACTIVE 1767225600  ← 2026-01-01 이후 만료

반환 코드 정리:

코드 의미
0 허용 (ACTIVE, 만료 없음)
-451 만료된 키 (expiresEpoch 초과)
-452 폐기된 키 (REVOKED)
-453 알 수 없는 키 (policy.conf 미등록)
-460 정책 파일 로딩 실패
-403 조직 권한 없음 (Forbidden)

5.5 RSA 개인키 로드 (keymgr.c)

RSA* load_private_key(const char* ns, const char* keyId) {
    char path[256];
    // 키 파일 경로: keys/teamA/key_v1.pem
    snprintf(path, sizeof(path), "keys/%s/%s.pem", ns, keyId);

    FILE *fp = fopen(path, "r");
    RSA *rsa = PEM_read_RSAPrivateKey(fp, NULL, NULL, NULL);
    fclose(fp);
    return rsa;
}

5.6 클라이언트 RsaCrypto.cs

// AES 키를 X.509 인증서의 RSA 공개키로 암호화
public static byte[] EncryptKey(byte[] plainKey, string pemText) {
    using (RSA rsa = LoadRsaFromCertificatePem(pemText)) {
        byte[] encrypted = rsa.Encrypt(plainKey, RSAEncryptionPadding.Pkcs1);

        // RSA 4096bit → 반드시 512 bytes
        if (encrypted.Length != rsa.KeySize / 8)
            throw new Exception("RSA encrypted length mismatch");
        return encrypted;
    }
}

5.7 PemParser.cs - ASN.1 직접 파싱

.NET Framework에서 표준 라이브러리가 미지원하는 PKCS#8 공개키를 ASN.1 DER 인코딩으로 직접 파싱합니다.

// SubjectPublicKeyInfo 구조체 직접 파싱
ReadSequence(r);   // SubjectPublicKeyInfo SEQUENCE
SkipElement(r);    // AlgorithmIdentifier 건너뜀
ReadBitString(r);  // BIT STRING
ReadSequence(r);   // RSAPublicKey SEQUENCE
byte[] modulus  = ReadInteger(r);  // n (RSA 모듈러스)
byte[] exponent = ReadInteger(r);  // e (공개 지수)

6. 보안 취약점 분석 및 개선 방안

위치 취약점 위험도 개선 방안
tls.c TLS 최소 버전 미설정 - TLS 1.0/1.1 허용 가능 중간 SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) 설정
tls.c 약한 암호 스위트 미배제 - RC4, DES 등 취약 알고리즘 허용 가능 중간 SSL_CTX_set_cipher_list()로 강력한 스위트만 허용
keymgr.c 경로 트래버설 미검증 - keyId에 '../' 포함 시 임의 파일 접근 높음 화이트리스트 검증(영문자/숫자/하이픈만 허용) + realpath() 정규화
AesCrypto.cs Decrypt에서 KeySize 미명시 - 256비트 의도가 코드에 불명확 낮음 aes.KeySize = 256 명시적 추가
protocol.c RSA PKCS#1 v1.5 패딩 - OAEP보다 취약 (Bleichenbacher 공격) 중간 RSA_PKCS1_OAEP_PADDING으로 전환 (서버/클라이언트 동시 변경)

6.1 경로 트래버설 취약점 상세

keymgr.c의 load_private_key는 ns와 keyId를 직접 경로에 삽입합니다. 클라이언트가 keyId로 ../../etc/passwd를 전송하면 예상치 못한 파일에 접근 가능합니다.

// 현재 코드 (취약)
snprintf(path, sizeof(path), "keys/%s/%s.pem", ns, keyId);
// keyId = "../../../etc/passwd" 입력 시 → 임의 파일 읽기 가능

// 개선 방안
// 1. 화이트리스트: 영문자, 숫자, 하이픈, 언더스코어만 허용
if (!is_safe_id(keyId)) { return NULL; }

// 2. realpath()로 경로 정규화 후 기준 디렉토리 벗어났는지 확인
char resolved[PATH_MAX];
realpath(path, resolved);
if (strncmp(resolved, base_dir, strlen(base_dir)) != 0) return NULL;

6.2 PKCS#1 v1.5 vs OAEP 패딩

현재 서버(RSA_PKCS1_PADDING)와 클라이언트(RSAEncryptionPadding.Pkcs1) 모두 PKCS#1 v1.5를 사용합니다.

  • PKCS#1 v1.5: Bleichenbacher 패딩 오라클 공격에 취약
  • RSAES-OAEP: 패딩 오라클 공격 차단, 현재 권장 표준
// 서버 개선 (C)
RSA_private_decrypt(enc_len, enc_buf, out_buf, rsa, RSA_PKCS1_OAEP_PADDING);

// 클라이언트 개선 (C#)
rsa.Encrypt(plainKey, RSAEncryptionPadding.OaepSHA256);

⚠️ 양측 코드를 동시에 변경해야 합니다.

7. 결론

KMS 프로젝트는 응용계층 보안의 핵심 요소를 충실히 구현하고 있습니다:

  • TLS: 전송 보안 (OpenSSL, X.509 인증서, TLS 핸드셰이크)
  • RSA 4096bit: AES 키 암호화 전달 (공개키 암호화 패턴)
  • AES-256-CBC: 파일 데이터 암호화 (고속 대칭키)
  • 조직 기반 접근 제어: 계층적 경로 구조 (teamA → teamA/teamA1)
  • 동적 키 정책: 서버 재시작 없이 실시간 폐기/만료 처리

실제 보안 시스템은 단일 알고리즘이 아니라, 여러 계층의 메커니즘이 서로 보완하며 동작합니다. 공개키의 안전한 키 배달 + 대칭키의 빠른 속도를 조합하는 하이브리드 암호화가 현대 보안 시스템의 표준 패턴임을 코드로 직접 확인할 수 있었습니다.

향후 학습 방향: TLS 1.3 핸드셰이크 방식, ECDHE를 이용한 순방향 기밀성(Perfect Forward Secrecy), 인증서 투명성(Certificate Transparency)


undefined