[JCA] HMAC을 이용한 API Request 변조 검증

2022. 7. 29. 15:44Dev/Java

300x250
반응형
  1. HMAC
  2. 해시 함수와 HMAC 의 코드 비교
    1. 해시 함수 - SHA-256
    2. HMAC - HmacSHA256

1. HMAC

Keyed-Hashing for Message Authentication Codes

메시지 변조 여부를 확인하는 MAC에 해싱을 접목한 기술입니다.
비밀키(= 대칭키)를 이용하여 digest를 생성합니다.

대칭키 알고리즘과 해시 알고리즘과는 아래와 같은 차이점이 있습니다.

  • 대칭키 알고리즘은 비밀키를 이용하여 원문으로 복호화가 가능하지만, MAC은 원문으로 복호화할 수는 없습니다.
  • 단방향 해시 알고리즘은 비밀키 없이 digest를 생성하지만, MAC은 digest 생성시 비밀키가 반드시 필요합니다.

digest의 무결성 검사는 비밀키를 아는 사람만이 할 수 있고, 무결성 검사는 해시 알고리즘 처럼 해싱한 값의 일치여부로 판단합니다.
메시지 내용에서 일부가 바뀔경우 digest값이 크게 바뀌는 Avalanche effect(산사태 효과)가 단방향 해시 알고리즘의 특성과 동일하게 적용되기 때문에 공격자가 임의로 메시지 변조를 할 수 없습니다.

HMAC 은 API의 request 변조를 막는데에 좋습니다.

  1. client 측에서는 server에 request를 전송할 때 전달하고자 하는 메시지 원문과 메시지 원문을 HMAC으로 암호화한 digest를 함께 전송합니다.
  2. server 측에서는 전달 받은 메시지 원문을 SECRET_KEY를 이용하여 HMAC 변환한 값digest 의 일치여부를 체크하여 메시지의 변조 여부를 검증합니다.

HMAC 종류로는 HmacMD5, HmacSHA1, HmacSHA256, HmacSHA512, HmacPBESHA256, HmacPBESHA512, PBEWithHmacSHA256 등이 있습니다.

참고: Java Security Standard Algorithm Names - Mac Algorithms


2. 해시 함수와 HMAC 의 코드 비교

2.1. 해시 함수 - SHA-256

단방향 해시 알고리즘은 java.security.MessageDigest 클래스를 이용하여 객체를 생성하며, 별도의 키 없이 해시를 생성할 수 있습니다.
해시 알고리즘의 특성 상, 원문이 조금만 수정되어도 해시값이 완전 바뀝니다.

※ 해시 알고리즘에 대한 자세한 설명을 보고 싶다면 [JCA] Hash 함수의 개요와 PBKDF2를 이용한 단방향 해시 알고리즘 구현 포스팅을 참고해주세요.

public class Sha256Example {

    public static void main(String[] args) {
        String data = "hello jiniworld", data2 = "hello jiniworld!";

        System.out.println(encrypt(data));
        System.out.println(encrypt(data));
        System.out.println(encrypt(data2));
    }

    static String encrypt(String message) {
        try {
            final MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
            messageDigest.update(message.getBytes());
            return Base64.getEncoder().encodeToString(messageDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
}
fBWgim+2DAp0+01x5Ylpr9pPFC7zbx5GMmofaw5+sGI=
fBWgim+2DAp0+01x5Ylpr9pPFC7zbx5GMmofaw5+sGI=
A7Z8clE8u2kBxXxYgArInfN49/VKiOjFXw5zXXvW74g=

2.2. HMAC - HmacSHA256

MAC + SHA-256 으로 암호화한 예제입니다.
일반 단방향 암호화 알고리즘과 차이점으로, 비밀키를 입력받습니다. (line 18)
동일한 문자열에 대해서 동일한 해시값을 출력하며, MessageDigest와 마찬가지로 원문에서 조금만 수정되어도 해시값이 완전 바뀝니다.

public class HmacExample {
    private static final String ALGORITHM = "HmacSHA256";
    private static final SecretKeySpec SECRET_KEY_SPEC;
    static {
        SECRET_KEY_SPEC = new SecretKeySpec("secretKe!y@@98".getBytes(), ALGORITHM);
    }

    public static void main(String[] args) {
        String message = "hello jiniworld", message2 = "hello jiniworld!";;
        System.out.println(encrypt(message));
        System.out.println(encrypt(message));
        System.out.println(encrypt(message2));
    }

    public static String encrypt(String message) {
        try {
            Mac mac = Mac.getInstance(ALGORITHM);
            mac.init(SECRET_KEY_SPEC);
            mac.update(message.getBytes());-
            return Base64.getEncoder().encodeToString(mac.doFinal(message.getBytes()));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
Ic29u6OwnxcnpV4ePBeLCewGU3kM8XbLznUjWCL5dPI=
Ic29u6OwnxcnpV4ePBeLCewGU3kM8XbLznUjWCL5dPI=
eyhcKY6i5iJkuz6iw99rj3SzKkpmR38brDlYyymUYAY=

3. HMAC을 이용한 메시지 검증 예제

위에 정의한 Hmac encrypt 메서드를 이용하여 API 메시지 검증을 해보도록 합시다.

  1. 아래의 POST body값으로 userId, name을 받는다.
  2. Basic 인증방식을 이용하며, Basic 토큰 내에 userId를 HMAC으로 변환한 값을 넣는다.
  3. API server 측에서는 Request Body에 들어있는 userId값과 Basic토큰 내에 들어있는 메시지인증코드를 비교하여 request가 변조되었는지를 체크한다.

@NoArgsConstructor
@Getter
@Setter
public class HmacTestValue {
    private String userId;
    private String name;

    public HmacTestValue(String userId, String name) {
        this.userId = userId;
        this.name = name;
    }
}

아주 간략하게, 메시지 검증을 하는 API를 만들어보았습니다.
만일 메시지가 변조되지 않았을 경우, 메시지 검증 성공!! {이름} 형식을 출력하고, 변조된 경우에는 Authorization 정보가 잘못되었습니다가 출력됩니다.

@RequestMapping("/crypto")
@RestController
public class CryptoController {

    @PostMapping("/hmac")
    public String hmac(@RequestHeader("Authorization") String authorization, @RequestBody HmacTestValue req) {
        if(Strings.isNotBlank(authorization)) {
            authorization = authorization.replaceAll("^Basic ", "");
        }
        if (authorization.equals(HmacExample.encrypt(req.getUserId()))) {
            return "메시지 검증 성공!! " + req.getName();
        } else {
            return "Authorization 정보가 잘못되었습니다";
        }
    }
}

Test코드를 추가했습니다.
첫번째 RestTemplate에서는 변조되지 않은 상태를 가정하고 넣었고
두번째 RestTemplate에서는 변조된 상황을 가정하여 예문을 작성했습니다.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CryptoControllerTest {
    @LocalServerPort
    int port;

    RestTemplate client = new RestTemplate();

    @DisplayName("1. Hmac 메시지 검증 테스트")
    @Test
    void hmacTest() {
        // AUTHORIZATION 에 이용된 HMAC 암호화값과 RequestBody에 들어있는 userId 가 일치한 경우
        String userId = "U001";
        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.AUTHORIZATION, "Basic "+ HmacExample.encrypt(userId));
        HmacTestValue body = new HmacTestValue(userId, "jini");
        HttpEntity entity = new HttpEntity(body, headers);
        ResponseEntity<String> resp = client.exchange("http://localhost:"+port+"/crypto/hmac", HttpMethod.POST, entity, String.class);
        System.out.println(resp.getBody());

        // AUTHORIZATION 에 이용된 HMAC 암호화값과 RequestBody에 들어있는 userId 가 일치하지 않은 경우
        HmacTestValue body2 = new HmacTestValue("U002", "jini");
        HttpEntity entity2 = new HttpEntity(body2, headers);
        ResponseEntity<String> resp2 = client.exchange("http://localhost:"+port+"/crypto/hmac", HttpMethod.POST, entity2, String.class);
        System.out.println(resp2.getBody());    

    }

}

출력 결과는 아래와 같습니다.

메시지 검증 성공!! jini
Authorization 정보가 잘못되었습니다
300x250
반응형