ガイド

ガイド · 全8部中 第4部

封緘済み PoE を作る

通常の存在証明(Proof of Existence, PoE)は、あるコンテンツが存在した「という事実」を証明します。封緘済み PoE は、同じことを証明しつつ、コンテンツそのものは秘密に保ちます。バイト列を1つ以上の受信者の鍵で暗号化し、暗号文だけを保存して、レコードをチェーン上に記録するのです。だれでもレコードの存在を確認し、その構造を検証できますが、ペイロードを復号できるのは、対応する秘密鍵を持つ者だけです。エンベロープの形式については封緘済み PoEを、脅威モデルについては受け取られるまで封緘されるを参照してください。

受信者を指定する

受信者は age スタイルの文字列で識別されます。次の2種類があり、プレフィックスで見分けられます。

  • 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 はリストなので、同じ受信者宛てに一連のファイルをまとめて1つのレコードに封緘できます。ここでは単一のペイロードだけを保持しています。

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 が封緘済みレコードの正確なサイズを測り、価格をロックし、各暗号文をアップロードして送信します。すべて1回の呼び出しで完結します。maxUsdMicros は上限です。提示価格がこれを超えると発行は拒否され、遅いアップロードが最初のロックより長引いた場合は最新の価格に対して上限を再確認するので、大きなアップロードが見積もりを追い越すことは決してありません。シードと平文が平文のまま手元のマシンを離れることはありません。

既定では、各項目のチェーン上の主張はその sha2-256 ダイジェストです。2つ目のダイジェストを同じレコードに束ねるには、その項目を両方のアルゴリズムでコハッシュします。SDK では publishSealedhashAlgs: ['sha2-256', 'blake2b-256'] を渡し、CLI では seal--hash-alg sha2-256 --hash-alg blake2b-256 を繰り返します。この2つのレジストリアルゴリズムは、どの封緘モードでもまったく同じように振る舞います。

パスフレーズに封緘する

だれもが鍵を持っているとは限りません。ときには、鍵ではなく秘密の合言葉を共有する人たちに封緘したいこともあります。チームのパスワード、通話で読み上げられたフレーズ、すでに CI のシークレットに入っている値などです。--to の代わりに seal にパスフレーズを渡せば、それを知っている人はだれでもレコードを開けます。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 で伸長されるので、暗号文と推測者との間に立つのは、強く高エントロピーなパスフレーズです。この経路の安全性は、選んだフレーズの強さで決まります。

開くのに必要なのはフレーズだけです。単独の検証者は復号してコンテンツハッシュを再確認し、インボックスは復元した平文をファイルに書き出します。

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

置き換え(supersede)の仕組みは、どちらのモードでも同じです。封緘し直すと必ず新しいレコードになります。暗号化は毎回ランダム化され(新しいコンテンツ鍵とノンス、パスフレーズ経路では新しい Argon2id ソルトも)、バイト列が重複除去されることはありません。新しい封緘を、以前のレコードの後継として印づけるには——修正したファイルや、入れ替えた受信者一式など——--supersedes でそれを指し示します。

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

クラッシュから再開する

封緘のたびに新しいコンテンツ鍵・ノンス・スロットごとの KEM 素材が引かれるため、単純にリトライすると再暗号化が行われ、2 度目の暗号文アップロードに課金され、異なるレコードバイト列が生成されてしまいます。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 を 1 回の呼び出しにまとめたものにすぎません。Python と Rust の SDK も同じペアを公開しています。

発行が途中で失敗したら

CLI も、2 フェーズのコードなしで同じように復旧します。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=...) がオンライン側の処理を実行します。上記と同じ 2 つのフェーズです。

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 が黙って自分を受信者リストに加えることはありません。自分の鍵を1つも含めなければ、二度と読み返せないレコードを発行することになります。送ったものへのアクセスを保ちたいときは、受信者の中に必ず me.age1pqc(または me.age)を含めてください。