보고서 개요
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)
'勉強 > Network' 카테고리의 다른 글
| Rust 기반 NAC 플랫폼 개발기: 세션 인증 + 캡티브 포털 리다이렉트 구현 (0) | 2026.06.10 |
|---|---|
| 문서 중앙화 DLP/ECM 시스템 전체 아키텔처 - 통신 흐름, 보안 설계, AWS 배포 정리 (0) | 2026.06.02 |