@canvas-js/signed-cid
This package implements a tiny signed data format for IPLD values. Any CID can be signed, and the resulting Signature
can be passed around as an independent value on its own.
Table of Contents
Overview
import type { CID } from "multiformats/cid"
export type Signature = {
publicKey: string
signature: Uint8Array
cid: CID
}
Signatures sign the bytes of the CID, which carries metadata about the encoding format and hashing algorithm in addition to the hash of the encoded value itself. This allows values and their signatures to be re-encoded with different codecs without breaking validation.
did:key
URIs are used to encode public keys.
Ed25519 and Secp256k1 signatures are supported, using the audited @noble/curves
library. Secp256k1 public keys are always compressed.
The dag-json
, dag-cbor
, and raw
IPLD codecs are supported by default, and others can be used by implementing the Codec
interface. Similarly, sha2-256
, blake3-256
, and blake3-128
multihash digests are supported by default, and others can be used by implementing the Digest
interface.
Usage
import { Ed25519Signer, verifySignedValue } from "@canvas-js/signed-cid"
const signer = new Ed25519Signer()
const value = { foo: "hello world", bar: [1, 2, 3] }
const signature = signer.sign(value)
console.log(signature)
verifySignedValue(signature, value)
We can see a detailed breakdown of the CID in the signature with the CID explorer tool here. The prefix bytes of the CID tell us that it carries the sha2-256 hash of a dag-cbor value, and that the hash digest is 0x305139BC0F5182D7C6D9710952581491032EFDC86806FC0F02D0A8804D898CF8
.
We can check this by encoding and hashing the value ourselves:
import { encode } from "@ipld/dag-cbor"
const value = encode({ foo: "hello world", bar: [1, 2, 3] })
console.log(value)
const hash = crypto.createHash("sha256").update(value).digest()
console.log(hash)
Again, signatures always sign the raw bytes of the entire CID, not just the hash digest.
API
Signing defaults to dag-cbor
and sha2-256
if options.codec
or options.digest
are not provided, respectively.
Ed25519Signer
export declare class Ed25519Signer<T = any> {
public static type: "ed25519"
public static code: number
public readonly uri: string
public constructor(privateKey?: Uint8Array)
public sign(value: T, options?: { codec?: string | Codec; digest?: string | Digest }): Signature
public export(): { type: "ed25519"; privateKey: Uint8Array }
}
Secp256k1Signer
export declare class Secp256k1Signer<T = any> {
public static type: "secp256k1"
public static code: number
public readonly uri: string
public constructor(privateKey?: Uint8Array)
public sign(value: T, options?: { codec?: string | Codec; digest?: string | Digest }): Signature
public export(): { type: "secp256k1"; privateKey: Uint8Array }
}
Verify a signed value
export type SignatureType = "ed25519" | "secp256k1"
export declare function verifySignedValue(
signature: Signature,
value: any,
options?: { types: SignatureType[]; codecs?: Codec[]; digests?: Digest[] }
): void
Verify a standalone signature
export declare function verifySignature(signature: Signature, options?: { types: SignatureType[] }): void
Utility types
Codec
and Digest
are similar to some existing interfaces in the JavaScript IPLD ecosystem, but are defined the way they are here to support synchronous zero-copy streaming encoders.
Digest
interface Digest {
name: string
code: number
digest: (iter: Iterable<Uint8Array>) => Uint8Array
}
Codec
interface Codec {
name: string
code: number
encode: (value: any) => Iterable<Uint8Array>
}