빌드노트
Node.js ECDSA 검증이 false라면 DER와 P1363부터 확인한다
Node.js ECDSA 검증의 기본 서명 형식은 DER다. 같은 P-256 키와 데이터로 만든 DER·P1363 서명의 검증 결과를 비교하고, 프로토콜에 맞게 dsaEncoding을 지정한다. 데이터·서명 변조 거부와 검증 API별 예외 차이도 확인한다.
Node.js에서 직접 만든 ECDSA 서명은 검증되는데 전달받은 서명만 false라면, 검증 호출의 dsaEncoding과 수신 규약을 먼저 대조한다. Node.js의 기본 서명 형식은 DER다. 상대가 r || s 형식으로 보낸다면 dsaEncoding: 'ieee-p1363'로 검증해야 한다. 같은 키 쌍과 같은 데이터를 쓰더라도 서명을 해석하는 형식까지 자동으로 맞춰 주지는 않는다. Node.js 22.23.2의 서명 검증 옵션 (새 탭에서 열림)
이것은 false의 원인이 항상 형식이라는 뜻은 아니다. 아래에서는 메모리에 만든 새 P-256 키 쌍 하나와 같은 데이터를 유지하고, 서명 형식과 검증 옵션만 바꿔 이 경우를 따로 확인한다. 실제 인증서나 토큰을 사용한 장애 사례는 아니다.
키 형식과 서명 형식을 구분해서 고친다
ECDSA 서명은 두 정수 r, s로 표현된다. DER는 이 둘을 ASN.1 구조로 인코딩하고, P1363는 r 뒤에 s를 이어 붙이는 형식이다. 공개키가 PEM인지 DER인지 정하는 옵션과 서명 바이트의 형식을 정하는 dsaEncoding은 서로 다른 설정이다. 공개키를 정상적으로 읽었다고 서명 형식도 일치하는 것은 아니다. Node.js의 DER·P1363 서명 형식 설명 (새 탭에서 열림)
기존 코드에서 서명을 전달하는 verify() 호출을 찾고, 송신 측의 프로토콜 문서나 서명 생성 옵션을 확인한다. 다음은 규약이 P-256·SHA-256·P1363로 정해진 경우에 적용할 검증 부분이다. publicKey는 송신자를 검증할 공개키, data는 서명 당시의 원본 바이트를 담은 Buffer, signature는 수신·디코딩을 끝낸 서명 Buffer다. 이 세 입력이 이미 준비된 코드에 넣는 발췌이며, 독립 실행 예제는 뒤에 있다.
const { verify } = require('node:crypto');
const ok = verify('sha256', data,
{ key: publicKey, dsaEncoding: 'ieee-p1363' }, signature);
if (!ok) throw new Error('서명 검증 실패');
규약이 DER라면 같은 자리에 'der'를 명시한다. 성공하는 옵션을 찾아 자동 선택하기보다, 그 연동에서 받기로 정한 형식을 고정한다. 서명 길이만으로 모든 곡선과 형식을 판별하는 분기를 만들지는 않는다.
전송 문자열의 인코딩도 구분해야 한다. Base64로 받은 서명은 먼저 규약에 맞게 바이트로 복원한다. Buffer.from(text, 'base64')는 그 변환에 쓰는 API이지 DER를 P1363로 변환하는 API가 아니다. 디코딩된 바이트를 어떻게 해석할지는 여전히 dsaEncoding이 정한다. Node.js Buffer의 문자열 변환 (새 탭에서 열림)
같은 입력에 검증 옵션만 바꿔 본다
다음 코드는 DER와 P1363 서명을 각각 만든 뒤, 기본값·명시적 DER·명시적 P1363로 검증한 결과를 나란히 출력한다. Node.js 22가 설치된 환경이면 별도 패키지 없이 실행할 수 있다. 빈 작업 폴더에 ecdsa-demo.cjs라는 파일로 저장하고 node ecdsa-demo.cjs를 실행한다. 키를 출력하거나 파일에 저장하지 않는다.
const { generateKeyPairSync, sign, verify } = require('node:crypto');
const { privateKey, publicKey } = generateKeyPairSync('ec', {
namedCurve: 'prime256v1', // P-256
});
const data = Buffer.from('note=demo&version=1', 'utf8');
const der = sign('sha256', data, { key: privateKey, dsaEncoding: 'der' });
const p1363 = sign('sha256', data,
{ key: privateKey, dsaEncoding: 'ieee-p1363' });
function results(signature) {
return [
verify('sha256', data, publicKey, signature),
verify('sha256', data, { key: publicKey, dsaEncoding: 'der' }, signature),
verify('sha256', data,
{ key: publicKey, dsaEncoding: 'ieee-p1363' }, signature),
];
}
console.log('columns: default, der, ieee-p1363');
console.log('DER', results(der));
console.log('P1363', results(p1363));
Windows의 Node.js v22.23.2, OpenSSL 3.5.7에서 실행한 출력이다. 여기서 verify는 한 번의 호출로 검증하는 crypto.verify()다.
columns: default, der, ieee-p1363
DER [ true, true, false ]
P1363 [ false, false, true ]
DER 행에서는 기본값과 'der'가 모두 성공했다. P1363 행에서는 같은 공개키와 데이터인데도 기본값이 실패했고, 'ieee-p1363'로 지정한 호출만 성공했다. 이 비교에서는 다른 입력을 고정했으므로, 검증 옵션과 실제 서명 형식의 불일치가 실패를 만든 조건임을 알 수 있다. 서명 생성 쪽의 dsaEncoding 옵션 (새 탭에서 열림)도 같은 두 형식을 제공한다.
자기 환경에서 형식을 맞춘 칸까지 실패한다면 옵션을 계속 바꾸기 전에 정상 대조부터 복원한다. 송신 측이 서명한 바이트와 검증 직전 data가 같은지, 올바른 공개키와 해시를 선택했는지 확인한다. 이 예제부터 다르게 나온다면 node --version과 호출한 API도 함께 확인한다.
형식을 맞춰도 바뀐 데이터는 통과하면 안 된다
앞의 파일 끝에 다음 코드를 덧붙여 같은 명령으로 다시 실행한다. P1363 옵션을 그대로 둔 채 데이터만 바꾸고, 이어서 원본 데이터에 서명 마지막 바이트의 한 비트를 바꾼 복사본을 넣는다. 마지막 호출은 원본 입력이 여전히 검증되는지 확인한다.
const changedData = Buffer.from('note=demo&version=2', 'utf8');
const changedSignature = Buffer.from(p1363);
changedSignature[changedSignature.length - 1] ^= 1;
const key = { key: publicKey, dsaEncoding: 'ieee-p1363' };
console.log('changed data', verify('sha256', changedData, key, p1363));
console.log('changed signature', verify('sha256', data, key, changedSignature));
console.log('original again', verify('sha256', data, key, p1363));
앞의 두 행짜리 비교 뒤에 다음 출력이 붙었다.
changed data false
changed signature false
original again true
서명 형식을 맞추는 것은 정상 서명을 제대로 읽게 하는 수정이다. 바뀐 데이터나 바뀐 서명을 유효하게 만드는 수정이 아니다. 별도의 DER 대조에서도 올바른 'der' 옵션으로 데이터 변경과 서명 한 비트 변경을 각각 검증했을 때 모두 false였고, 원본은 다시 true였다.
거부가 언제나 false로만 나타나는 것은 아니다. 같은 환경에서 스트림 방식인 createVerify()로 별도 비교했을 때, DER 서명을 P1363로 해석하게 한 호출은 ERR_CRYPTO_OPERATION_FAILED 코드와 Malformed signature 메시지를 가진 예외를 던졌다. P1363 서명을 기본 DER로 검증한 호출은 false였다. 예외를 잡는 코드에서도 성공으로 바꾸지 않고 처리를 끝내거나 호출자에게 오류를 전달해야 한다. 스트림 방식은 검증할 때마다 새 Verify 객체를 사용해야 한다. Verify 객체의 사용 조건 (새 탭에서 열림)
직접 확인한 범위는 Node.js 22.23.2의 P-256·SHA-256 합성 입력과 두 검증 API다. 다른 곡선·Node.js 버전·외부 서명 라이브러리, 실제 전송이나 Base64 왕복은 검증하지 않았다. 전달받은 서명의 길이나 false 하나만으로 원인을 확정할 수는 없으며, 실제 연동에서는 프로토콜이 정한 형식과 서명 대상 바이트를 기준으로 대조해야 한다.