Guides

Guides · Partie 4 sur 8

Construire une PoE scellée

Une PoE simple prouve _qu’_un contenu donné a existé. Une PoE scellée prouve la même chose tout en gardant le contenu lui-même secret : vous chiffrez les octets pour une ou plusieurs clés de destinataire, ne stockez que le texte chiffré et ancrez l’enregistrement sur la chaîne. Quiconque peut constater que l’enregistrement existe et en vérifier la structure ; seul un détenteur d’une clé privée correspondante peut déchiffrer la charge utile. Voir Sealed PoE pour le format de l’enveloppe et Sealed until claimed pour le modèle de menace.

Adresser vos destinataires

Un destinataire est identifié par une chaîne de style age. Il en existe deux types, que le préfixe permet de distinguer :

  • age1… — une clé classique X25519 (32 octets).
  • age1pqc… — une clé hybride X-Wing (ML-KEM-768 + X25519, 1216 octets).

X-Wing (mlkem768x25519) est le KEM par défaut : il reste sûr face à un futur adversaire quantique, et toute identité dispose toujours d’une adresse age1pqc….

Un destinataire vous transmet sa chaîne par un canal externe. Vous la décodez pour retrouver la clé publique brute dont l’assistant de scellement a besoin à l’aide de parseAgeRecipient :

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

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

Si vous détenez vous-même un seed de 32 octets, recipientsFromSeed vous fournit vos deux adresses propres : partagez-en une pour que d’autres puissent vous sceller des enregistrements, et incluez votre propre clé dans la liste des destinataires afin de conserver l’accès en lecture à ce que vous envoyez :

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

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

Sceller et publier

Le scellement passe par une passerelle, qui construit et diffuse la transaction Cardano et stocke le texte chiffré de chaque élément à votre place. Pointez le client vers la passerelle que vous utilisez ; le SDK est indépendant de la passerelle.

publishSealed reçoit des clés publiques de destinataire brutes ; recueillez donc le publicKey de chaque adresse décodée. Tous les destinataires doivent partager un même KEM : gardez les clés age1pqc… ensemble. items est une liste : vous pouvez donc sceller tout un ensemble de fichiers pour les mêmes destinataires dans un seul enregistrement ; ici, elle ne contient qu’une seule charge utile :

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

Il n’y a pas d’appel quote distinct ni d’estimation de recordBytes de votre part : publishSealed mesure l’enregistrement scellé exact, verrouille un prix, téléverse chaque texte chiffré et le soumet, le tout en un seul appel. maxUsdMicros est votre plafond ; la publication est refusée si le prix proposé le dépasse, et le plafond est revérifié contre un prix rafraîchi si un téléversement lent a survécu au premier verrou, de sorte qu’un gros téléversement ne peut jamais distancer son devis. Votre seed et le texte en clair ne quittent jamais votre machine en clair.

Par défaut, l’affirmation sur la chaîne de chaque élément est son condensé sha2-256. Pour lier un second condensé au même enregistrement, calculez l’empreinte de l’élément sous les deux algorithmes à la fois : passez hashAlgs: ['sha2-256', 'blake2b-256'] à publishSealed dans le SDK, ou répétez --hash-alg sha2-256 --hash-alg blake2b-256 sur le seal de la CLI. Les deux algorithmes du registre se comportent de façon identique dans tous les modes de scellement.

Sceller avec une phrase secrète

Tout le monde n’a pas de clé. Parfois, vous voulez sceller pour des personnes qui partagent une phrase secrète, non une clé — un mot de passe d’équipe, une phrase lue à voix haute lors d’un appel, une valeur déjà présente dans vos secrets de CI. Donnez à seal une phrase secrète au lieu de --to, et quiconque la connaît peut ouvrir l’enregistrement : aucune adresse age, aucun échange de clés.

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

Un enregistrement est scellé pour des destinataires ou pour une phrase secrète, jamais les deux — passer --passphrase en même temps que --to/--to-self est refusé. Gardez la phrase hors de la ligne de commande : --passphrase-file, --passphrase-stdin ou la variable d’environnement CARDANOWALL_PASSPHRASE la lisent toutes sans qu’elle n’atterrisse dans l’historique du shell. La clé de contenu est étirée depuis la phrase avec Argon2id, de sorte que seule une phrase secrète forte, à haute entropie, se dresse entre le texte chiffré et quiconque cherche à la deviner — ce chemin n’est jamais plus sûr que la phrase que vous choisissez.

Pour l’ouvrir, il ne faut rien d’autre que la phrase. Le vérificateur autonome déchiffre et recontrôle l’empreinte du contenu ; la commande inbox écrit le texte en clair récupéré dans un fichier :

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

Le remplacement fonctionne de la même façon dans les deux modes. Un nouveau scellement est toujours un enregistrement flambant neuf — le chiffrement est randomisé à chaque fois (une nouvelle clé de contenu et un nouveau nonce, ainsi qu’un nouveau sel Argon2id sur le chemin de la phrase secrète), de sorte que les octets ne se dédupliquent jamais. Pour marquer un nouveau scellement comme le successeur d’un précédent — un fichier corrigé, un ensemble de destinataires renouvelé — pointez-le en retour avec --supersedes :

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

Reprendre après une panne

Le scellement tire à chaque fois une nouvelle clé de contenu, un nouveau nonce et un nouveau matériel KEM par emplacement ; une reprise naïve rechiffrerait donc, paierait un second téléversement de texte chiffré et produirait des octets d’enregistrement différents. Pour les tâches de CI et les charges utiles de plusieurs gigaoctets, scindez le flux : sealPrepare chiffre hors ligne et renvoie un artefact portable que vous pouvez conserver ; submitSealed en exécute la moitié en ligne.

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 ci-dessus n’est que sealPrepare + submitSealed en un seul appel ; les SDK Python et Rust exposent la même paire.

Si la publication échoue en cours de route

La CLI se rétablit de la même manière, sans le code en deux phases. Une exécution seal --to … qui meurt après le téléversement d’un texte chiffré écrit un <seal-fingerprint>.l309-seal-resume.json sans secret (les téléversements achevés, et aucune clé ni texte en clair). Relancez avec cardanowall seal --resume <seal-fingerprint>.l309-seal-resume.json : elle redemande un devis, ne termine que les téléversements encore dus et publie. Une exécution signée a de nouveau besoin de son seed ; les scellements par phrase secrète ne conservent aucun état de reprise, relancez-les donc depuis le début.

Avec Python

cardanowall-sdk est un jumeau octet pour octet : même KEM par défaut, même enveloppe :

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

Pour un flux avec reprise, seal_prepare(...) (de cardanowall.client) renvoie l’artefact préparé et client.poe.submit_sealed(prepared=...) en exécute la moitié en ligne : les deux mêmes phases que ci-dessus.

Avec Rust

Le crate cardanowall scelle via le même client de passerelle. parse_age_recipient décode chaque adresse en clé brute, et le KEM adopte par défaut l’hybride 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(())
}

Pour un flux avec reprise, seal_prepare renvoie un PreparedSeal que vous pouvez conserver et client.poe().submit_sealed(&SubmitSealedInput::new(&prepared)) en exécute la moitié en ligne. La CLI scelle elle aussi depuis la ligne de commande, avec les mêmes phases en coulisses : cardanowall seal --file <path> --to <address>.

Une fois l’enregistrement consolidé, chaque destinataire le découvre, déchiffre la charge utile avec sa clé privée et recalcule l’empreinte du texte en clair pour boucler la boucle : c’est la moitié destinataire de Vérifier un enregistrement.

Scellez aussi pour vous-même

publishSealed ne vous ajoute jamais en silence à la liste des destinataires. Si vous n’incluez pas l’une de vos propres clés, vous publiez un enregistrement que vous ne pourrez jamais relire. Incluez me.age1pqc (ou me.age) parmi les destinataires chaque fois que vous voulez conserver l’accès à ce que vous avez envoyé.