Guide

Guide · Parte 4 di 8

Costruire una PoE sigillata

Una PoE semplice dimostra che un certo contenuto è esistito. Una PoE sigillata dimostra la stessa cosa mantenendo segreto il contenuto stesso: cifri i byte per una o più chiavi di destinatario, conservi solo il testo cifrato e ancori il record on-chain (sulla blockchain). Chiunque può vedere che il record esiste e verificarne la struttura; solo chi possiede una chiave privata corrispondente può decifrare il payload. Vedi Sealed PoE per il formato della busta e Sealed until claimed per il modello di minaccia.

Indirizzare i destinatari

Un destinatario è identificato da una stringa in stile age. Ne esistono due tipi, distinti dal prefisso:

  • age1… — una chiave classica X25519 (32 byte).
  • age1pqc… — una chiave ibrida X-Wing (ML-KEM-768 + X25519, 1216 byte).

X-Wing (mlkem768x25519) è il KEM predefinito: resta sicuro contro un futuro avversario quantistico, e ogni identità dispone sempre di un indirizzo age1pqc….

Un destinatario ti consegna la propria stringa fuori banda. La decodifichi nella chiave pubblica grezza di cui ha bisogno l'helper di sigillatura con parseAgeRecipient:

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

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

Se hai tu stesso un seed a 32 byte, recipientsFromSeed ti restituisce entrambi i tuoi indirizzi: condividine uno perché altri possano sigillare per te, e includi la tua chiave nella lista dei destinatari per mantenere l'accesso in lettura a ciò che invii:

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

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

Sigillare e pubblicare

La sigillatura passa per un gateway, che costruisce e trasmette la transazione Cardano e conserva il testo cifrato di ogni elemento al posto tuo. Punta il client al gateway che usi; l'SDK è indipendente dal gateway.

publishSealed accetta le chiavi pubbliche grezze dei destinatari, quindi raccogli la publicKey da ciascun indirizzo decodificato. Tutti i destinatari devono condividere lo stesso KEM: tieni insieme le chiavi age1pqc…. items è una lista, quindi puoi sigillare un intero insieme di file per gli stessi destinatari in un unico record; qui contiene un solo payload:

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);

Non c'è una chiamata quote separata né una tua stima di recordBytes: publishSealed misura il record sigillato esatto, blocca un prezzo, carica ogni testo cifrato e lo invia, tutto in un'unica chiamata. maxUsdMicros è il tuo tetto massimo; la pubblicazione viene rifiutata se il prezzo offerto lo supera e il tetto viene ricontrollato rispetto a un prezzo aggiornato se un caricamento lento è sopravvissuto al primo blocco, così un caricamento grande non può mai superare il suo preventivo. Il tuo seed e il testo in chiaro non lasciano mai la tua macchina in chiaro.

Per impostazione predefinita l'affermazione on-chain di ogni elemento è il suo digest sha2-256. Per legare un secondo digest allo stesso record, calcola l'hash dell'elemento sotto entrambi gli algoritmi: passa hashAlgs: ['sha2-256', 'blake2b-256'] a publishSealed nell'SDK, oppure ripeti --hash-alg sha2-256 --hash-alg blake2b-256 sul comando seal della CLI. I due algoritmi del registro si comportano in modo identico in ogni modalità di sigillatura.

Sigillare verso una passphrase

Non tutti hanno una chiave. A volte vuoi sigillare verso persone che condividono una frase segreta, non una chiave: una password di squadra, una frase dettata durante una chiamata, un valore già presente tra i secret della tua CI. Dai a seal una passphrase invece di --to, e chiunque la conosca può aprire il record: nessun indirizzo age, nessuno scambio di chiavi.

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

Un record è sigillato verso dei destinatari oppure verso una passphrase, mai entrambi: passare --passphrase insieme a --to/--to-self viene rifiutato. Tieni la frase fuori dalla riga di comando: --passphrase-file, --passphrase-stdin o la variabile d'ambiente CARDANOWALL_PASSPHRASE la leggono tutte senza che finisca nella cronologia della shell. La chiave di contenuto viene derivata dalla frase con Argon2id, quindi una passphrase robusta e ad alta entropia è tutto ciò che si frappone tra il testo cifrato e chi prova a indovinarla: questo percorso è sicuro solo quanto la frase che scegli.

Per aprirlo non serve altro che la frase. Il verificatore autonomo decifra e ricontrolla l'hash del contenuto; l'inbox scrive il testo in chiaro recuperato su un file:

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

La sostituzione funziona allo stesso modo in entrambe le modalità. Una nuova sigillatura è sempre un record del tutto nuovo: la cifratura è randomizzata ogni volta (una nuova chiave di contenuto e un nuovo nonce, più un nuovo salt Argon2id sul percorso con passphrase), così i byte non si deduplicano mai. Per contrassegnare una nuova sigillatura come successore di una precedente —un file corretto, un insieme di destinatari ruotato— rimandala indietro con --supersedes:

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

Riprendere dopo un crash

La sigillatura estrae ogni volta una nuova chiave di contenuto, un nuovo nonce e nuovo materiale KEM per slot, quindi un tentativo ingenuo ricifrerebbe, pagherebbe un secondo caricamento del testo cifrato e produrrebbe byte del record diversi. Per i job di CI e i payload di più gigabyte, dividi il flusso: sealPrepare cifra offline e restituisce un artefatto portabile che puoi conservare; submitSealed esegue la metà online su di esso.

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 qui sopra non è altro che sealPrepare + submitSealed in un'unica chiamata; gli SDK Python e Rust espongono la stessa coppia.

Se la pubblicazione fallisce a metà

La CLI si riprende allo stesso modo, senza il codice a due fasi. Un'esecuzione seal --to … che muore dopo il caricamento di un testo cifrato scrive un <seal-fingerprint>.l309-seal-resume.json privo di segreti (i caricamenti completati, e nessuna chiave o testo in chiaro). Rilancia con cardanowall seal --resume <seal-fingerprint>.l309-seal-resume.json: richiede un nuovo preventivo, completa solo i caricamenti ancora dovuti e pubblica. Un'esecuzione firmata ha di nuovo bisogno del suo seed; le sigillature con passphrase non conservano alcuno stato di ripresa, perciò vanno rilanciate da capo.

Con Python

cardanowall-sdk è un gemello byte per byte: stesso KEM predefinito, stessa busta:

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())

Per un flusso ripristinabile, seal_prepare(...) (da cardanowall.client) restituisce l'artefatto preparato e client.poe.submit_sealed(prepared=...) esegue la metà online: le stesse due fasi di cui sopra.

Con Rust

Il crate cardanowall sigilla tramite lo stesso client gateway. parse_age_recipient decodifica ogni indirizzo nella chiave grezza, e il KEM usa come impostazione predefinita l'ibrido 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(())
}

Per un flusso ripristinabile, seal_prepare restituisce un PreparedSeal che puoi conservare e client.poe().submit_sealed(&SubmitSealedInput::new(&prepared)) esegue la metà online. Anche la CLI sigilla dalla riga di comando, con le stesse fasi dietro le quinte: cardanowall seal --file <path> --to <address>.

Una volta che il record è definitivamente registrato, ogni destinatario lo scopre, decifra il payload con la propria chiave privata e ricalcola l'hash del testo in chiaro per chiudere il cerchio: è la parte del destinatario in Verificare un record.

Sigilla anche per te stesso

publishSealed non ti aggiunge mai in silenzio alla lista dei destinatari. Se non includi una delle tue chiavi, pubblichi un record che non potrai mai più rileggere. Includi me.age1pqc (o me.age) tra i destinatari ogni volta che vuoi mantenere l'accesso a ciò che hai inviato.