Class SubHandle

Represents a context-isolated child Handle in the Me2eM hierarchy.

A SubHandle is always derived from a parent Handle and represents a leaf node in the derivation tree (MAX_DEPTH = 2). It inherits all cryptographic capabilities of Handle (signing, key derivation, etc.) but adds:

  • A derivation path ([handleName, subName]) carried in session tokens.
  • Constraint enforcement on audience, scopes, and TTL at session creation.
  • An optional expiration timestamp for temporary access grants.

SubHandle is used for polymorphism in Session: both Handle and SubHandle can create and verify sessions, but SubHandle sessions carry the additional hPath field for hierarchical auditing.

The class is defined in the same module as Handle on purpose (D4, PR-10): SubHandle extends Handle plus a static import of the subclass from handle.ts would form an ESM module cycle that crashes at import time depending on the graph root; co-location keeps the graph acyclic. subhandle.ts re-exports this class unchanged, so the public import paths are unaffected.

Hierarchy (view full)

Implements

Methods

  • Returns a URL-safe Base64-encoded identifier for this Handle, derived from its public key.

    Returns string

    A unique, URL-safe string identifier.

  • Cryptographically signs arbitrary data using the Handle's private key (implements Signer — a Handle is acceptable as the signer argument of Attestation.issue).

    Parameters

    • data: Uint8Array

      The data to be signed, as a Uint8Array.

    Returns Promise<Uint8Array>

    A Promise resolving to the Ed25519 signature as a Uint8Array.

    If the Handle has been disposed.

  • Verifies an Ed25519 signature against the provided data and public key.

    A signature whose length differs from the 64-byte Ed25519 size is rejected with false before any crypto runs (PR-7 pre-check) — the method therefore never throws for short/long signatures.

    Parameters

    • signature: Uint8Array

      The signature to verify (Uint8Array).

    • data: Uint8Array

      The original data that was signed (Uint8Array).

    • publicKey: Uint8Array

      The public key to verify against (Uint8Array).

    Returns Promise<boolean>

    A Promise resolving to true if the signature is valid, false otherwise.

  • Returns undefined | HandleMetadata

    A deep copy of the metadata object associated with this Handle — mutating the returned object does not affect the Handle. Custom metadata is deep-copied (JSON-safe structures up to depth 8); non-plain values (Date, Map, functions) remain shared by reference — same caveats as getSubMetadata().

  • Issues an attestation for a derived SubHandle, binding its derived key to the name and grant. Autonomous: requires only this Handle's own key, no Identity and no network.

    The child key is derived internally, so the attestation's subjectId always equals the key produced by identity.deriveSubHandle(this.name, subName).

    Note: this method does not check this Handle's own grant — it may not have one (Mode 1). Nesting is enforced by verifiers in Session.verifyAttested (a child grant exceeding the parent's is rejected with SCOPE_EXCEEDED/TTL_EXCEEDED at level ROOT).

    Parameters

    • subName: string
    • grant: AttestationGrant
    • Optionalopts: {
          ttlSeconds?: number;
          expiresAt?: number;
          jti?: string;
          now?: number;
      }
      • OptionalttlSeconds?: number
      • OptionalexpiresAt?: number
      • Optionaljti?: string
      • Optionalnow?: number

    Returns Promise<Attestation>

    const B = await station.attestSubHandle('connector-ccs', {
    audiences: ['ev-app.com'],
    scopes: ['charge:start', 'charge:stop'],
    maxSessionTtl: 7200,
    }, { ttlSeconds: 1800 });
    // Ship [A.token, B.token] together with the session token.
  • Deterministically derives a secret (e.g., a password or API key) for a specific service context.

    The private key never leaves this class, ensuring maximum security.

    The context is canonicalized NFKC → lowercase → trim before use (' GitHub ', 'github' and 'GitHub' yield the same secret). NFKC applies to the string context only — options.revision is an integer and is canonical by construction (no normalization).

    Revision model (since 0.8.0, D3): options.revision (positive integer, default 1) selects an independent secret for the same context — revision 1 is the initial secret, revision 2, 3, … are for rotation (e.g. after a breach). Revision 1 is byte-identical to the pre-revision v1.0 derivation.

    Parameters

    • context: string

      A unique identifier for the service (e.g., 'google', 'github').

    • options: DerivePasswordOptions = {}

      Derivation options.

    Returns string

    A URL-safe base64 string suitable for use as a strong password.

    AttestationError MALFORMED/FORMAT if revision is not a positive integer, byteLength is not an integer in [1, 8160], or the context is empty after canonicalization.

    const githubPassword = handle.derivePassword('github', { byteLength: 20 });
    // after rotating the GitHub credential:
    const rotated = handle.derivePassword('github', { revision: 2 });
  • Derives a symmetric 256-bit channel key for secure communication between the Identity (controller) and this Handle (device/context).

    Both parties can independently compute this key because:

    • The Handle possesses its own private key directly.
    • The Identity can derive the same private key via identity.deriveHandle(name).

    This enables zero-knowledge encrypted channels without key exchange protocols.

    The context is canonicalized NFKC → lowercase → trim before use. NFKC applies to the string context only — options.revision is an integer and is canonical by construction (no normalization).

    Revision model (since 0.8.0, D3): options.revision (positive integer, default 1) selects an independent 32-byte key for the same context — the sanctioned rotation path for a compromised channel without changing the context string. Revision 1 is byte-identical to the pre-revision v1.0 derivation.

    Parameters

    • context: string

      Channel identifier for domain separation (e.g., 'drone-001'). Both parties MUST use the same context and the same revision.

    • options: DeriveChannelKeyOptions = {}

      Derivation options.

    Returns Uint8Array

    A 32-byte Uint8Array suitable for AES-256-GCM encryption.

    AttestationError MALFORMED/FORMAT if revision is not a positive integer or the context is empty after canonicalization.

    // AES-GCM helpers are application-side; the protocol provides
    // only the key:
    const channelKey = droneHandle.deriveChannelKey('telemetry-v1');
    // after rotating the channel (both sides migrate in lockstep):
    const rotatedKey = droneHandle.deriveChannelKey('telemetry-v1', { revision: 2 });
  • Derives a shared secret using ECDH (X25519) for P2P key exchange.

    Converts the Ed25519 keypair to X25519 for Diffie-Hellman key agreement, then applies HKDF-SHA256 to produce a clean 32-byte channel key.

    Parameters

    • otherPublicKey: Uint8Array

      The Ed25519 public key of the other party (32 bytes).

    Returns Promise<Uint8Array>

    A Promise resolving to a 32-byte shared secret suitable for AES-256-GCM.

    const aliceShared = await aliceHandle.deriveSharedSecret(bobHandle.getPublicKey());
    const bobShared = await bobHandle.deriveSharedSecret(aliceHandle.getPublicKey());
    // aliceShared === bobShared
  • Returns the derivation path as a slash-separated string.

    Returns string

    The path string, e.g. "station-001/connector-1".

  • Returns the depth of this SubHandle in the derivation hierarchy.

    With MAX_DEPTH = 2, this is always 2 (Identity = 0, Handle = 1, SubHandle = 2).

    Returns number

    The depth, always 2.

  • Returns whether this SubHandle is a leaf node.

    With MAX_DEPTH = 2, a SubHandle is always a leaf and cannot derive children.

    Returns boolean

    Always true.

  • SubHandle is a leaf node (depth = 2) and cannot derive children. Overrides the inherited Handle.deriveSubHandle to throw AttestationError MAX_DEPTH_EXCEEDED/FORMAT.

    Returns Promise<never>

  • Returns the SubHandle-specific metadata (constraints).

    The returned object is a deep copy (PR-7): JSON-safe nested structures (plain objects, arrays) are fully duplicated, so mutating the result — at any depth — cannot alter the constraints this SubHandle enforces. Non-plain values (Date, Map, class instances) are shared by reference; functions abort the deep copy and fall back to the shallow constraint-array-duplicating copy.

    Returns SubHandleMetadata

    A deep copy of the SubHandleMetadata object.

  • Validates session options against this SubHandle's constraints.

    Called internally by Session.create before signing. Throws if any constraint is violated:

    • audience not in allowedAudiences
    • any scope not in allowedScopes
    • ttl exceeds maxSessionTtl
    • current time is past expiresAt

    Parameters

    • options: {
          audience: string;
          scopes: string[];
          ttl: number;
      }

      The session options to validate.

      • audience: string
      • scopes: string[]
      • ttl: number

    Returns void

    If any constraint is violated.

  • Redacted serialization contract: JSON.stringify(sub) exposes only the object kind, the public identifier, the name and the derivation path — never key material or constraint metadata.

    Returns {
        kind: string;
        id: string;
        name: string;
        path: string[];
    }

    • kind: string
    • id: string
    • name: string
    • path: string[]