전자봉투 기반 IR 시스템 개요

·2025년 5월 3일

Security

목록 보기
46/60

어떤 주제로 잡을지 고민을 많이 했는데.. 그래도 나한테 의미있는 주제로 하고 싶어서 침해사고와 관련된 시나리오로 만들었다.

주제는 거창해보이지만 많은 기능이 있지는 않다.
근데 하다보니 뭔가 생각보다 사이즈가 좀 커지고 있는 느낌이긴 하지만 일단 킵고잉.

프로젝트 개요

개요는 대충 아래와 같다.

  • 프로젝트명 : 전자봉투 기반의 IR 로그 봉투화 검증 시스템
  • 목표 : 침해사고 시 발생한 로그를 전자봉투화하여 위조 및 유출을 방지하고, 보안팀의 관리자만이 검증과 복호화가 가능하도록 구현하고자 했다.
  • 역할(role)을 쉽게 구분하기 위해 관리자는 leader로, 담당자는 writer로 관리.

가상 시나리오

  • 기업의 내부에서 침해사고 발생
  • SIEM에 .log 파일 생성 및 보관
  • 보안 담당자는 시스템에 로그인 후 보고하고자 하는 로그 파일과 개인키를 업로드
  • 서버는 자동으로 전자봉투를 생성 후 저장
  • 보안팀의 관리자가 로그인 후, 봉투+공개키+AES 키로 무결성 검증 및 복호화를 수행
  • 검증에 성공하게 되면 원본 로그 확인 가능

주요 기능

  • 로그인
    • 사용자 인증 : leader / writer 선택하여 로그인
  • 키 생성
    • 개인키/공개키 생성
  • 봉투 생성
    • 암호화 : AES로 로그 암호화
    • 해시 생성 : SHA-512로 무결성 확인용 해시 생성
    • 서명 생성 : writer의 개인키로 전자서명
  • 봉투 구조
    • .envpkg 생성 : ZIP 파일로 구조화
  • 검증
    • 해시 검증 : 복호화 이전의 해시 비교
    • 서명 검증 : 공개키로 인증 확인
    • 복호화 : AES로 로그 복호화 및 열람 제공

흐름

  • 봉투 생성 (writer 수행)
    1. AES 키 생성
    2. 로그 파일을 AES 키로 암호화 -> encrypted_log.enc
    3. 로그 해시 생성 (SHA512) -> log.hash
    4. 해시 값에 전자서명 (RSA, writer 개인키) -> signature.sig
    5. AES 키를 관리자 공개키(RSA)로 암호화 -> aes_key.enc
    6. ZIP 구조로 .envpkg 생성
  • 봉투 검증 (leader 수행)
    1. aes_key.enc를 자신의 개인키로 복호화 -> AES 키 획득
    2. encrypted_log.enc를 AES 키로 복호화 -> 원본 로그 획득
    3. SHA-512로 복호화된 로그의 해시 계산
    4. writer의 공개키로 signature.sig 서명 검증
    5. 계산된 해시와 log.hash 비교 -> 일치 여부 확인

암호화 알고리즘의 역할

  • AES: 로그 내용을 암호화/복호화하기 위한 비밀키
  • RSA : AES 키를 전달하고, 작성자의 신원 증명을 위한 공개키/개인키
  • SHA-512 : 로그의 위변조 검증을 위한 생성

index.jsp

  • 로그인 과정을 임시로 이렇게 만들어놓았는데 구현하다보니 DB가 필요할 것 같아 DB 설계 중입니다..
  • 토글로 작성자와 리더 역할을 선택할 수 있게 된다.
  • 로그인 시에 작성자로 선택한다면 전자봉투 생성 화면이, 리더 역할을 선택 시 전자봉투 검증 화면이 디폴트로 뜨게 된다.
  • 이 부분은 고민 중인데 role을 나누는 게 불필요할 것 같다는 생각도 든다.. 키만 있다면 누구나 전자봉투를 생성하고 검증할 수 있지 않을까?

uploadLog.jsp

  • 로그 파일과 개인키를 파일 형태로 넣은 후, 전자봉투 생성 버튼을 누르면 생성된다.

verifyLog.jsp

  • 전자봉투 파일과 공개키, AES 키 파일을 함께 넣어 검증 과정과 열람을 동시에 가능하게끔 화면을 구성했다.

generateKey.jsp

  • 키 길이는 1024, 2048, 4096 세 가지 버전이 있다.
  • 생성 버튼을 누르게 되면 키를 생성하고자 하는 사람의 PC에 public.key, private.key, README.txt가 들어있는 ZIP 파일이 다운로드된다.

Backend

  • 프로젝트 구조는 일단 이렇게 있고, 대부분은 아직 미완성이라 코드는 나중에 다듬어서 올려보겠습니다.
  • 아래의 4개 클래스는 내용이 크게 바뀌진 않을 것 같아 일단 올리는 것..

CryptoUtil.java

package service;

import java.nio.file.*;
import java.security.*;

import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.security.spec.*;

public class CryptoUtil {

	public static SecretKey generateAESKey() throws Exception {
		KeyGenerator keyGen = KeyGenerator.getInstance("AES");
		keyGen.init(128);
		return keyGen.generateKey();
	}
	
	public static byte[] encryptAES(byte[] data, SecretKey key) throws Exception{
		Cipher cipher = Cipher.getInstance("AES");
		cipher.init(Cipher.ENCRYPT_MODE, key);
		return cipher.doFinal(data);
	}
	
	public static byte[] decryptAES(byte[] encrypted, SecretKey key) throws Exception{
		Cipher cipher = Cipher.getInstance("AES");
		cipher.init(Cipher.DECRYPT_MODE, key);
		return cipher.doFinal(encrypted);
	}
	
	public static byte[] generateHash(byte[] data) throws Exception{
		MessageDigest md = MessageDigest.getInstance("SHA-512");
		return md.digest(data);
	}
	
	public static byte[] sign(byte[] hash, PrivateKey privateKey) throws Exception {
		Signature sig = Signature.getInstance("SHA512withRSA");
		sig.initSign(privateKey);
		sig.update(hash);
		return sig.sign();
	}
	
	public static boolean verifySignature(byte[] hash, byte[] signature, PublicKey publicKey) throws Exception {
		Signature sig = Signature.getInstance("SHA512withRSA");
		sig.initVerify(publicKey);
		sig.update(hash);
		return sig.verify(signature);
	}
	
	public static void saveAESKey(SecretKey key, Path path) throws Exception {
		byte[] encoded = key.getEncoded();
		Files.write(path,  encoded);
	}
	
	public static SecretKey loadAESKey(Path path) throws Exception {
		byte[] encoded = Files.readAllBytes(path);
		return new SecretKeySpec(encoded, "AES");
	}
	
	public static PrivateKey loadPrivateKey(Path path) throws Exception {
		byte[] keyBytes = Files.readAllBytes(path);
		PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
		return KeyFactory.getInstance("RSA").generatePrivate(spec);
	}
	
	public static PublicKey loadPublicKey(Path path) throws Exception {
		byte[] keyBytes = Files.readAllBytes(path);
		X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
		return KeyFactory.getInstance("RSA").generatePublic(spec);
	}
}

KeyManager.java

package service;

import java.io.IOException;
import java.nio.file.*;
import java.security.*;

import javax.crypto.*;

public class KeyManager {
	public static KeyPair generateRSAKeyPair(int keySize) throws NoSuchAlgorithmException{
		KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
		generator.initialize(keySize);
		return generator.generateKeyPair();
	}
	
	public static void savePrivateKey(PrivateKey key, Path path) throws IOException {
		Files.write(path, key.getEncoded());
	}
	
	public static void savePublicKey(PublicKey key, Path path) throws IOException {
		Files.write(path,key.getEncoded());
	}
	
	public static SecretKey generateAESKey() throws Exception {
		return CryptoUtil.generateAESKey();
	}
	
	public static void saveAESKey(SecretKey key, Path path) throws Exception{
		CryptoUtil.saveAESKey(key, path);
	}
}

EnvelopeService.java

package service;

import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.crypto.*;

public class EnvelopeService {
	public static Path createEnvelope(Path logFile, Path privateKeyFile, Path outputDir) throws Exception {
		byte[] logData = Files.readAllBytes(logFile);
		PrivateKey privateKey = CryptoUtil.loadPrivateKey(privateKeyFile);
		SecretKey key = CryptoUtil.generateAESKey();
		
		byte[] encrypted = CryptoUtil.encryptAES(logData, key);
		
		byte[] hash =  CryptoUtil.generateHash(logData);
		byte[] signature = CryptoUtil.sign(hash, privateKey);
		
		Path keyPath = outputDir.resolve("aes.key");
		CryptoUtil.saveAESKey(key, keyPath);
		
		String baseName = logFile.getFileName().toString().replace(".log", "");
		Path envelopePath = outputDir.resolve(baseName+".envpkg");
		
		try(FileOutputStream fos = new FileOutputStream(envelopePath.toFile());
				ZipOutputStream zos = new ZipOutputStream(fos)){
			
			zos.putNextEntry(new ZipEntry("encrypted_log.bin"));
			zos.write(encrypted);
			zos.closeEntry();
			
			zos.putNextEntry(new ZipEntry("signature.sig"));
			zos.write(signature);
			zos.closeEntry();
			
			zos.putNextEntry(new ZipEntry("hash.txt"));
			zos.write(hash);
			zos.closeEntry();
		}
		
		return envelopePath;
	}
}

EnvelopeVerifier.java

package service;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.PublicKey;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.crypto.SecretKey;

public class EnvelopeVerifier {
	public static boolean verifyEnvelope(Path envelopePath, Path publicKeyPath, Path aesKeyPath, Path outputLogPath) throws Exception {
		byte[] encrypted = null;
		byte[] signature = null;
		byte[] hash = null;
		
		try(ZipInputStream zis = new ZipInputStream(new FileInputStream(envelopePath.toFile()))){
			ZipEntry entry;
			
			while((entry = zis.getNextEntry()) != null ) {
				ByteArrayOutputStream baos = new ByteArrayOutputStream();
				byte[] buffer = new byte[1024];
				int len;
				while((len=zis.read(buffer)) > 0) {
					baos.write(buffer, 0, len);
				}
				switch(entry.getName()) {
				case "encrypted_log.bin": 
					encrypted = baos.toByteArray();
					break;
				case "signature.sig":
					signature = baos.toByteArray();
					break;
				case "hash.txt":
					hash = baos.toByteArray();
					break;
				}
				zis.closeEntry();
			}
		}
		if(encrypted == null || signature == null || hash == null) {
			throw new IOException("봉투 내 필수 파일이 누락되었습니다.");
		}
		
		PublicKey publicKey = CryptoUtil.loadPublicKey(publicKeyPath);
		SecretKey aesKey = CryptoUtil.loadAESKey(aesKeyPath);
		
		// 검증
		boolean verified = CryptoUtil.verifySignature(hash, signature, publicKey);
		if (!verified) {
			return false;
		}
		
		// 복호화
		byte[] decrypted = CryptoUtil.decryptAES(encrypted, aesKey);
		Files.write(outputLogPath, decrypted);
		return true;
	}
}

꼭 DB가 필요할지 내 욕심인지
다들 DB를 써서 만들지 어떨진 모르겠지만 . . . 점점 일이 커지는 느낌 엉엉

Spring으로 처음부터 다시 만들기로 했습니당,,

profile
Whatever I want | Interested in DFIR, Security, Infra, Cloud

0개의 댓글