가이드

가이드 · 전체 8부 중 4부

봉인된 PoE 만들기

일반적인 PoE는 어떤 콘텐츠가 존재했다는 사실 을 증명합니다. 봉인된 PoE 는 동일한 사실을 증명하면서도 콘텐츠 자체는 비밀로 유지합니다. 바이트를 한 명 이상의 수신자 키로 암호화하고, 암호문만 저장한 뒤, 레코드를 온체인에 기록하는 것입니다. 누구나 레코드의 존재를 확인하고 그 구조를 검증할 수 있지만, 페이로드를 복호화할 수 있는 것은 일치하는 개인 키를 가진 사람뿐입니다. 봉투 형식은 봉인된 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 다이제스트입니다. 두 번째 다이제스트를 같은 레코드에 함께 묶으려면, 그 항목을 두 알고리즘으로 코해시하십시오. SDK에서는 publishSealedhashAlgs: ['sha2-256', 'blake2b-256']를 넘기고, CLI에서는 seal--hash-alg sha2-256 --hash-alg blake2b-256을 반복하면 됩니다. 이 두 레지스트리 알고리즘은 어떤 봉인 모드에서도 완전히 동일하게 동작합니다.

패스프레이즈로 봉인하기

누구나 키를 가지고 있는 것은 아닙니다. 때로는 키가 아니라 비밀 문구를 공유하는 사람들에게 봉인하고 싶을 때가 있습니다. 팀 비밀번호, 통화 중에 불러 준 문구, 이미 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 자료가 생성되므로, 단순히 재시도하면 다시 암호화하고 두 번째 암호문 업로드 비용을 지불하며 다른 레코드 바이트가 생성됩니다. 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);

위의 publishSealedsealPrepare + submitSealed를 한 번의 호출로 합친 것에 불과합니다. 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=...)가 온라인 절반을 실행합니다. 위와 동일한 두 단계입니다.

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)를 포함하십시오.