Metadata Signature Creation
To ensure the declaration is verifiable, all metadata must be signed using a private key, which generates a cryptographic signature proving its authenticity. Liccium supports two approaches for signing: X.509 Certificate-based signing and Keypair-based signing with embedded credentials.
Overview
Metadata signatures in Liccium serve multiple purposes:
- Authentication: Proves the identity of the declarer
- Integrity: Ensures the metadata hasn't been tampered with
- Non-repudiation: Provides proof that the declaration was made by the certificate holder
Two Signing Approaches
Approach 1: X.509 Certificate
- ✓ Uses X.509 certificate chain
- ✓ Certificate embedded in JWT header (x5c)
- ✓ Requires certificate authority validation
- ✓ Best for enterprise/organizational use
Approach 2: Generated Keypair
- ✓ Uses self-generated EC keypair
- ✓ Public key embedded as JWK in JWT header
- ✓ Credentials included in publicMetadata
- ✓ Best for flexible, self-managed identities
Prerequisites
Before creating signatures, ensure you have:
Required Components
- ✓ EC (P-256) private key
- ✓ Node.js environment
- ✓ Structured public metadata
- ✓ Verifiable credentials (for keypair approach)
Setup Requirements
- ✓ JWT signing library (jsonwebtoken)
- ✓ Crypto module for key operations
- ✓ Valid declarerId (DID format)
- ✓ Complete metadata structure
Approach 1: X.509 Certificate-Based Signing
This approach uses X.509 certificates to establish identity. The certificate chain is embedded in the JWT header using the x5c field.
Step 1: Prepare Your Public Metadata
Structure your public metadata according to Liccium requirements:
Code
Metadata Requirements: Ensure your metadata includes all required fields: $schema, @context, iscc, name, timestamp, declarerId, and credentials. Missing required fields will cause signature validation to fail.
Step 2: Load Your Certificate
Load your X.509 certificate and private key:
Code
Step 3: Create the JWT Signature
Generate a JWT signature using your certificate and private key:
Code
Step 4: Complete Certificate-Based Example
Full example with validation:
Code
Approach 2: Keypair-Based Signing with Embedded JWK
This approach uses a self-generated EC keypair. The public key is embedded in the JWT header as a JSON Web Key (JWK), and verifiable credentials are included in the publicMetadata.
Step 1: Generate an EC Keypair
Code
Step 2: Convert Public Key to JWK
Code
Step 3: Create JWT Signature with JWK in Header
Code
Step 5: Complete Keypair-Based Example
Code
Signature Verification
Decode the JWT (without verification) to inspect structure:
Code
Certificate-based (x5c): header contains x5c with base64-encoded certificate.
Keypair-based (jwk): header contains jwk with EC P-256 public key.
Security Best Practices
Security Considerations:
- Never log or expose private keys
- Use secure key storage in production (HSM, key vaults)
- Implement proper error handling for signing failures
- Validate all input data before signing
- Store credentials securely and validate before inclusion
Key Management
- Secure Storage – Store private keys in HSM or key vaults
- Access Control – Limit access to private keys to authorized systems
- Key Rotation – Regularly rotate keys/certificates and update DID documents
- Monitoring – Monitor key usage and detect unauthorized signing
Common Issues and Solutions
| Issue | Cause | Solution |
|---|---|---|
| Invalid signature format | Incorrect algorithm or header | Use ES256 and proper x5c or jwk header |
| Certificate/JWK not found | Missing x5c or jwk in JWT header | Include base64 certificate or JWK in header |
| Validation fails | Metadata missing required fields | Validate structure before signing ($schema, @context, iscc, name, timestamp, declarerId, credentials) |
| Key mismatch | Private key doesn't match certificate/public key | Verify key pair correspondence |
| Credentials validation fails | Missing or invalid credentials array | Ensure credentials is a non-empty array with valid VC structure |
Integration with Liccium
Include your signature in the declaration payload:
Code
For complete declaration examples, see the Declaration API.
Signing Registry Objects
Besides publicMetadata, a declaration can carry one registry object per registry it should be published to. Each registry object is signed on its own, with the same private key and approach chosen above, and carries its own TSA timestamp:
| Registry | Object | Signature | Timestamp |
|---|---|---|---|
| TDM·AI registry (AI preferences) | declarationMetadata.tdmaiRegistry | tdmaiRegistrySignature | tdmaiRegistryTsaSignature |
| FAIA registry (AI attribution, faia.io) | declarationMetadata.faiaRegistry | faiaRegistrySignature | faiaRegistryTsaSignature |
A declaration is a record of a registry only when it carries that registry's object. A faia block inside liccium_plugins or inside tdmaiRegistry describes the content, but does not publish the declaration to the FAIA registry. A declaration may carry both objects; it is then a record of both registries.
TDM·AI registry: tdmaiRegistry
If your declaration includes a tdmaiRegistry record, the same signing process must be applied to it independently, using the same private key and approach chosen above.
declarationMetadata.tdmaiRegistry is the TDM-AI registry record: an object with the required fields iscc, declarerId and timestamp (milliseconds, within 60 seconds of server time), plus one sub-object per active opt-out plugin — tdmrep ({"tdm-reservation": 0|1, "tdm-policy": "<url>"}), faia ({"faiaFlag", "activityCode", "systemAttribution", "systemVersion"}), and further plugins (ietf, c2pa, ...) keyed by plugin name. It is transmitted as this JSON object — a JSON-serialized string is not accepted. The JWT payload carries the same object (key order does not matter — the registry compares the records canonically).
The record requires two additional top-level fields in the declaration payload:
tdmaiRegistrySignature– an ES256 JWT (public JWK embedded in the header) whose payload is{ "tdmaiRegistry": <the same record object> }tdmaiRegistryTsaSignature– an RFC 3161 TSA timestamp (tsq/tsr, base64) over thetdmaiRegistrySignatureJWT, produced identically to the maintsaSignature
Example
Code
The tdmaiRegistrySignature and tdmaiRegistryTsaSignature fields are only required when a tdmaiRegistry value is present in the declaration. If no registry payload is provided, these fields can be omitted entirely.
FAIA registry: faiaRegistry
declarationMetadata.faiaRegistry is the FAIA registry record: an object with iscc, declarerId and timestamp — the same values as in publicMetadata (the timestamp within 60 seconds of publicMetadata.timestamp) — plus the faia statement (faiaFlag, and optionally activityCode, systemAttribution, systemVersion; see the FAIA Plugin guide). It is transmitted as this JSON object — a JSON-serialized string is not accepted.
The record requires two additional top-level fields in the declaration payload:
faiaRegistrySignature– an ES256 JWT (public JWK embedded in the header) whose payload is{ "faiaRegistry": <the same record object> }, signed with the same key assignaturefaiaRegistryTsaSignature– an RFC 3161 TSA timestamp (tsq/tsr, base64) over thefaiaRegistrySignatureJWT
The FAIA registry verifies all of this before publishing. A declaration whose faiaRegistry fails any of these checks is stored by Liccium but never appears in the FAIA registry.
Example
Code
faiaRegistrySignature and faiaRegistryTsaSignature are only required when faiaRegistry is present. The legacy faiaMetadata block is not accepted by the FAIA registry.
Next Steps
- TSA Signature Creation – Add timestamp signatures for temporal verification
- Test your signatures with the Declaration API
- Implement error handling for production use
- Monitor certificate/key expiration and plan for renewal