본문 바로가기
  • Floodnut's Home Directory
보안/OAuth 2.0

[OAuth 2.0 API 보안] TLS를 통한 API 보안

by Floodnut 2022. 5. 19.

1. Order API 확인

스프링부트를 통한 예제 Order API를 이용하여 간단하게 알아보고자 한다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

예제에서 위 종속성은 각각 다음을 제공한다.

  • 다른 스프링 모듈의 통합을 위한 starter
  • 톰캣과 스프링 MVC, REST 등의 환경과 구조를 제공하는 starter-web
  • 애플리케이션 모니터링 및 관리를 위한 starter-actuater
@RestController
@RequestMapping(value = "/order")
public class OrderProcessing {

    @RequestMapping(value = "/{id}/status", method = RequestMethod.GET)
    public ResponseEntity<?> checkOrderStatus(@PathVariable("id") String orderId) {
        return ResponseEntity.ok("{'status' : 'shipped'}");
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public ResponseEntity<?> getOrder(@PathVariable("id") String orderId) {
        Item book1 = new Item("101", 1);
        Item book2 = new Item("103", 5);
        PaymentMethod myvisa = new PaymentMethod("VISA", "01/22", "John Doe", "201, 1st Street, San Jose, CA");
        Order order = new Order("101021", orderId, myvisa, new Item[] { book1, book2 },
                "201, 1st Street, San Jose, CA");
        return ResponseEntity.ok(order);
    }

}

위 코드를 살펴보자.

  • checkOrderStatus 메서드는 URI를 통해 인자를 넘겨받고 200_ok를 반환한다.
  • getOrder 메서드는 URI를 통해 인자를 넘겨받고 Order 객체를 JSON 형태로 반환해준다.
//curl http://localhost:8080/order/11
{
  "customer_id": "101021",
  "order_id": "11",
  "payment_method": {
    "card_type": "VISA",
    "expiration": "01/22",
    "name": "John Doe",
    "billing_address": "201, 1st Street, San Jose, CA"
  },
  "items": [
    {
      "code": "101",
      "qty": 1
    },
    {
      "code": "103",
      "qty": 5
    }
  ],
  "shipping_address": "201, 1st Street, San Jose, CA"
}

http://127.0.0.1:8080/order/11 접근

 

앞서 확인한 메서드에 대해 URL로 접근하면 위와 같은 형태의 JSON 문자열을 반환한다.

2. TLS 동작 원리

API에 TLS를 대입하기 전에 TLS의 동작원리를 간단하게 알아보려 한다. TLS 동작 과정 중의 선택 사항들은 최소화하겠다.

클라이언트와 서버 간 보호된 통신 채널d을 제공해주는 것이 TLS이고 이를 통해 클라이언트와 서버는 서로를 식별할 수 있다. 일반적으로는 서버만이 클라이언트를 인증하는 단방향 TLS를 사용한다.

TCP는 신뢰할 수 없는 채널에서 신뢰할 수 있는 네트워크처럼 동작하는 추상화 계층이다. TCP는 핸드셰이킹 과정을 통해 연결성 채널을 유지한다.

TLS는 TCP와 같은 핸드셰이크 단계와 데이터 전송 단계로 이루어진다.

  • TLS 핸드셰이크 : 아래의 프로토콜을 포함한다.
    • Handshake 프로토콜 : 서버-클라이언트 간 사용할 암호 키에 대한 합의 과정
    • Change Cipher Spec 프로토콜 : 이후 통신을 위한 암호화 채널로의 전환
    • Alert 프로토콜 : 경고 생성 및 TLS 연결 당사자에게 알림 (인증서 폐기와 같은 경우)
    • 과정
      1. TCP 핸드셰이킹 이후 TLS 핸드셰이킹을 시작한다.
      2. 클라이언트 → 서버 의 Client Hello 메시지를 보낸다.
      3. 이때, Client Hello를 통해 클라이언트가 지원하는 가장 높은 TLS 버전, 클라이언트 난수, 지원 암호 스위트 및 압축 알고리즘, 세션 식별자(옵션)을 보낸다.
      4. 서버 → 클라이언트로 Server Hello 응답 메시지를 보낸다.이 메시지에는 서버와 클라이언트 상호 간의 가장 높은 TLS 프로토콜 버전, 압축 알고리즘과 서버 난수, 가장 강력한 암호 스위트가 포함된다.
      5. 이 과정 직전 TCP ACK 메시지를 클라이언트로 응답한다.
      6. 클라이언트와 서버는 각각의 난수를 통해 마스터 시크릿 키를 생성한다.
      7. Server Hello 메시지 전송 이후, 서버는 CA를 통한 인증서와 함께 공인 인증서를 보낸다.
      8. 클라이언트는 서버 인증서의 유효성을 검사한다.
      9. 서버는 클라이언트로 인증서를 요청하면서 신뢰 인증기관 목록과 인증서 유형을 함께 보낸다.
      10. 서버는 클라이언트로 Server Hello Done 메시지를 보내며 초기 단계를 마무리한다.
  • 애플리케이션 데이터 전송
    • TLS 핸드셰이크 이후 민감 데이터를 상호 간에 교환할 수 있다.
    • 앞선 핸드셰이킹 과정 중 생성한 클라이언트의 임의의 키, 서버의 임의의 키, 프리 마스터 시크릿 키는 서로 공유한다. 이 키를 통해 마스터 시크릿을 생성하며 마스터 시크릿은 전송하지 않는다.
    • 마스터 시크릿을 통해 키를 추가로 생성하고 이를 바탕으로 MAC를 계산한다.
    • 나가는 데이터는 블록화하고 들어오는 데이터는 조립하며 블록은 MAC를 통해 계산되고 암호화되고 검증된다.

3. TLS를 이용한 Order API 보안

TLS를 이용하기 위해서 공개키와 개인키의 쌍이 필요하다.

$ keytool -genkey -alias spring -keyalg RSA -keysize 4096 -validity 3650
\-dname "CN=foo,OU=bar,O=zee,L=sjc,S=ca,C=us" -keypass springboot 
\-keystore keystore.jks -storeType jks -storepass springboot

자바의 기본 배포에서 제공되는 keytool로 키 쌍을 생성하고 keystore.jks 파일에 저장하자.

일반적으로 JKS 포맷과 PKCS#12 포맷이 주로 사용되며 JKS는 자바에서만 사용할 수 있다.

위 명령에 대해 간단히 알아보면 다음과 같다.

  • alias 옵션은 키 저장소의 키를 식별하고자 할 때 사용한다. 이 옵션을 통해 지정하는 값은 고유 값이다.
  • validity 옵션은 키의 유효 기간을 일 단위로 지정한다.
  • genkey 옵션을 통해 키를 생성하며 genkeypair로 대체할 수 있다.

이렇게 생성한 키는 공인된 CA를 통해 서명된 키는 아니여서 배포 시 외부로 노출되는 영역보다는 내부 시스템 간의 보호를 위해 사용한다.

# application.properties

server.ssl.key-store: keystore.jks
server.ssl.key-store-password: springboot
server.ssl.keyAlias: spring

스프링 부트에서 TLS를 활용하기 위해 위의 설정 값을 추가한다. 이때, 키 저장소에 접근하는 암호로 “springboot”를 사용한다.

TLS 적용 이후 http://127.0.0.1:8080/order/11 접근
TLS 적용 이후 https://127.0.0.1:8080/order/11 접근

//curl -k https://localhost:8080/order/11
{
    "customer_id":"101021",
    "order_id":"11",
    "payment_method":{
        "card_type":"VISA",
        "expiration":"01/22",
        "name":"John Doe",
        "billing_address":"201, 1st Street, San Jose, CA"
    },
    "items":[
        {
            "code":"101",
            "qty":1
        },
        {
            "code":"103",
            "qty":5
        }
    ],
    "shipping_address":"201, 1st Street, San Jose, CA"
}

자체 서명한 TLS 이기에 외부에서 접근할 수 없다.

그럼에도 TLS가 서명되어 있기에 HTTP를 통한 요청은 Bad Request를 반환한다.

반대로 HTTPS를 통한 요청은 정상적인 응답을 반환하는 것을 확인할 수 있다.

여기서 curl의 -k 옵션은 자체 서명한 인증서의 신뢰 여부를 따지지 않겠다는 의미이다.

 

 

keytool -export -file ca.crt -alias spring -rfc 
    \-keystore keystore.jks -storePass springboot

위 명령을 통해 키 저장소 내부의 인증서를 PEM 포맷의 ca.crt 파일로 내보낼 수 있다.

ca.crt 인증서를 통한 cURL 요청

$ curl --cacert ca.crt https://127.0.0.1:8080/order/11
curl: (60) SSL: certificate subject name 'foo' does not match target host name '127.0.0.1'

위에서 추출한 ca.crt 파일의 인증서와 함께 cURL 요청을 보내면 앞서 인증서를 생성하며 지정했던 foo와 localhost의 호스트 명이 일치하지 않아 에러를 발생시키는 것을 확인할 수 있다.

위와 같은 예제를 통해 내부 시스템 간의 보호에서 TLS를 적용할 때, 인증서의 명칭과 호스트 명을 일치시켜 행위에 대한 승인을 따질 수 있다.

4. TLS 상호 인증을 통한 Order API 보호

앞서서는 인증서와 호스트 간의 명칭 일치에 관해 알아보았다면 이번에는 API와 클라이언트 간의 상호 인증에 대해 알아본다.

# application.properties

server.ssl.client-auth:need

스프링 부트의 application.properties 파일에 위 설정 값을 추가한다.

이후 API 서버를 시작해보자.

 

인증 거부

위의 명령을 보자.

TLS 상호 인증을 추가한 이후 cURL 요청을 통한 명령은 에러를 발생시킨다.

클라이언트는 상호 인증되지 않은 상태이기에 연결이 거부된 것이다.

이를 해결하기 위해선 cURL 클라이언트에 대응되는 공개키-개인키 쌍을 만들고 Order API가 공개키를 신뢰하도록 등록해야 한다.

 

openssl을 통한 키 생성

$ openssl req -key privkey.pem -new -x509 -sha256 -nodes -out client.crt 
    \-subj "/C=us/ST=ca/L=sjc/O=zee/OU=bar/CN=client"

$ keytool -import -file client.crt -alias client -keystore keystore.jks 
    \-storepass springboot

OpenSSL을 통해 클라이언트의 개인키를 생성하자.

이 개인키를 바탕으로 자체 서명 인증서를 발급한다. 이후 스프링부트의 키 저장소에 cURL 클라이언트의 인증서를 등록한다.

 

 

TLS 상호 인증

TLS 상호 인증을 통해 위와 같이 정상적인 응답이 오는 것을 확인할 수 있다.

만약 키 저장소에 등록되지 않은 키 쌍일 경우 인증서 오류가 발생할 것이다.

현재는 호스트에서 인증서 생성과 등록 과정을 알아보았지만 도커와 같은 컨테이너 환경에서 인증 서버를 두거나 API 서버를 격리하여 인증에 관한 키 쌍을 등록할 수 있다.

이러한 방식은 API 내부에 접근하는 공격자의 클라이언트에 대한 인증을 시행하여 API를 보호할 수 있다.

태그

,

댓글0