Руководства

Руководства · Часть 4 из 8

Создайте запечатанное подтверждение существования

Обычное подтверждение существования (PoE) показывает сам факт того, что некое содержимое существовало. Запечатанное подтверждение существования доказывает то же самое, но при этом держит содержимое в тайне: вы шифруете байты для одного или нескольких ключей получателей, храните только шифртекст, а запись закрепляете в блокчейне. Любой может увидеть, что запись существует, и проверить её структуру; расшифровать полезную нагрузку способен только обладатель подходящего закрытого ключа. Формат конверта описан в разделе Sealed PoE, а модель угроз — в разделе Запечатано до получения.

Кому адресовать запись

Получатель задаётся строкой в стиле age. Таких строк два вида, и различает их префикс:

  • age1… — классический ключ X25519 (32 байта).
  • age1pqc… — гибридный ключ X-Wing (ML-KEM-768 + X25519, 1216 байт).

X-Wing (mlkem768x25519) — это KEM по умолчанию: он остаётся стойким против будущего квантового противника, и у каждой идентичности всегда есть адрес age1pqc….

Получатель передаёт вам свою строку по внешнему каналу. Декодируйте её обратно в сырой открытый ключ, который нужен помощнику запечатывания, с помощью parseAgeRecipient:

import { parseAgeRecipient } from '@cardanowall/sdk-ts';

const them = parseAgeRecipient('age1pqc…'); // { kem: 'mlkem768x25519', publicKey: Uint8Array }

Если у вас есть собственный 32-байтовый сид, recipientsFromSeed вернёт оба ваших адреса: одним поделитесь, чтобы другие могли запечатывать записи для вас, а собственный ключ добавьте в список получателей, чтобы сохранить доступ к тому, что отправляете:

import { recipientsFromSeed } from '@cardanowall/sdk-ts';

const me = recipientsFromSeed(mySeed); // { age: 'age1…', age1pqc: 'age1pqc…' }

Запечатывание и публикация

Запечатывание идёт через шлюз: он собирает и рассылает транзакцию Cardano и хранит шифртекст каждого элемента за вас. Направьте клиент на тот шлюз, которым пользуетесь; SDK не привязан к конкретному шлюзу.

publishSealed принимает сырые открытые ключи получателей, поэтому возьмите поле publicKey из каждого разобранного адреса. Все получатели должны использовать один и тот же KEM — держите ключи age1pqc… в одной группе. items — это список, так что можно запечатать целый набор файлов для одних и тех же получателей в одной записи; здесь он содержит одну полезную нагрузку:

import { Label309Client, parseAgeRecipient } from '@cardanowall/sdk-ts';

const client = new Label309Client({
  baseUrl: 'https://your-gateway.example',
  apiKey: process.env.CW_API_KEY,
});

const content = new TextEncoder().encode('the secret payload');
const recipients = ['age1pqc…recipient', me.age1pqc].map((r) => parseAgeRecipient(r).publicKey);

const result = await client.poe.publishSealed({
  items: [{ content }],
  recipients,
  maxUsdMicros: 2_000_000n, // refuse to spend more than $2
  // kem defaults to 'mlkem768x25519' (X-Wing); pass 'x25519' only for age1… keys.
});

console.log(result.response.id, result.uris);

Отдельного вызова quote и собственной оценки recordBytes больше не нужно: publishSealed измеряет точный размер запечатанной записи, фиксирует цену, выгружает каждый шифртекст и отправляет запись — всё за один вызов. maxUsdMicros — ваш потолок: публикация отклоняется, если названная цена его превышает, и потолок повторно проверяется по свежей цене, если медленная выгрузка пережила первую фиксацию, так что крупная выгрузка никогда не обгонит свою котировку. Ваш сид и открытый текст никогда не покидают вашу машину в открытом виде.

По умолчанию ончейн-утверждением каждого элемента служит его цифровой отпечаток sha2-256. Чтобы привязать к той же записи второй отпечаток, захешируйте элемент сразу под обоими алгоритмами: передайте hashAlgs: ['sha2-256', 'blake2b-256'] в publishSealed в SDK или повторите --hash-alg sha2-256 --hash-alg blake2b-256 у команды seal в CLI. Оба алгоритма из реестра ведут себя одинаково в любом режиме запечатывания.

Запечатывание на парольную фразу

Ключ есть не у всех. Иногда нужно запечатать запись для людей, у которых есть общая секретная фраза, а не ключ: командный пароль, фраза, продиктованная в разговоре, значение, уже лежащее в секретах вашей CI. Передайте seal парольную фразу вместо --to — и открыть запись сможет любой, кто её знает: ни адреса age, ни обмена ключами.

cardanowall seal \
  --file ./contract.pdf \
  --passphrase-file ./pw.txt \
  --base-url https://your-gateway.example \
  --api-key "$CW_API_KEY"

Запись запечатывается либо на получателей, либо на парольную фразу — никогда на то и другое сразу: передать --passphrase вместе с --to/--to-self не разрешается. Держите фразу вне командной строки: --passphrase-file, --passphrase-stdin и переменная окружения CARDANOWALL_PASSPHRASE считывают её так, что она не оседает в истории оболочки. Ключ содержимого растягивается из фразы с помощью Argon2id, поэтому между шифртекстом и тем, кто пытается его подобрать, стоит только надёжная парольная фраза с высокой энтропией — этот путь ровно настолько безопасен, насколько надёжна выбранная вами фраза.

Чтобы открыть запись, не нужно ничего, кроме самой фразы. Автономный верификатор расшифровывает её и заново сверяет хеш содержимого; а inbox записывает восстановленный открытый текст в файл:

cardanowall verify <tx-hash> --passphrase-file ./pw.txt
cardanowall inbox decrypt <tx-hash> --passphrase-file ./pw.txt --out ./recovered.pdf

Замещение работает одинаково в обоих режимах. Повторное запечатывание — это всегда совершенно новая запись: шифрование каждый раз рандомизируется (новый ключ содержимого и новый nonce, а на пути с парольной фразой — ещё и новая соль Argon2id), так что байты никогда не дедуплицируются. Чтобы пометить новое запечатывание как преемника предыдущего — исправленный файл, обновлённый набор получателей — укажите на него через --supersedes:

cardanowall seal --file ./contract-v2.pdf --to age1pqc…recipient --supersedes <tx-hash>

Возобновление после сбоя

При каждом запечатывании берутся новый ключ содержимого, новый nonce и новый KEM-материал для каждого слота, поэтому наивный повтор заново зашифрует данные, оплатит повторную выгрузку шифртекста и породит другие байты записи. Для задач CI и полезных нагрузок в несколько гигабайт разделите поток: sealPrepare шифрует офлайн и возвращает переносимый артефакт, который можно сохранить; submitSealed выполняет над ним онлайн-половину.

import { sealPrepare, preparedSealToJson, preparedSealFromJson } from '@cardanowall/sdk-ts';

// Phase 1 — pure and offline: encrypt every item to the recipient set.
const prepared = sealPrepare({ items: [{ content }], recipients });
await save(preparedSealToJson(prepared)); // the portable prepared_seal_json_v1 artifact

// Phase 2 — online: quote, upload each ciphertext, publish. If it throws after
// an upload, the error's `uploads` carry validated receipts — pass them back as
// `uploaded` and the finished uploads are never paid for again.
const submission = await client.poe.submitSealed({
  prepared: preparedSealFromJson(await load()),
  maxUsdMicros: 2_000_000n,
});
console.log(submission.response.id, submission.uris);

publishSealed выше — это просто sealPrepare + submitSealed в одном вызове; SDK для Python и Rust предоставляют ту же пару.

Если публикация прервётся на полпути

CLI восстанавливается точно так же, без двухфазного кода. Запуск seal --to …, который обрывается после выгрузки шифртекста, пишет файл <seal-fingerprint>.l309-seal-resume.json без секретов (завершённые выгрузки — и ни ключей, ни открытого текста). Перезапустите командой cardanowall seal --resume <seal-fingerprint>.l309-seal-resume.json: она заново запрашивает цену, доводит до конца только оставшиеся выгрузки и публикует. Подписанному запуску снова нужен его сид; запечатывания на парольную фразу не хранят состояния возобновления, поэтому их придётся запускать с начала.

На Python

cardanowall-sdk — побайтный близнец: тот же KEM по умолчанию, тот же конверт:

import asyncio
import os
from cardanowall import Label309Client, parse_age_recipient

async def main():
    content = b"the secret payload"
    recipients = [parse_age_recipient("age1pqc…recipient").public_key]

    async with Label309Client(
        base_url="https://your-gateway.example",
        api_key=os.environ["CW_API_KEY"],
    ) as client:
        submission = await client.poe.publish_sealed(
            items=[content],           # a list of plaintext items (bytes or str)
            recipients=recipients,
            max_usd_micros=2_000_000,  # refuse to spend more than $2
        )
        print(submission.response["id"], submission.uris)

asyncio.run(main())

Для возобновляемого потока seal_prepare(...) (из cardanowall.client) возвращает подготовленный артефакт, а client.poe.submit_sealed(prepared=...) выполняет онлайн-половину — те же две фазы, что и выше.

На Rust

Крейт cardanowall запечатывает через тот же клиент шлюза. parse_age_recipient декодирует каждый адрес в сырой ключ, а KEM по умолчанию использует гибрид X-Wing:

use cardanowall::client::{
    Label309Client, Label309ClientConfig, PublishSealedInput, SealPrepareItem,
};
use cardanowall::recipient::parse_age_recipient;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Label309Client::new(Label309ClientConfig {
        base_url: Some("https://your-gateway.example".into()),
        api_key: std::env::var("CW_API_KEY").ok(),
    })?;

    let content = b"the secret payload".to_vec();
    let recipients = vec![parse_age_recipient("age1pqc…recipient")?.public_key];

    // One-shot: seal, quote the exact record size, upload, publish. The KEM
    // defaults to mlkem768x25519 (X-Wing); `with_kem` selects x25519.
    let submission = client.poe().publish_sealed(
        &PublishSealedInput::new(vec![SealPrepareItem::new(&content)], recipients)
            .with_max_usd_micros(2_000_000), // refuse to spend more than $2
    )?;

    println!("{}", submission.response.id);
    Ok(())
}

Для возобновляемого потока seal_prepare возвращает PreparedSeal, который можно сохранить, а client.poe().submit_sealed(&SubmitSealedInput::new(&prepared)) выполняет онлайн-половину. CLI тоже умеет запечатывать из командной строки, с теми же фазами под капотом: cardanowall seal --file <path> --to <address>.

Как только запись зафиксируется в блокчейне, каждый получатель обнаруживает её, расшифровывает полезную нагрузку своим закрытым ключом и заново вычисляет хеш открытого текста, замыкая цикл, — это и есть та половина из раздела Проверка записи, что относится к получателю.

Запечатайте и для себя

publishSealed никогда не добавляет вас в список получателей молча. Если вы не включите туда один из собственных ключей, то опубликуете запись, которую сами уже никогда не прочитаете. Добавляйте me.age1pqc (или me.age) в список получателей всякий раз, когда хотите сохранить доступ к тому, что отправили.