To ensure the declaration is verifiable, all metadata must be signed using a private key, which generates a cryptographic signature proving its authenticity. CommonsDB supports two approaches for signing: X.509 Certificate-based signing and Keypair-based signing with embedded credentials .
Overview
Metadata signatures in CommonsDB 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
CommonsDB supports two methods for creating signatures:
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 CommonsDB requirements:
const publicMetadata = {
// Required schema/context references
"$schema" : "https://w3id.org/commonsdb/schema/0.2.0.json" ,
"@context" : "https://w3id.org/commonsdb/context/0.2.0.json" ,
// Core ISCC fields
iscc: "ISCC:KACYPXW445FNGZZ2" ,
name: "Example Content Title" ,
description: "Detailed description of the content" ,
mediatype: "image/jpeg" ,
timestamp: Date. now (),
declarerId: "did:web:yourdomain.com" ,
// Required: Credentials array
credentials: [
{
"@context" : [ "https://www.w3.org/ns/credentials/v2" ],
type: [ "VerifiableCredential" , "VerifiableAttestation" ],
proof: {
type: "JwtProof2020" ,
jwt: "eyJhbGciOiJSUzI1NiJ9..." // Your credential JWT
}
}
],
// Optional supplier metadata
supplierMetadata: {
location: "https://example.com/content" ,
name: "Example Content Title" ,
description: "Detailed description" ,
rightsStatement: "https://creativecommons.org/publicdomain/mark/1.0/"
}
};
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:
const fs = require ( 'fs' );
const jwt = require ( 'jsonwebtoken' );
// Load the certificate and private key
const certificatePem = fs. readFileSync ( 'path/to/your/cert.pem' , 'utf8' );
const privateKeyPem = fs. readFileSync ( 'path/to/your/private-key.pem' , 'utf8' );
// Convert certificate to base64 for JWT header (x5c)
// Remove PEM headers/footers and newlines
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:
const signature = jwt. sign (publicMetadata, privateKeyPem, {
algorithm: 'ES256' , // Use ES256 for P-256 elliptic curve
header: {
typ: 'JWT' ,
alg: 'ES256' ,
x5c: [certBase64], // Include the base64-encoded certificate
}
});
Step 4: Complete Certificate-Based Example
Here's a complete example:
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' );
// Extract base64 certificate (remove PEM formatting)
this .certBase64 = certPem
. replace ( / -----BEGIN CERTIFICATE----- / g , '' )
. replace ( / -----END CERTIFICATE----- / g , '' )
. replace ( / \n / g , '' )
. trim ();
}
signMetadata ( publicMetadata ) {
// Validate required fields
this . validateMetadata (publicMetadata);
// Create JWT signature with x5c header
const signature = jwt. sign (publicMetadata, this .privateKey, {
algorithm: 'ES256' ,
header: {
typ: 'JWT' ,
alg: 'ES256' ,
x5c: [ this .certBase64],
}
});
return signature;
}
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' );
}
}
}
// Usage example
const signer = new CertificateSignatureCreator (
'path/to/cert.pem' ,
'path/to/private-key.pem'
);
const signature = signer. signMetadata (publicMetadata);
console. log ( 'Generated signature:' , signature);
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
First, generate an EC (P-256) keypair:
# Generate private key
openssl ecparam -name prime256v1 -genkey -noout -out private_key.pem
# Extract public key
openssl ec -in private_key.pem -pubout -out public_key.pem
Step 2: Prepare Your Public Metadata with Credentials
Include your verifiable credentials in the publicMetadata:
const publicMetadata = {
// Required schema/context references
"$schema" : "https://w3id.org/commonsdb/schema/0.2.0.json" ,
"@context" : "https://w3id.org/commonsdb/context/0.2.0.json" ,
// Core ISCC fields
iscc: "ISCC:KACYPXW445FNGZZ2" ,
name: "Example Content Title" ,
description: "Detailed description of the content" ,
mediatype: "image/jpeg" ,
timestamp: Date. now (),
declarerId: "did:key:zXwpS3znFpTwEPepQh9pDBEXJe15DjPeCQyhJ1gv5ukdtCoHsMkSahUM1Br5Zufb7EqkzbYaGayuNqa9Yn1cRV2ECvsS" ,
// Required: Credentials array with verifiable credentials
credentials: [
{
"@context" : [ "https://www.w3.org/ns/credentials/v2" ],
id: "urn:uuid:ec41919f-fb54-4f53-8234-d7c0f7b76828" ,
type: [ "VerifiableCredential" , "VerifiableAttestation" , "VerifiableSupplier" ],
issuer: "did:web:openfuture.eu" ,
validFrom: "2025-07-09T10:56:50.328Z" ,
validUntil: "2028-07-09T10:56:50.328Z" ,
credentialSubject: {
id: "did:key:zXwpS3znFpTwEPepQh9pDBEXJe15DjPeCQyhJ1gv5ukdtCoHsMkSahUM1Br5Zufb7EqkzbYaGayuNqa9Yn1cRV2ECvsS" ,
sameAs: "Your Organization Name" ,
dataSupplierFor: "registry.commonsdb.org"
},
proof: {
type: "JwtProof2020" ,
jwt: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlHa1RDQ0JQbWdBd0lCQWdJREZTeWpNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1Gd3hDekFKQmdOVkJBWVRBbE5KTVJRd0VnWURWUVFLRXd0SVlXeGpiMjBnWkM1a0xqRVhNQlVHQTFVRVlSTU9Wa0ZVVTBrdE5ETXpOVE14TWpZeEhqQWNCZ05WQkFNVEZVaGhiR052YlNCRFFTQlFUeUJsTFhObFlXd2dNakFlRncweU5UQTFNRFV4TURFd05ERmFGdzB5T0RBMU1EVXhNREV3TkRGYU1JR0lNUXN3Q1FZRFZRUUdFd0pPVERFZU1Cd0dBMVVFQ2hNVlUxUkpRMGhVU1U1SElFOVFSVTRnUmxWVVZWSkZNUnN3R1FZRFZRUmhFeEpXUVZST1RDMDROakUzTnpnMU16UkNNREV4SERBYUJnTlZCQXNURTJVdGMyVmhiQ0JqWlhKMGFXWnBZMkYwWlhNeEhqQWNCZ05WQkFNVEZXUnBaRHAzWldJNmIzQmxibVoxZEhWeVpTNWxkVENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFNb2h1ZnkvV3lqVzRHL0gzRDlvVFo3d3R5MTdTQ1NOQWYyOXhBM1oxdWtldkNHb3hweHJTdXl3MDc3UnRoc2NHNUZueklVMkVoOXBIMENXSDkxQmpwVjFHdU9kUGZIdzZ1aWMrdmlreVoxNGFXVDhMMnZNcGFUZmVmOXgybWtrUUNkU0FTOFFDZjYrRHpuL3FEVzBLRGtiYkltVmxWRUhYVzdLdk9ZVllqVXdHT3Q4WGc0Y2JDWG12RnNkTWpycmVwd01ubWtjQ1REdDFuQ3ltbDBtZGJJWTFoR29aOHVBUENrcUNQeEkxZmVHZFRTcWxJSzlnYjZOT29yMFMzTXFFeFdhSFJMRENCNGY0VSsxdnJZamRNakZjQ09ERk1PcjF4RWNuSVc1ZUxubGR6WnBuR2JwTHQvN0UyYzJvbzllenNRSmF1c1VyWFBWRWxaOW95MGlXaTBDQXdFQUFhT0NBcTB3Z2dLcE1DY0dBMVVkSlFRZ01CNEdDQ3NHQVFVRkJ3TUJCZ2dyQmdFRkJRY0RBZ1lJS3dZQkJRVUhBd1F3RXdZRFZSMGpCQXd3Q29BSVJ6WEl2R0hpWFo0d2VBWUlLd1lCQlFVSEFRTUViREJxTUJVR0NDc0dBUVVGQndzQ01Ba0dCd1FBaSt4SkFRSXdDQVlHQkFDT1JnRUJNRElHQmdRQWprWUJCVEFvTUNZV0lHaDBkSEJ6T2k4dmQzZDNMbWhoYkdOdmJTNXphUzl5WlhCdmMybDBiM0o1RXdKRlRqQVRCZ1lFQUk1R0FRWXdDUVlIQkFDT1JnRUdBakNCZ0FZSUt3WUJCUVVIQVFFRWREQnlNRTBHQ0NzR0FRVUZCekFDaGtGb2RIUndPaTh2ZDNkM0xtaGhiR052YlM1emFTOTFjR3h2WVdSekwzSmxjRzl6YVhSdmNua3ZTR0ZzWTI5dFgwTkJYMUJQWDJVdGMyVmhiRjh5TG1ObGNqQWhCZ2dyQmdFRkJRY3dBWVlWYUhSMGNEb3ZMMjlqYzNBdWFHRnNZMjl0TG5OcE1HWUdBMVVkSUFSZk1GMHdVQVlLS3dZQkJBR3VNd1VFQXpCQ01FQUdDQ3NHQVFVRkJ3SUJGalJvZEhSd09pOHZkM2QzTG1oaGJHTnZiUzV6YVM5MWNHeHZZV1J6TDJacGJHVnpMME5RVTE5b1lXeGpiMjFmWTJFdWNHUm1NQWtHQndRQWkreEFBUUV3Z2JNR0ExVWRId1NCcXpDQnFEQ0JwYUNCb3FDQm40WmxiR1JoY0RvdkwyeGtZWEF1YUdGc1kyOXRMbk5wTDJOdVBVaGhiR052YlNVeU1FTkJKVEl3VUU4bE1qQmxMWE5sWVd3bE1qQXlMRzg5U0dGc1kyOXRMR005VTBrL1kyVnlkR2xtYVdOaGRHVnlaWFp2WTJGMGFXOXViR2x6ZER0aWFXNWhjbm1HTm1oMGRIQTZMeTlrYjIxcGJtRXVhR0ZzWTI5dExuTnBMMk55YkhNdmFHRnNZMjl0WDJOaFgzQnZYMlV0YzJWaGJGOHlMbU55YkRBUkJnTlZIUTRFQ2dRSVFLRE52WFZ1eG9Bd0RnWURWUjBQQVFIL0JBUURBZ1hnTUNBR0ExVWRFUVFaTUJlQ0ZXUnBaRHAzWldJNmIzQmxibVoxZEhWeVpTNWxkVEFKQmdOVkhSTUVBakFBTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCZ1FBSjd1dWh0RlBOVlk3QXFYcWNTbVBuV0FrMURFZVJsemY3RjBqMURra05yZ1RiOTdML2FCaVJFZi9IRDQxZUNjaDVmZmFOUmJ3VmhLaFhDTG82MS9EY3RJbGxZcXRERDJ5R1o5bWhseGRPNFBtaWJhQW9lVUp4RmEyZm5QWVpPSEd0cjhRNFFTeU0vYWZzaHBnZDdEOVJ0Q2l2SjBZWTQ3ZWpyZG9yT3lUZXJZdUttQUtZVUZYMWlJMFA2VjhTQmRkOC9wdG0zWmpjbnRtRXdDZTRYUmowSGUzTjRWRUp3WkdhR3JNdmV5SmR0Z0RNZUpzYmVld3NpRkY4RGJ6S0xUZGpRc2dGQURFQ0kzL3FxTFo2UG1nUEZ3YUF3cDMvSDZHc2hONVovMlNnWUhXbGorWVYvTkphUWxiNmM2MjhmbndRLytzTEJvSnJ2aThTbFhrekZxSG53V1cvMTg3WC9OSEYyclFHQVZPanVIMGR2WklpaWZjZnZLVEFySzJBaWtldENuZWlpeFY1NllSZ3BsU3FXNHZ0VDdCTzIzN2R3a3NlV3g0Wkx6emo0M3d6RThBSEJ1ZWpaY3FnUWtzTlRlMmtISCtLMXo1cDd0SVBuMjdHYUcwL2Ryay83UGFtQkZ2c0pUTXBUZkhycTRaRUZiSEF5Z2FrNzdxV0srTmNnVzg9Il19.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiXSwiaWQiOiJ1cm46dXVpZDplYzQxOTE5Zi1mYjU0LTRmNTMtODIzNC1kN2MwZjdiNzY4MjgiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVmVyaWZpYWJsZUF0dGVzdGF0aW9uIiwiVmVyaWZpYWJsZVN1cHBsaWVyIl0sImlzc3VlciI6ImRpZDp3ZWI6b3BlbmZ1dHVyZS5ldSIsInZhbGlkRnJvbSI6IjIwMjUtMDctMDlUMTA6NTY6NTAuMzI4WiIsInZhbGlkVW50aWwiOiIyMDI4LTA3LTA5VDEwOjU2OjUwLjMyOFoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6elh3cFMzem5GcFR3RVBlcFFoOXBEQkVYSmUxNURqUGVDUXloSjFndjV1a2R0Q29Ic01rU2FoVU0xQnI1WnVmYjdFcWt6YllhR2F5dU5xYTlZbjFjUlYyRUN2c1MiLCJzYW1lQXMiOiJFdXJvcGVhbmEgRm91bmRhdGlvbiIsImRhdGFTdXBwbGllckZvciI6InJlZ2lzdHJ5LmNvbW1vbnNkYi5vcmcifSwiY3JlZGVudGlhbFNjaGVtYSI6W3siaWQiOiJodHRwczovL2dpdGh1Yi5jb20vQ3JlYXRvckNyZWRlbnRpYWxzL3NwZWNpZmljYXRpb25zL2Jsb2IvbWFpbi9qc29uLXNjaGVtYS92ZXJpZmljYXRpb24tY3JlZGVudGlhbHMvc3VwcGxpZXIvc2NoZW1hLmpzb24iLCJ0eXBlIjoiSnNvblNjaGVtYSJ9XSwiaWF0IjoxNzUyMDU0Nzk3fQ.JIKkpqH9mcoEoM4a20E462nUYCsm5ypkTcwgr2PNpOMomXRwZ5M_k6DlCRw4C85ejjxZPBm5P2uLiMsHJKpjayW9GsuJiL-_QlE_e70UiFEhW_gqNoXPEYdr1JL6DY0zcK3huhVpqpEBuPL4q0_ehAqmF-2sJI3_nVAXvcvDC0FJU3iAvZiKRpZfo9wAvlyi7URWZqzZqp0DRNPJVofCM_cmylQXwzU7VUofp-NLmpGtSpgio-GJi-hJbO3VwaxDQHIAcJp02i9jVVF8ii-8HNUqtZAoUQmbzA83eebl5rGEckH9dD7sBYYsKimse9t2A527zcnRbtabwPWfScP7lg"
},
credentialSchema: [
{
id: "https://github.com/CreatorCredentials/specifications/blob/main/json-schema/verification-credentials/supplier/schema.json" ,
type: "JsonSchema"
}
]
}
],
// Optional supplier metadata
supplierMetadata: {
location: "https://example.com/content" ,
name: "Example Content Title" ,
description: "Detailed description" ,
rightsStatement: "https://creativecommons.org/publicdomain/mark/1.0/"
}
};
Step 3: Convert Public Key to JWK Format
Convert your public key to JSON Web Key (JWK) format:
const fs = require ( 'fs' );
const crypto = require ( 'crypto' );
// Load public key
const publicKeyPem = fs. readFileSync ( 'public_key.pem' , 'utf8' );
// Convert to JWK
const keyObject = crypto. createPublicKey (publicKeyPem);
const jwk = keyObject. export ({ format: 'jwk' });
console. log ( 'JWK:' , JSON . stringify (jwk, null , 2 ));
Step 4: Create JWT Signature with Embedded JWK
Generate a JWT signature with the JWK embedded in the header:
const fs = require ( 'fs' );
const jwt = require ( 'jsonwebtoken' );
const crypto = require ( 'crypto' );
// Load keys
const privateKeyPem = fs. readFileSync ( 'private_key.pem' , 'utf8' );
const publicKeyPem = fs. readFileSync ( 'public_key.pem' , 'utf8' );
// Convert public key to JWK
const keyObject = crypto. createPublicKey (publicKeyPem);
const jwk = keyObject. export ({ format: 'jwk' });
// Create JWT signature with JWK in header
const signature = jwt. sign (publicMetadata, privateKeyPem, {
algorithm: 'ES256' ,
header: {
typ: 'JWT' ,
alg: 'ES256' ,
jwk: jwk, // Embed JWK in header
}
});
Step 5: Complete Keypair-Based Example
Here's a complete example:
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' );
// Convert public key to JWK
const keyObject = crypto. createPublicKey (publicKeyPem);
this .jwk = keyObject. export ({ format: 'jwk' });
}
signMetadata ( publicMetadata ) {
// Validate required fields
this . validateMetadata (publicMetadata);
// Create JWT signature with JWK in header
const signature = jwt. sign (publicMetadata, this .privateKey, {
algorithm: 'ES256' ,
header: {
typ: 'JWT' ,
alg: 'ES256' ,
jwk: this .jwk, // Embed JWK in header
}
});
return signature;
}
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' );
}
}
}
// Usage example
const signer = new KeypairSignatureCreator (
'private_key.pem' ,
'public_key.pem'
);
const signature = signer. signMetadata (publicMetadata);
console. log ( 'Generated signature:' , signature);
Signature Verification
To verify that your signature is correctly formatted, you can decode the JWT:
const jwt = require ( 'jsonwebtoken' );
// Decode JWT without verification to inspect structure
const decoded = jwt. decode (signature, { complete: true });
console. log ( 'JWT Header:' , JSON . stringify (decoded.header, null , 2 ));
console. log ( 'JWT Payload:' , JSON . stringify (decoded.payload, null , 2 ));
Certificate-based (x5c):
{
"typ" : "JWT" ,
"alg" : "ES256" ,
"x5c" : [ "base64-encoded-certificate" ]
}
Keypair-based (jwk):
{
"typ" : "JWT" ,
"alg" : "ES256" ,
"jwk" : {
"kty" : "EC" ,
"crv" : "P-256" ,
"x" : "base64url-encoded-x-coordinate" ,
"y" : "base64url-encoded-y-coordinate"
}
}
Security Best Practices
Security Considerations :
Never log or expose private keys
Use secure key storage solutions in production (HSM, key vaults, encrypted storage)
Implement proper error handling for signing failures
Validate all input data before signing
Store credentials securely and validate them before inclusion
Key Management
Secure Storage
Store private keys in secure locations (HSM, key vaults, encrypted storage)
Access Control
Limit access to private keys to authorized systems only
Key Rotation
Regularly rotate keys/certificates and update DID documents
Monitoring
Monitor key usage and detect unauthorized signing attempts
Common Issues and Solutions
Issue Cause Solution Invalid signature format Incorrect algorithm or header Ensure ES256 algorithm and proper x5c or jwk header Certificate/JWK not found Missing x5c or jwk in JWT header Include base64-encoded certificate or JWK in header Validation fails Metadata missing required fields Validate metadata structure before signing (check $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 verifiable credential structure
Integration with CommonsDB
Once you have your signature, include it in your CommonsDB declaration:
const declarationPayload = {
signature: signature, // Your generated JWT signature
tsaSignature: {
tsr: "base64-encoded-timestamp-response" ,
tsq: "base64-encoded-timestamp-request"
},
declarationMetadata: {
publicMetadata: publicMetadata
}
};
For complete declaration examples, see the Declaration API documentation .
Next Steps
After creating your signature:
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
Your signature is now ready for use with CommonsDB declarations, providing cryptographic proof of authenticity and integrity for your metadata.
Last modified on January 14, 2026