Published on

SSL 2.0과 SSL 3.0 직접 구현한 개인적인 정리

Authors
  • avatar
    Name
    김민석
    Twitter

Introduction

상황

SSL(Secure Sockets Layer)은 인터넷 통신을 보장하기 위해 설계된 암호화 프로토콜로, HTTPS와 같은 보안 프로토콜의 기반이 된다.

하지만 SSL은 여러 버전을 거치며 보안성과 성능 측면에서 문제가 발생했다. SSL 2.0, SSL 3.0은 보안 기준에서 취약하며, 이후 TLS로 대체되었다.

이번 글에서는 SSL의 각 버전(2.0, 3.0)을 직접 구현하고, 이를 통해 발생한 보안 및 성능 문제를 분석해 보겠다.

문제

SSL 2.0

SSL 2.0은 처음 도입된 암호화 프로토콜로, 초창기 인터넷 보안을 위한 표준으로 사용되었다.

  • 취약한 암호화: 충분히 강력한 암호화 알고리즘을 사용하지 않아 데이터 탈취 가능성이 높다.
  • 핸드셰이크의 취약성: 핸드셰이크 과정에서 다운그레이드 공격이 가능하며, 암호화 키가 노출될 수 있다.
  • 메시지 인증 부족: 메시지 변조에 취약하며, 보안성을 보장하지 못한다.

SSL 3.0

SSL 3.0은 SSL 2.0의 문제를 해결하며 보안성과 호환성을 개선했다. 하지만 문제점도 있다.

  • POODLE 공격: SSL 3.0의 CBC 암호화 모드가 POODLE 공격에 취약하다.
  • 암호화 알고리즘 제한: 보안 기준에 부합하지 않는 RC4와 같은 구식 암호화 알고리즘을 사용한다.
  • 세션 재사용 미비: 연결 유지가 어렵고, 성능이 낮다.

시퀀스 다이어그램

  • SSL 2.0 시퀀스 다이어그램
image.png

SSL 2.0의 핸드셰이크는 단순하지만 보안이 취약하다. 특히, Master Secret 전송 과정이 암호화되지 않아 탈취 위험이 있다.

  • SSL 3.0 시퀀스 다이어그램
image.png

SSL 3.0에서는 ClientKeyExchange 단계에서 키 교환이 이루어지며, 데이터 암호화를 위한 기반이 마련 된다. 하지만 CBC 암호화 모드와 POODLE 공격에 취약하다.

구현

SSL 2.0의 간단한 흐름 시뮬레이션

public class SSL2Server {
    public static void main(String[] args) throws Exception {
        // 서버 소켓 생성
        ServerSocket serverSocket = new ServerSocket(8443);
        System.out.println("SSL 2.0 시뮬레이션 서버가 포트 8443에서 시작되었습니다.");

        while (true) {
            try (Socket socket = serverSocket.accept()) {
                // 클라이언트와 연결
                System.out.println("클라이언트 연결 성공!");

                // 데이터 송수신 스트림 생성
                BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);

                // 핸드셰이크 과정 시뮬레이션
                writer.println("ServerHello: 지원 가능한 암호화 알고리즘: [None]");
                String clientMessage = reader.readLine();
                System.out.println("클라이언트로부터 수신한 메시지: " + clientMessage);

                if ("ClientHello: 선택된 암호화 없음".equals(clientMessage)) {
                    writer.println("Connection Established: 암호화 없음");
                } else {
                    writer.println("Connection Failed");
                }

                // 클라이언트와 데이터 송수신
                String clientData = reader.readLine();
                System.out.println("클라이언트로부터 데이터 수신: " + clientData);

                writer.println("서버 응답: " + clientData.toUpperCase());
            }
        }
    }
}

클라이언트 구현 (SSL 2.0 시뮬레이션)

public class SSL2Client {
    public static void main(String[] args) throws Exception {
        try (Socket socket = new Socket("localhost", 8443)) {
            System.out.println("SSL 2.0 시뮬레이션 클라이언트가 서버에 연결되었습니다.");

            // 데이터 송수신 스트림 생성
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);

            // 핸드셰이크 과정 시뮬레이션
            String serverMessage = reader.readLine();
            System.out.println("서버로부터 메시지 수신: " + serverMessage);

            writer.println("ClientHello: 선택된 암호화 없음");
            String serverResponse = reader.readLine();
            System.out.println("서버 응답: " + serverResponse);

            if ("Connection Established: 암호화 없음".equals(serverResponse)) {
                // 데이터 전송
                writer.println("Hello, Server!");
                String response = reader.readLine();
                System.out.println("서버로부터 수신한 응답: " + response);
            } else {
                System.out.println("연결 실패");
            }
        }
    }
}

실행 결과

  • 서버 출력
SSL 2.0 시뮬레이션 서버가 포트 8443에서 시작되었습니다.
클라이언트 연결 성공!
클라이언트로부터 수신한 메시지: ClientHello: 선택된 암호화 없음
클라이언트로부터 데이터 수신: Hello, Server!
  • 클라이언트 출력
SSL 2.0 시뮬레이션 클라이언트가 서버에 연결되었습니다.
서버로부터 메시지 수신: ServerHello: 지원 가능한 암호화 알고리즘: [None]
서버 응답: Connection Established: 암호화 없음
서버로부터 수신한 응답: HELLO, SERVER!

설명

  • 핸드셰이크 시뮬레이션
    • 서버는 ServerHello 메시지를 전송하며 암호화 방식을 제안한다.
    • 클라이언트는 ClientHello 메시지로 선택된 암호화 방식을 서버에 알린다.
    • SSL 2.0의 단순성을 반영하여, 암호화 없는 연결을 시뮬레이션 한다.
  • 데이터 송수신
    • 연결이 성공하면 클라이언트가 데이터를 전송하고, 서버는 데이터를 처리하여 응답을 반환한다.
    • 암호화가 없는 상태로 데이터가 전송되므로, 보안 취약점을 보여준다.

SSL 3.0 서버 구현

public class SSL3Server {
    public static void main(String[] args) throws Exception {
        // 키 저장소 로드
        KeyStore keyStore = KeyStore.getInstance("JKS");
        try (InputStream keyStoreStream = new FileInputStream("server.keystore")) {
            keyStore.load(keyStoreStream, "password".toCharArray());
        }

        // KeyManager 초기화
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
        keyManagerFactory.init(keyStore, "password".toCharArray());

        // SSLContext 설정 (SSL 3.0)
        SSLContext sslContext = SSLContext.getInstance("SSLv3");
        sslContext.init(keyManagerFactory.getKeyManagers(), null, null);

        // SSL 서버 소켓 생성
        SSLServerSocketFactory serverSocketFactory = sslContext.getServerSocketFactory();
        try (SSLServerSocket serverSocket = (SSLServerSocket) serverSocketFactory.createServerSocket(8443)) {
            System.out.println("SSL 3.0 서버가 포트 8443에서 시작되었습니다.");

            while (true) {
                try (Socket socket = serverSocket.accept()) {
                    BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);

                    String message = reader.readLine();
                    System.out.println("클라이언트로부터 수신한 메시지: " + message);
                    writer.println("안녕하세요, 클라이언트!");
                }
            }
        }
    }
}

SSL 3.0 클라이언트 코드

public class SSL3Client {
    public static void main(String[] args) throws Exception {
        // 신뢰 저장소(truststore) 로드
        KeyStore trustStore = KeyStore.getInstance("JKS");
        try (InputStream trustStoreStream = new FileInputStream("client.truststore")) {
            trustStore.load(trustStoreStream, "password".toCharArray());
        }

        // TrustManager 초기화
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
        trustManagerFactory.init(trustStore);

        // SSLContext 설정 (SSL 3.0)
        SSLContext sslContext = SSLContext.getInstance("SSLv3");
        sslContext.init(null, trustManagerFactory.getTrustManagers(), null);

        // SSL 소켓 생성
        SSLSocketFactory socketFactory = sslContext.getSocketFactory();
        try (SSLSocket socket = (SSLSocket) socketFactory.createSocket("localhost", 8443)) {
            socket.startHandshake(); // SSL 핸드셰이크 시작

            PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            // 서버로 메시지 전송
            writer.println("안녕하세요, 서버!");

            // 서버로부터 응답 수신
            String response = reader.readLine();
            System.out.println("서버로부터 수신한 응답: " + response);
        }
    }
}

실행 결과

  • 서버 출력

서버는 클라이언트의 메시지를 수신하고 응답을 전송한다.

SSL 3.0 서버가 포트 8443에서 시작되었습니다.
클라이언트로부터 수신한 메시지: 안녕하세요, 서버!
  • 클라이언트 출력

클라이언트는 서버와 SSL 3.0 핸드셰이크를 완료한 뒤, 메시지를 주고 받는다.

서버로부터 수신한 응답: 안녕하세요, 클라이언트!

설명

  • SSL 핸드셰이크
    • 클라이언트와 서버는 핸드셰이크를 통해 암호화된 안전한 연결을 설정한다.
    • 클라이언트가 ClientHello를 보내면, 서버는 인증서를 포함한 ServerHello를 반환하여 인증 절차를 완료한다.
  • 데이터 송수신
    • 클라이언트가 "안녕하세요, 서버!" 메시지를 서버에 전송한다.
    • 서버는 메시지를 수신하고, 응답 메시지 "안녕하세요, 클라이언트!"를 반환한다.
  • SSL 3.0 사용
    • SSLContext.getInstance("SSLv3")를 사용하여 SSL 3.0 연결을 설정한다.
    • SSL 3.0은 보안 취약점(PADDING 등)으로 인해 실제 환경에서는 사용이 권장되지 않는다.

해결

SSL 2.0은 심각한 보안 취약점으로 인해 요즘은 사용이 권장되지 않는다. 다운그레이드 공격과 암호화 키 노출 위험이 존재하며, 충분히 강력한 암호화 알고리즘을 제공하지 못한다.

따라서 SSL 2.0을 비활성화하고 서버 설정에서 지원되는 프로토콜 목록에서 완전히 제거하였다.

SSL 3.0은 SSL 2.0의 문제를 해결하기 위해 도입되었지만, POODLE공격에 취약하다는 한계를 가지고 있다. 이를 방지하기 위해 CBC 모드 대신 AES-GCM과 같은 강력한 암호화 모드를 사용하는 방향으로 개선하였다.

더불어, 암호화 알고리즘(AES, SHA-256)을 추가로 지원하도록 서버를 구성하고, 서버와 클라이언트 간의 인증서 검증 과정을 강화하였다.

결과적으로 SSL 2.0을 제거하고 SSL 3.0의 문제점을 해결함으로써, 보안 취약점을 보완하고 암호화된 안전한 데이터 전송이 가능해졌다. 이번 작업을 통해 구형 프로토콜의 한계를 인식하고, 최신 보안 표준에 맞는 환경을 설계하는 데 중요한 경험을 얻을 수 있었다.

결말

SSL 2.0과 3.0은 각각 초기 인터넷 보안을 제공했지만, 요즘 환경에서는 심각한 보안 문제로 인해 사용이 권장되지 않는다. 이번 구현을 통해 SSL 2.0과 3.0의 작동 방식이해하고, 프로토콜(TLS)로의 전환 필요성을 실감했다. SSL은 보안 프로토콜의 기초를 제공했지만, TLS의 발전이 필요했던 이유를 명확히 이해할 수 있었다. 이러한 경험으로 안전한 네트워크 통신을 설계하고 구현하는 데 중요한 공부가 되었다.