picocert is a minimal certificate library for handling compact X.509-like certificates, designed for embedded systems. It provides certificate chain validation and data signature verification using ECDSA (P-256) and SHA-256.
The library consists of:
- C Library: Core certificate validation and signature verification (header-only
picocert.h) - Go Package: Certificate management tools and CLI (
pkg/picocert,cmd/picocert) - CLI Tool: Command-line interface for certificate operations
- PKI Scripts: Helper scripts for setting up certificate hierarchies
Certificates are represented by the picocert_t struct (see picocert.h):
typedef struct {
uint8_t version;
char issuer[PICOCERT_MAX_NAME_LEN];
char subject[PICOCERT_MAX_NAME_LEN];
uint64_t valid_from; // Any monotonic timestamp, e.g. a unix timestamp or counter
uint64_t valid_to; // Ditto
picocert_curve_t curve; // Only PICOCERT_P256 supported
picocert_hash_t hash; // Only PICOCERT_SHA256 supported
uint32_t reserved; // Must be zero
uint8_t public_key[PICOCERT_MAX_PUBKEY_LEN]; // Uncompressed ECC public key
uint8_t signature[PICOCERT_MAX_SIG_LEN]; // ECDSA signature
} PACKED picocert_t;Field layout:
| Field | Size (bytes) | Description |
|---|---|---|
| version | 1 | Certificate version |
| issuer | 32 | Issuer name (null-terminated) |
| subject | 32 | Subject name (null-terminated) |
| valid_from | 8 | Start of validity |
| valid_to | 8 | End of validity |
| curve | 1 | Curve ID (0 = P-256) |
| hash | 1 | Hash ID (0 = SHA-256) |
| reserved | 4 | Reserved, must be zero |
| public_key | 65 | Uncompressed ECC public key (SEC1) |
| signature | 64 | ECDSA signature raw (r||s) |
Total size: 184 bytes (packed)
The C library is header-only and uses a context for dependency injection of function pointers.
The main API functions are:
// Initialize context with crypto functions
static picocert_err_t picocert_init_context(picocert_context_t* ctx,
picocert_hash_fn_t hash_fn,
picocert_ecc_verify_fn_t ecc_verify_fn,
picocert_time_fn_t time_fn);
// Verify a hash with certificate chain validation
static picocert_err_t picocert_verify_hash_and_validate_chain(picocert_context_t* ctx,
const picocert_t* cert_chain,
const uint32_t chain_len,
const picocert_t* trust_anchor,
const uint8_t hash[PICOCERT_HASH_SHA256_DIGEST_SIZE],
const uint8_t signature[PICOCERT_ECC_SIG_SIZE]);
// Verify hash with a single certificate (no chain validation)
static picocert_err_t picocert_verify_hash(picocert_context_t* ctx,
const picocert_t* cert,
const uint8_t hash[PICOCERT_HASH_SHA256_DIGEST_SIZE],
const uint8_t signature[PICOCERT_ECC_SIG_SIZE]);
// Validate certificate chain (without data verification)
static picocert_err_t picocert_validate_cert_chain(picocert_context_t* ctx,
const picocert_t* cert_chain,
const uint32_t chain_len,
const picocert_t* trust_anchor);
// Validate a single certificate against its issuer
static picocert_err_t picocert_validate_cert(picocert_context_t* ctx,
const picocert_t* issuer,
const picocert_t* subject);#include "picocert.h"
// Example hash function (you must provide your own)
bool my_sha256_hash(const uint8_t* data, uint32_t data_len,
uint8_t* digest, uint32_t digest_len) {
return true;
}
// Example ECC verification function (you must provide your own)
bool my_ecc_verify(const uint8_t* key, size_t key_size,
const uint8_t* hash, uint32_t hash_len,
const uint8_t* signature) {
return true;
}
// Time callback for certificate validity checking
uint64_t get_current_time(void) {
return (uint64_t)time(NULL);
}
picocert_context_t ctx;
picocert_init_context(&ctx, my_sha256_hash, my_ecc_verify, get_current_time);
picocert_t certs[2]; // [0] = leaf, [1] = root supplied with the payload
picocert_t trusted_root; // Load this independently from trusted storage.
// ... fill certs, load trusted_root, hash the data, and obtain signature ...
uint8_t data_hash[PICOCERT_HASH_SHA256_DIGEST_SIZE] = { ... };
uint8_t signature[PICOCERT_ECC_SIG_SIZE] = { ... };
picocert_err_t err = picocert_verify_hash_and_validate_chain(
&ctx, certs, 2, &trusted_root, data_hash, signature);
if (err == PICOCERT_OK) {
// Data is valid and cert chain is trusted
} else {
// Handle error
}| Error Code | Value | Meaning |
|---|---|---|
| PICOCERT_OK | 0 | Success |
| PICOCERT_ERR_INVALID | 1 | Invalid argument |
| PICOCERT_ERR_EXPIRED | 2 | Certificate expired/not yet valid |
| PICOCERT_ERR_SIGNATURE | 3 | Signature verification failed |
| PICOCERT_ERR_ISSUER | 4 | Issuer/subject mismatch |
| PICOCERT_ERR_VERSION | 5 | Version mismatch |
| PICOCERT_ERR_RESERVED | 6 | Reserved field nonzero |
| PICOCERT_ERR_NOT_SELF_SIGNED | 7 | Root not self-signed |
| PICOCERT_ERR_INVALID_FORMAT | 8 | Invalid certificate format |
| PICOCERT_ERR_CONTEXT_NOT_INITIALIZED | 9 | Context not properly initialized |
| PICOCERT_ERR_HASH_FAILED | 10 | Hash computation failed |
| PICOCERT_ERR_UNSUPPORTED_CURVE | 11 | Unsupported curve |
| PICOCERT_ERR_UNSUPPORTED_HASH | 12 | Unsupported hash algorithm |
| PICOCERT_ERR_INVALID_VALIDITY_PERIOD | 13 | Invalid validity period |
| PICOCERT_ERR_CHAIN_TOO_LONG | 14 | Certificate chain too long |
| PICOCERT_ERR_UNTRUSTED_ROOT | 15 | Chain does not end at trusted root |
| PICOCERT_ERR_UNKNOWN | 255 | Unknown error |
The picocert command-line tool provides certificate management functionality:
cd cmd/picocert
go build -o picocert# Create a self-signed root certificate
picocert issue --subject "MyRoot" --output my-root \
--validity_in_days 3650 --self_signed
# Create a certificate signed by an issuer
picocert issue --subject "MyLeaf" --output my-leaf --validity_in_days 365 \
--issuer my-root.pct --issuer_key my-root.priv.der--subject is certificate metadata. --output is the filesystem path prefix;
the command creates <output>.pct and
<output>.priv.der without replacing existing files.
# Sign a binary file
picocert sign --key private.priv.der --binary firmware.bin --output firmware.sig
# Sign and print signature to stdout
picocert sign --key private.priv.der --binary firmware.bin# Verify a signed binary and its certificate chain against a pinned root
picocert verify --cert firmware-leaf.pct \
--intermediate firmware-intermediate.pct \
--root /trusted/firmware-root.pct \
--binary firmware.bin --signature firmware.sigPass --intermediate more than once for deeper chains, ordered from the leaf's
issuer toward the root. The --root file must come from trusted storage rather
than from the same untrusted source as the binary and certificate chain. The CLI
checks every certificate's validity period as part of verification.
--quiet, -q: Suppress output messages
Use the included script to set up a PKI hierarchy:
# Set up a three-tier PKI for firmware signing
./three-tier-pki.sh ./cmd/picocert/picocert firmware
# This creates:
# - firmware-root.pct and firmware-root.priv.der (Root CA, 10-year validity)
# - firmware-intermediate.pct and firmware-intermediate.priv.der (Intermediate CA, 6-year validity)
# - firmware-leaf.pct and firmware-leaf.priv.der (Leaf certificate, 4-year validity)The Go package github.com/block/picocert/pkg/picocert provides high-level certificate operations.
go get github.com/block/picocert/pkg/picocertpackage main
import (
"time"
"github.com/block/picocert/pkg/picocert"
)
func main() {
// Issue a self-signed certificate
validFrom := uint64(time.Now().Unix())
validTo := validFrom + 365*24*60*60 // 1 year
cert, err := picocert.Issue(nil, "MyDevice", validFrom, validTo)
if err != nil {
panic(err)
}
// cert.Cert contains the certificate
// cert.PrivateKey contains the PKCS#8 encoded private key
}// Issue a certificate signed by an issuer
issuerCert := &picocert.CertificateWithKey{
Cert: issuerCertificate,
PrivateKey: issuerPrivateKeyBytes,
}
signedCert, err := picocert.Issue(issuerCert, "SubjectName", validFrom, validTo)
if err != nil {
panic(err)
}// Sign data
privateKey, err := picocert.ParsePrivateKey(privateKeyBytes)
if err != nil {
panic(err)
}
signature, err := picocert.Sign(privateKey, data)
if err != nil {
panic(err)
}
// Verify signature
err = picocert.Verify(&certificate, data, signature)
if err != nil {
// Verification failed
}Verify only checks that the supplied certificate's public key produced the
signature. It does not authenticate that certificate or check its validity
period. Use VerifyAndValidateChain with an independently provisioned root for
authentication.
// Validate a certificate chain against a root loaded from trusted storage.
chain := []picocert.Certificate{leafCert, intermediateCert, rootCert}
trustedRoot := loadPinnedRoot()
err := picocert.ValidateCertChain(chain, &trustedRoot)
if err != nil {
// Chain validation failed
}
// Verify data against the chain
err = picocert.VerifyAndValidateChain(chain, &trustedRoot, data, signature)
if err != nil {
// Verification or validation failed
}// Parse certificate from bytes
cert, err := picocert.ParseCertificate(certBytes)
if err != nil {
panic(err)
}
// Parse private key from PKCS#8 format
privateKey, err := picocert.ParsePrivateKey(keyBytes)
if err != nil {
panic(err)
}
// Convert certificate to bytes
certBytes := cert.ToBytes()type Certificate struct {
Version uint8
Issuer [32]byte
Subject [32]byte
ValidFrom uint64
ValidTo uint64
Curve Curve
Hash Hash
Reserved uint32
PubKey [65]byte
Signature [64]byte
}
type CertificateWithKey struct {
Cert Certificate
PrivateKey []byte // PKCS#8 encoded
}
// Constants
const (
P256 Curve = 0 // ECDSA P-256
Sha256 Hash = 0 // SHA-256
MaxChainLen = 10
)- Only ECDSA P-256 and SHA-256 are supported.
- All fields are packed; no padding.
- Names are fixed-length, null-terminated strings.
- Public key is uncompressed (0x04 | X | Y, 65 bytes).
- Signature is raw ECDSA (r||s, 64 bytes).
- Timestamps are Unix epoch seconds.
- Certificate data is stored in little-endian format for embedded compatibility.
- The C and Go validators both accept at most 10 certificates in a chain.
- Certificates: Binary format (
.pctextension), 184 bytes each - Private Keys: Unencrypted PKCS#8 DER format (
.priv.derextension), created with owner-only permissions (0600) - Signatures: Raw binary, 64 bytes (r||s format)
- Private keys are not encrypted. Store them securely and never transmit them in plaintext.
- Consider using hardware security modules (HSMs) for root CA key storage
- Implement proper key rotation and certificate renewal procedures
- Provision trust anchors independently from untrusted certificate chains, and pass the expected root to every chain-validation call
- Use appropriate validity periods for your security requirements
- Provide secure implementations of the required cryptographic functions
- The library enforces a maximum chain length to prevent DoS