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

빌드노트

Web Crypto의 RSA-OAEP 암호문이 Node에서 풀리지 않을 때

Web Crypto의 RSA-OAEP 키에 지정한 hash와 Node의 oaepHash를 맞춘다. 합성 키로 SHA-256 양방향 복호화와 SHA-1 기본값의 차이를 확인하고, label·개인 키가 달라도 같은 오류가 나므로 메시지만으로 원인을 단정하지 않는 방법을 설명한다.

Web Crypto에서 RSA-OAEP로 암호화했는데 Node의 privateDecrypt()가 ERR_OSSL_RSA_OAEP_DECODING_ERROR를 낸다면, 양쪽의 OAEP 해시부터 대조한다. Web Crypto에서 키를 가져올 때 지정한 hash와 Node 복호화의 oaepHash가 같아야 한다. Web Crypto는 SHA-256으로 설정했는데 Node에서는 옵션을 생략해 기본값인 SHA-1을 쓰는 조합이 이 글의 재현 조건이다. 알고리즘 이름이 둘 다 RSA-OAEP라는 것만으로 이 설정까지 일치하지는 않는다.

암호화에 쓴 키의 hash부터 읽는다

기존 코드에서는 subtle.encrypt()에 실제로 전달한 공개 키를 확인한다. 아래의 webPublicKey를 그 CryptoKey로 바꾸어 실행하면, 키 바이트를 출력하지 않고 적용된 알고리즘과 해시를 볼 수 있다.

console.log(webPublicKey.algorithm.name);
console.log(webPublicKey.algorithm.hash.name);

출력이 RSA-OAEP, SHA-256이라면 Node 쪽 privateDecrypt()의 옵션에서 oaepHash: 'sha256'을 확인한다. 빠져 있다면 SHA-1이 사용된다. Node.js v22.23.2의 privateDecrypt 문서 (새 탭에서 열림)는 이 기본값과 함께, oaepHash가 OAEP 패딩과 마스크 생성 함수 MGF1에 쓰이는 해시라고 설명한다. 패딩도 constants.RSA_PKCS1_OAEP_PADDING으로 명시해 양쪽을 대조하기 쉽게 만든다.

Web Crypto의 해시는 encrypt() 호출에서 새로 고르는 값이 아니다. 공개 키를 importKey()로 가져오는 코드의 { name: 'RSA-OAEP', hash: 'SHA-256' }를 확인해야 한다. 키를 직접 만들었다면 generateKey()의 설정을 본다. Node의 RsaHashedImportParams 설명 (새 탭에서 열림)과 W3C의 RSA-OAEP 연산 정의 (새 탭에서 열림)는 해시가 키의 알고리즘 설정에 속하고, 암·복호화에서 그 값을 사용한다는 관계를 보여 준다. encrypt()의 이름만 바꾸거나 임의의 hash 속성을 더해서 이미 가져온 키의 해시를 바꾸려 하지 않는다.

같은 암호문에서 Node의 해시 설정만 바꿨다

다음 예제는 새 RSA 키 한 쌍으로 memo를 암호화한 뒤, 같은 암호문을 Node 기본 설정과 명시적 SHA-256으로 각각 복호화한다. Node.js 22가 설치된 환경에서 아래 내용을 UTF-8 파일 oaep-demo.mjs로 저장하고 그 폴더에서 node oaep-demo.mjs를 실행한다. 외부 패키지는 필요 없다. 키는 새로 생성하며 공개 키의 DER 변환도 메모리 안에서만 한다.

import { constants, generateKeyPairSync, privateDecrypt,
  webcrypto } from 'node:crypto';

const { publicKey, privateKey } = generateKeyPairSync('rsa', {
  modulusLength: 2048
});
const webPublicKey = await webcrypto.subtle.importKey(
  'spki', publicKey.export({ type: 'spki', format: 'der' }),
  { name: 'RSA-OAEP', hash: 'SHA-256' }, false, ['encrypt']
);
console.log('web hash:', webPublicKey.algorithm.hash.name);
const ciphertext = await webcrypto.subtle.encrypt(
  { name: 'RSA-OAEP' }, webPublicKey,
  new TextEncoder().encode('memo')
);

try {
  privateDecrypt({ key: privateKey }, Buffer.from(ciphertext));
} catch (error) {
  console.log('default', error.name, error.code);
}
const plain = privateDecrypt({
  key: privateKey, padding: constants.RSA_PKCS1_OAEP_PADDING,
  oaepHash: 'sha256'
}, Buffer.from(ciphertext));
console.log('sha256', plain.toString('utf8'));

2026년 10월 23일 Windows x64, Node.js v22.23.2와 OpenSSL 3.5.7에서 실행한 출력이다. 두 호출 모두 OAEP 패딩을 사용하며 label은 생략했다. 이 버전의 privateDecrypt()는 위의 첫 호출처럼 키만 주어도 OAEP 패딩을 사용한다.

web hash: SHA-256
default Error ERR_OSSL_RSA_OAEP_DECODING_ERROR
sha256 memo

sha256 memo는 동일한 개인 키와 암호문으로 복호화가 성공했고 원문도 같았다는 뜻이다. 기본 호출이 실패한 뒤에는 해시를 명시한 호출까지 진행하도록 예외를 잡았다. 실제 수신 처리에서 실패를 무시하고 평문 처리를 계속하라는 코드는 아니다.

별도 대조에서는 첫 호출에 oaepHash: 'sha1'을 명시해도 같은 오류 코드가 나왔다. 이어서 같은 공개 키의 SPKI 바이트를 SHA-1로 가져와 새로 암호화하자 Node의 기본값과 명시적 SHA-1 모두 memo를 반환했다. 이때 Node를 SHA-256으로 설정하면 다시 거절됐다. 키 쌍이 맞는 것과 OAEP 해시가 맞는 것은 각각 확인해야 한다는 관찰이다. SHA-1 대조는 기본값을 확인하기 위한 것이며, SHA-256으로 정한 연동의 해시를 바꾸라는 조치가 아니다.

반대 방향도 확인했다. Node의 publicEncrypt()에 oaepHash: 'sha256'을 주고 암호화하면 SHA-256으로 가져온 Web Crypto 개인 키가 memo를 복원했다. Node에서 해시를 생략하거나 SHA-1을 명시해 암호화한 경우에는 SHA-1로 가져온 개인 키가 성공했다. 기본값으로 암호화한 결과를 SHA-256 개인 키로 복호화하면 OperationError였다. Node의 publicEncrypt 문서 (새 탭에서 열림)에도 같은 SHA-1 기본값이 명시돼 있다.

해시를 맞춘 뒤에도 실패하면 label과 키 선택을 본다

label을 사용했다면 암·복호화에 같은 바이트를 전달해야 한다. Web Crypto에서는 label, Node에서는 oaepLabel이라는 이름이다. 예를 들어 Web Crypto에서 label: new TextEncoder().encode('purpose=note')를 썼다면, Node에는 oaepLabel: Buffer.from('purpose=note', 'utf8')를 전달한다. label을 쓰지 않는 약속이라면 양쪽 모두 비어 있는 값으로 맞춘다. W3C의 위 연산 정의는 label을 생략하면 빈 바이트열로 취급한다.

실제로 SHA-256과 키 쌍을 유지한 채 purpose=note label을 붙여 암호화했다. Node에서 label을 생략하거나 purpose=other로 바꾸면 둘 다 ERR_OSSL_RSA_OAEP_DECODING_ERROR였고, 같은 UTF-8 label을 전달하면 memo가 나왔다. 별도로 생성한 다른 개인 키를 사용한 경우도 이번 실행에서는 같은 오류 코드였다. 이 오류 코드만으로 해시 불일치라고 확정할 수 없는 이유다. Web Crypto 쪽에서도 label을 빠뜨린 복호화는 해시 불일치와 같은 OperationError였다.

따라서 해시를 명시해도 실패한다면, 암호화에 쓴 공개 키와 짝인 개인 키를 실제로 선택했는지 확인한다. 키 식별자에서 어떤 키를 불러오는지 따라가고, label과 수신 암호문 바이트가 암호화 때의 값과 같은지도 대조한다. 새 키를 생성하는 것은 기존 암호문의 복구 방법이 아니다. 오류를 보고 여러 해시를 차례로 시도하는 대신, 사용 중인 프로토콜에 정해진 해시·label·키 식별자를 확인해 호출 인자에 반영한다.

직접 실행한 범위는 위 Node 버전의 node:crypto와 그 안의 Web Crypto 사이 상호 운용이다. 새 2048비트 합성 RSA 키와 짧은 문자열만 사용했다. 실제 브라우저, 네트워크·Base64 전송 경로, 다른 Node·OpenSSL 버전, 운영 키·인증서와 기존 서비스는 검증하지 않았다. 오류 코드와 메시지는 이 환경의 관찰값이며, 이 예제만으로 운영 암호 프로토콜의 안전성을 판단할 수는 없다.

← 목록으로 돌아가기