본문 바로가기
빌드노트 검색

빌드노트

Node에서 암호화된 PKCS#8 PEM을 읽을 때 passphrase가 빠졌다면

암호화된 PKCS#8 PEM을 문자열만 넘기면 createPrivateKey에서 실패할 수 있다. 전체 PEM과 암호화 당시의 passphrase를 함께 전달하는 수정, 짧은 합성 입력 예제, 누락·오입력·정상 입력의 실제 차이를 설명한다.

Node의 createPrivateKey(pemText)에 개인키 PEM 문자열을 넘겼는데 서명을 시작하기도 전에 예외가 난다면, 그 PEM이 암호화되어 있는지 확인한다. 암호화된 PKCS#8 개인키는 PEM 내용과 그 키를 보호한 passphrase를 함께 전달해야 읽을 수 있다. 문자열만 넘기는 호출에는 비밀번호를 전달할 자리가 없다.

먼저 PEM의 시작 줄을 확인한다. PKCS#8의 -----BEGIN ENCRYPTED PRIVATE KEY-----는 암호화된 컨테이너, -----BEGIN PRIVATE KEY-----는 암호화되지 않은 컨테이너를 표시한다. 헤더·Base64 본문·푸터와 줄바꿈 구조를 그대로 전달한다. 헤더 이름을 바꾸거나 Base64 부분만 떼어 내는 것은 복호화가 아니다. RFC 7468의 개인키 텍스트 형식 (새 탭에서 열림)

암호화된 PEM이라면 기존 호출을 아래처럼 바꾼다. pemText는 애플리케이션에서 읽어 둔 전체 PEM 문자열 또는 Buffer, passphrase는 그 개인키를 암호화할 때 사용한 비밀번호 문자열 또는 Buffer다. 두 입력이 준비된 위치에서 실행하며, 마지막 줄은 키의 종류만 출력한다.

const { createPrivateKey } = require('node:crypto');

const signingKey = createPrivateKey({
  key: pemText,
  format: 'pem',
  passphrase,
});
console.log(signingKey.type, signingKey.asymmetricKeyType);

Node 공식 문서에서 문자열이나 Buffer만 전달할 때의 기본 형식은 PEM이며, 암호화된 개인키에는 passphrase가 필요하다. format: 'pem'은 입력 형식을 명시하는 옵션이다. type: 'pkcs8'을 더해도 비밀번호를 대신하지 못한다. import의 type은 DER 입력에서 필요하고 PEM에서는 무시된다. createPrivateKey 입력 옵션 (새 탭에서 열림)

같은 PEM에서 비밀번호 입력을 바꾸면

기존 키 없이 확인하려면 Node.js가 설치된 빈 폴더에서 아래 준비 코드를 check.cjs에 저장한다. 이어서 앞의 수정 코드를 같은 파일 끝에 붙이고 node check.cjs를 실행한다. 별도 패키지는 필요 없다. 새 2048비트 RSA 키를 메모리에서만 만들며, 코드의 비밀번호는 공개 실습 전용 값이다.

const { generateKeyPairSync } = require('node:crypto');
const passphrase = 'public-pkcs8-lab-only';
const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
const pemText = privateKey.export({
  type: 'pkcs8', format: 'pem',
  cipher: 'aes-256-cbc', passphrase,
});

이 준비 코드의 cipher와 passphrase는 개인키를 저장할 PKCS#8 컨테이너를 암호화한다. KeyObject.export 옵션 (새 탭에서 열림) 정상 입력으로 실행한 출력은 private rsa였다. AES-256-CBC는 저장 컨테이너의 암호화 방식이고, 읽어 낸 서명용 키는 RSA 개인키다.

누락과 오입력도 비교하려면 준비 코드는 유지하고, 아래쪽 createPrivateKey 객체에서만 passphrase를 빼거나 passphrase: 'wrong-lab-value'로 바꿔 각각 다시 실행한다. 새 키를 만들고 같은 실행에서 내보내기와 읽기를 비교하므로 기존 키 파일이나 인증서는 필요 없다. Windows x64의 Node.js 22.23.2, 내장 OpenSSL 3.5.7에서 직접 실행한 결과는 다음과 같았다.

  • createPrivateKey(pemText)와 비밀번호 없는 { key: pemText, format: 'pem' }는 모두 실패했다. 코드와 메시지는 ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED, error:07880109:common libcrypto routines::interrupted or cancelled였다.
  • 잘못된 비밀번호는 ERR_OSSL_BAD_DECRYPT, error:1C800064:Provider routines::bad decrypt로 실패했다.
  • 내보낼 때 사용한 비밀번호를 전달한 호출은 성공했다. 객체 형태로 바꾸는 것만으로는 부족하고, 해당 키를 보호한 값이 연결되어야 했다.

별도 비교 실행에서는 성공한 signingKey로 UTF-8 메시지 invoice=17을 sign('sha256', message, signingKey)로 서명했다. 처음 생성한 공개키로 검증한 결과는 true, 같은 서명을 메시지만 invoice=18로 바꿔 검증한 결과는 false였다. 이는 이번 import로 얻은 키를 실제 서명에 사용할 수 있었고 메시지 변조는 거절됐다는 관찰이다. crypto.sign (새 탭에서 열림), crypto.verify (새 탭에서 열림)

이번 누락 오류에는 passphrase required 대신 interrupted or cancelled라는 문구가 나왔다. 오류 문구만으로 원인을 확정하지 말고, 실패 위치가 createPrivateKey인지와 암호화된 PEM에 맞는 passphrase가 실제로 전달되는지부터 확인한다. 비밀번호를 연결한 뒤에도 실패하면 전달 과정에서 PEM이 달라졌는지와 암호화 당시의 입력을 확인한다. 진단 로그에는 개인키·PEM 본문·실제 비밀번호 원문을 남기지 않는다.

확인한 범위는 같은 Node 프로세스에서 내보내고 읽은 RSA/AES-256-CBC PKCS#8 PEM이다. 다른 Node/OpenSSL 버전의 오류 코드, 외부 도구가 만든 키, 다른 cipher·비ASCII 비밀번호, 실제 서비스의 비밀번호 공급 경로는 검증하지 않았다. 위 오류 문자열을 모든 환경의 고정 판별값으로 사용하지 않는다.

← 전체 글 보기