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.
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:
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.
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. |
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"
The LocalMembership component (token/services/identity/membership) plays a pivotal role in managing local identities for a specific role (e.g., Owner, Issuer).
LocalMembership automatically wraps it using WrapWithType.
This ensures that the generated identity carries the correct type information required by the system (as defined in token/services/identity/typed.go).LocalMembership serves as the foundational implementation for role.Role.
When you interact with a Role to resolve an identity or sign a transaction, you are effectively delegating to the underlying LocalMembership.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
}
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 (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.
SEQUENCE.Type (string): The identifier of the identity scheme (e.g., "x509", "idemix").Identity (bytes): The raw payload of the identity, specific to the key manager.The identity service includes two primary implementations for concrete identities:
Standard PKIX identities.
AuditInfo structure containing the Enrollment ID and Revocation Handle.
EID (string): The enrollment identifier.RH (bytes): The revocation handle.TypedIdentity payload: Raw X.509 certificate bytes.token/services/identity/x509.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.
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)
signcerts/ (Required): This folder must contain at least one PEM-encoded X.509 certificate. The Key Manager loads the first valid PEM certificate found in this directory as the public identity/signer.keystore/ (Optional): This folder holds the corresponding private key.
priv_sk.PRIVATE KEY, RSA PRIVATE KEY, or EC PRIVATE KEY.KeyManager operates in signing mode (capable of generating signatures).KeyManager operates in verifying-only mode (only capable of verifying signatures).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).
Advanced identity encryption based on Zero-Knowledge Proofs (ZKP).
SerializedIdemixIdentity message.
NymPublicKey (bytes): The pseudonym public key ($N = g^{sk} \cdot h^r$).Proof (bytes): A zero-knowledge proof of credential possession and nym derivation.Schema (string): The version of the credential schema.AuditInfo structure.
EidNymAuditData: Cryptographic data required to de-anonymize the Enrollment ID.RhNymAuditData: Cryptographic data required to de-anonymize the Revocation Handle.Attributes (array of bytes): The cleartext values of the attributes (e.g., EID at index 2, RH at index 3).Schema (string): The credential schema version.TypedIdentity payload: Protobuf.token/services/identity/idemix.The Idemix Key Manager expects a specific folder structure when loading configurations from a local directory. It supports two different formats for cryptographic configurations:
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]
SignerConfigFullis checked first and used if it exists when the service is configured to force the load of secret keys (i.e.ignoreVerifyOnlyWalletis set totrue).
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)
To accommodate different deployment structures, the Key Manager performs directory resolution using a fallback strategy:
<dir>).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).An extension of Idemix that uses a commitment to the Enrollment ID (EID) as the identity instead of the full Idemix signature.
AuditInfo.
AuditInfo.IdemixSignature (bytes): The full Idemix signature that would have been the identity in the standard Idemix manager.TypedIdentity payload: Raw bytes of the nym.SEQUENCE containing:
Creator (bytes): The full Idemix signature (enabling verification against the IPK).Signature (bytes): The actual pseudonym signature bytes.token/services/identity/idemixnym.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 |
The architecture supports specialized identity types for complex use cases:
Located in token/services/identity/multisig.
MultiIdentity sequence.
Identities (array of TypedIdentity bytes): The constituent identities.AuditInfo structure.
IdentityAuditInfos (array of IdentityAuditInfo): A list of audit information blobs for each constituent identity.TypedIdentity payload: ASN.1.Located in token/services/identity/boolpolicy.
$N slot references and the operators AND, OR, and parentheses:
$0 OR $1 — either component identity 0 or 1 can satisfy ownership alone.$0 AND $1 — both component identity 0 and 1 must sign.($0 OR $1) AND $2 — one of the first two parties plus the third must sign.PolicyIdentity sequence:
policy (UTF8String): the boolean expression, e.g. "$0 OR $1".identities (SEQUENCE OF OCTET STRING): ordered list of raw component identity bytes; $N indexes into this list.AuditInfo structure.
IdentityAuditInfos (array of IdentityAuditInfo): per-component audit info blobs in the same order as identities.TypedIdentity payload: ASN.1 DER.PolicySignature (SEQUENCE OF OCTET STRING) where each slot corresponds to one component identity. A slot may be nil/empty when that component does not need to sign (valid for OR branches).token/services/identity/boolpolicy.Located in token/services/identity/interop/htlc.
Script structure defining the swap conditions.
Sender (bytes): The wrapped identity of the sender.Recipient (bytes): The wrapped identity of the recipient.Deadline (uint64): The timeout period.HashInfo: Information about the hash lock.ScriptInfo structure.
Sender (bytes): The audit info for the sender’s identity.Recipient (bytes): The audit info for the recipient’s identity.TypedIdentity payload: JSON.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:
KeyManagerKeyManagerKeyManagerProvider to plug new identity mechanisms into LocalMembershipThe 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).
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.
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.
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).
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.
Authorization checkerCreate 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
)
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).
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.
Create initiator and responder views in the integration layer (e.g. integration/token/fungible/views/mynew.go) following the pattern in boolpolicy.go:
PolicyOwnedBalanceView).Register all view factories and responders in the integration SDK (integration/token/fungible/sdk/party/sdk.go).
sig_test.go pattern) and for EscrowAuth.IsMine (auth_test.go pattern).integration/token/fungible/tests.go + the relevant dlog_test.go Describe block, following TestPolicyOR / TestPolicyAND.| # | 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 |