Liccium Developer Portal
Authentication

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:

JavascriptCode
const publicMetadata = { "$schema": "https://w3id.org/liccium/schema/0.1.0.json", "@context": "https://w3id.org/liccium/context/0.1.0.json", iscc: "ISCC:KACYPXW445FNGZZ2", name: "Example Content Title", description: "Detailed description of the content", mediatype: "image/jpeg", timestamp: Date.now(), declarerId: "did:web:yourdomain.com", credentials: [ { "@context": ["https://www.w3.org/ns/credentials/v2"], type: ["VerifiableCredential", "VerifiableAttestation"], proof: { type: "JwtProof2020", jwt: "eyJhbGciOiJSUzI1NiJ9..." } } ] };

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:

JavascriptCode
const fs = require('fs'); const jwt = require('jsonwebtoken'); const certificatePem = fs.readFileSync('path/to/your/cert.pem', 'utf8'); const privateKeyPem = fs.readFileSync('path/to/your/private-key.pem', 'utf8'); const certBase64 = certificatePem .replace(/-----BEGIN CERTIFICATE-----/g, '') .replace(/-----END CERTIFICATE-----/g, '') .replace(/\n/g, '') .trim();

Step 3: Create the JWT Signature

Generate a JWT signature using your certificate and private key:

JavascriptCode
const signature = jwt.sign(publicMetadata, privateKeyPem, { algorithm: 'ES256', header: { typ: 'JWT', alg: 'ES256', x5c: [certBase64], } });

Step 4: Complete Certificate-Based Example

Full example with validation:

JavascriptCode
const fs = require('fs'); const jwt = require('jsonwebtoken'); class CertificateSignatureCreator { constructor(certPath, privateKeyPath) { const certPem = fs.readFileSync(certPath, 'utf8'); this.privateKey = fs.readFileSync(privateKeyPath, 'utf8'); this.certBase64 = certPem .replace(/-----BEGIN CERTIFICATE-----/g, '') .replace(/-----END CERTIFICATE-----/g, '') .replace(/\n/g, '') .trim(); } signMetadata(publicMetadata) { this.validateMetadata(publicMetadata); return jwt.sign(publicMetadata, this.privateKey, { algorithm: 'ES256', header: { typ: 'JWT', alg: 'ES256', x5c: [this.certBase64] } }); } validateMetadata(metadata) { const requiredFields = ['$schema', '@context', 'iscc', 'name', 'timestamp', 'declarerId', 'credentials']; for (const field of requiredFields) { if (!metadata[field]) throw new Error(`Missing required field: ${field}`); } if (!Array.isArray(metadata.credentials) || metadata.credentials.length === 0) { throw new Error('credentials must be a non-empty array'); } } } const signer = new CertificateSignatureCreator('path/to/cert.pem', 'path/to/private-key.pem'); const signature = signer.signMetadata(publicMetadata);

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

TerminalCode
openssl ecparam -name prime256v1 -genkey -noout -out private_key.pem openssl ec -in private_key.pem -pubout -out public_key.pem

Step 2: Convert Public Key to JWK

JavascriptCode
const fs = require('fs'); const crypto = require('crypto'); const publicKeyPem = fs.readFileSync('public_key.pem', 'utf8'); const keyObject = crypto.createPublicKey(publicKeyPem); const jwk = keyObject.export({ format: 'jwk' });

Step 3: Create JWT Signature with JWK in Header

JavascriptCode
const signature = jwt.sign(publicMetadata, privateKeyPem, { algorithm: 'ES256', header: { typ: 'JWT', alg: 'ES256', jwk: jwk, } });

Step 5: Complete Keypair-Based Example

JavascriptCode
const fs = require('fs'); const jwt = require('jsonwebtoken'); const crypto = require('crypto'); class KeypairSignatureCreator { constructor(privateKeyPath, publicKeyPath) { this.privateKey = fs.readFileSync(privateKeyPath, 'utf8'); const publicKeyPem = fs.readFileSync(publicKeyPath, 'utf8'); const keyObject = crypto.createPublicKey(publicKeyPem); this.jwk = keyObject.export({ format: 'jwk' }); } signMetadata(publicMetadata) { this.validateMetadata(publicMetadata); return jwt.sign(publicMetadata, this.privateKey, { algorithm: 'ES256', header: { typ: 'JWT', alg: 'ES256', jwk: this.jwk } }); } validateMetadata(metadata) { const requiredFields = ['$schema', '@context', 'iscc', 'name', 'timestamp', 'declarerId', 'credentials']; for (const field of requiredFields) { if (!metadata[field]) throw new Error(`Missing required field: ${field}`); } if (!Array.isArray(metadata.credentials) || metadata.credentials.length === 0) { throw new Error('credentials must be a non-empty array'); } } } const signer = new KeypairSignatureCreator('private_key.pem', 'public_key.pem'); const signature = signer.signMetadata(publicMetadata);

Signature Verification

Decode the JWT (without verification) to inspect structure:

JavascriptCode
const decoded = jwt.decode(signature, { complete: true }); console.log('JWT Header:', JSON.stringify(decoded.header, null, 2));

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

  1. Secure Storage – Store private keys in HSM or key vaults
  2. Access Control – Limit access to private keys to authorized systems
  3. Key Rotation – Regularly rotate keys/certificates and update DID documents
  4. Monitoring – Monitor key usage and detect unauthorized signing

Common Issues and Solutions

IssueCauseSolution
Invalid signature formatIncorrect algorithm or headerUse ES256 and proper x5c or jwk header
Certificate/JWK not foundMissing x5c or jwk in JWT headerInclude base64 certificate or JWK in header
Validation failsMetadata missing required fieldsValidate structure before signing ($schema, @context, iscc, name, timestamp, declarerId, credentials)
Key mismatchPrivate key doesn't match certificate/public keyVerify key pair correspondence
Credentials validation failsMissing or invalid credentials arrayEnsure credentials is a non-empty array with valid VC structure

Integration with Liccium

Include your signature in the declaration payload:

JavascriptCode
const declarationPayload = { signature: signature, tsaSignature: { tsr: "base64-encoded-timestamp-response", tsq: "base64-encoded-timestamp-request" }, declarationMetadata: { publicMetadata: publicMetadata } };

For complete declaration examples, see the Declaration API.


Signing Registry Objects

The following is an example for the TDMAI registry for AI preferences.

If your declaration includes a tdmaiRegistry payload (aggregated opt-out data from plugins such as TDMrep and FAIA), the same signing process must be applied to it independently, using the same private key and approach chosen above.

The registry object requires two additional fields in the declaration payload:

  • tdmaiRegistrySignature – a JWT signature of the tdmaiRegistry string, produced identically to the main signature
  • tdmaiRegistryTsaSignature – a TSA timestamp signature over the tdmaiRegistry payload, produced identically to the main tsaSignature

Example

JavascriptCode
// Sign the tdmaiRegistry payload the same way as publicMetadata const tdmaiRegistry = "<aggregated opt-out data from tdmrep plugins>"; const tdmaiRegistrySignature = jwt.sign({ tdmaiRegistry }, privateKeyPem, { algorithm: 'ES256', header: { typ: 'JWT', alg: 'ES256', jwk: jwk, // or x5c: [certBase64] for the certificate-based approach } }); // Then obtain a TSA timestamp over the registry payload (see TSA Signature guide) // and include both in the declaration: const declarationPayload = { signature: signature, tsaSignature: { tsr: "base64-encoded-timestamp-response", tsq: "base64-encoded-timestamp-request" }, tdmaiRegistrySignature: tdmaiRegistrySignature, tdmaiRegistryTsaSignature: { tsr: "base64-encoded-tdmairegistry-timestamp-response", tsq: "base64-encoded-tdmairegistry-timestamp-request" }, declarationMetadata: { publicMetadata: { ...publicMetadata, tdmaiRegistry: tdmaiRegistry } } };

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.


Next Steps

  1. TSA Signature Creation – Add timestamp signatures for temporal verification
  2. Test your signatures with the Declaration API
  3. Implement error handling for production use
  4. Monitor certificate/key expiration and plan for renewal
Last modified on