This is an advanced technical reference for implementing blob signing. It covers the exact byte-level process for creating valid SHM signatures.
The Zero-Fill-Sign Pattern
SHM uses a specific pattern for signing that ensures the signature is part of the signed content:
1. Create blob with sig = 64 zero bytes
2. CBOR-encode the blob
3. Sign the encoded bytes
4. Replace zeros with actual signature
5. CBOR-encode again → final blobThis ensures verifiers can reconstruct exactly what was signed by replacing the signature with zeros.
CBOR Encoding Rules
CBOR encoding must be deterministic for signatures to verify. Key rules:
// Use canonical CBOR encoding:
- Maps: keys in lexicographic order
- Numbers: smallest possible encoding
- No indefinite-length items
// JavaScript example with cbor-x:
import { encode } from 'cbor-x';
const bytes = encode(data); // Deterministic by defaultChange Blob Fields
Exact field order and types for a Change blob:
{
"@type": "Change", // String: always "Change"
"signer": Uint8Array(34), // Multicodec-prefixed pubkey
"delegateOf": null | Uint8Array(34),
"account": Uint8Array(34), // Target account pubkey
"path": "/doc-path", // String
"genesis": Uint8Array(36), // CID bytes of first version
"deps": [Uint8Array(36)], // Array of CID bytes
"ops": [...], // Operations array
"hlcTime": BigInt, // Hybrid logical clock
"capability": "write", // String: write|read|comment
"sig": Uint8Array(64) // Ed25519 signature
}Public Key Format
Account IDs (z6Mk...) are multibase-encoded public keys. To convert:
// Account ID → bytes
const { base58btc } = require('multiformats/bases/base58');
const bytes = base58btc.decode('z6MkvYf14...');
// bytes[0:2] = multicodec prefix (0xed 0x01 = Ed25519 pubkey)
// bytes[2:34] = 32-byte public key
// For signing, use full 34-byte prefixed keyCID Bytes
CIDs in blobs are stored as raw bytes, not strings:
// String CID → bytes
import { CID } from 'multiformats/cid';
const cid = CID.parse('bafy2bzace...');
const bytes = cid.bytes; // Uint8Array(36)
// Bytes → string CID
const cidString = CID.decode(bytes).toString();Signing Implementation
Complete JavaScript signing example:
import * as ed from '@noble/ed25519';
import { encode } from 'cbor-x';
async function signChange(change, privateKey) {
// 1. Insert zero signature
const toSign = { ...change, sig: new Uint8Array(64) };
// 2. CBOR encode
const bytes = encode(toSign);
// 3. Sign
const sig = await ed.signAsync(bytes, privateKey);
// 4. Replace signature
change.sig = sig;
// 5. Final encoding
return encode(change);
}Verification Implementation
import * as ed from '@noble/ed25519';
import { decode, encode } from 'cbor-x';
async function verifyChange(blob) {
// 1. Decode
const change = decode(blob);
// 2. Extract and zero signature
const sig = change.sig;
const toVerify = { ...change, sig: new Uint8Array(64) };
// 3. Re-encode
const bytes = encode(toVerify);
// 4. Extract public key from signer field
const pubkey = change.signer.slice(2); // Remove multicodec prefix
// 5. Verify
return await ed.verifyAsync(sig, bytes, pubkey);
}Common Mistakes
• Using string CIDs instead of bytes
• Forgetting multicodec prefix on public keys
• Non-deterministic CBOR encoding
• Wrong signature length (must be exactly 64 bytes)
• Using 32-byte pubkey instead of 34-byte prefixed version
See the original Blob Signing page for more context, and Cryptographic Signing for the conceptual overview.