Module 6 · Lesson 1 · ~20 min read
Canton uses cryptography heavily — every transaction is signed, every contract has a hash-based identity, key management is a first-class concept. You don't need to be a cryptographer; you need to recognize the primitives, use the standard library safely, and avoid the common pitfalls.
| Primitive | What it gives you | Go package |
|---|---|---|
| Hash | Fingerprint of data — same input → same output, different inputs → almost certainly different outputs. | crypto/sha256, crypto/sha512 |
| HMAC | Hash that only the holder of a secret key can produce. Used for tamper detection. | crypto/hmac |
| Symmetric encryption | Same key encrypts and decrypts. Fast. | crypto/aes + crypto/cipher |
| Asymmetric (public-key) crypto | Sign with private, verify with public; encrypt to public, decrypt with private. Slow but enables identity and trust without shared secrets. | crypto/ed25519, crypto/ecdsa, crypto/rsa |
import "crypto/sha256"
h := sha256.Sum256([]byte("hello"))
fmt.Printf("%x\n", h)
// 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
For streaming, use the hash.Hash interface (which is also an io.Writer):
h := sha256.New()
io.Copy(h, file) // hash everything in file
sum := h.Sum(nil) // returns []byte of length 32
SHA-256 is the default in modern systems and Canton uses it broadly. Don't reach for MD5 or SHA-1 — both are broken for security purposes.
In Canton, contract IDs, transaction IDs, package IDs are typically prefixed hashes — content-addressed. A contract's identity is a hash of its template, payload, and metadata. This means:
This pattern (content-addressing) is everywhere in distributed-ledger and version-control systems (Git uses SHA-1, IPFS uses multi-hash, etc.). When you see a long hex string in Canton's docs, it's almost always a content-addressed identifier.
The pattern that matters most for Canton: sign with a private key, anyone can verify with the public key.
import "crypto/ed25519"
import "crypto/rand"
// One-time setup: generate a key pair
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
// Sign a message
msg := []byte("submit command #1")
sig := ed25519.Sign(priv, msg)
// Verify (anyone with the public key)
ok := ed25519.Verify(pub, msg, sig) // true
Ed25519 is the modern default — fast, small keys (32 bytes), small signatures (64 bytes), no parameter choices to get wrong. ECDSA over P-256 is also common in Canton's ecosystem (it integrates better with HSMs and JCA-style Java tooling). RSA is rarer in new code but you'll still see it in some certs.
When your Go code submits a command to a Canton participant, the participant's view of "who is allowed to do what" depends on:
This is what makes "Alice authorized this transfer" provable later, and what makes "Mallory can't impersonate Alice" enforceable. Real Canton key management is more nuanced (key rotation, hierarchical keys, HSM integration), but the underlying primitive is asymmetric signatures.
import "crypto/rand"
// Bytes
buf := make([]byte, 16)
rand.Read(buf)
// Bounded integer
n, _ := rand.Int(rand.Reader, big.NewInt(100))
For anything security-relevant — keys, nonces, command IDs that must be unguessable — use crypto/rand. math/rand is fine for jitter or test fixtures but is predictable and unsafe for crypto.
Hashes and signatures are byte slices. To pass them through JSON, headers, logs, or text protocols, encode them:
// Hex — most common in distributed systems for IDs and short payloads
hex.EncodeToString(hash) // "2cf24..."
// Base64 — denser, common for keys and signatures in PEM/JWT
base64.StdEncoding.EncodeToString(sig)
// PEM — wraps base64 with framing, used for keys/certs in files
pem.Encode(out, &pem.Block{Type: "PUBLIC KEY", Bytes: pub})
Canton APIs and configs use a mix — JSON tokens carry base64, contract IDs come back as hex, key files are PEM. Recognize each and don't try to convert without checking.
Canton participants typically authenticate Ledger API calls via JWT (JSON Web Token) with a configurable claim shape. JWTs are: header.payload.signature, base64-url-encoded.
// Decode a JWT for inspection (NOT for verification)
import "encoding/base64"
import "encoding/json"
import "strings"
parts := strings.Split(token, ".")
payloadJSON, _ := base64.RawURLEncoding.DecodeString(parts[1])
var claims map[string]any
json.Unmarshal(payloadJSON, &claims)
For real verification (signature check, expiration, audience), use github.com/golang-jwt/jwt/v5. Don't roll your own JWT verification.
Use md5 or sha1 for security. Both are broken. sha256 is the default. sha512 if you specifically need 64-byte digests.
Compare cryptographic outputs with ==. Use hmac.Equal or subtle.ConstantTimeCompare for things like comparing MAC tags or session tokens — short-circuit comparison leaks timing info.
Implement crypto yourself. Use the stdlib or well-known libraries. The bar is "don't even consider this." Half the world's CVEs are home-rolled crypto.
Hold private keys in memory longer than needed. For HSM-backed keys, sign through the HSM API; the key never reaches your process. For software keys, scope them tightly and consider crypto/x509's helpers.
crypto.Signer as the abstraction; HSM adapters implement it without exposing private bytes.