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.


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