[794-3][@ant] fix(shared): регистронезависимые хэш-проверки C1–C3 и сертификат C5 под реальный формат mono — doc_hash приходит из фабрики в верхнем регистре, у сертификата нет public_key; иначе подлинные документы ложно не проходят
CI / build (push) Failing after 7s

This commit is contained in:
coopops
2026-06-03 08:07:14 +00:00
parent 98453ccda3
commit 25269a9cb1
3 changed files with 46 additions and 23 deletions
+4 -4
View File
@@ -26,10 +26,11 @@ async function buildValidSig(): Promise<{ pdf: Uint8Array; sig: SigFile; priv: P
const priv = PrivateKey.generate('K1');
const pub = priv.toPublic();
const doc_hash = await sha256(pdf);
// Реальная система: doc_hash из фабрики в ВЕРХНЕМ регистре, meta_hash/hash из SDK — в нижнем.
const doc_hash = (await sha256(pdf)).toUpperCase();
const meta_hash = await hashText(jcs(meta));
const hash = await hashText(meta_hash + doc_hash);
const signed_at = '2026-06-03T10:00:00.000Z';
const hash = await hashText(meta_hash + doc_hash); // concat как считает SDK (meta_hash + UPPER doc_hash)
const signed_at = '2026-06-03T10:00:00';
const signed_hash = await hashText(hash + signed_at);
const signature = priv.signDigest(Buffer.from(signed_hash, 'hex')).toString();
@@ -50,7 +51,6 @@ async function buildValidSig(): Promise<{ pdf: Uint8Array; sig: SigFile; priv: P
signer_certificate: {
type: 'individual',
username: 'ant',
public_key: pub.toString(),
first_name: 'Антон',
last_name: 'Иванов',
},
+22 -7
View File
@@ -13,6 +13,15 @@ export interface CheckDeps {
sha256: Sha256Hex;
}
/**
* Регистронезависимое сравнение hex-хэшей. В системе Coopenomics doc_hash приходит из фабрики
* в ВЕРХНЕМ регистре (calculateSha256 → toUpperCase), а meta_hash/hash из SDK — в нижнем.
* Цепочка консистентна, но сравнивать надо без учёта регистра.
*/
function eqHash(a: string, b: string): boolean {
return a.toLowerCase() === b.toLowerCase();
}
/** C1 — целостность документа: SHA-256(pdf) === sig.canonical.doc_hash. */
export async function checkIntegrity(
pdf: Uint8Array,
@@ -21,7 +30,7 @@ export async function checkIntegrity(
): Promise<CheckResult> {
try {
const actual = await deps.sha256(pdf);
return actual === sig.canonical.doc_hash
return eqHash(actual, sig.canonical.doc_hash)
? pass('C1', 'Хэш документа совпадает — файл не изменён')
: fail('C1', `doc_hash не совпал: ожидалось ${sig.canonical.doc_hash}, получено ${actual}`);
} catch (e) {
@@ -34,7 +43,7 @@ export async function checkMetaHash(sig: SigFile, deps: CheckDeps): Promise<Chec
try {
const text = canonicalize(sig.meta, sig.canonicalization);
const actual = await makeTextHasher(deps.sha256)(text);
return actual === sig.canonical.meta_hash
return eqHash(actual, sig.canonical.meta_hash)
? pass('C2', `meta_hash совпадает (канонизация ${sig.canonicalization})`)
: fail('C2', `meta_hash не совпал: ожидалось ${sig.canonical.meta_hash}, получено ${actual}`);
} catch (e) {
@@ -45,10 +54,12 @@ export async function checkMetaHash(sig: SigFile, deps: CheckDeps): Promise<Chec
/** C3 — композитный хэш: SHA-256(meta_hash || doc_hash) === sig.canonical.hash. */
export async function checkCompositeHash(sig: SigFile, deps: CheckDeps): Promise<CheckResult> {
try {
// Конкатенируем СТРОГО как стороны хранятся (meta_hash строчный + doc_hash как есть) —
// SDK так и считал hash. Регистр не нормализуем до хэширования, только финальное сравнение.
const actual = await makeTextHasher(deps.sha256)(
sig.canonical.meta_hash + sig.canonical.doc_hash,
);
return actual === sig.canonical.hash
return eqHash(actual, sig.canonical.hash)
? pass('C3', 'Композитный хэш совпадает')
: fail('C3', `composite hash не совпал: ожидалось ${sig.canonical.hash}, получено ${actual}`);
} catch (e) {
@@ -66,7 +77,7 @@ export async function checkSignature(sig: SigFile, deps: CheckDeps): Promise<Che
const hasher = makeTextHasher(deps.sha256);
for (const s of sig.signatures) {
const expectedSignedHash = await hasher(sig.canonical.hash + s.signed_at);
if (expectedSignedHash !== s.signed_hash) {
if (!eqHash(expectedSignedHash, s.signed_hash)) {
return fail(
'C4',
`signed_hash не совпал для ключа ${s.public_key}: подписан иной (hash||signed_at)`,
@@ -84,7 +95,11 @@ export async function checkSignature(sig: SigFile, deps: CheckDeps): Promise<Che
}
}
/** C5 — наличие и читаемость сертификата подписанта в каждой подписи. */
/**
* C5 — наличие и читаемость сертификата подписанта в каждой подписи.
* У сертификата нет public_key (он удостоверяет личность=username); связку ключ↔username
* проверяет C6 on-chain. Здесь — что сертификат есть, тип валиден и username присутствует.
*/
export function checkCertificatePresence(sig: SigFile): CheckResult {
for (const s of sig.signatures) {
const cert = s.signer_certificate;
@@ -94,8 +109,8 @@ export function checkCertificatePresence(sig: SigFile): CheckResult {
if (!['individual', 'entrepreneur', 'organization'].includes(cert.type)) {
return fail('C5', `Неизвестный тип сертификата: ${String((cert as { type?: unknown }).type)}`);
}
if (cert.public_key !== s.public_key) {
return fail('C5', `Ключ сертификата ${cert.public_key} != ключ подписи ${s.public_key}`);
if (!cert.username) {
return fail('C5', `В сертификате нет username (подписант ${s.public_key})`);
}
}
return pass('C5', 'Сертификаты присутствуют и читаемы');
+20 -12
View File
@@ -1,17 +1,19 @@
/**
* Сертификаты подписанта, встраиваемые в `.sig` (ADR-002: самодостаточный архив).
* Discriminated union по полю `type`. Структура выровнена с онтологией Coopenomics
* (физлицо / ИП / организация). issuer_signature в v1 = null (самоподписанный, C7 ⚠).
* Discriminated union по полю `type`. Структура выровнена с GraphQL-union'ом Coopenomics
* (IndividualCertificate / EntrepreneurCertificate / OrganizationCertificate) — проверено по
* `mono/components/sdk/.../userCertificateUnionSelector.ts`.
*
* ВАЖНО: у сертификата НЕТ поля public_key — он удостоверяет личность (`username`), а привязка
* `public_key ↔ username` проверяется отдельно on-chain (C6). issuer_signature в v1 = null (C7 ⚠).
*/
export type CertificateType = 'individual' | 'entrepreneur' | 'organization';
interface CertificateBase {
type: CertificateType;
/** Аккаунт-логин в блокчейне (12-симв. EOSIO-имя). */
/** Аккаунт-логин в блокчейне (eosio::name, 12 симв.). */
username: string;
/** Публичный ключ подписанта (EOS...). */
public_key: string;
}
export interface IndividualCertificate extends CertificateBase {
@@ -26,16 +28,22 @@ export interface EntrepreneurCertificate extends CertificateBase {
first_name: string;
last_name: string;
middle_name?: string;
/** ОГРНИП. */
registration?: string;
/** ИНН. */
inn?: string;
}
export interface OrganizationCertificate extends CertificateBase {
type: 'organization';
/** Полное наименование юрлица. */
name: string;
/** ОГРН. */
registration?: string;
/** Краткое наименование юрлица. */
short_name: string;
inn?: string;
ogrn?: string;
represented_by?: {
first_name: string;
last_name: string;
middle_name?: string;
position?: string;
};
}
export type UserCertificateUnion =
@@ -47,7 +55,7 @@ export type UserCertificateUnion =
export function certificateDisplayName(cert: UserCertificateUnion): string {
switch (cert.type) {
case 'organization':
return cert.name;
return cert.short_name;
case 'individual':
case 'entrepreneur':
return [cert.last_name, cert.first_name, cert.middle_name].filter(Boolean).join(' ');