feat(agorapura): identidad humana federada — core + grafo de confianza

agorapura-core: identidades fractales (persona/comunidad/alianza/
institución) sobre claves ed25519, Claims sujeto-predicado-valor y
Attestations firmadas y autoverificables (la prueba viaja con el
dato). agorapura-graph: TrustGraph guarda sólo atestaciones con firma
válida; corroboration() devuelve evidencia cruda y TrustPolicy —un
umbral negociado, no una verdad del sistema— la traduce a sí/no.

22 tests. Cero red, cero estado global, #![forbid(unsafe_code)].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-20 16:38:20 +00:00
parent ad9781c2ee
commit c1c136954e
10 changed files with 847 additions and 0 deletions
@@ -0,0 +1,14 @@
[package]
name = "agorapura-core"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "agorapura — núcleo de identidad federada: identidades fractales (persona/comunidad/alianza/institución), claims y atestaciones firmadas ed25519."
[dependencies]
ed25519-dalek = { workspace = true }
blake3 = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
@@ -0,0 +1,131 @@
//! Atestaciones — un claim respaldado por la firma de una identidad.
//!
//! La atestación es la unidad de confianza de agorapura: «la institución
//! *Venezuela* atestigua que el claim *nacionalidad = venezolana* sobre
//! *Yumaira* es cierto». Cualquiera puede verificar la firma sin
//! consultar a nadie — la prueba viaja con el dato.
use serde::{Deserialize, Serialize};
use crate::claim::Claim;
use crate::identity::{verify_signature, IdentityId, Keypair};
use crate::AgoraError;
/// Adaptador serde para `[u8; 64]` — serde sólo cubre arrays hasta 32,
/// así que la firma viaja como secuencia y se revalida su largo al leer.
mod sig_serde {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S: Serializer>(sig: &[u8; 64], s: S) -> Result<S::Ok, S::Error> {
sig.as_slice().serialize(s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 64], D::Error> {
let v = Vec::<u8>::deserialize(d)?;
v.try_into()
.map_err(|_| serde::de::Error::custom("la firma debe ser de 64 bytes"))
}
}
/// Un claim firmado por un atestador. Autoverificable y autónomo.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Attestation {
/// El claim respaldado.
pub claim: Claim,
/// Identidad que firma — derivada de `attester_key`.
pub attester: IdentityId,
/// Clave pública del atestador (para verificar sin un directorio).
pub attester_key: [u8; 32],
/// Firma ed25519 sobre `claim.canonical_bytes()`.
#[serde(with = "sig_serde")]
pub signature: [u8; 64],
}
impl Attestation {
/// Crea una atestación firmando `claim` con `keypair`.
pub fn create(keypair: &Keypair, claim: Claim) -> Self {
let signature = keypair.sign(&claim.canonical_bytes());
Self {
claim,
attester: keypair.identity_id(),
attester_key: keypair.public_key(),
signature,
}
}
/// Verifica la atestación. Comprueba dos cosas:
/// 1. la firma cubre el claim bajo `attester_key`;
/// 2. `attester` coincide con el id derivado de `attester_key`
/// (nadie puede atribuir su firma a otra identidad).
pub fn verify(&self) -> Result<(), AgoraError> {
if self.attester != IdentityId::from_public_key(&self.attester_key) {
return Err(AgoraError::AttesterMismatch);
}
verify_signature(&self.attester_key, &self.claim.canonical_bytes(), &self.signature)
}
/// `true` si la atestación es de un atestador hablando de sí mismo.
/// Una identidad puede declararse cosas, pero esa evidencia vale
/// distinto que la de un tercero — quien evalúa decide cuánto.
pub fn is_self_attested(&self) -> bool {
self.attester == self.claim.subject
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::IdentityKind;
#[test]
fn created_attestation_verifies() {
let venezuela = Keypair::from_seed([10; 32]);
let yumaira = Keypair::from_seed([20; 32]);
let claim = Claim::new(yumaira.identity_id(), "nacionalidad", "venezolana", 1_700_000_000);
let att = Attestation::create(&venezuela, claim);
assert!(att.verify().is_ok());
assert_eq!(att.attester, venezuela.identity_id());
}
#[test]
fn tampered_claim_fails_verification() {
let venezuela = Keypair::from_seed([10; 32]);
let yumaira = Keypair::from_seed([20; 32]);
let claim = Claim::new(yumaira.identity_id(), "nacionalidad", "venezolana", 1_700_000_000);
let mut att = Attestation::create(&venezuela, claim);
// Alterar el valor invalida la firma.
att.claim.value = "marciana".into();
assert!(matches!(att.verify(), Err(AgoraError::BadSignature)));
}
#[test]
fn spoofed_attester_is_rejected() {
let real = Keypair::from_seed([10; 32]);
let impostor = Keypair::from_seed([99; 32]);
let yumaira = Keypair::from_seed([20; 32]);
let claim = Claim::new(yumaira.identity_id(), "habilidad", "soldadura", 0);
let mut att = Attestation::create(&real, claim);
// Reatribuir la atestación a otra identidad la rompe.
att.attester = impostor.identity_id();
assert!(matches!(att.verify(), Err(AgoraError::AttesterMismatch)));
}
#[test]
fn self_attestation_is_flagged() {
let yumaira = Keypair::from_seed([20; 32]);
let claim = Claim::new(yumaira.identity_id(), "habilidad", "carpintería", 0);
let att = Attestation::create(&yumaira, claim);
assert!(att.verify().is_ok());
assert!(att.is_self_attested());
}
#[test]
fn third_party_attestation_is_not_self() {
let comunidad = Keypair::from_seed([30; 32]);
let _ = comunidad.identity(IdentityKind::Community, "Vecinos del Valle");
let yumaira = Keypair::from_seed([20; 32]);
let claim = Claim::new(yumaira.identity_id(), "miembro-de", "Vecinos del Valle", 0);
let att = Attestation::create(&comunidad, claim);
assert!(!att.is_self_attested());
}
}
@@ -0,0 +1,87 @@
//! Claims — afirmaciones sobre una identidad.
//!
//! Un [`Claim`] es una tripleta sujetopredicadovalor: *«de Yumaira
//! (sujeto), su nacionalidad (predicado) es venezolana (valor)»*. El
//! claim por sí solo no afirma nada — sólo cuando alguien lo firma
//! ([`crate::Attestation`]) adquiere peso.
use serde::{Deserialize, Serialize};
use crate::identity::IdentityId;
/// Una afirmación sobre una identidad. Inerte hasta que una atestación
/// la respalda.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Claim {
/// La identidad de la que trata el claim.
pub subject: IdentityId,
/// Qué se afirma — `"nacionalidad"`, `"miembro-de"`, `"habilidad"`.
pub predicate: String,
/// El valor afirmado — `"venezolana"`, el id de una comunidad, etc.
pub value: String,
/// Segundos Unix en que se emitió el claim.
pub issued_at: u64,
}
impl Claim {
/// Construye un claim.
pub fn new(
subject: IdentityId,
predicate: impl Into<String>,
value: impl Into<String>,
issued_at: u64,
) -> Self {
Self {
subject,
predicate: predicate.into(),
value: value.into(),
issued_at,
}
}
/// Serialización canónica determinista — el mensaje exacto que se
/// firma. Cada campo va con prefijo de largo para que no haya
/// ambigüedad de fronteras entre `predicate` y `value`.
pub fn canonical_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(64 + self.predicate.len() + self.value.len());
out.extend_from_slice(b"agorapura-claim\x01");
out.extend_from_slice(self.subject.as_bytes());
out.extend_from_slice(&self.issued_at.to_le_bytes());
out.extend_from_slice(&(self.predicate.len() as u64).to_le_bytes());
out.extend_from_slice(self.predicate.as_bytes());
out.extend_from_slice(&(self.value.len() as u64).to_le_bytes());
out.extend_from_slice(self.value.as_bytes());
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::Keypair;
fn subject() -> IdentityId {
Keypair::from_seed([9; 32]).identity_id()
}
#[test]
fn canonical_bytes_are_deterministic() {
let c = Claim::new(subject(), "nacionalidad", "venezolana", 1_700_000_000);
assert_eq!(c.canonical_bytes(), c.clone().canonical_bytes());
}
#[test]
fn distinct_claims_have_distinct_canonical_bytes() {
let a = Claim::new(subject(), "nacionalidad", "venezolana", 1_700_000_000);
let b = Claim::new(subject(), "nacionalidad", "colombiana", 1_700_000_000);
assert_ne!(a.canonical_bytes(), b.canonical_bytes());
}
#[test]
fn field_boundaries_are_unambiguous() {
// Sin prefijo de largo, "ab"+"c" colisionaría con "a"+"bc".
let a = Claim::new(subject(), "ab", "c", 0);
let b = Claim::new(subject(), "a", "bc", 0);
assert_ne!(a.canonical_bytes(), b.canonical_bytes());
}
}
@@ -0,0 +1,180 @@
//! Identidades fractales — y el par de claves que las firma.
//!
//! Una persona, una comunidad, una alianza y una institución comparten
//! exactamente la misma estructura: una clave pública, un tipo y un
//! nombre. La identidad es *fractal* — autosemejante en cada escala. Que
//! una comunidad atestigüe sobre una persona, o una institución sobre
//! una comunidad, no es un caso especial: es la misma operación.
use serde::{Deserialize, Serialize};
use std::fmt;
use crate::AgoraError;
/// Naturaleza de una identidad. No cambia su estructura — sólo informa
/// a quien la lee a qué escala social pertenece.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum IdentityKind {
/// Un ser humano individual.
Person,
/// Una agrupación de identidades con un propósito común.
Community,
/// Una federación de comunidades.
Alliance,
/// Una entidad formal y persistente (un Estado, una universidad).
Institution,
}
/// Identificador estable de una identidad: BLAKE3 de su clave pública.
/// Inmutable mientras la clave no cambie.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IdentityId([u8; 32]);
impl IdentityId {
/// Deriva el id desde una clave pública ed25519.
pub fn from_public_key(public_key: &[u8; 32]) -> Self {
Self(*blake3::hash(public_key).as_bytes())
}
/// Bytes crudos del identificador.
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl fmt::Display for IdentityId {
/// Prefijo hex abreviado — suficiente para distinguir identidades.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for b in &self.0[..6] {
write!(f, "{b:02x}")?;
}
write!(f, "")
}
}
/// La cara pública de una identidad: lo que se publica y se comparte.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Identity {
pub kind: IdentityKind,
/// Clave pública ed25519.
pub public_key: [u8; 32],
/// Nombre legible — no es único ni autoritativo, sólo presentación.
pub display_name: String,
}
impl Identity {
/// Identificador derivado de la clave pública.
pub fn id(&self) -> IdentityId {
IdentityId::from_public_key(&self.public_key)
}
}
/// El par de claves de una identidad. La clave privada vive **sólo** en
/// poder de su dueño; nunca se serializa ni viaja por la red.
pub struct Keypair {
signing: ed25519_dalek::SigningKey,
}
impl Keypair {
/// Construye un par determinista desde una semilla de 32 bytes.
///
/// La reproducibilidad es deliberada: tests y derivación jerárquica
/// de claves dependen de ella. Para una identidad real, la semilla
/// debe venir de un CSPRNG.
pub fn from_seed(seed: [u8; 32]) -> Self {
Self { signing: ed25519_dalek::SigningKey::from_bytes(&seed) }
}
/// Clave pública (32 bytes) de este par.
pub fn public_key(&self) -> [u8; 32] {
self.signing.verifying_key().to_bytes()
}
/// Identificador de la identidad de este par.
pub fn identity_id(&self) -> IdentityId {
IdentityId::from_public_key(&self.public_key())
}
/// Arma la `Identity` pública correspondiente.
pub fn identity(&self, kind: IdentityKind, display_name: impl Into<String>) -> Identity {
Identity {
kind,
public_key: self.public_key(),
display_name: display_name.into(),
}
}
/// Firma un mensaje arbitrario, devolviendo los 64 bytes de la firma.
pub fn sign(&self, message: &[u8]) -> [u8; 64] {
use ed25519_dalek::Signer;
self.signing.sign(message).to_bytes()
}
}
/// Verifica una firma ed25519 contra una clave pública y un mensaje.
pub fn verify_signature(
public_key: &[u8; 32],
message: &[u8],
signature: &[u8; 64],
) -> Result<(), AgoraError> {
use ed25519_dalek::Verifier;
let vk = ed25519_dalek::VerifyingKey::from_bytes(public_key)
.map_err(|_| AgoraError::BadPublicKey)?;
let sig = ed25519_dalek::Signature::from_bytes(signature);
vk.verify(message, &sig).map_err(|_| AgoraError::BadSignature)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn id_is_stable_for_a_key() {
let kp = Keypair::from_seed([7; 32]);
let a = kp.identity(IdentityKind::Person, "Yumaira");
let b = kp.identity(IdentityKind::Person, "otro nombre");
// El id depende de la clave, no del nombre.
assert_eq!(a.id(), b.id());
}
#[test]
fn different_seeds_yield_different_identities() {
let a = Keypair::from_seed([1; 32]).identity_id();
let b = Keypair::from_seed([2; 32]).identity_id();
assert_ne!(a, b);
}
#[test]
fn signature_roundtrips() {
let kp = Keypair::from_seed([42; 32]);
let sig = kp.sign(b"mensaje de prueba");
assert!(verify_signature(&kp.public_key(), b"mensaje de prueba", &sig).is_ok());
}
#[test]
fn tampered_message_fails_verification() {
let kp = Keypair::from_seed([42; 32]);
let sig = kp.sign(b"original");
assert!(matches!(
verify_signature(&kp.public_key(), b"manipulado", &sig),
Err(AgoraError::BadSignature)
));
}
#[test]
fn wrong_key_fails_verification() {
let signer = Keypair::from_seed([1; 32]);
let other = Keypair::from_seed([2; 32]);
let sig = signer.sign(b"msg");
assert!(verify_signature(&other.public_key(), b"msg", &sig).is_err());
}
#[test]
fn id_display_is_abbreviated_hex() {
let id = Keypair::from_seed([0; 32]).identity_id();
let s = id.to_string();
// 6 bytes → 12 dígitos hex, más el elipsis.
assert!(s.ends_with('…') && s.chars().count() == 13);
}
}
@@ -0,0 +1,36 @@
//! `agorapura-core` — identidad humana federada, sin autoridad central.
//!
//! El ágora no tiene un registro maestro. Cada identidad —persona,
//! comunidad, alianza, institución— es una clave pública; cada afirmación
//! sobre ella es un [`Claim`]; cada respaldo es una [`Attestation`]
//! firmada que viaja con su propia prueba. La verdad no la dicta un
//! servidor: emerge de quién atestigua qué, y de cuánto peso le da a
//! cada atestador quien la lee.
//!
//! - [`identity`] — identidades fractales + claves ed25519.
//! - [`claim`] — afirmaciones sujetopredicadovalor.
//! - [`attest`] — claims firmados, autoverificables.
//!
//! Cero estado global, cero red: tipos puros. La red de confianza vive
//! en `agorapura-graph`; el transporte, en capas superiores.
#![forbid(unsafe_code)]
pub mod attest;
pub mod claim;
pub mod identity;
pub use attest::Attestation;
pub use claim::Claim;
pub use identity::{verify_signature, Identity, IdentityId, IdentityKind, Keypair};
/// Falla de una operación de identidad o atestación.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum AgoraError {
#[error("clave pública ed25519 inválida")]
BadPublicKey,
#[error("firma inválida: no corresponde al mensaje y la clave")]
BadSignature,
#[error("el atestador declarado no corresponde a su clave pública")]
AttesterMismatch,
}