aethel-sdk 0.7.1

Ergonomic Rust SDK over aethel-core's post-quantum identity primitives: generate, sign, verify, project, disclose, recover
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
//! Selective disclosure: prove you hold a credential, reveal only what you choose.
//!
//! # Attributes are named, never a bitmask
//!
//! A credential is issued over named attributes, and a presentation discloses
//! them by name. No bitmask appears anywhere in this API. That is deliberate:
//! the underlying world uses `flags`, and P3-01 called out raw bitmasks on the
//! wire specifically, but the reason it matters here is simpler. `disclose(&[
//! "tier"])` is a thing you can read and get right; `disclose(0b0000_1000)` is a
//! thing you get wrong once and never notice, because disclosing the wrong
//! attribute produces a perfectly valid proof.
//!
//! A name that is not in the credential's schema is an error, not a silent
//! no-op, for the same reason.
//!
//! # What this can and cannot prove
//!
//! It proves: *"I hold a credential that opens under issuer X's parameters, one
//! of whose attributes is `tier = 3`"*, while revealing nothing about the other
//! attributes. The verifier learns the disclosed values, learns that the
//! presentation opens under the issuer parameters they supplied, and learns that
//! the holder is the identity whose projection they are checking against.
//!
//! **"Opens under X's parameters" is weaker than "X issued this."** The
//! verification relation checks that the presentation opens to a short preimage
//! under the issuer's parameters. It does not check that an issuer authorised
//! the attribute values, and a holder must hold those parameters to present at
//! all, so a holder can construct a credential over their own identity with
//! attributes of their choosing and it will verify. Treat disclosed attributes
//! as self-asserted unless you have out-of-band reason not to. Closing this
//! needs issuer-authenticated issuance, which is not shipped: see
//! `docs/ISSUER-AUTHENTICATION.md` in `aethel-core`.
//!
//! **It cannot prove a predicate over a hidden value.** "Age is at least 21,
//! without revealing the age" is the case people usually want, and it is not
//! implemented: relation 3 of the protocol is scoped out upstream. If you need
//! that today, you disclose the value.
//!
//! # No crypto here
//!
//! Every operation is a call into the embedded component. The commitment
//! randomness and attribute values live inside it, exactly as the identity's
//! secret does.

use alloc_shim::BTreeMap;

use crate::component::exports::aethel::core::identity::{
    DisclosureAttributes, SaapPresentation as WitPresentation,
};
use crate::identity::{Error, Identity};
use wasmtime::component::ResourceAny;

mod alloc_shim {
    pub use std::collections::BTreeMap;
}

/// Attribute slots a credential can carry: 8.
///
/// Issuing over more than this is [`Error::TooManyAttributes`]. A credential
/// always occupies all eight internally; the ones you did not issue over are
/// padded, and neither [`Credential::attribute_names`] nor disclosure by name
/// can see them.
pub const MAX_ATTRIBUTES: usize = 8;

/// Minimum issuer seed length: 32 bytes.
///
/// The component refuses anything shorter. As with [`crate::MIN_SEAL_KEY_BYTES`],
/// the length is enforced and the quality of the bytes is not: a low-entropy
/// seed satisfies this and still gives away the issuer's whole authority to
/// anyone who guesses it.
pub const MIN_ISSUER_SEED_BYTES: usize = 32;

/// Bytes of randomness the component requires for each of its inputs.
const RANDOMNESS_BYTES: usize = 32;

fn fresh_randomness() -> Result<[u8; RANDOMNESS_BYTES], Error> {
    let mut bytes = [0u8; RANDOMNESS_BYTES];
    getrandom::getrandom(&mut bytes).map_err(Error::Entropy)?;
    Ok(bytes)
}

/// A credential issued over an identity.
///
/// Holds a handle into the identity's component instance, not the credential
/// itself: the commitment randomness and attribute values stay inside the
/// component. Issue once, present many times.
pub struct Credential {
    pub(crate) handle: ResourceAny,
    /// All `MAX_ATTRIBUTES` slot names, padded. Internal: the padding exists so
    /// a disclose-by-name lookup cannot match an unused slot by accident, and
    /// it is not something a caller should ever see.
    pub(crate) schema: Vec<String>,
    /// How many of `schema` the caller actually issued over.
    pub(crate) issued: usize,
}

impl core::fmt::Debug for Credential {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("Credential")
            .field("attributes", &self.attribute_names())
            .field(
                "values",
                &format_args!("<held in the component, never here>"),
            )
            .finish()
    }
}

impl Credential {
    /// The attribute names this credential was issued over, in slot order.
    /// Exactly the names that were issued over, in slot order.
    ///
    /// A credential always occupies `MAX_ATTRIBUTES` slots internally, and the
    /// unused ones carry placeholder names so that disclosing by name cannot
    /// match one by accident. Those placeholders are an implementation detail
    /// and are not returned here: issuing over three attributes gives three
    /// names back.
    pub fn attribute_names(&self) -> &[String] {
        &self.schema[..self.issued]
    }

    fn mask_for(&self, disclose: &[&str]) -> Result<DisclosureAttributes, Error> {
        let mut mask = DisclosureAttributes::empty();
        for name in disclose {
            // Only the issued prefix is searchable. The padding names exist so
            // that an unissued slot has *a* name and cannot be matched
            // positionally by accident; they were never meant to be a disclosable
            // surface of their own, and searching the full schema made them one.
            let slot = self.schema[..self.issued]
                .iter()
                .position(|n| n == name)
                .ok_or_else(|| Error::UnknownAttribute((*name).to_string()))?;
            mask |= slot_flag(slot);
        }
        Ok(mask)
    }
}

/// Map a slot index to its named flag.
///
/// Spelled out rather than computed as `1 << slot`, because the whole point of
/// the world declaring named flags instead of an integer is that the mapping is
/// explicit somewhere a reader can check it.
fn slot_flag(slot: usize) -> DisclosureAttributes {
    match slot {
        0 => DisclosureAttributes::ATTRIBUTE0,
        1 => DisclosureAttributes::ATTRIBUTE1,
        2 => DisclosureAttributes::ATTRIBUTE2,
        3 => DisclosureAttributes::ATTRIBUTE3,
        4 => DisclosureAttributes::ATTRIBUTE4,
        5 => DisclosureAttributes::ATTRIBUTE5,
        6 => DisclosureAttributes::ATTRIBUTE6,
        _ => DisclosureAttributes::ATTRIBUTE7,
    }
}

/// A presentation: what the holder sends and the verifier checks.
///
/// Carries the disclosed values, the blinded commitment and the responses.
/// Nothing in it is key material, and nothing in it identifies the credential
/// across presentations.
/// What the holder produces and the verifier checks.
///
/// **This type has no serialised form yet.** It is described as what the holder
/// sends, and in a deployment it would have to travel between processes, but
/// there is no encoding on it at all, the way [`crate::RecoveryShare`] and
/// [`crate::RecoveryShareSet`] have `to_bytes`/`from_bytes` and
/// [`crate::Projection`] has `to_bytes`. Today a presentation can only be verified in the
/// process that produced it. Combined with the issuer-seed limitation in the
/// crate documentation, cross-party disclosure is not yet deployable; both are
/// tracked, and neither is a limitation of the underlying construction.
pub struct Presentation {
    pub(crate) inner: WitPresentation,
    pub(crate) projection: crate::identity::Projection,
    pub(crate) schema: Vec<String>,
}

impl core::fmt::Debug for Presentation {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("Presentation")
            .field("disclosed", &self.disclosed())
            .finish_non_exhaustive()
    }
}

impl Presentation {
    /// The attributes this presentation actually discloses, by name.
    ///
    /// Undisclosed attributes are absent from the map rather than present with
    /// a zero, because a zero is a value an attribute could legitimately have
    /// and "not disclosed" is not "disclosed as zero".
    pub fn disclosed(&self) -> BTreeMap<&str, u64> {
        let mut out = BTreeMap::new();
        for (slot, name) in self.schema.iter().enumerate() {
            if slot < MAX_ATTRIBUTES && self.inner.disclosed.contains(slot_flag(slot)) {
                out.insert(name.as_str(), self.inner.disclosed_values[slot]);
            }
        }
        out
    }

    /// The holder's projection at this context. Public, and what verification
    /// anchors on.
    /// Returns [`crate::Projection`], the same type [`crate::Identity::project_at`]
    /// produces, so its `tau()`, `salt()`, `public_b()` and `to_bytes()` are
    /// available here too. It used to return the raw component type, which no
    /// consuming crate could name: that type comes from `aethel-core`, which is
    /// a dev-dependency here, so the accessor was unusable from outside.
    pub fn projection(&self) -> &crate::identity::Projection {
        &self.projection
    }
}

impl Identity {
    /// Issue a credential over this identity.
    ///
    /// In production the issuer runs this, not the holder: `issuer_seed` is the
    /// issuer's secret, and whoever holds it can issue credentials that verify
    /// under the issuer's public parameters. It is exposed here because
    /// `aethel-core` has no issuer key management yet, and a caller should know
    /// that this is the whole of the issuer's authority.
    /// Attribute values are `u64`. There is no encoding for strings or
    /// structured values: the component's relation is arithmetic over field
    /// elements, so a name or a country code has to be mapped to a number by
    /// the caller, and both issuer and verifier have to agree on that mapping
    /// out of band. `issuer_seed` must be at least
    /// [`MIN_ISSUER_SEED_BYTES`].
    pub fn issue_credential(
        &mut self,
        issuer_seed: &[u8],
        attributes: &[(&str, u64)],
    ) -> Result<Credential, Error> {
        if attributes.len() > MAX_ATTRIBUTES {
            return Err(Error::TooManyAttributes(attributes.len()));
        }

        let issued = attributes.len();
        let mut names: Vec<String> = attributes.iter().map(|(n, _)| (*n).to_string()).collect();
        let mut values = [0u64; MAX_ATTRIBUTES];
        for (i, (_, v)) in attributes.iter().enumerate() {
            values[i] = *v;
        }
        // Unused slots still need a name, or a later disclose-by-name lookup
        // could match one of them by accident.
        while names.len() < MAX_ATTRIBUTES {
            names.push(format!("__unused_{}", names.len()));
        }

        let randomness = fresh_randomness()?;
        let handle = self
            .bindings
            .aethel_core_identity()
            .credential()
            .call_issue(
                &mut self.store,
                self.handle,
                issuer_seed,
                &values,
                &randomness,
            )??;

        Ok(Credential {
            handle,
            schema: names,
            issued,
        })
    }

    /// Present a credential at `context`, disclosing the named attributes.
    ///
    /// Blinding randomness is fresh on every call, which is what makes two
    /// presentations of the same credential unlinkable.
    pub fn present(
        &mut self,
        credential: &Credential,
        context: &[u8],
        disclose: &[&str],
    ) -> Result<Presentation, Error> {
        let mask = credential.mask_for(disclose)?;

        let projection_randomness = fresh_randomness()?;
        let blinding_randomness = fresh_randomness()?;
        let presentation_randomness = fresh_randomness()?;

        let inner = self
            .bindings
            .aethel_core_identity()
            .credential()
            .call_present(
                &mut self.store,
                credential.handle,
                self.handle,
                context,
                &projection_randomness,
                mask,
                &blinding_randomness,
                &presentation_randomness,
            )??;

        // The verifier needs the projection this was proved against. It is
        // public, and deriving it here with the same randomness is what makes
        // the presentation self-contained.
        let projection = self
            .bindings
            .aethel_core_identity()
            .master_identity()
            .call_project_at_context(
                &mut self.store,
                self.handle,
                context,
                &projection_randomness,
            )??;

        Ok(Presentation {
            inner,
            projection: crate::identity::Projection::from_component(projection)?,
            schema: credential.schema.clone(),
        })
    }
}

/// Verify a presentation.
///
/// `expected_context` is supplied by the verifier and must match the one the
/// presentation was made for. A presentation is not allowed to certify its own
/// context, so passing `presentation`'s own context back in would defeat the
/// check rather than satisfy it.
///
/// An issuer's public parameters: everything a verifier needs, and nothing that
/// lets it issue.
///
/// Derive these once from the issuer seed with [`IssuerPublicParameters::derive`],
/// publish the bytes, and give them to whoever verifies. Verification takes
/// these; issuance takes the seed. They are different types here for the same
/// reason they are different types in the component: so a verifier cannot be
/// wired to the seed by accident.
///
/// # What holding these grants, and what it does not
///
/// Deriving them from a seed is one-way, so holding them does not let their
/// holder issue credentials against someone else's identity, and a compromised
/// verifier does not compromise the issuer.
///
/// What they do **not** give you is unforgeable attributes. The verification
/// relation checks that a presentation opens to a short preimage under the
/// issuer's parameters; it does not check that an issuer authorised the
/// attribute values. Anyone holding these parameters can construct that opening
/// over their own identity with attributes of their choosing, and it will
/// verify. Holders must hold these to present at all, so in effect a holder can
/// self-assert. Deployments where holders are not trusted to state their own
/// attributes need issuer-authenticated issuance, which is not shipped: see
/// `docs/ISSUER-AUTHENTICATION.md` in `aethel-core` for the gap and the
/// construction that closes it.
///
/// Held as bytes rather than as a live component resource, because that is what
/// these are for: publishing, storing, and handing to another process.
#[derive(Clone, PartialEq, Eq)]
pub struct IssuerPublicParameters {
    bytes: Vec<u8>,
}

impl IssuerPublicParameters {
    /// Derive the public parameters for the issuer identified by `issuer_seed`.
    ///
    /// The seed is the issuer's whole authority and does not leave this call.
    pub fn derive(issuer_seed: &[u8]) -> Result<Self, Error> {
        let (mut store, bindings) = crate::component::load()?;
        let resource = bindings
            .aethel_core_identity()
            .issuer_public_parameters()
            .call_derive(&mut store, issuer_seed)??;
        let bytes = bindings
            .aethel_core_identity()
            .issuer_public_parameters()
            .call_serialize(&mut store, resource)?;
        Ok(Self { bytes })
    }

    /// Reconstruct published parameters. Round-trips with [`Self::as_bytes`].
    ///
    /// The bytes are checked by the component, so malformed input is an error
    /// here rather than a confusing verification failure later.
    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
        let (mut store, bindings) = crate::component::load()?;
        bindings
            .aethel_core_identity()
            .issuer_public_parameters()
            .call_deserialize(&mut store, bytes)??;
        Ok(Self {
            bytes: bytes.to_vec(),
        })
    }

    /// The published form. Safe to publish: that is the point of them.
    pub fn as_bytes(&self) -> &[u8] {
        &self.bytes
    }
}

impl core::fmt::Debug for IssuerPublicParameters {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("IssuerPublicParameters")
            .field("bytes", &self.bytes.len())
            .finish()
    }
}

/// Verify a presentation against an issuer's public parameters.
///
/// Returns `Ok(false)` for a well-formed presentation that does not verify, and
/// an error only when the input could not be processed at all.
///
/// # What `Ok(true)` does and does not mean
///
/// It means: the disclosed values are the ones committed to, the presentation
/// opens under the issuer parameters you supplied, and it is bound to the
/// context you passed and to the holder's projection.
///
/// It does **not** mean an issuer vouched for those values. The relation checks
/// that the presentation opens to a short preimage under the parameters; it does
/// not check that anyone authorised the attributes, and a holder must hold the
/// parameters to present at all. So a holder can build a credential over their
/// own identity with attributes of their choosing and it will verify here.
/// **Treat disclosed attributes as self-asserted** unless something outside this
/// library tells you otherwise.
///
/// This takes [`IssuerPublicParameters`], not the issuer seed, so a verifier
/// holds no secret and a compromised verifier cannot issue against anyone else's
/// identity. That is what the split buys. It does not yet buy a remote verifier:
/// [`Presentation`] has no serialised form, so a presentation can only be
/// verified in the process that produced it.
pub fn verify_presentation(
    issuer: &IssuerPublicParameters,
    presentation: &Presentation,
    expected_context: &[u8],
) -> Result<bool, Error> {
    let (mut store, bindings) = crate::component::load()?;
    let resource = bindings
        .aethel_core_identity()
        .issuer_public_parameters()
        .call_deserialize(&mut store, &issuer.bytes)??;
    let verified = bindings
        .aethel_core_identity()
        .call_saap_verify_presentation(
            &mut store,
            resource,
            &presentation.inner,
            presentation.projection.as_component(),
            expected_context,
        )??;
    Ok(verified)
}