Delphi에서 ML-DSA로 JWT 서명하기

· 컴포넌트
Delphi에서 ML-DSA로 JWT 서명하기

JWT는 작은 것이지만 그 영향력은 넓게 미칩니다. 서비스 간 신원 증명의 역할을 하며, 이를 서명하는 알고리즘은 시스템에서 바꾸기 가장 느린 부분이 되는 경향이 있습니다. 모든 발급자와 모든 검증자가 동시에 옮겨가야 하기 때문입니다. 그래서 필요해지기 전에 이런 선택지가 존재한다는 것을 알아 두는 것이 가치가 있습니다.

sgcWebSockets 2026.10은 RFC 9964의 포스트 양자 JWT 알고리즘인 ML-DSA-44, ML-DSA-65, ML-DSA-87을 JWT 클라이언트와 서버에 추가합니다. 이들은 HS, RS, ES와 나란히 같은 속성의 값으로 세 개가 추가된 것이며, 토큰에 관한 나머지 부분은 그대로 유지됩니다.

토큰 발급하기

헤더에서 알고리즘을 선택하고, 컴포넌트에 개인키를 제공한 뒤 서명합니다.

oJWT := TsgcHTTP_JWT_Client.Create(nil);
oJWT.JWTOptions.Header.alg := jwtMLDSA65;
oJWT.JWTOptions.Algorithms.MLDSA.PrivateKey.Text := vPrivatePEM;
oJWT.JWTOptions.Payload.iss := 'my-service';
oJWT.JWTOptions.Payload.sub := 'user-1';

vToken := oJWT.Sign;

개인키는 일반적인 PKCS#8 PEM입니다. RFC 9881이 정의하는 세 가지 형식을 모두 읽을 수 있으므로, 32바이트 시드로 저장된 키, 확장된 키, 또는 둘 다로 저장된 키 모두 사용할 수 있습니다.

검증하기

서버 쪽도 동일한 구조이며, 공개키와 해당 계열을 활성화합니다.

oServer := TsgcHTTP_JWT_Server.Create(nil);
oServer.JWTOptions.Algorithms.MLDSA.Enabled := True;
oServer.JWTOptions.Algorithms.MLDSA.PublicKey.Text := vPublicPEM;

if oServer.Validate(vToken, vHeader, vPayload, vError) then
  ShowMessage(vPayload);

각 계열은 개별적으로 켜고 끌 수 있으므로, ML-DSA 토큰만 발급하는 배포 환경에서는 나머지를 꺼 둘 수 있으며, alg가 다른 값으로 설정된 채 도착한 토큰은 검사되지 않고 거부됩니다.

매개변수 집합이 일치해야 합니다

RFC 9964에는 굳이 명확히 짚어 둘 만한 규칙이 하나 있습니다. 그렇지 않으면 눈에 띄지 않고 넘어갈 수 있는 실수이기 때문입니다. 바로 키가 alg가 지정하는 매개변수 집합에 속해야 한다는 것입니다. 일치하지 않는 키는 거부됩니다. 잘못된 것으로 서명하면 예외가 발생하고, 잘못된 것으로 검증하면 예외 대신 false가 반환되므로, 공격자는 그 차이를 이용해 아무것도 알아낼 수 없습니다.

JSON Web Key로서의 키

RFC 9964는 이 알고리즘들을 위한 키 유형인 AKP를 등록하고 있으며, 이 라이브러리는 이를 읽고 쓸 수 있습니다. 키를 파일에 두는 대신 JWKS 엔드포인트에 공개할 때 필요한 기능입니다.

uses
  sgcHTTP_JWT_MLDSA;

var
  vJWK, vPublicPEM, vPrivatePEM: string;
begin
  // {"kty":"AKP","alg":"ML-DSA-65","pub":"...","priv":"..."}
  vJWK := sgcMLDSA_ExportPrivateJWK(mldsa65, oSeed);

  // and back again, as the PEM the components read
  sgcMLDSA_ImportJWKAsPEM(vJWK, vPublicPEM, vPrivatePEM);
end;

개인키 멤버는 RFC가 정의하는 32바이트 시드이며, 읽기 처리는 이에 대해 엄격합니다. 각 멤버는 정확한 길이의 정규 base64url이어야 하고, alg는 필수이며, 개인키가 존재할 경우 같은 JWK 안의 공개키와 일치해야 합니다. 이 중 하나라도 충족하지 못하는 JWK는 절반만 읽히는 대신 거부됩니다.

실증된 상호운용성

서명 형식은 다른 누군가가 같은 바이트를 만들어낼 수 있을 때에만 쓸모가 있습니다. 다음 두 가지 확인은 단순한 주장이 아니라 테스트로 제공됩니다.

겸사겸사: 바이너리 HMAC 시크릿

같은 작업 중에 관련 없는 제약 하나도 함께 사라졌습니다. HS 알고리즘은 키를 텍스트로 받았지만, 실제 HMAC 키는 무작위 바이트이며 그중 대부분은 유효한 텍스트가 전혀 아닙니다. 이제 oct JWK의 k 멤버가 사용하는 것과 같은 인코딩인 base64url로 키를 받는 두 번째 속성이 추가되었습니다.

oJWT.JWTOptions.Algorithms.HS.SecretBase64URL := 'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ...';

이 값이 설정되어 있으면 클라이언트와 서버 모두에서 텍스트 시크릿보다 우선합니다.

다른 누군가가 작성한 클레임

다른 발급자의 토큰을 받아들이는 경우 알아 둘 가치가 있는 수정 사항이 하나 더 있습니다. JSON 문자열은 원본 UTF-8로 작성될 수도 있고, 모든 비ASCII 문자를 이스케이프하여 작성될 수도 있으며, 라이브러리마다 선택이 다릅니다. 이제는 두 형식 모두 같은 텍스트로 해석되므로, 악센트가 있는 이름은 어느 발급자가 만들었든 올바르게 읽히며, 이스케이프로 작성된 alg 헤더도 그것이 의미하는 알고리즘으로 올바르게 인식됩니다.

업그레이드

이 세 가지 알고리즘은 기존 목록의 끝에 추가된 것이므로, 저장된 서수는 의미를 그대로 유지하며 기존에 하던 작업은 아무것도 바뀌지 않습니다. ML-DSA로 서명하려면 sgcCrypto 팩이 필요하며, 이를 포함하지 않은 빌드는 알 수 없는 오류로 실패하는 대신 명확한 메시지와 함께 해당 토큰을 거부합니다.

다음 읽을거리

영상으로 보기

eSeGeCe 채널에 이 내용을 담은 짧은 영상이 있습니다.

질문이나 피드백, 마이그레이션 지원이 필요하신가요? 문의하기—코드를 직접 작성한 사람에게서 답변을 받으실 수 있습니다.