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

빌드노트

Node.js에서 Ed25519 서명에 sha256을 넣으면 실패한다

Node.js의 Ed25519 서명·검증에는 algorithm 인자로 null을 명시한다. 새 메모리 키로 sha256의 invalid digest 오류와 원문 true·변조 false를 비교하고, Node.js v22.23.2에서 undefined가 허용되는 범위도 직접 확인했다.

Node의 crypto.sign('sha256', data, privateKey)가 Ed25519 키에서 invalid digest 오류로 실패한다면 첫 번째 algorithm 인자를 확인한다. Ed25519 서명과 검증에는 null을 명시해 키 타입이 알고리즘을 결정하도록 한다. RSA나 ECDSA 예제에서 가져온 'sha256'을 이 키에도 그대로 넣으면 실패한다. Node의 sign 문서 (새 탭에서 열림)와 verify 문서 (새 탭에서 열림)는 Ed25519에서 이 인자가 null 또는 undefined여야 한다고 명시한다.

기존 코드에서는 서명 호출에 실제로 넘기는 KeyObject의 asymmetricKeyType부터 읽는다. 개인 키 변수 이름이 privateKey라면 console.log(privateKey.asymmetricKeyType)으로 확인할 수 있다. 이 속성의 공식 설명 (새 탭에서 열림)에서 Ed25519의 값은 'ed25519'다. 이 값이라면 서명을 sign(null, data, privateKey)로 바꾸고, 대응하는 검증도 verify(null, data, publicKey, signature)로 맞춘다. 여기서 data는 원래 서명하려던 메시지의 Buffer다. 이 수정은 Ed25519를 대상으로 한다.

같은 키에서 sha256과 null을 비교한다

다음 예제는 새 Ed25519 키 한 쌍과 합성 메시지 memo=ready로 오류와 정상 서명을 비교한다. Node.js 22가 설치된 환경에서 아래 내용을 ed25519-demo.cjs로 저장한 뒤, 그 폴더에서 node ed25519-demo.cjs를 실행한다. 외부 패키지는 필요 없으며 키와 서명은 메모리에만 둔다. 키 내용과 서명 바이트는 출력하지 않는다.

const { generateKeyPairSync, sign, verify } = require('node:crypto');

const { privateKey, publicKey } = generateKeyPairSync('ed25519');
const data = Buffer.from('memo=ready', 'utf8');
console.log('key type:', privateKey.asymmetricKeyType);

try {
  sign('sha256', data, privateKey);
} catch (error) {
  console.log('sha256:', error.code);
  console.log('message:', error.message);
}

const signature = sign(null, data, privateKey);
console.log('null original:', verify(null, data, publicKey, signature));
console.log('null changed:', verify(
  null, Buffer.from('memo=READY', 'utf8'), publicKey, signature
));

2026년 10월 24일 KST에 Windows x64, Node.js v22.23.2, OpenSSL 3.5.7에서 실행한 출력이다. 이 실행의 FIPS 모드는 0이었다.

key type: ed25519
sha256: ERR_OSSL_INVALID_DIGEST
message: error:1C80007A:Provider routines::invalid digest
null original: true
null changed: false

sha256 호출은 서명을 반환하기 전에 예외를 던졌다. 이어서 같은 키와 같은 data에 null을 전달하자 서명을 만들고 원문 검증에도 성공했다. 이 비교에서 바꾼 것은 첫 번째 인자다. 오류를 보고 SHA-256 자체가 이 Node 설치에 없다고 결론 내릴 근거는 없다. 여기서는 Ed25519 키와 그 인자의 조합이 문제였다. 오류 코드와 문구는 이 Node·OpenSSL 조합의 관찰값이며 다른 버전에서 같은 문자열을 보장하지는 않는다.

마지막 줄은 서명과 공개 키를 그대로 두고 메시지만 memo=READY로 바꾼 결과다. null로 바꾼 뒤에도 메시지 변경은 검증에서 거절됐다. 예제의 catch는 실패한 호출 뒤에 정상 호출을 비교하기 위한 것이다. 실제 수신 처리에서는 검증 결과가 false이거나 검증 호출이 예외로 끝나면 해당 메시지 처리를 중단해야 한다. 별도 비교에서 verify('sha256', data, publicKey, signature)도 위와 같은 오류 코드와 메시지를 던졌다.

undefined도 첫 번째 자리에 전달할 수 있다

Node.js v22.23.2의 sign 설명 (새 탭에서 열림)과 verify 설명 (새 탭에서 열림)에도 null과 undefined일 때 키 타입에 따라 알고리즘이 결정된다고 적혀 있다. 같은 실행 환경에서 sign(undefined, data, privateKey)로 만든 서명을 verify(undefined, data, publicKey, signature)로 검증하자 원문은 true, 변경한 메시지는 false였다. null로 서명하고 undefined로 검증하는 조합과 그 반대도 원문에서 true였다.

따라서 이 버전에서는 두 값 모두 사용할 수 있다. undefined를 쓰더라도 첫 번째 인자 자리에 전달한다. 위 예제는 Ed25519 호출임을 드러내도록 null로 통일했다. 여러 키 타입을 받는 공통 함수라면 모든 호출의 해시 이름을 일괄 치환하지 말고, 실제 키 타입과 해당 서명 방식의 API 계약을 확인한다.

null로 수정한 뒤 원문 검증이 false라면 서명에 쓴 원본 Buffer, 검증에 넘긴 메시지 바이트, 공개 키 선택과 서명 전달 과정을 대조한다. 이번 실험은 그 바이트와 키를 같은 프로세스에서 그대로 전달한 조건이다. 다른 Node·OpenSSL 버전, FIPS 활성 환경, 콜백 방식, Web Crypto와 외부 서비스의 상호 운용은 검증하지 않았다.

← 전체 글 보기