시냅스

Spring 예제로 보는 보안을 위한 HMAC 본문

Java, Spring

Spring 예제로 보는 보안을 위한 HMAC

ted k 2023. 3. 11. 21:39

https://www.okta.com/identity-101/hmac/

 

HMAC, Hash-based Message Authentication Code

 

HMAC 은 메시지 인증 코드 (message authentication code) 를 생성하기 위한 알고리즘입니다.

송신자와 수신자만이 알고있는 secret key 값을 가지고 해싱하여 요청을 신뢰할 수 있는지 판단합니다.

checksum 의 아이디어에서 기인합니다.

  • checksum
    • 송신자는 데이터를 송신하면서 데이터의 일부분 혹은 전체의 합 또는 XOR 등의 연산으로 데이터와 함께 전송
    • 수신자는 데이터를 받아 checksum 을 다시 계산하여 송신측에서 보낸 값과 일치하는지 확인한다.
    • 다만 checksum은 오류 검출에만 사용되며 데이터의 정확성을 보장하지는 않는다.

 

다음과 같이 동작합니다.

 

  1. 송신할 메시지와 공유 비밀키를 함께 해싱합니다.
  2. 생성된 해시값과 메시지를 함께 전송합니다.
  3. 수신자는 요청으로 받은 메시지를 가지고 공유 비밀키와 함께 해싱합니다.
  4. 수신 받은 해시값과 수신자가 생성한 해시값이 동일한지 비교합니다.
  5. 해시값이 동일하다면 신뢰할 수 있는 것으로 판단합니다.

 

언제 사용할까?

 

서버 A 에서 서버 B 로 결제 요청을 보내는 상황을 가정하겠습니다.

만약 B 서버에서 제공하는 REST API 를 해커가 알아냈고,

요청을 보내며 authentication 이 jwt 밖에 없는 상황이고 jwt 또한 탈취 당했다면

다른 보안 절차가 없으므로 해커는 jwt와 함께 B 서버에 요청을 보내 이득을 취할 것입니다.

 

혹은 해커가 A 에서 B 로 보내는 결제 요청을 중간에 가로채

자신의 계좌로 변경하여 이득을 취할 수도 있을 것입니다.

 

그러므로 송수신자 간의 공유하고 있는 secret key 를 통해 해싱하는 방식으로

요청에 대한 변조, 신뢰성을 확인하여 보안을 강화할 수 있습니다.

 

이러한 방식은 대칭키 방식으로 분류할 수 있습니다.

대칭키방식은 대칭키가 탈취된다면 보안에 취약하다는 단점이 있습니다.

이를 보완하기 위해 비대칭키 방식을 사용하기도 합니다.

차후 비대칭키 방식 또한 살펴볼 수 있도록 하겠습니다.

 

 

예시

 

class Request {
	String mac;
	String message;

	public Request(String mac, String message) {
		this.mac = mac;
		this.message = message;
	}

	public String getMac() {
		return mac;
	}

	public String getMessage() {
		return message;
	}
}
  • 데이터 송수신을 위한 예시 객체입니다.

 

@RestController
class HmacTestController {
	// secret key
	private static final String SECRET_KEY = "hmac";
	// hash 알고리즘 규격
	private static final String ALGORITHM = "HmacSHA256";

	@PostMapping("/")
	public String test(@RequestBody Request request) throws Throwable {
		// key 생성
		SecretKey secretKey = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);

		// MAC 알고리즘 구현하는 MAC 객체
		Mac hasher = Mac.getInstance(ALGORITHM);
		// MAC 객체 init
		hasher.init(secretKey);
		// 데이터를 암호화
		byte[] hash = hasher.doFinal(request.getMessage().getBytes());
		// String 으로 변환
		String hashed = HexUtils.toHexString(hash);

		if (Objects.equals(hashed, request.getMac())) return "OK";
		else return "false";
	}

}
  • message 와 mac 을 갖고 있는 request 객체가 요청으로 들어오면
  • 서버에서는 요청을 토대로 위에서 설명한대로 진행합니다.
  • 만약 요청이 신뢰할 수 있다면 OK 를 반환하고 아니라면 false를 반환합니다.

 

@SpringBootTest
class HamcApplicationTests {
	private static final String SECRET_KEY = "hmac";
	private static final String ALGORITHM = "HmacSHA256";

	@Test
	void 올바른_요청일시() throws Exception{
		RestTemplate restTemplate = new RestTemplate();
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_JSON);

		SecretKey secretKey = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);
		Mac hasher = Mac.getInstance(ALGORITHM);
		hasher.init(secretKey);

		String message = "this is message";
		byte[] hash = hasher.doFinal(message.getBytes());
		String hashed = HexUtils.toHexString(hash);

		HttpEntity<Request> requestHttpEntity = new HttpEntity<>(new Request(hashed, message), headers);
		String url = "http://localhost:8080";

		ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestHttpEntity, String.class);
		System.out.println(response.getBody());
	}

}
  • 이를 위해 restTemplate 로 요청을 보내어 확인합니다.
  • 위의 예시는 올바른 요청일 때를 가정합니다.

 

  • 원하는 OK sign이 떨어졌습니다.

 

 

  • 실패하는 상황을 가정합니다.
  • 위의 올바른 요청에서 message만 다른 것으로 바꿔 보냈습니다.
  • hashing 할 때 쓴 message 와 httpentity 를 생성하며 만든 message가 다른 것으로 변조되었다는 것을 가정합니다.

 

  • false 를 반환하며 우리가 기대했던 값임을 확인할 수 있습니다.

 

끝!

Comments