PDFlibPas는 순수 Object Pascal로 Ed448과 세 가지 Brainpool ECDSA 곡선으로 서명하고 검증합니다. 외부 암호화 라이브러리도, 플랫폼 provider도, DLL도 없이 PDFlibEd448는 edwards448 위에서 RFC 8032 PureEdDSA를 구현하고, PDFlibBrainpool는 RFC 5639의 brainpoolP256r1, brainpoolP384r1, brainpoolP512r1을 구현합니다. 두 유닛 모두 같은 방식으로, Pascal 코드를 한 줄 쓰기 전에 독립적으로 생성된 known-answer 벡터를 기준으로 만들어졌으며, 다룰 가치가 주로 버그 덕분에 생겼다는 점도 같습니다
유한체 연산은 유달리 정직한 코드입니다. 공개된 벡터와 바이트 단위로 일치하거나 그렇지 않거나 둘 중 하나이므로 "거의 작동"이 통하는 여지가 없습니다. 어려운 점은, 잘못된 구현이라도 여전히 서명을 만들어 내고, 자신의 서명을 검증하며, 완전히 그럴듯해 보인다는 데 있습니다
이 곡선을 선택한 이유, 그리고 Pascal인 이유
Brainpool 곡선은 유럽의 적격 서명(qualified signature) 프로파일에 등장하므로, 해당 시장용 문서에 서명하는 라이브러리는 이를 특이한 사례로 취급할 수 없습니다. Ed448은 ISO/TS 32002가 PDF에 도입하는 알고리즘 집합에 포함되어 있으며, 이때 내부 다이제스트는 SHA-2가 아니라 SHAKE256입니다. 두 계열 모두 널리 쓰이는 Pascal 암호화 라이브러리에는 존재하지 않으므로, 이 곡선들이 필요한 PDF 라이브러리는 직접 소유해야 합니다
배포 측면의 논거는 이 라이브러리의 모든 암호화에 공통으로 적용되는 것입니다. 암호화 의존성 없이 단일 바이너리로 배포되는 애플리케이션은 탐지할 provider도, 맞춰야 할 버전도, 호스트가 패치될 때 달라지는 동작도 갖지 않습니다. 서명은 바로 이렇게 움직이는 의존성을 가장 원치 않는 영역입니다
상수는 규격 본문에서 가져오며, 절대 기억으로부터가 아닙니다
edwards448 기저점(base point)에 대한 첫 시도는 기억으로 작성했는데 틀렸습니다. 놀라운 실수는 아니지만 값비싼 실수입니다. 잘못된 기저점은 자기 일관적인 시스템을 만들어 내기 때문입니다. 키 생성, 서명, 검증이 서로는 모두 일치하면서 바깥 세상과는 어긋납니다
실제로 통하는 절차는 모든 도메인 파라미터를 규격 본문에서 가져온 뒤 교차 검증하는 것입니다. edwards448의 경우 RFC 8032에서 소수, 곡선 상수, 군의 위수, 기저점의 십진 좌표 두 값을 가져와 내부 limb 표현으로 변환한 다음, 같은 문서의 공개 테스트 벡터와 대조한다는 뜻입니다. Brainpool 곡선의 경우 RFC 5639의 파라미터를 가져오고, 벡터 생성용으로 따로 작성한 독립 구현을 두며, Pascal 코드를 실행하기 전에 시스템 라이브러리와 양방향 교차 검증을 수행한다는 뜻입니다
한 가지 유도 지름길에는 경고가 필요합니다. 범용처럼 보이지만 그렇지 않기 때문입니다. 고정된 y 값에서 기저점을 복원하는 방법은 25519 곡선에서는 통하지만 edwards448에서는 통하지 않는데, 이 곡선에서는 그 값이 제곱근을 갖지 않습니다. 스크립트가 몇 초 만에 이를 반증했고, 디버거로 발견하는 것보다 훨씬 값쌌습니다
방법론: Pascal 코드 이전의 limb 수준 미러
두 유닛을 다룰 수 있게 만든 기법은 무한 정수를 지원하는 언어로 작성한 미러 구현을 상향식으로 만드는 것입니다. 먼저 산술 레이어만 따로 다룹니다. 유한체 곱셈, 뺄셈, 캐리 전파를 대수적 불변식 기준으로 수백 개의 무작위 사례로 스트레스 테스트합니다. 그다음 미러 안에서 전체 키 생성을 돌립니다. 의미론적 버그가 사는 곳이자 값을 싸게 찾을 수 있는 곳이 바로 여기입니다. 그 후에야 Pascal로 옮겨 적습니다
이 방식의 성과는 개발이 아니라 진단 쪽에 있습니다. 미러가 올바르다고 확인되면 미러와 Pascal 사이의 불일치는 전사 실수이며, 양쪽 구현에서 같은 중간값을 찔러 보면 즉시 위치가 드러납니다. 스칼라 곱셈 깊은 곳의 단 하나의 잘못된 limb처럼 사실상 디버깅이 거의 불가능한 부류의 버그를 5분짜리 비교 작업으로 바꿔 줍니다
Ed448에서 발견된 네 가지 근본 원인
네 가지 모두 중간값을 찔러 보며 발견했고, 네 가지 모두 그럴듯해 보이는 출력을 만들어 내는 부류입니다
첫 번째는 기호 표기의 함정입니다. 공개된 통일 Edwards 덧셈 공식 대부분은 곡선 상수가 마이너스 일이라고 가정하는데, edwards448은 플러스 일입니다. 그대로 가져오면 y 좌표의 분자가 차여야 할 자리에 합으로 적힙니다. 해결책은 부호를 덧붙이는 것이 아니라 올바른 곡선의 아핀 덧셈 법칙에서 역원 없는 곱 형태를 다시 유도하는 것이고, 그러면 네 개의 좌표식이 나오며 잘못된 원천에서 부호를 물려받을 여지가 사라집니다
두 번째는 점 압축 해제에 있습니다. 사영 좌표에서 아핀 x를 복원하려면 Z의 역원으로 한 번 곱해야 합니다. 역원의 제곱으로 곱하면 여전히 유효한 사영 표현이면서 잘못된 아핀 좌표가 나오므로, 증상은 y가 맞고 x가 틀린 형태입니다. 한 좌표는 맞고 다른 좌표가 틀릴 때는 버그가 산술이 아니라 정규화에 있습니다
세 번째는 더 짧은 곡선에서 들여온 습관입니다. 서명별 스칼라와 챌린지 스칼라 모두 전체 다이제스트, 즉 Ed448의 경우 114바이트에서 축약해야 하며 처음 57바이트에서가 아닙니다. 32바이트 곡선도 전체 64바이트 다이제스트를 사용하므로 규칙 자체는 일관됩니다. 틀린 것은 "다이제스트의 절반이 스칼라 폭"이라는 가정일 뿐입니다
네 번째는 순서입니다. 도메인 분리 접두어가 맨 앞에 오고, 그다음에 컨텍스트 접두어와 메시지가 옵니다. 이는 규격의 R과 A를 직관적으로 읽었을 때 떠오르는 순서가 아닙니다. 이를 틀리면 자신의 구현에 대해서만 검증되고 그 외에는 아무것도 통과하지 않는 서명이 만들어지는데, 이보다 오도적인 실패는 없습니다
// 유한체 캐리 설계: 순수 floor 의미론 전파 방식이므로 양수 limb과
// 음수 limb이 모두 동작하며 뺄셈에 바이어스가 필요 없습니다.
// 최상위 캐리는 2^448 = 2^224 + 1 (mod p)을 통해 되돌아와
// limb 0과 limb 8에 반영됩니다. 4회 라운드로 제한되며 실제로는
// 2회 관측되었습니다
procedure FeCarry(var A: TFe448);
var
I, Round: Integer;
Carry: Int64;
begin
for Round := 1 to 4 do
begin
Carry := 0;
for I := 0 to 15 do
begin
A[I] := A[I] + Carry;
Carry := Floor28(A[I]); // floor, truncation 아님
A[I] := A[I] - (Carry shl 28);
end;
if Carry = 0 then
Break;
A[0] := A[0] + Carry; // 2^448 == 1
A[8] := A[8] + Carry; // 2^448 == 2^224
end;
end;
이 루틴의 이전 버전은 전파 전에 바이어스를 적용했고, 큰 입력에서는 잘못된 크기의 가짜 캐리가 하위 limb으로 접혀 들어갔습니다. 바이어스 기반 캐리 방식은 이 부류의 결함이 끊이지 않게 만드는 원천입니다. 상한이 있는 repeat 루프를 곁들인 floor 의미론이 추론하기 쉽고 측정상으로도 충분히 빠릅니다
Brainpool에서 발견된 두 가지 근본 원인
첫 번째는 암호학과 전혀 무관합니다. 실제로 쓰는 표현이 33개 limb이므로 두 값의 곱에는 66개가 필요한데, 곱 배열은 64로 선언되어 있었습니다. 배열 끝을 넘어 쓰기가 인접 메모리를 망가뜨렸고, 처음에는 잘못된 결과로 나타나다가 더 넓은 스캔을 추가한 뒤에야 크래시로 바뀌었습니다. 여기서 나온 규칙은 크기가 고정된 모든 숫자 버퍼에 적용할 가치가 있습니다. 최악의 경우 곱 폭으로 크기를 정하고 여유를 더한 뒤에는 다시 생각하지 않는 것입니다. 출시 코드의 배열은 68개 limb입니다
두 번째는 뒤섞인 거듭제곱 형태입니다. 올바른 square-and-multiply 형태는 두 가지이며 서로 반대 방향으로 지수를 소비합니다. 오른쪽에서 왼쪽 형태는 밑을 곱한 뒤 제곱하며 비트를 최하위 끝부터 읽어야 하고, 왼쪽에서 오른쪽 형태는 제곱한 뒤 곱하며 최상위 끝부터 읽습니다. 모듈러 역원 루프는 오른쪽에서 왼쪽 본체에 최상위 우선 비트 순회를 얹은 상태였습니다. 두 절반 각각은 교과서적이지만 조합은 그렇지 않고, 결과는 여전히 그럴듯한 유한체 원소로 보이는 잘못된 역원입니다
// 대상 레코드가 소스와 같은 변수일 수 있는 Jacobian 배가와 덧셈입니다.
// 진입 시 레코드 전체를 복사하는 것이 유일하게 믿을 수 있는 방어책입니다:
// R의 limb에 쓰기가 나중의 P 읽기를 오염시킵니다
procedure BPPointDouble(var R: TBPPoint; const P: TBPPoint;
const Curve: TBPCurve);
var
Pin: TBPPoint;
begin
Pin := P; // 먼저 복사한 뒤 Pin으로만 계산
// ... M = 3X^2 + A*Z^4, S = 4*X*Y^2, X3 = M^2 - 2S, ...
end;
버그보다 비용이 큰 두 가지 프로세스 교훈
암호화 유닛에는 점진적 핫픽스가 수렴하지 않습니다. 한 초안은 패치를 거듭한 끝에 중복 루틴 32개와 망가진 구조를 안게 되었고, 결국 재작성으로만 해결되었습니다. 취할 패턴은 검증된 미러에서 한 번에 쓰거나 아예 재작성하는 것입니다. 아직 이해하지 못한 산술에 대한 국소 수정의 나열은 수정보다 빠르게 쌓입니다
그리고 테스트 결과를 믿기 전에 실행 파일의 타임스탬프를 확인하십시오. 컴파일은 되지만 재링크되지 않는 증분 빌드는 이전 바이너리를 실행하며, 이것이 누락된 프로브와 중복 출력에 관한 가짜 단서 한 바퀴를 통째로 만들어 냈습니다. 암호화를 디버깅할 때 설명되지 않는 결과가 나오면 "알고리즘이 틀렸나"보다 "내가 방금 빌드한 바이너리인가"를 먼저 물어야 합니다
성능, 범위, 호출 방법
Brainpool 유닛의 모듈러 축약은 곱의 최상위 설정 비트부터 시작하는 비트 직렬 shift-subtract 방식이므로, 곱셈 하나의 비용은 대략 비트 폭에 비례하는 수준입니다. P-256 검증은 수백 밀리초의 하단에 들어오는데, 문서 서명이나 검증용으로는 특별할 것이 없고 TLS 종단 장치에는 부적합한 수준입니다. Barrett 축약이 명백한 업그레이드이지만 현재 표현이 담는 것보다 넓은 작업값이 필요하므로, 미리 해 두는 변경이 아니라 워크로드가 요구할 때 하는 변경입니다
uses
PDFlibEd448, PDFlibBrainpool;
var
PublicKey, Signature: AnsiString;
Curve: TBPCurve;
R, S, PubX, PubY: TBPValue;
begin
// Ed448: PureEdDSA, 내부적으로 SHAKE256, 57바이트 키
if Ed448PublicKeyFromSeed(Seed, PublicKey) and
Ed448Sign(DocumentDigest, Seed, Signature) then
Assert(Ed448Verify(DocumentDigest, PublicKey, Signature));
// Brainpool: 호출자가 서명별 nonce를 제공하므로 nonce
// 정책은 애플리케이션에 남습니다
Curve := BPLoadCurve(bpP256r1);
if BPKeyGen(PubX, PubY, PrivateD, Curve) and
BPSignFixedK(R, S, Hash, PrivateD, Nonce, Curve) then
Assert(BPVerify(R, S, Hash, PubX, PubY, Curve));
end;
Brainpool 서명 진입점은 nonce를 생성하지 않고 인자로 받는다는 점에 유의하십시오. 의도된 설계입니다. nonce 생성은 ECDSA에서 틀렸을 때 가장 치명적인 단 한 가지이며, 반복되거나 예측 가능한 값은 개인 키를 노출합니다. 무작위성이 어디서 오는지에 대한 결정은 PDF 라이브러리가 아니라 애플리케이션과 그 컴플라이언스 체계의 몫입니다
이 곡선들은 FIPS 204 ML-DSA 문서에서 다룬 포스트 퀀텀 작업과 나란히 놓이며, PAdES 서명 및 검증에서 다루는 것과 같은 서명·검증 파이프라인에 연결됩니다. 이 곡선들의 테스트 인증서는 CryptoAPI 자체 서명 인증서 문서에서 로컬 생성 경로를 설명합니다. 전체 알고리즘 매트릭스는 losLab PDF Developer Library 제품 페이지에 정리되어 있습니다