[Spring Cloud Config] 비대칭 키로 암호화하는 과정

yeahdy_:)·2024년 6월 5일

Spring

목록 보기
2/3
post-thumbnail

spring cloud config 공식문서에서는 비대칭키 사용 시 “암호화는 공개키로 수행되며, 복호화에는 개인키가 필요” 하다고 한다.

그래서 처음 예상한 것은 spring cloud config의 설정파일에 공개키와 개인키를 둘 다 명시하고,
암호화 할 때는 공개키를 읽어오고, 복호화할 때는 개인키를 가져오는 것이라고 생각했다.

그런데 spring cloud config의 설정파일에 개인키(private key)만 설정했는데 암/복호화가 가능해서 어떻게 공개키로 암호화를 하고 복호화는 개인키로 하는지 궁금했다.

배경

비대칭키를 사용하기 위해 keytool을 통해 키 저장소 파일을 생성 해 놓은 상태이다.

  • public Key(공개키): publicKey.jks
  • private Key(개인키): bookmarkEncryptionKey.jks

만든 비대칭키를 사용하기 위해 spring cloud config의 설정파일에 encrypt.key-store 를 작성했다. 여기서 bookmarkEncryptionKey.jks 는 개인키이다.

encrypt:
  key-store:
    location: file:///${user.home}\springcloud\keystore\bookmarkEncryptionKey.jks
    password: 비밀번호
    alias: 별칭키

Spring Cloud Config는 암호화와 복호화를 모두 처리할 수 있도록 /encrypt와 /decrypt 엔드포인트를 제공하는데, 위 설정대로 /encrypt 호출 시 암호화된 데이터를 얻고, /decrypt 호출 시 복호화된 데이터를 얻을 수 있다.

그럼 개인키(private key)만 설정했는데 어떻게 암호화가 가능한걸까?

private Key 살펴보기

맨 처음에 키 저장소를 생성할 때 keytool -genkeypair 명령어를 통해 공개키와 개인키를 모두 생성하고, 이들을 지정된 키 저장소 파일(bookmarkEncryptionKey.jks)에 저장한다.

keytool -genkeypair -alias bookmarkEncryptionKey -keyalg RSA -dname "CN=Lee, OU=API Development, O=joneconsulting.co.kr, L=Seoul, C=KR" -keypass "비밀번호" -keystore bookmarkEncryptionKey.jks -storepass “비밀번호"

그럼 bookmarkEncryptionKey.jks에 저장된 모든 키의 별칭과 상세정보를 조회 해 보면

keytool -list -v -keystore {키저장소명.jks}

PrivateKeyEntry 로 해당 키는 개인키라는걸 알 수 있고, Public Key Algorithm 을 통해 공개키에 대한 알고리즘과 아래에는 공개키 정보가 함께 명시되어 있다.

따라서 bookmarkEncryptionKey.jks 개인키에는 공개키와 함께 쌍으로 저장되어 있는 것이다.

그럼 /encrypt 호출 시 암호화 되는 과정에서 공개키를 가지고 오는지 확인 해 보았다.

/encryp 암호화 디버깅

/encrypt에 평문 데이터를 전송하면 아래의 encrypt메소드로 들어온다. (EncryptionController.class에 위치)

@PostMapping({"encrypt"})
public String encrypt(@RequestBody String data, @RequestHeader("Content-Type") MediaType type) {
    return this.encrypt(this.defaultApplicationName, this.defaultProfile, data, type);
}

@PostMapping({"/encrypt/{name}/{profiles}"})
public String encrypt(@PathVariable String name, @PathVariable String profiles, @RequestBody String data, @RequestHeader("Content-Type") MediaType type) {
    String input = this.stripFormData(data, type, false);
    TextEncryptor encryptor = this.getEncryptor(name, profiles, input);
    this.validateEncryptionWeakness(encryptor);
    Map<String, String> keys = this.helper.getEncryptorKeys(name, profiles, input);
    String textToEncrypt = this.helper.stripPrefix(input);
    String encrypted = this.helper.addPrefix(keys, encryptor.encrypt(textToEncrypt));
    if (logger.isInfoEnabled()) {
logger.info("Encrypted data");
    }

TextEncryptor encryptor = this.getEncryptor(name, profiles, input); 에서 getEncryptor메소드를 쭉 타고 들어가면

아래와 같이 getKeyPair메소드(KeyStoreKeyFactory.class)가 호출 되는데, 키 저장소에 접근해서 공개키를 추출하는 역할을 한다.

public KeyPair getKeyPair(String alias, char[] password) {
    try {
        synchronized(this.lock) {
            ...
        }

        RSAPrivateCrtKey key = (RSAPrivateCrtKey)this.store.getKey(alias, password);
        Certificate certificate = this.store.getCertificate(alias);
        PublicKey publicKey = null;
        if (certificate != null) {
            publicKey = certificate.getPublicKey();
        } else if (key != null) {
            ...
        }

        return new KeyPair(publicKey, key);
    } catch (Exception var16) {
        throw new IllegalStateException("Cannot load keys from store: " + this.resource, var16);
    }
}

Certificate certificate = this.store.getCertificate(alias); 에서 인증서를 가지고 오고, 인증서가 있으면 getPublicKey() 로 공개키를 가져오는데,

디버깅 해보니 이미지와 같이 인증서에서 publicKey를 가지고 오고, 맨 처음에 keytool 명령어로 bookmarkEncryptionKey.jks를 조회했을 때 표시된 public key와 같은 것을 확인할 수 있었다.

그리고 리턴 시 공개키(PublicKey)와 개인키(key)를 쌍으로 가지는 KeyPair객체를 반환한다. (KeyPair객체는 암호화 및 복호화 작업에 사용된다.)

따라서 개인키에 저장된 공개키를 가지고 와서 암호화 하는 것을 알 수 있다.


서버에서 공개키-암호화, 클라이언트에서 개인키-복호화

위에서 설명한 방식은 spring cloud config에서 암호화와 복호화를 모두 담당하고 있지만, 공식문서에서는 서버와 클라이언트로 암호화 복호화를 분리할 수 있다고 한다.

The encryption is done with the public key, and a private key is needed for decryption. Thus, in principle, you can configure only the public key in the server if you want to only encrypt (and are prepared to decrypt the values yourself locally with the private key).
참고: https://docs.spring.io/spring-cloud-config/docs/current/reference/html/

해석하면 "암호화는 공개키로 수행되며, 복호화에는 개인키가 필요합니다. 따라서 원칙적으로, 만약 암호화만을 원하고 개인 키로 로컬에서 직접 값을 복호화할 준비가 되어 있다면, 서버에서 공개 키만 설정할 수 있습니다._"

이 말에 따르면 spring cloud config 서버에서는 공개키로 암호화만 하고, 해당 암호화된 데이터를 사용하는 클라이언트에서 개인키를 사용 해 복호화를 진행할 수도 있다는 것이다.

https://joomn11.tistory.com/101 에서 서버에서 암호화, 클라이언트에서 복호화 하는 과정을 볼 수 있었다.

그런데 클라이언트에서 복호화를 수행하게 되면 키 관리 과정이 모든 클라이언트에 분산되기 때문에 서버에서 암/복호화를 모두 집중적으로 관리하는 것이 더 효율적일 수도 있다.

여기서 드는 궁금증은 그럼 공개키로는 복호화가 안되는건가?

공개키로는 복호화 해 보기

공개키를 통해서 /decrypt 로 호출 했을 때 어떻게 작동하는지 decrypt메소드를 디버깅 해 보았다.

공개키는 만들어 놓은 publicKey.jks 를 사용했다.

@PostMapping({"decrypt"})
public String decrypt(@RequestBody String data, @RequestHeader("Content-Type") MediaType type) {
    return this.decrypt(this.defaultApplicationName, this.defaultProfile, data, type);
}

@PostMapping({"/decrypt/{name}/{profiles}"})
public String decrypt(@PathVariable String name, @PathVariable String profiles, @RequestBody String data, @RequestHeader("Content-Type") MediaType type) {
    try {
        TextEncryptor encryptor = this.getEncryptor(name, profiles, data);
        this.checkDecryptionPossible(encryptor);
        ...
        return decrypted;
    } catch (IllegalStateException | IllegalArgumentException var8) {
        ...
    }
}

TextEncryptor encryptor = this.getEncryptor(name, profiles, data); 를 보면 getEncryptor메소드를 통해 TextEncryptor객체를 가져온다.

가져온 TextEncryptor객체를 보면 아래와 같은 정보를 가지고 있는데 privateKey = null이 담겨있다.

그리고 그다음 줄에 checkDecryptionPossible메소드를 호출하는데,

private void checkDecryptionPossible(TextEncryptor textEncryptor) {
    if (textEncryptor instanceof RsaSecretEncryptor && !((RsaSecretEncryptor)textEncryptor).canDecrypt()) {
        throw new DecryptionNotSupportedException();
    }
}

조건문의 textEncryptor instanceof RsaSecretEncryptor 는 true 지만,

((RsaSecretEncryptor)textEncryptor).canDecrypt()는 false 이기 때문에 예외가 발생한다.

((RsaSecretEncryptor)textEncryptor).canDecrypt() 는 false가 발생할까? canDecrypt() 를 확인하면 결정적인 단서가 나온다!

public boolean canDecrypt() {
    return this.privateKey != null;
}

읽어온 publicKey.jks 의 privateKey 가 null이기 때문에 false를 반환하기 때문이다.

따라서 포스트맨 호출 결과 공개키로 복호화를 시도하면 400 에러를 반환하고, 복호화가 불가능하다.

profile
기억하기 위해 기록하고 있습니다. 포스트 중 잘못된 정보가 있다면 코멘트 남겨주세요🐰

0개의 댓글