panurus

Identity Service

The Identity Service (token/services/identity) is an internal infrastructure service of Panurus. It provides a unified interface for managing identities, signatures, and verification, operating independently of the core Fabric Smart Client (FSC) identity service.

This independence ensures that token-related cryptographic material (such as Idemix pseudonyms or X.509 certificates used for token ownership) is managed according to the specific privacy and security requirements of the Token Drivers, regardless of the underlying DLT platform.

Overview

The Identity Service abstracts the complexity of different cryptographic schemes, allowing Panurus to support multiple identity types (e.g., X.509, Idemix) and different storage backends seamlessly.

It is a fundamental component used by token drivers and application services (like the TTX service) to handle:

Architecture

The Identity Service implements the Driver API interfaces defined in token/driver/wallet.go. This ensures that the Token Management System (TMS) can interact with any identity implementation through a standard set of methods.

Component Mapping

The following table shows how the internal components map to the Driver API interfaces:

Component Implements Driver Interface Description
identity.Provider driver.IdentityProvider Core identity management & verification.
wallet.Service driver.WalletService Registry for all wallets (Owner, Issuer, etc.).
role.LongTermOwnerWallet driver.OwnerWallet Long-Term Identity-based Owner wallet functionality.
role.AnonymousOwnerWallet driver.OwnerWallet Anonymous Identity-based Owner wallet functionality.
role.IssuerWallet driver.IssuerWallet Issuer wallet functionality.
role.AuditorWallet driver.AuditorWallet Auditor wallet functionality.
role.CertifierWallet driver.CertifierWallet Certifier wallet functionality.

Component Interaction

classDiagram
    direction TB
%% Driver Interfaces
    class IdentityProvider {
        <<interface>>
        +GetSigner()
        +GetAuditInfo()
        +IsMe()
    }
    class WalletService {
        <<interface>>
        +OwnerWallet()
        +IssuerWallet()
        +RegisterRecipientIdentity()
    }

%% Concrete Implementations
    class identity_Provider["identity.Provider"] {
        -Storage
        -Deserializers
        -SignerCache
    }
    class wallet_Service["wallet.Service"] {
        -RoleRegistry
        -IdentityProvider
        -OwnerWallet
        -IssuerWallet
        -AuditorWallet
        -CertifierWallet
    }
    class role_Role["role.Role"] {
        -LocalMembership
        +GetIdentityInfo()
    }
    class membership_KeyManagerProvider["membership.KeyManagerProvider"] {
        <<interface>>
        +Get() KeyManager
    }

    identity_Provider ..|> IdentityProvider : Implements
    wallet_Service ..|> WalletService : Implements
    wallet_Service --> identity_Provider : Uses
    wallet_Service --> role_Role : Uses (via RoleRegistry)
    role_Role --> membership_KeyManagerProvider : Uses (via LocalMembership)

    note for membership_KeyManagerProvider "Handles low-level crypto<br/>and identity verification"
    note for wallet_Service "High-level management<br/>of wallets and roles"

LocalMembership

The LocalMembership component (token/services/identity/membership) plays a pivotal role in managing local identities for a specific role (e.g., Owner, Issuer).

Example: Wiring Services

The following example demonstrates how these services are instantiated and wired together, as seen in the ZKATDLog driver:

func (d *Base) NewWalletService(...) (*wallet.Service, error) {
    // 1. Create Identity Provider
    identityProvider := identity.NewProvider(...)

    // 2. Initialize Membership Role Factory
    roleFactory := membership.NewRoleFactory(...)

    // 3. Configure Key Managers (e.g. Idemix and X.509 for Owner role)
    // we have one key manager to handle fabtoken tokens and one for each idemix issuer public key in the public parameters
    kmps := make([]membership.KeyManagerProvider, 0)
    // ... add Idemix Key Manager Providers ...
    kmps = append(kmps, x509.NewKeyManagerProvider(...))

    // 4. Create and Register Roles
    roles := role.NewRoles()
    
    // Owner Role (with anonymous identities)
    ownerRole, err := roleFactory.NewRole(identity.OwnerRole, true, nil, kmps...)
    roles.Register(identity.OwnerRole, ownerRole)
    
    // Issuer Role (no anonymous identities)
    issuerRole, err := roleFactory.NewRole(identity.IssuerRole, false, pp.Issuers(), x509.NewKeyManagerProvider(...))
    roles.Register(identity.IssuerRole, issuerRole)
    
    // ... Register Auditor and Certifier roles ...

    // 5. Create Wallet Service with the registered roles
    return wallet.NewService(
        logger,
        identityProvider,
        deserializer,
        // Convert the roles registry into the format expected by the wallet service
        wallet.Convert(roles.Registries(...)),
    ), nil
}

Identity Types

The Identity Service leverages a wrapper called TypedIdentity to support various identity schemes uniformly. This allows Panurus to be extensible and capable of handling different cryptographic requirements.

TypedIdentity

TypedIdentity (defined in token/services/identity/typed.go) acts as a generic container. It wraps the raw identity bytes with a type label, enabling the system to verify deserializers and process signatures correctly without hardcoding implementation details.

Default Key Managers

The identity service includes two primary implementations for concrete identities:

1. X.509

Standard PKIX identities.

Expected Folder Structure

The X.509 Key Manager expects a specific folder structure when loading configurations from a local directory. It supports loading public signing certificates and, optionally, private keys for signing capabilities.

Directory Structure

The cryptographic materials are stored in standard PEM format. By default, the directory layout is as follows:

<dir>/
├── signcerts/
│   └── <cert>.pem          # Public signing certificate (X.509 PEM format)
└── keystore/
    └── priv_sk             # (Optional) Private key file (PEM format)
Detailed Structure Components
Custom Key Store Directory

While keystore is the default directory name for the private key, a custom keystore directory name can be passed as an argument when initializing the key manager (e.g. to load priv_sk from <dir>/<custom-keystore-name>/priv_sk).

2. Idemix (Identity Mixer)

Advanced identity encryption based on Zero-Knowledge Proofs (ZKP).

Expected Folder Structure

The Idemix Key Manager expects a specific folder structure when loading configurations from a local directory. It supports two different formats for cryptographic configurations:

1. Standard Idemix Format (Protobuf)

In this format, cryptographic materials are stored in binary protobuf format (generated by idemixgen). The directory structure is as follows:

<dir>/
├── msp/
│   └── IssuerPublicKey      # Issuer Public Key (binary protobuf)
└── user/
    ├── SignerConfig         # Signer configuration (binary protobuf)
    └── SignerConfigFull     # (Optional) Full signer config with secret keys

[!NOTE] SignerConfigFull is checked first and used if it exists when the service is configured to force the load of secret keys (i.e. ignoreVerifyOnlyWallet is set to true).

2. Fabric-CA Idemix Format (JSON)

In this format (typically generated by Fabric-CA), the signer configuration is stored as a JSON file:

<dir>/
├── msp/
│   └── IssuerPublicKey      # Issuer Public Key (binary protobuf)
└── user/
    └── SignerConfig         # Signer configuration (JSON format)
Directory Path Fallback

To accommodate different deployment structures, the Key Manager performs directory resolution using a fallback strategy:

  1. It first attempts to load the files directly from the configured directory (<dir>).
  2. If this fails, it appends an extra msp path element to the directory (i.e., <dir>/msp/) and tries again (e.g. searching for <dir>/msp/msp/IssuerPublicKey and <dir>/msp/user/SignerConfig).

3. IdemixNym (Idemix with Pseudonym-based Identity)

An extension of Idemix that uses a commitment to the Enrollment ID (EID) as the identity instead of the full Idemix signature.

Key Differences from Standard Idemix:

Aspect Idemix IdemixNym
Identity (Token Owner) Full Idemix signature with attributes Nym EID (commitment to enrollment ID)
Identity Payload Encoding Protobuf Raw bytes
Audit Info Encoding JSON JSON (extended)
Signature Encoding Raw bytes ASN.1 (Creator + Signature)
Identity Size Large (~several KB) Small (~32-64 bytes)
Storage Overhead High Low

Other Identity Types

The architecture supports specialized identity types for complex use cases:

Multisig

Located in token/services/identity/multisig.

PolicyIdentity (Boolean-Expression-Governed Ownership)

Located in token/services/identity/boolpolicy.

HTLC (Hashed Time Lock Contract)

Located in token/services/identity/interop/htlc.

Extending the Identity Service

The Identity Service is designed to be extensible through the driver interfaces defined in Panurus. Custom identity implementations can be provided by implementing the required identity and wallet interfaces.

Typical extension scenarios include:

Step-by-Step Guide: Introducing a New Identity Type

The steps below describe how to add a new composite identity type end-to-end, based on the pattern used for PolicyIdentity (token/services/identity/boolpolicy).

Step 1 — Reserve a type tag

Add a new constant to token/driver/wallet.go alongside the existing tags:

const (
    // ...existing tags...
    MyNewIdentityType       IdentityType = 7
    MyNewIdentityTypeString              = "mynew"
)

The integer must be unique across all registered identity types.

Step 2 — Define the wire format

Create a package (e.g. token/services/identity/mynew/) and define the identity struct. Use ASN.1 DER for structured binary data (as PolicyIdentity does) or JSON for human-readable payloads (as HTLC does):

type MyNewIdentity struct {
    SomeField string `asn1:"utf8"`
    Parts     [][]byte
}

func (m *MyNewIdentity) Serialize() ([]byte, error) { return asn1.Marshal(*m) }
func (m *MyNewIdentity) Deserialize(raw []byte) error {
    _, err := asn1.Unmarshal(raw, m)
    return err
}

Expose Wrap / Unwrap helpers (see boolpolicy.WrapPolicyIdentity / boolpolicy.Unwrap) that embed the serialized struct inside a TypedIdentity envelope with the new type tag.

Step 3 — Implement signature verification

Add a Verifier that accepts the new signature format and a Deserializer that reconstructs a Verifier from raw identity bytes. Register the deserializer via des.AddTypedVerifierDeserializer(mynew.MyNewIdentityType, ...) in each driver’s NewTokenService (see token/core/fabtoken/v1/driver/driver.go and the zkatdlog equivalent).

Step 4 — Define the signature format

Define a struct for the signature produced over the token request (analogous to PolicySignature in boolpolicy/sig.go). Include ASN.1 or JSON encoding helpers and a JoinSignatures function if multiple parties contribute partial signatures.

Step 5 — Implement the Authorization checker

Create an EscrowAuth struct (see token/services/ttx/boolpolicy/auth.go) that implements the Authorization interface:

type EscrowAuth struct{ WalletService driver.WalletService }
func (a *EscrowAuth) AmIAnAuditor() bool                                  { return false }
func (a *EscrowAuth) IsMine(ctx context.Context, tok *token.Token) (string, []string, bool) { ... }
func (a *EscrowAuth) Issued(_ context.Context, _ driver.Identity, _ *token.Token) bool { return false }
func (a *EscrowAuth) OwnerType(raw []byte) (driver.IdentityType, []byte, error)        { ... }

Register it in both driver files inside NewAuthorizationMultiplexer:

// token/core/fabtoken/v1/driver/driver.go  (and the zkatdlog equivalent)
authorization := common.NewAuthorizationMultiplexer(
    common.NewTMSAuthorization(...),
    htlc.NewScriptAuth(ws),
    multisig.NewEscrowAuth(ws),
    boolpolicy.NewEscrowAuth(ws),
    mynew.NewEscrowAuth(ws),   // ← add here
)

Step 6 — Add a wallet wrapper

Create an OwnerWallet wrapper (see token/services/ttx/boolpolicy/wallet.go) that filters the unspent token list to tokens whose owner is the new identity type, and exposes domain-specific helpers (e.g. VerifyApprover).

Step 7 — Wire the recipient-negotiation protocol

If the new identity requires interactive negotiation between parties to assemble the composite identity before a transfer, add a RequestMyNewIdentity function following the pattern of ttx.RequestPolicyIdentity (token/services/ttx/recipients.go). The function sends a typed request, each counterparty responds with its component data, and the initiator assembles the final composite identity.

Step 8 — Add integration views

Create initiator and responder views in the integration layer (e.g. integration/token/fungible/views/mynew.go) following the pattern in boolpolicy.go:

Register all view factories and responders in the integration SDK (integration/token/fungible/sdk/party/sdk.go).

Step 9 — Add tests

Summary checklist

# What Where
1 Reserve type tag token/driver/wallet.go
2 Wire format + Wrap/Unwrap token/services/identity/mynew/
3 Verifier + Deserializer same package; register in both drivers
4 Signature format + JoinSignatures same package
5 EscrowAuth + register in drivers token/services/ttx/mynew/auth.go
6 OwnerWallet wrapper token/services/ttx/mynew/wallet.go
7 Recipient-negotiation protocol token/services/ttx/recipients.go
8 Integration views + SDK registration integration/token/fungible/views/mynew.go
9 Unit + integration tests alongside each new file