BYOK(키 주입) 키 준비

Prev Next

VPC 환경에서 이용 가능합니다.

이 문서는 BYOK(키 주입)에 사용할 키를 준비하는 방법을 안내합니다. 애플리케이션은 보통 키를 파일이 아니라 Java 키 객체로 다룹니다. 여기서는 애플리케이션 코드에서 키 객체가 흔히 등장하는 지점을 예로 들고, 그 객체에서 주입 입력 파일을 꺼내는 방법을 설명합니다. 고객마다 코드 구성이 다르므로 대표적인 경우만 예로 듭니다.

:::(info) (참고)
이 문서에서 만든 파일은 BYOK(키 주입) Java 예제 또는 BYOK(키 주입) CLI 예제의 주입 절차에 그대로 입력합니다. 주입 원리와 지원 범위는 BYOK(키 주입) 개요를 참고해 주십시오.

이 문서의 작업은 모두 키를 보유한 시스템에서 수행합니다. HSM 클라이언트 서버나 LunaProvider는 필요하지 않습니다.

:::

주입 입력 형식

키 유형 키 객체 주입 입력 형식 꺼내는 방법
AES-256 대칭키 SecretKey 원시 32바이트 (.bin) key.getEncoded()
RSA/EC 개인키 PrivateKey PKCS#8 DER (.pk8.der) privateKey.getEncoded()
RSA/EC 공개키 PublicKey X.509 PEM (.pub.pem) publicKey.getEncoded()를 Base64로 인코딩해 PEM으로 감쌈

:::(warning) (주의)

  • getEncoded()가 null을 돌려주는 키는 추출할 수 없는 키입니다. 다른 HSM이나 운영체제 키 저장소 안에서 생성되어 밖으로 꺼낼 수 없게 만든 키가 여기에 해당하며, BYOK 대상이 아닙니다.
  • 꺼낸 .bin과 .pk8.der는 키 평문입니다. 키를 보유한 시스템에서만 다루고, 아래 예시처럼 소유자만 읽고 쓸 수 있는 권한(600)으로 만든 뒤 주입이 끝나면 즉시 삭제하십시오.

:::

파일 이름

이 문서의 예시는 주입 가이드와 같은 /tmp/sample-{유형}-$TS.{확장자} 형식으로 파일을 저장합니다. TS는 파일 이름이 겹치지 않도록 붙이는 타임스탬프입니다. 주입 가이드의 실행 공통값(Java, CLI)에서도 TS를 정하는데, 이 문서를 먼저 진행했다면 그때 TS를 새로 정하지 말고 여기서 정한 값을 그대로 사용하십시오. 그러면 주입 절차의 명령을 파일 경로 수정 없이 그대로 사용할 수 있습니다.

export TS=${TS:-$(date +%Y%m%d%H%M%S)}; echo "TS=$TS"

이미 TS가 설정되어 있으면 그 값을 그대로 사용합니다. 출력된 값을 기록해 두고, 셸을 새로 열거나 HSM 클라이언트 서버에서 주입 절차를 진행할 때 export TS=<기록한 값>으로 같은 값을 지정하십시오.

아래 Java 예시는 이 값을 환경변수에서 읽어 파일 이름에 씁니다. 예시 코드는 JDK 11 이상의 API(Path.of, Files.writeString)를 사용합니다.

String ts = System.getenv("TS");   // 위에서 export 한 TS

평문 키 파일은 처음부터 소유자만 읽고 쓸 수 있는 권한으로 만들도록, 아래 예시들은 다음 메서드로 파일을 씁니다(Linux 기준). 같은 이름의 파일이 이미 있으면 덮어쓰지 않고 오류를 냅니다.

// 평문 키 파일을 권한 600(rw-------)으로 새로 만들어 씁니다.
static void writeSecret(Path path, byte[] data) throws IOException {
    Files.write(Files.createFile(path,
            PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------"))), data);
}

AES-256 키 — AES-GCM 암호화 코드

애플리케이션에 다음과 같은 암호화 코드가 있다면, cipher.init에 넘기는 key가 주입할 AES 키입니다.

SecretKey key = ...;   // 애플리케이션이 쓰던 AES-256 키

// Provider를 지정하지 않으면 JDK 기본 Provider(SunJCE)를 사용합니다.
// Cipher.getInstance("AES/GCM/NoPadding", "BC")처럼 다른 Provider를 지정해도 아래 꺼내는 방법은 같습니다.
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));

같은 key 객체에서 원시 32바이트를 꺼내 저장합니다.

byte[] raw = key.getEncoded();   // 32바이트. null이면 추출할 수 없는 키입니다.
writeSecret(Path.of("/tmp/sample-aes-" + ts + ".bin"), raw);

RSA·EC 키쌍 — 서명 코드

애플리케이션에 다음과 같은 서명 코드가 있다면, keyPair가 주입할 키쌍입니다.

KeyPair keyPair = ...;   // 애플리케이션이 쓰던 RSA 또는 EC 키쌍

// Provider에 관한 내용은 위 AES 예시와 같습니다.
Signature signer = Signature.getInstance("SHA256withRSA");        // RSA 키
// Signature signer = Signature.getInstance("SHA256withECDSA");   // EC 키
signer.initSign(keyPair.getPrivate());

개인키는 PKCS#8 DER로, 공개키는 PEM으로 저장합니다. EC 키라면 파일 이름의 rsa를 ec로 바꾸십시오.

// 개인키 → PKCS#8 DER
writeSecret(Path.of("/tmp/sample-rsa-" + ts + ".pk8.der"), keyPair.getPrivate().getEncoded());

// 공개키 → X.509 PEM
String pem = "-----BEGIN PUBLIC KEY-----\n"
        + Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.US_ASCII))
                .encodeToString(keyPair.getPublic().getEncoded())
        + "\n-----END PUBLIC KEY-----\n";
Files.writeString(Path.of("/tmp/sample-rsa-" + ts + ".pub.pem"), pem);

KeyStore에 보관한 키

키를 KeyStore 파일에 보관하고 있다면, getKey로 꺼낸 객체가 주입할 키입니다. KeyStore 형식에 맞게 getInstance의 인자를 지정하십시오. AES 키는 PKCS12 또는 JCEKS에 보관할 수 있고, JKS에는 보관할 수 없습니다.

KeyStore ks = KeyStore.getInstance("PKCS12");   // JCEKS라면 "JCEKS", JKS라면 "JKS"
try (FileInputStream in = new FileInputStream("my.p12")) {
    ks.load(in, password);
}
Key key = ks.getKey(alias, password);   // AES 키면 SecretKey, RSA/EC 키면 PrivateKey
if (key instanceof PrivateKey) {
    // RSA/EC 키는 함께 저장된 인증서에서 공개키를 꺼냅니다. AES 키에는 인증서가 없습니다.
    PublicKey publicKey = ks.getCertificate(alias).getPublicKey();
}

꺼낸 key가 SecretKey이면 AES 예시와 같이, PrivateKey이면 RSA·EC 예시와 같이 저장합니다. 공개키는 위 publicKey를 같은 방법으로 PEM으로 저장합니다.

파일 대신 화면 출력으로 옮기기

애플리케이션이 실행되는 곳에서 파일을 바로 쓰기 어렵다면, 값을 Base64로 출력한 뒤 복사해 키 보유 측 작업 시스템에서 파일로 저장할 수 있습니다.

// AES 키
System.out.println(Base64.getEncoder().encodeToString(key.getEncoded()));
// 개인키
System.out.println(Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.US_ASCII))
        .encodeToString(keyPair.getPrivate().getEncoded()));
// 공개키는 위 RSA·EC 예시의 pem 문자열을 그대로 출력합니다.
System.out.println(pem);

실행 위치 — 키 보유 측 작업

출력된 값을 복사한 뒤 다음 명령을 실행하고, 붙여넣은 다음 Enter와 Ctrl+D를 차례로 입력합니다.

umask 077   # 이 셸에서 만드는 파일을 소유자만 읽고 쓸 수 있게 함

# AES 키: Base64를 디코딩해 저장
base64 -d > "/tmp/sample-aes-$TS.bin"

# 개인키: Base64를 디코딩해 저장 (EC 키라면 rsa 를 ec 로)
base64 -d > "/tmp/sample-rsa-$TS.pk8.der"

# 공개키: PEM을 그대로 저장 (EC 키라면 rsa 를 ec 로)
cat > "/tmp/sample-rsa-$TS.pub.pem"

옮길 값마다 해당하는 명령 하나를 실행하고 붙여넣으십시오.

:::(warning) (주의)
화면에 출력한 키 값은 터미널 스크롤 기록, 클립보드, 애플리케이션 로그에 남을 수 있습니다. 옮긴 뒤에는 출력 코드를 제거하고 터미널 기록과 클립보드를 지우십시오. 키 값을 명령행 인자(echo '...' 등)로 넘기면 셸 기록에 남으므로 위처럼 표준입력으로 붙여넣으십시오.

:::

Java 코드 없이 PKCS#12 파일에서 꺼내기

키를 PKCS#12 파일로 보관하고 있다면 Java 코드 없이 OpenSSL로 꺼낼 수도 있습니다. PKCS#12 파일에 개인키가 하나만 들어 있는 경우의 예입니다.

실행 위치 — 키 보유 측 작업

umask 077
# 개인키 → PKCS#8 DER
openssl pkcs12 -in my.p12 -nocerts -nodes \
  | openssl pkcs8 -topk8 -nocrypt -outform DER -out "/tmp/sample-rsa-$TS.pk8.der"
# 인증서의 공개키 → X.509 PEM
openssl pkcs12 -in my.p12 -clcerts -nokeys \
  | openssl x509 -pubkey -noout > "/tmp/sample-rsa-$TS.pub.pem"

OpenSSL 3에서 오래된 방식(RC2, 3DES 등)으로 암호화된 PKCS#12 파일을 열면 unsupported 오류가 날 수 있습니다. 이 경우 두 openssl pkcs12 명령에 -legacy를 붙여 다시 실행하십시오.

다음 단계