[794-3][@ant] fix(shared): регистронезависимые хэш-проверки C1–C3 и сертификат C5 под реальный формат mono — doc_hash приходит из фабрики в верхнем регистре, у сертификата нет public_key; иначе подлинные документы ложно не проходят
CI / build (push) Failing after 7s
CI / build (push) Failing after 7s
This commit is contained in:
@@ -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: 'Иванов',
|
||||
},
|
||||
|
||||
@@ -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', 'Сертификаты присутствуют и читаемы');
|
||||
|
||||
@@ -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(' ');
|
||||
|
||||
Reference in New Issue
Block a user