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

빌드노트

같은 HMAC 키인데 Web Crypto sign이 거절될 때

HMAC 키를 verify 전용으로 가져오면 같은 바이트와 해시를 써도 sign은 거절된다. Node.js 22.23.2에서 용도별 결과를 비교하고, usages와 extractable을 구분해 키 공급자와 호출자의 최소 연산 계약을 정한다.

Web Crypto에서 HMAC 키 바이트와 해시 알고리즘이 맞는데도 subtle.sign()이 InvalidAccessError로 거절된다면, 그 호출에 넘긴 CryptoKey.usages를 먼저 확인한다. ['verify']로 가져온 키는 검증에만 사용할 수 있다. extractable을 true로 바꿔도 서명 생성 용도가 추가되지는 않는다.

기존 코드에서는 sign()에 넘기기 직전의 key를 대상으로 아래 속성만 출력한다. 키 원문을 꺼낼 필요는 없다.

console.log({
  type: key.type,
  algorithm: key.algorithm.name,
  hash: key.algorithm.hash?.name,
  usages: key.usages,
  extractable: key.extractable
});

type이 secret, algorithm이 HMAC, hash가 SHA-256인데 usages가 ['verify']라면, 알고리즘이 맞아도 서명 생성은 허용되지 않는다. 서명을 맡아야 하는 코드라면 키를 공급하는 importKey()의 마지막 인자에 필요한 용도를 명시해야 한다. 네 번째 인자인 extractable은 내보내기를 허용할지 정하는 별도 값이다. Node.js 22.23.2의 키 용도 설명 (새 탭에서 열림)

W3C의 sign 처리 절차 (새 탭에서 열림)는 호출 알고리즘과 키의 알고리즘 이름이 맞는지, 키의 허용 용도에 sign이 있는지를 각각 검사한다. 따라서 InvalidAccessError라는 이름 하나로 용도 누락을 확정할 수는 없다. 이미 sign이 들어 있다면 호출에 지정한 알고리즘과 실제로 전달된 키를 다시 확인한다.

같은 바이트를 두 용도로 가져와 비교한다

다음 예제는 같은 합성 HMAC 바이트로 검증 전용 키와 서명·검증 키를 만든다. 두 키 모두 내보내기를 허용하지 않고, 용도만 바꾼다. Node.js 22가 설치된 환경에서 내용을 UTF-8 파일 reader-example.mjs로 저장한 뒤 그 폴더에서 node reader-example.mjs를 실행한다. 외부 패키지는 필요 없다.

import { webcrypto } from 'node:crypto';

const { subtle } = webcrypto;
const raw = webcrypto.getRandomValues(new Uint8Array(32));
const algorithm = { name: 'HMAC', hash: 'SHA-256' };
const data = new TextEncoder().encode('webcrypto usages demo');
const verifyOnly = await subtle.importKey(
  'raw', raw, algorithm, false, ['verify']
);
const signAndVerify = await subtle.importKey(
  'raw', raw, algorithm, false, ['sign', 'verify']
);

console.log(`verifyOnly: usages=${verifyOnly.usages.join(',')} extractable=${verifyOnly.extractable}`);
console.log(`signAndVerify: usages=${signAndVerify.usages.join(',')} extractable=${signAndVerify.extractable}`);
try {
  await subtle.sign('HMAC', verifyOnly, data);
  console.log('verifyOnly sign: OK');
} catch (error) {
  console.log(`verifyOnly sign: ${error.name}`);
}

const signature = await subtle.sign('HMAC', signAndVerify, data);
console.log('signAndVerify sign: OK');
console.log(`verifyOnly verify: ${await subtle.verify('HMAC', verifyOnly, signature, data)}`);

실제 계정 키는 사용하지 않는다. 실행할 때마다 임의의 키 바이트를 새로 만들고, 공개 샘플 문자열만 처리한다. 출력에도 키 바이트와 서명값은 넣지 않는다. 실패하는 호출은 await와 catch로 Promise 거절을 확인한다.

2026년 10월 24일 Windows x64, Node.js 22.23.2에서 실행한 출력은 다음과 같다.

verifyOnly: usages=verify extractable=false
signAndVerify: usages=sign,verify extractable=false
verifyOnly sign: InvalidAccessError
signAndVerify sign: OK
verifyOnly verify: true

verifyOnly는 서명 생성을 거절했지만, signAndVerify가 만든 서명을 검증할 때는 true를 반환했다. 두 객체에 넣은 키 바이트와 해시는 같았다. 이 비교에서는 서명 생성 용도의 유무가 성공과 거절을 갈랐다. 또한 두 키 모두 extractable: false였으므로, 비추출 키도 허용된 연산에는 사용할 수 있음을 확인했다.

추가 대조에서 검증 전용 키의 extractable만 true로 바꿔 가져와도 sign()은 같은 InvalidAccessError로 거절됐다. 반대로 위의 비추출 키 두 개는 raw 내보내기가 모두 거절됐다. 내보내기 허용 여부와 서명 허용 여부를 서로 대신하는 설정으로 취급하면 안 된다. Node의 extractable 설명 (새 탭에서 열림)

예제와 달리 검증까지 false라면, 검증 호출은 수행됐지만 서명이 일치하지 않은 것이다. 같은 바이트·해시·메시지를 사용했는지 확인한다. 별도 실행에서 샘플 문자열 끝에 !만 붙였을 때 검증 결과는 false였다. 이 결과는 용도가 없어 호출 자체가 거절된 경우와 구별해야 한다. W3C의 HMAC 검증 절차 (새 탭에서 열림)

필요한 용도는 키를 공급할 때 정한다

예제의 importKey()는 네 번째 인자 false로 내보내기를 막고, 다섯 번째 인자로 연산 용도를 정했다. 실제 코드에서는 그 키가 맡는 일에 맞춰 목록을 좁힌다.

  • 받은 서명만 검사하는 코드는 ['verify']를 유지한다.
  • 서명만 만드는 코드는 ['sign']을 요청한다.
  • 같은 키 객체로 두 연산을 수행해야 할 때 ['sign', 'verify']를 요청한다.

서명 코드에 검증 전용 키가 전달됐다면 키를 만드는 함수, 가져오는 함수와 호출자의 계약을 함께 고친다. CryptoKey.usages는 읽기 전용 속성이므로 기존 객체의 목록을 대입해서 권한을 늘리는 방식으로 수정하지 않는다. 예제는 처음부터 합성 원본 바이트를 보유했기 때문에 두 객체를 따로 가져올 수 있었다. 기존 비추출 키를 export한 뒤 다시 import하는 복구 절차가 아니다. CryptoKey 인터페이스 (새 탭에서 열림), Node의 importKey 인자 (새 탭에서 열림)

generateKey()를 쓰는 코드도 생성 시점에 용도를 정한다. 이때 인자를 바꿔 다시 생성하면 새 키 재료가 만들어진다. 기존 키의 허용 용도가 바뀌는 것은 아니다. Node의 generateKey 설명 (새 탭에서 열림)

직접 실행한 범위는 Node.js 22.23.2의 HMAC/SHA-256, raw 가져오기, 합성 메시지의 서명·검증과 비추출 키의 raw 내보내기 거절이다. 다른 Node 버전, 브라우저, JWK 가져오기와 실제 키 저장소는 실행하지 않았다. 여기서 확인한 usages는 Web Crypto가 해당 키 객체의 연산을 제한하는 설정이며, 원본 HMAC 바이트를 가진 모든 코드의 계산을 막는 권한 체계를 검증한 것은 아니다.

← 목록으로 돌아가기