기술 문서

델파이 ML-DSA: PDFlibPas의 FIPS 204 포스트 양자

PDFlibPas는 FIPS 204로 표준화된 Module-Lattice-Based Digital Signature Algorithm, ML-DSA를 전부 Object Pascal로 구현합니다. 세 파라미터 셋 전부가 평범한 함수로 배송됩니다. MLDSA44Sign, MLDSA65Sign, MLDSA87Sign과 짝을 이루는 KeyGen과 Verify 진입점까지입니다. OpenSSL도, 플랫폼 DLL도, C 접착제도 없습니다. 단일 유닛 PDFlibMLDSA는 라이브러리 SHAKE 스펀지 외에 아무것도 의존하지 않으며, 그 출력은 공식 FIPS 204 known-answer 테스트 벡터와 바이트 단위까지 일치합니다

마지막 문장이 유일하게 진짜 작업이 들어간 부분입니다. Pascal로 격자 연산을 쓰는 것은 기계적입니다. NIST와 일치시키는 것은 아닙니다. 이식 작업의 엔지니어링 기록이 이어집니다. 세 파라미터 셋이 어떻게 하나의 엔진을 공유하게 되었는가, 그리고 컴파일되고 돌아간다KAT와 일치한다를 가른 구체적 결함들은 무엇이었는가. 델파이나 C++Builder 문서 파이프라인의 포스트 양자 선택지를 평가 중이라면 결함이 유용한 부분입니다. 그것들 하나하나가 그럴듯해 보이는 출력을 내며 조용히 상호운용성을 깨뜨리기 때문입니다

왜 순수 Object Pascal로 포스트 양자 서명기를 쓰는가

대안은 대상 플랫폼마다 네이티브 의존성 하나씩이고, 델파이 PDF 라이브러리에는 이미 그런 것들이 충분하기 때문입니다. PDFlibPas는 Delphi, C++Builder, FPC/Lazarus로 Win32, Win64, Unix 대상을 빌드합니다. C 포스트 양자 라이브러리에 바인딩하면 그 모든 슬롯마다 빌드를 추적해야 하고, 그 사이의 호출 규약과 메모리 소유권 표면도 따라옵니다. 순수 Pascal 유닛은 라이브러리의 나머지가 컴파일되는 곳이면 어디서든 컴파일됩니다. 논거는 그게 전부입니다

ML-DSA는 이것을 유난히 싸게 만듭니다. 유일한 원시 의존성이 SHAKE이기 때문입니다. 큰 정수 계층도, 타원 곡선도, 별도 해시 스위트도 없습니다. PDFlibPas는 이식 직전 릴리스에서 스트리밍 XOF를 얻었습니다. PDFlibDigestTPLShakeXOF입니다. PLShakeXOFInit이 SHAKE128(레이트 168)이나 SHAKE256(레이트 136)을 고르고, PLShakeXOFAbsorb, PLShakeXOFFinalize, 임의 출력 길이만큼 계속 순열하는 PLShakeXOFSqueeze 루프가 뒤따릅니다. ML-DSA 유닛의 모든 거부 샘플링 루틴은 그 4회 호출 API에 대해 직접 쓰였습니다

하나의 엔진, 세 개의 파라미터 셋: TMLDSAParams

PDFlibPas는 ML-DSA 파라미터 셋 전체를 하나의 레코드로 서술하고 셋 번호로 선택하므로, ML-DSA-44, 65, 87이 같은 코드 경로를 돕니다. 처음 동작한 구현은 ML-DSA-44에 하드와이어된 고정 4x4 빌드였습니다. 일반화는 k와 l, eta, tau, beta, gamma1과 gamma2, omega, 챌린지 길이를 TMLDSAParams로 끌어올리고 나머지 전부를 유도하는 일이었습니다. 공개 진입점은 3줄짜리 래퍼가 되었습니다

Type
  TMLDSAParams= Record
    K, L, D, Eta, Tau, Beta, Gamma1, Gamma2, Omega: Integer;
    Alpha, MW1: Cardinal;
    W1BW, EtaBW, Gamma1BW, T1BW: Integer;
    T0Rng: Cardinal;
    CTildaBytes: Integer;
    PublicKeyBytes, SecretKeyBytes, SignatureBytes: Integer;
  End;

// 파생 필드는 계산된다. 표에서 베껴 쓰지 않는다
Params.Alpha:= 2* Cardinal(Params.Gamma2);
Params.MW1:= (Q- 1)div Params.Alpha;
Params.W1BW:= BitWidth(Params.MW1- 1);
Params.EtaBW:= BitWidth(2* Cardinal(Params.Eta));
Params.Gamma1BW:= BitWidth(Cardinal(Params.Gamma1));

Function MLDSA65Sign(Const SecretKey, Message, Context, Rnd: AnsiString;
  Out Signature: AnsiString): Boolean;
Var
  Params: TMLDSAParams;
Begin
  BuildMLDSAParams(65, Params);
  Result:= MLDSASignInternal(Params, SecretKey, Message, Context, Rnd,
    Signature);
End;

다섯 개의 파생 필드가 FIPS 204 표에서 베끼지 않고 계산되는 것은 의도입니다. 손으로 옮겨 적은 비트 폭은 리뷰에서는 맞아 보이고 프로덕션에서는 하나 어긋나는 상수의 전형이며, 이 이식의 실제 결함 두 개가 정확히 그 모양이었습니다. 선언된 크기는 검증용 명명 상수로 남습니다. ML-DSA-44의 공개 키, 비밀 키, 서명은 1312 / 2560 / 2420바이트, ML-DSA-65는 1952 / 4032 / 3309, ML-DSA-87은 2592 / 4896 / 4627입니다

PDFlibPas는 MLDSA44Sign, MLDSA65Sign, MLDSA87Sign을 BuildMLDSAParams를 통해 단일 TMLDSAParams 레코드로 보내고, 파생 필드는 베끼지 않고 계산되므로 하나의 공유 MLDSASignInternal 엔진이 세 개의 FIPS 204 파라미터 셋을 모두 서비스한다
세 파라미터 셋이 하나의 엔진을 공유하는 이유는 셋 번호가 레코드만 선택하고, 파생 비트 폭은 FIPS 204 표에서 베끼지 않고 계산되기 때문입니다

백지에서 시작한 ML-DSA 이식이 처음 어디서 틀어지는가

expand_a, FIPS 204 알고리즘 32에서이고, 실패 양상은 아름답게 기만적입니다. 행렬 A는 SHAKE128에 rho와 두 개의 인덱스 바이트를 이어 붙여 시딩되므로, 시드 버퍼는 34바이트입니다. rho(32), 그다음 j, 그다음 i. 1 기반 AnsiString 인덱싱의 Pascal로 쓰면 그 두 바이트는 Msg[33]Msg[34]입니다. 이 이식의 초안은 Msg[34]Msg[35]에 썼고, 정확히 한 바이트 밀렸으며, 결과는 rho가 테스트 벡터와 완벽히 일치하는데 t의 모든 계수가 틀린 키 쌍이었습니다. 오염된 것은 행렬뿐이고, 행렬은 공개 키가 그대로 실어 나르지 않는 단 하나의 것입니다

같은 루틴에 결함이 두 개 더 살았습니다. 흡수 길이는 35가 아니라 34여야 합니다. 쓰레기 바이트 하나만 더해져도 스퀴즈된 스트림 전체가 바뀝니다. 그리고 내부 거부 루프는 블록이 줄 수 있는 3바이트 그룹 전부를 소비해야 하는데, 168바이트 SHAKE128 블록의 오프셋 165에서 시작하는 그룹을 포함해 블록당 56개 그룹입니다. 오프셋 162에서 멈춘 교차 검증 스크립트는 모든 블록의 꼬리를 떨어뜨렸고, 샘플된 t1 접두어가 대략 13번째 바이트부터 어긋났습니다

SetLength(Msg, 34);
Move(Rho[1], Msg[1], 32);
Msg[33]:= AnsiChar(J);          // 먼저 열 인덱스
Msg[34]:= AnsiChar(I);          // 그다음 행 인덱스
PLShakeXOFInit(Ctx, True);      // SHAKE128, 레이트 168
PLShakeXOFAbsorb(Ctx, @Msg[1], 34);
PLShakeXOFFinalize(Ctx);
Cnt:= 0;
While Cnt< N Do
Begin
  PLShakeXOFSqueeze(Ctx, @Buf[0], 168);
  BOff:= 0;
  // BOff+2 <= 167은 오프셋 165의 그룹을 유지한다. 블록당 56개 트리플
  While (BOff+ 2<= High(Buf))And (Cnt< N) Do
  Begin
    T3:= ((Buf[BOff+ 2]and $7F)shl 16)xor (Buf[BOff+ 1]shl 8)xor Buf[BOff];
    If T3< Q Then
    Begin
      Poly^[Cnt]:= T3;
      Inc(Cnt);
    End;
    Inc(BOff, 3);
  End;
End;

그 세 가지를 바로잡자 ML-DSA-44 공개 키와 비밀 키 전체의 SHA-256 다이제스트가 FIPS 204 known-answer 벡터와 일치했습니다. 디버깅 교훈 하나도 이름 붙일 가치가 있습니다. 세션 하나를 삼켰기 때문입니다. XOF 구동 거부 루프용 Python 교차 검증을 만들 때 hashlib.shake_128().digest(n)은 스트림을 이어가는 대신 매 호출마다 같은 접두어를 반환합니다. 전체 길이를 한 번 받아 레이트 크기 블록으로 슬라이스하십시오. 아니면 여러분의 참조 구현이 Pascal이 올바르게 거부한 바로 그 값들을 즐겁게 다시 소비합니다

PDFlibPas ML-DSA expand_a 루틴은 rho, 열 인덱스, 행 인덱스를 담은 34바이트 버퍼로 SHAKE128을 시딩한다. 옆에는 인덱스 바이트를 한 자릿수 늦게 쓴 초안과 모든 블록의 마지막 3바이트 그룹을 떨어뜨린 교차 검증이 있다
같은 루틴의 오프바이원 결함 두 개. 인덱스 바이트가 한 위치 늦게 쓰인 것과, 오프셋 165의 3바이트 그룹 앞에서 멈춘 거부 루프입니다

eta 샘플링: ML-DSA-65에 자기 브랜치가 필요한 이유

PDFlibPas는 expand_s에 두 개의 별도 경로를 유지합니다. FIPS 204 알고리즘 33이 실제로 둘을 정의하기 때문입니다. eta = 2에서는 니블이 15에 닿으면 거부되고 그렇지 않으면 mod 5로 축소됩니다. eta = 4에서는 니블이 9 이상이면 거부되고 그다음 모듈러 축소 없이 그대로 사용됩니다. ML-DSA-65는 eta = 4인 유일한 배송 셋이며, mod 5 경로를 재사용하면 s1과 s2가 첫 계수부터 어긋나고, 내부적으로는 일관되고 자기 검증에 통과하지만 아무도 만들지 않는 키 쌍이 나옵니다

Procedure StoreNibble(Nibble: Byte);
Var
  M: Integer;
  Centered: Cardinal;
Begin
  If Cnt>= N Then
    Exit;
  If Eta= 4 Then
  Begin
    If Nibble>= 9 Then        // 거부 후 니블을 그대로 사용
      Exit;
    M:= Nibble;
  End
  Else
  Begin
    If Nibble>= 15 Then       // eta = 2: 15를 거부한 뒤 mod 5로 축소
      Exit;
    M:= Nibble mod 5;
  End;
  If Eta>= M Then
    Centered:= Eta- M
  Else
    Centered:= Q- (M- Eta);
  Vec[I][Cnt]:= Centered;
  Inc(Cnt);
End;

크기가 곧 테스트다: c-틸드 길이와 gamma1 비트 폭

보안 수준에 따라 달라지는 인코딩 매개변수가 둘 있는데, 동작하는 ML-DSA-44 빌드가 바로 옆에 있는 동안에는 놓치기 쉽습니다. 챌린지 해시 c-틸드는 2 x lambda / 8바이트로, ML-DSA-44는 32, ML-DSA-65는 48, ML-DSA-87은 64입니다. 32로 고정하면 표준 3309가 아니라 3293바이트의 ML-DSA-65 서명이 나오고, KAT 접두어는 즉시 어긋납니다. 레코드 필드 CTildaBytes는 바로 그 숫자를 잊을 수 없게 하려고 존재합니다

두 번째는 마스크 다항식 z의 패킹 폭입니다. PDFlibPas는 지수가 아니라 BitWidth(Gamma1)으로 계산합니다. ML-DSA-65와 87의 gamma1 = 2^19는 계수당 19비트가 아니라 20비트를 필요로 하고, 그 한 비트가 각 z 다항식이 640바이트를 차지할지 아무 검증자도 파싱하지 않을 무언가를 차지할지 결정합니다. 검증자도 이식 중 같은 결함을 지니고 있었습니다. z 역직렬화 버퍼가 576이 아니라 192바이트로 잡힌 것입니다. 서명 길이는 여러분이 쓰게 될 가장 싼 회귀 테스트입니다. Length(Signature)에 대해 2420, 3309, 4627을 단언하면 대부분의 파라미터화 실수가 암호학적 단언 하나에 닿기 전에 스스로 알립니다

PDFlibPas는 두 ML-DSA 인코딩 매개변수를 보안 수준에 묶는다. c-틸드 챌린지 해시는 32에서 48, 64바이트로 자라고, 마스크 다항식 z는 gamma1 비트 폭으로 패킹되며, 서명 길이가 회귀 테스트다
보안 수준에 따라 달라지는 매개변수 둘. 3309 대신 3293바이트로 나오는 서명은 암호학적 단언이 돌기 전에 실수를 알립니다

무한 루프 없이 서명하기

ML-DSA 서명은 거부 기반이므로, 후보 서명이 노름과 힌트 검사를 통과할 때까지 kappa를 증가시키며 재시도합니다. PDFlibPas는 명시적 외부 예산 65535회 시도로 이를 제한합니다. 소진되면 MLDSASignInternal은 False를 반환하고 서명을 빈 채로 남기며, 문서 생산 스레드 안에서 빙빵 도는 일이 없습니다. 실제로 공식 ML-DSA-44 벡터는 omega 상한 80에 대해 힌트 55개로 kappa = 4에서 성공하므로, 예산은 작동 한계가 아니라 안전 난간입니다

그 난간을 필요하게 느끼게 한 버그는 숫자와 전혀 무관했습니다. 서명이 멈춘 것처럼 보였고, 의심은 decomposemake_hint(FIPS 204 알고리즘 36과 39)로 향했고, 진짜 원인은 뒤집힌 누적 대상이었습니다. 힌트 계산에 들어가는 벡터는 c*t0를 누적해야 하는 반면, 원본 c*t0는 노름 검사를 위해 손대지 않은 채 살아남아야 합니다. 둘을 같은 버퍼로 향하게 하면 산술은 완벽히 올바른데 루프는 영원히 거부합니다. 성공 경로와 예산 소진 경로 모두에서 유닛은 유도된 시드, 비밀 다항식, 마스크, 챌린지, 인코딩 버퍼를 제로화하고, 호출자가 준 시드, 비밀 키, rnd는 호출자의 책임으로 남습니다. 그 문자열이 어디서 왔는지 알 수 없는 라이브러리에 맞는 올바른 분배입니다

오늘날 ML-DSA는 PDF 서명 스택에서 어디에서 만나는가

존재하는 것에 대해 정확히 말합시다. PDFlibPas는 현재 PAdES 출력의 드롭인 대체가 아니라, 검증된 서명 원시 기능과 PKCS #11 메커니즘 바인딩으로 ML-DSA를 배송합니다. 토큰 경로는 TPDFlibPKCS11Client.SignMLDSA이고, 의도적으로 별도 진입점입니다. CKM_ML_DSA는 사전 계산된 다이제스트가 아니라 원시 메시지를 소비하므로, 기존 SignHash와 외부 다이제스트 콜백은 재사용될 수 없습니다. 인증서 없는 탐색은 CertificateOptional을 개인 키 레이블이나 ID와 함께 명시적으로 켜야 하며, 클라이언트는 연결 시점에 CKA_PARAMETER_SETCKP_ML_DSA_44 / 65 / 87 화이트리스트에 대해 검증합니다. 그래서 기본 RSA와 ECDSA 인증서 짝이 사고로 느슨해지는 일은 없습니다

문서 수준 통합은 라이브러리 코드가 아니라 표준 작업이 지배하는 부분입니다. ISO 32000-2 §12.8이 서명 사전과 그 CMS 페이로드를 정의하고, ISO/TS 32002가 그 지원을 더 새로운 해시와 서명 알고리즘으로 확장하는 수단입니다. 검증자와 거래 상대가 따라오기 전까지는 클래식 서명이 프로덕션 경로로 남습니다. 실용적 자세는 평행 트랙입니다. 제3자가 오늘 검증해야 하는 모든 것에는 타임스탬프와 장기 검증 데이터를 갖춘 PAdES B-B부터 B-LTA까지의 서명을 계속 배송하면서, 그 옆에서 ML-DSA 키 처리와 토큰 통합을 증명해 나갑니다. 로컬 실험에는 CryptoAPI 기반 자체 서명 인증서 워크플로가 공개 CA를 거치지 않고 서명 신원을 줍니다

파라미터 셋 변경은 다른 서명 변경을 테스트하듯 테스트하십시오. 먼저 크기, 그다음 공식 벡터, 그다음 부정 사례입니다. 변조된 서명 바이트, 어긋난 컨텍스트 문자열, 잘린 키. PDFlibPas는 그 전부를 DUnitX 스위트에서 커버하고, 같은 규율이 여러분의 파이프라인에도 속합니다. 가급적 문서 코퍼스에 걸쳐 검증을 일괄 처리하는 컴플라이언스와 서명 워크벤치와 함께여야, 회귀가 고객에게 알려지지 않은 채 닿지 않습니다

문서 소프트웨어의 포스트 양자 대비는 스위치 하나로 도착하지 않습니다. 테스트할 수 있는 원시 기능, 연결할 수 있는 토큰 경로, 현재 릴리스를 걸지 않고 따라가는 표준 트랙으로 도착합니다. ML-DSA 유닛이 네이티브 Object Pascal 코드베이스의 나머지 서명, 암호화, PDF/A 도구와 어떻게 나란히 서는지 보려면 PDFlibPas Delphi PDF library 제품 페이지가 전체 구성 요소 셋과 지원 컴파일러 매트릭스를 나열합니다