TamgaNetwork

Tamga nasıl çalışır

Tamga ile giriş yap

“Google ile giriş yap”ı biliyorsun. “Tamga ile giriş yap” aynı görünür ama tersine çalışır: kimlik senin cüzdanındadır, site yalnızca onayladığın alanları alır ve ilk seferden sonra hiçbir şey almaz.

İki adım: bir kez kayıt, sonra passkey

  1. Kayıt (bir kez). Site az sayıda alan ister — örneğin ad ve soyad — vesana bu siteye özel bir takma ad alır. Bilgisayarda QR kodu okutursun; telefonda cüzdan doğrudan açılır. Cüzdan tam olarak neyin, kim tarafından istendiğini gösterir; onaylarsın.
  2. Doğrulayıcı sunumu denetler — imza, güven listesi, iptal durumu ve cihaz bağı. Site sonucu kendi sunucusunda alır ve hesabını açar.
  3. Passkey ekle. Kayıttan hemen sonra site “bu cihaza passkey ekle” önerir. Bundan sonra Face ID ya da parmak iziyle girersin: cüzdan açılmaz, hiçbir alan paylaşılmaz. Passkey yalnızca o siteye özeldir.
  4. Yeni cihaz, passkey yok mu? “Tamga ile giriş yap” yalnızca o siteye özel takma adını gönderir — hiçbir belge alanı — sonra yeniden passkey eklersin.

Web sitesi

alan ister

Tamga Wallet

sen onaylarsın

Doğrulayıcı

kanıtı denetler

Passkey

günlük giriş

Alan alan sen onaylarsın

Site yalnızca ihtiyacı olanı ister; onaylamadığın alan asla gönderilmez. Bazı bilgiler açıklanmadan kanıtlanabilir — doğum tarihi vermeden “18 yaş üstü”.

Tamga Wallet

example.com şunlara erişmek istiyor:

Ad soyad shared
18 yaş üstü proven
E-posta shared
Ev adresi hidden
İzin ver
Reddet

Şifre paylaşılmaz · her alanı sen kontrol edersin

Bir site neyi alabilir — neyi alamaz

Site yalnızca onayladığın alanları alır. Çalınacak şifre yoktur. Kayıtlı kapsamının dışına çıkamaz: her doğrulayıcı, isteyebileceği alanlarla birlikte güven listesinde kayıtlıdır ve cüzdanın fazlasını uyarır. Hesap anahtarın yalnız o siteye özel bir takma addır: başka bir site başka bir takma ad görür, siteler seni eşleştiremez. Doğrulanmış kimliğinden türediği için yeni telefonda kimliğini yeniden doğrulayınca aynı takma adlar geri gelir. Kimlik ve belge numaran hiç gönderilmez.

Geliştiriciler için

Sayfa kiti @tamga-network/verifier/web’dir (doğrulayıcı bunu betik olarak da sunar). QR kodunu ya da “cüzdanda aç” düğmesini çizer ve sonucu bildirir — kararı vermez: tarayıcı kodu kullanıcı tarafından değiştirilebilir, bu yüzden karar her zaman senin sunucunda verilir.

/**
 * Example 01 — "Sign up / Sign in with Tamga" on your website, using the hosted verifier.
 *
 * Your SERVER opens each presentation, proving who it is with a short-lived assertion signed by the key of
 * your trust-list registration (ADR-0017). The page only shows the QR code and polls a status token; the
 * values the person approved are handed to your server once.
 *
 *   npm install @tamga-network/verifier
 */
import { createHmac } from "node:crypto";
import { createRpAssertion, type RpSigner } from "@tamga-network/verifier";

export interface SignInConfig {
  /** The hosted verifier, e.g. "https://verify.tamga.network". */
  verifier: string;
  /** Your registration: pemRpSigner(KEY_PEM, CERT_PEM) — client_id is x509_hash of your access certificate (HAIP 1.0). */
  rp: RpSigner;
  /** Your own secret; the stored account key is a keyed hash of the site pseudonym. */
  siteSecret: string;
  fetch?: typeof fetch;
}

/** What your page needs to draw the QR code and poll the status. */
export interface StartedPresentation {
  presentation_id: string;
  qr_payload: string;
  expires_at: string;
  status_token: string;
}

export type SignInResult =
  | { ok: true; accountKey: string; givenName?: string; familyName?: string }
  | { ok: false; outcome: "PENDING" | "REJECTED" | "INDETERMINATE" | "ALREADY_USED" };

export function tamgaSignIn(cfg: SignInConfig) {
  const f = cfg.fetch ?? fetch;
  const auth = async () => ({ authorization: `Bearer ${await createRpAssertion(cfg.rp, cfg.verifier)}` });

  /** POST /tamga/start → return this JSON to your page (TamgaVerifier.mount({ start })). */
  async function start(policy: "site-signup" | "site-signin"): Promise<StartedPresentation> {
    const r = await f(`${cfg.verifier}/presentations`, {
      method: "POST",
      headers: { ...(await auth()), "content-type": "application/json", accept: "application/json" },
      body: JSON.stringify({ policy_id: policy }),
    });
    if (!r.ok) throw new Error(`verifier refused the request (${r.status})`);
    return (await r.json()) as StartedPresentation;
  }

  /** POST /tamga/session → your page sends the presentation id after ACCEPTED; decide here, on the server. */
  async function finish(presentationId: string): Promise<SignInResult> {
    const id = encodeURIComponent(presentationId);
    const result = (await (await f(`${cfg.verifier}/presentations/${id}`, { headers: await auth() })).json()) as {
      outcome?: "ACCEPTED" | "REJECTED" | "INDETERMINATE";
    };
    if (result.outcome !== "ACCEPTED") return { ok: false, outcome: result.outcome ?? "PENDING" };

    const r = await f(`${cfg.verifier}/presentations/${id}/claims`, { headers: await auth() });
    if (r.status === 410) return { ok: false, outcome: "ALREADY_USED" }; // values are handed out once
    const { claims } = (await r.json()) as { claims: Record<string, unknown> };

    // ADR-0031: the account key is the wallet's pseudonym for YOUR site (another site sees a different one); the verifier
    // has checked its signature, audience, nonce and the wallet instance attestation. No document value is sent.
    if (typeof claims.pseudonym !== "string") return { ok: false, outcome: "REJECTED" };
    const accountKey = createHmac("sha256", cfg.siteSecret).update(claims.pseudonym).digest("base64url");
    return {
      ok: true,
      accountKey,
      givenName: claims.given_name as string | undefined,
      familyName: claims.family_name as string | undefined,
    };
  }

  return { start, finish };
}
Passkey (WebAuthn)
import { passkey } from "@tamga-network/verifier/web";
// after sign-up, while the session is open:
const o = await post("/passkey/register/options");
await post("/passkey/register/verify", await passkey.create(o));
// daily sign-in — no wallet, no fields shared:
const { flow, options } = await post("/passkey/login/options");
await post("/passkey/login/verify", { flow, response: await passkey.get(options) });
  • Tek kullanım: bir sunum tam olarak bir oturum açar.
  • Üç sonuç: ACCEPTED, REJECTED ya da INDETERMINATE. INDETERMINATE “şu an denetlenemedi — tekrar dene” demektir, asla “belge geçersiz” değil.
  • Çerez: HttpOnly, SameSite=Lax, Secure, sunucu tarafında süre; ad, anahtar ya da passkey kimliği asla loglanmaz.
  • WebAuthn çıplak IP adresinde çalışmaz — localhost ya da HTTPS alan adı kullan.

Açık standartlar üzerine

Kapalı bir giriş sistemi değil: sunumlar SD-JWT VC belgeleriyle OpenID for Verifiable Presentations (OpenID4VP) kullanır — AB’nin EUDI Wallet’ıyla aynı profiller — günlük giriş ise standart WebAuthn passkey’dir.

Bugünkü durum

Örnek sitemizde uçtan uca çalışıyor (kayıt, tekrar kullanımın reddi, giriş, passkey). Barındırılan doğrulayıcı sonucu yalnızca sunumu açan siteye verir — site bunu güven listesindeki anahtarıyla imzalayarak kanıtlar — ve değerleri bir kez teslim eder. Cüzdan her siteye ayrı takma ad verir; doğrulayıcı onu cüzdan örneği kanıtıyla denetler.