Reference
Go

Go SDK

Reference Implementation

Use epack as a Go library to build, verify, diff, and sign Evidence Packs programmatically. The same code that powers the CLI is available as importable packages.

v0.1.29 Released Feb 2026
Keyless signing Pack diffing Streaming builds
View changelog β†’

What you'll need

βœ“
Go 1.26+ Latest two major versions supported
βœ“
Go modules Project with go.mod initialized
βœ“
Basic Go knowledge Familiarity with error handling, interfaces

Installation

Beginner

Install the SDK using Go modules. We recommend pinning to a specific version.

go get github.com/locktivity/epack@v0.1.29
# Pin to a specific version (recommended for production)
go get github.com/locktivity/epack@v0.1.29
πŸ’‘

Pin your dependencies to a specific version to ensure reproducible builds.

Quickstart

Beginner

Get started in under 30 seconds with these common operations.

1

Open and verify a pack

package main

import (
    "fmt"
    "log"

    "github.com/locktivity/epack/pack"
)

func main() {
    // Open a pack
    p, err := pack.Open("vendor.epack")
    if err != nil {
        log.Fatal(err)
    }
    defer p.Close()

    // Access manifest metadata
    m := p.Manifest()
    fmt.Printf("Stream: %s\n", m.Stream)
    fmt.Printf("Digest: %s\n", m.PackDigest)

    // Verify integrity
    if err := p.VerifyIntegrity(); err != nil {
        log.Fatal("integrity check failed:", err)
    }
    fmt.Println("Pack is valid!")
}
Learn more about pack operations β†’
2

Build a pack

package main

import (
    "log"

    "github.com/locktivity/epack/pack/builder"
)

func main() {
    // Create a builder with stream ID
    b := builder.New("myorg/prod")

    // Add source metadata
    b.AddSource("github-collector", "1.0.0")

    // Add artifacts
    b.AddFile("artifacts/config.json", "./config.json")
    b.AddFile("artifacts/report.pdf", "./report.pdf")

    // Build the pack
    if err := b.Build("evidence.epack"); err != nil {
        log.Fatal(err)
    }
}
Learn more about building packs β†’

Requirements

Go
Go 1.22+

Go 1.26 or later is required. We support the two most recent major releases.

Dependencies

The SDK has minimal dependencies, relying primarily on the Go standard library:

  • github.com/sigstore/sigstore-go β€” Sigstore verification
  • github.com/sigstore/cosign β€” Keyless signing
Packages
pack core
pkg.go.dev

Open, read, and verify evidence packs. Access manifest metadata and read artifacts.

import "github.com/locktivity/epack/pack"

Types

Pack

An opened evidence pack. Provides access to manifest and artifacts.

Manifest

Pack metadata including stream, digest, artifacts, and attestations.

Artifact

An individual artifact with path, digest, size, and content type.

Functions

func Open(path string) (*Pack, error)

Open opens an evidence pack file for reading. The caller must call Close when done.

p, err := pack.Open("evidence.epack")
if err != nil {
    return err
}
defer p.Close()
func (p *Pack) Manifest() Manifest

Manifest returns the pack's manifest containing metadata, artifact list, and attestations.

m := p.Manifest()
fmt.Printf("Stream: %s\n", m.Stream)
fmt.Printf("Created: %s\n", m.CreatedAt)
fmt.Printf("Artifacts: %d\n", len(m.Artifacts))
func (p *Pack) VerifyIntegrity() error

VerifyIntegrity verifies that all artifact digests match their contents. Does not verify signatures.

if err := p.VerifyIntegrity(); err != nil {
    // Handle integrity failure
    var mismatch *epackerr.DigestMismatchError
    if errors.As(err, &mismatch) {
        fmt.Printf("File %s was modified\n", mismatch.Path)
    }
}
func (p *Pack) ReadArtifact(path string) (TrustedBytes, error)

ReadArtifact reads the contents of an artifact by path.

data, err := p.ReadArtifact("artifacts/config.json")
if err != nil {
    return err
}
fmt.Println(string(data.Bytes()))
func (p *Pack) VerifyAllAttestations(ctx context.Context, v verify.Verifier) (map[string]*verify.Result, error)

VerifyAllAttestations verifies all attestations in the pack using the provided verifier.

verifier, _ := verify.NewSigstoreVerifier(
    verify.WithIssuer("https://accounts.google.com"),
    verify.WithSubject("security@vendor.com"),
)

results, err := p.VerifyAllAttestations(ctx, verifier)
if err != nil {
    return err
}

for path, r := range results {
    if r.Identity != nil {
        fmt.Printf("%s signed by: %s\n", path, r.Identity.Subject)
    }
}
pack/builder
pkg.go.dev

Create evidence packs. Add artifacts and sources, compute digests, generate manifests.

import "github.com/locktivity/epack/pack/builder"

Functions

func New(stream string) *Builder

New creates a new pack builder with the given stream identifier.

b := builder.New("myorg/prod")
func (b *Builder) AddFile(packPath, srcPath string) error

AddFile adds a file from the filesystem to the pack at the specified path.

// Add file with custom path inside pack
b.AddFile("artifacts/config.json", "./local/config.json")

// Add multiple files
files := []struct{ packPath, src string }{
    {"artifacts/report.pdf", "./reports/q4.pdf"},
    {"artifacts/data.csv", "./exports/data.csv"},
}
for _, f := range files {
    if err := b.AddFile(f.packPath, f.src); err != nil {
        return err
    }
}
func (b *Builder) AddBytes(path string, data []byte) error

AddBytes adds in-memory data as an artifact.

// Add JSON data directly
configJSON := []byte(`{"version": "1.0"}`)
b.AddBytes("artifacts/config.json", configJSON)

// Add data from an API response
resp, _ := http.Get("https://api.example.com/data")
data, _ := io.ReadAll(resp.Body)
b.AddBytes("artifacts/api-response.json", data)
func (b *Builder) AddSource(name, version string) *Builder

AddSource records metadata about the collector or tool that produced the artifacts.

b.AddSource("github-collector", "1.2.0")
b.AddSource("custom-scanner", "0.5.0")
func (b *Builder) Build(path string) error

Build writes the pack to the specified path. Computes all digests and generates the manifest.

if err := b.Build("evidence.epack"); err != nil {
    log.Fatal(err)
}
fmt.Println("Pack created successfully")
pack/verify
pkg.go.dev

Verify Sigstore attestations with identity constraints. The primary package for signature verification.

import "github.com/locktivity/epack/pack/verify"
// Create a verifier with identity constraints
verifier, err := verify.NewSigstoreVerifier(
    verify.WithIssuer("https://accounts.google.com"),
    verify.WithSubject("security@vendor.com"),
)
if err != nil {
    return err
}

// Verify all attestations
results, err := p.VerifyAllAttestations(ctx, verifier)
if err != nil {
    // Verification failed
    return err
}

// Inspect signer details
for _, r := range results {
    fmt.Printf("Signed by: %s (issuer: %s)\n",
        r.Identity.Subject, r.Identity.Issuer)
}
// Verify packs came from GitHub Actions
verifier, err := verify.NewSigstoreVerifier(
    verify.WithIssuer("https://token.actions.githubusercontent.com"),
    verify.WithSubjectRegexp(regexp.MustCompile(`https://github\.com/myorg/.*`)),
)
if err != nil {
    return err
}

results, err := p.VerifyAllAttestations(ctx, verifier)
if err != nil {
    // Pack was not signed by trusted CI
    return fmt.Errorf("untrusted pack: %w", err)
}
// Offline verification (no network calls)
verifier, err := verify.NewSigstoreVerifier(
    verify.WithIssuer("https://accounts.google.com"),
    verify.WithSubject("security@vendor.com"),
    verify.WithOffline(),  // Skip transparency log check
)
if err != nil {
    return err
}

// Works without internet access
results, err := p.VerifyAllAttestations(ctx, verifier)

Options

Function Description
WithIssuer(url) Require exact OIDC issuer match
WithIssuerRegexp(*regexp.Regexp) Require OIDC issuer matches pattern
WithSubject(email) Require exact subject (email/identity) match
WithSubjectRegexp(*regexp.Regexp) Require subject matches pattern
WithOffline() Skip transparency log verification
WithTrustedRoot(root.TrustedMaterial) Use custom Sigstore trust root
⚠️
Security: Always verify signer identity in production. Without WithIssuer and WithSubject, you're only checking that someone signed it.
pack/diff
pkg.go.dev

Compare two packs. Identify added, removed, and changed artifacts.

import "github.com/locktivity/epack/pack/diff"
// Open both packs
p1, _ := pack.Open("vendor-2024.epack")
defer p1.Close()

p2, _ := pack.Open("vendor-2025.epack")
defer p2.Close()

// Compare them
result := diff.Packs(p1, p2)

// Inspect changes
fmt.Printf("Added: %d\n", len(result.Added))
fmt.Printf("Removed: %d\n", len(result.Removed))
fmt.Printf("Changed: %d\n", len(result.Changed))
fmt.Printf("Unchanged: %d\n", len(result.Unchanged))

if result.IsIdentical() {
    fmt.Println("No differences")
}

// Get detailed changes for a specific artifact
for _, change := range result.Changed {
    fmt.Printf("Changed: %s\n", change.Path)
    fmt.Printf("  Before: %s\n", change.OldDigest)
    fmt.Printf("  After:  %s\n", change.NewDigest)
}

Types

Result

Contains Added, Removed, Changed, and Unchanged artifact lists.

Change

Details about a changed artifact including old/new digests.

pack/merge
pkg.go.dev

Combine multiple packs into one. Preserves attestation provenance.

import "github.com/locktivity/epack/pack/merge"
// Merge multiple vendor packs
opts := merge.Options{
    Stream:              "myorg/all-vendors",
    MergedBy:            "security-team",
    IncludeAttestations: true, // Embed original signatures
}

sources := []merge.SourcePack{
    {Path: "vendor-a.epack"},
    {Path: "vendor-b.epack"},
    {Path: "vendor-c.epack"},
}

if err := merge.Merge(ctx, sources, "combined.epack", opts); err != nil {
    log.Fatal(err)
}

Create Sigstore attestations for packs.

import "github.com/locktivity/epack/sign"
// Sign a pack file
if err := sign.SignPackFile(ctx, "evidence.epack", signer); err != nil {
    log.Fatal(err)
}
fmt.Println("Pack signed successfully")
sign/sigstore
pkg.go.dev

Keyless OIDC or key-based signing with Sigstore.

import "github.com/locktivity/epack/sign/sigstore"
// Keyless signing (opens browser for OIDC auth)
signer, err := sigstore.NewSigner(ctx, sigstore.Options{
    OIDC: &sigstore.OIDCOptions{
        Interactive: true,
    },
})
if err != nil {
    log.Fatal(err)
}

// Sign the pack
if err := sign.SignPackFile(ctx, "evidence.epack", signer); err != nil {
    log.Fatal(err)
}

Opens your browser to authenticate with Google, GitHub, or Microsoft. No keys to manage!

// CI/CD signing with OIDC token
token := os.Getenv("EPACK_OIDC_TOKEN") // From CI environment

signer, err := sigstore.NewSigner(ctx, sigstore.Options{
    OIDC: &sigstore.OIDCOptions{
        Token: token,
    },
})
if err != nil {
    log.Fatal(err)
}

if err := sign.SignPackFile(ctx, "evidence.epack", signer); err != nil {
    log.Fatal(err)
}
// Key-based signing
privateKey, err := os.ReadFile("private.pem")
if err != nil {
    log.Fatal(err)
}

signer, err := sigstore.NewSigner(ctx, sigstore.Options{
    PrivateKey: privateKey,
})
if err != nil {
    log.Fatal(err)
}

if err := sign.SignPackFile(ctx, "evidence.epack", signer); err != nil {
    log.Fatal(err)
}
⚠️
Note: Key-based signing requires you to manage and secure your private keys. Keyless signing is recommended for most use cases.

Typed errors with stable codes for programmatic handling.

import epackerr "github.com/locktivity/epack/errors"
err := p.VerifyIntegrity()
if err != nil {
    // Get the error code
    code := epackerr.CodeOf(err)

    switch code {
    case epackerr.DigestMismatch:
        fmt.Println("Artifact was modified")
    case epackerr.SignatureInvalid:
        fmt.Println("Invalid signature")
    case epackerr.IdentityMismatch:
        fmt.Println("Wrong signer identity")
    case epackerr.PackMalformed:
        fmt.Println("Pack file is corrupted")
    default:
        fmt.Println("Unknown error:", err)
    }
}

// Or use errors.As for typed errors
var mismatch *epackerr.DigestMismatchError
if errors.As(err, &mismatch) {
    fmt.Printf("File %s: expected %s, got %s\n",
        mismatch.Path, mismatch.Expected, mismatch.Actual)
}
Advanced

Configuration

Intermediate

Configure SDK behavior including timeouts and verification trust policy.

Custom Trusted Root

// Use a custom trusted root for verification
trustedRoot, err := root.NewLiveTrustedRoot(tuf.DefaultOptions())
if err != nil {
    return err
}

verifier, err := verify.NewSigstoreVerifier(
    verify.WithTrustedRoot(trustedRoot),
    verify.WithIssuer("https://accounts.google.com"),
)

Context & Timeouts

// Use context for timeouts and cancellation
ctx, cancel := context.WithTimeout(
    context.Background(),
    60*time.Second,
)
defer cancel()

results, err := p.VerifyAllAttestations(ctx, verifier)
if errors.Is(err, context.DeadlineExceeded) {
    log.Println("Verification timed out")
}

Testing & Mocking

Intermediate

Test your code without making real network calls or signing operations.

// Mock verifier for testing
type mockVerifier struct {
    result []verify.Result
    err    error
}

func (m *mockVerifier) Verify(ctx context.Context, att []byte) (*verify.Result, error) {
    if m.err != nil {
        return nil, m.err
    }
    return &m.result[0], nil
}

// Use in tests
func TestVerification(t *testing.T) {
    mock := &mockVerifier{
        result: []verify.Result{{
            Identity: verify.Identity{
                Subject: "test@example.com",
                Issuer:  "https://accounts.google.com",
            },
        }},
    }

    p, _ := pack.Open("testdata/test.epack")
    defer p.Close()

    results, err := p.VerifyAllAttestations(ctx, mock)
    // Assert...
}
// Create test packs in memory
func createTestPack(t *testing.T) string {
    t.Helper()

    dir := t.TempDir()
    packPath := filepath.Join(dir, "test.epack")

    b := builder.New("test/stream")
    b.AddBytes("test.json", []byte(`{"test": true}`), "application/json")

    if err := b.Build(packPath); err != nil {
        t.Fatal(err)
    }

    return packPath
}
// Test pack building
func TestBuildPack(t *testing.T) {
    dir := t.TempDir()
    packPath := filepath.Join(dir, "output.epack")

    // Create test input file
    inputPath := filepath.Join(dir, "input.txt")
    os.WriteFile(inputPath, []byte("test content"), 0644)

    // Build pack
    b := builder.New("test/stream")
    if err := b.AddFile("data/input.txt", inputPath); err != nil {
        t.Fatal(err)
    }

    if err := b.Build(packPath); err != nil {
        t.Fatal(err)
    }

    // Verify the pack
    p, err := pack.Open(packPath)
    if err != nil {
        t.Fatal(err)
    }
    defer p.Close()

    if p.Manifest().Stream != "test/stream" {
        t.Error("unexpected stream")
    }
}

Error Reference

Complete list of error codes returned by the SDK.

Code Constant Description Recovery
E001 PackMalformed Pack file cannot be opened or parsed Check file exists and is a valid pack
E002 DigestMismatch Artifact content doesn't match its digest Pack may be corrupted or tampered with
E003 SignatureInvalid Cryptographic signature verification failed Pack was signed incorrectly or tampered with
E004 IdentityMismatch Signer identity doesn't match requirements Pack signed by unexpected identity
E005 NoAttestations Pack has no attestations but verification was required Sign the pack before verification
E006 ArtifactNotFound Requested artifact path doesn't exist in pack Check artifact path spelling
E007 TransparencyLogError Could not verify against transparency log Use WithOffline() or check network

Troubleshooting

Solutions to common issues when using the SDK.

"certificate has expired" error during verification

Sigstore certificates are short-lived by design. The signature remains valid as long as it was recorded in the transparency log before expiration.

Solution: Ensure you're not using WithOffline() if you need transparency log verification, or update your trust root.

"context deadline exceeded" during signing

The default context timeout may be too short for signing operations that require network calls.

Solution: Use a longer timeout:

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
"no attestations found" when verifying

The pack hasn't been signed yet.

Solution: Sign the pack first, or use VerifyIntegrity() if you only need digest verification.

High memory usage when building large packs

By default, artifacts are buffered in memory.

Solution: Use AddFile() instead of AddBytes() for large files. The builder will stream them directly.

Performance Tips

Advanced

Optimize SDK usage for production workloads.

πŸ”„

Reuse Verifiers

Create a single verifier and reuse it for multiple packs. The verifier caches trust roots.

// Create once
verifier, _ := verify.NewSigstoreVerifier(...)

// Reuse for all packs
for _, path := range packs {
    p, _ := pack.Open(path)
    p.VerifyAllAttestations(ctx, verifier)
    p.Close()
}
πŸ“

Stream Large Files

Use AddFile() for large artifacts to avoid loading them into memory.

⚑

Parallel Verification

Verify multiple packs concurrently using goroutines.

var wg sync.WaitGroup
for _, path := range packs {
    wg.Add(1)
    go func(p string) {
        defer wg.Done()
        verifyPack(ctx, p, verifier)
    }(path)
}
wg.Wait()
🌐

Offline Mode

Use WithOffline() when transparency log checks aren't required to eliminate network latency.

Security Best Practices

Intermediate

Keep your integration secure.

βœ“
Always verify signer identity

Use WithIssuer() and WithSubject() to ensure packs come from trusted sources.

βœ“
Use keyless signing in CI/CD

OIDC-based signing eliminates the need to manage and rotate private keys.

βœ“
Pin SDK versions

Use specific versions in go.mod to ensure reproducible builds.

βœ—
Don't skip identity verification

Avoid WithInsecureSkipIdentityCheck() in productionβ€”it accepts any valid signer.

βœ—
Don't embed private keys in code

If using key-based signing, load keys from secure storage or environment variables.

βœ—
Don't ignore errors

Always check error returnsβ€”silent failures can lead to security bypasses.

Real-world Scenarios

Advanced

Complete examples for common use cases.

Verify uploaded packs in a web service

package main

import (
    "net/http"
    "regexp"
    "github.com/locktivity/epack/pack"
    "github.com/locktivity/epack/pack/verify"
)

var verifier *verify.SigstoreVerifier

func init() {
    // Create verifier once at startup
    var err error
    verifier, err = verify.NewSigstoreVerifier(
        verify.WithIssuer("https://token.actions.githubusercontent.com"),
        verify.WithSubjectRegexp(regexp.MustCompile(`https://github\.com/trusted-org/.*`)),
    )
    if err != nil {
        panic(err)
    }
}

func handleUpload(w http.ResponseWriter, r *http.Request) {
    // Save uploaded file to temp location
    file, _, err := r.FormFile("pack")
    if err != nil {
        http.Error(w, "Invalid upload", 400)
        return
    }
    defer file.Close()

    tmpFile, _ := os.CreateTemp("", "upload-*.epack")
    defer os.Remove(tmpFile.Name())
    io.Copy(tmpFile, file)
    tmpFile.Close()

    // Verify the pack
    p, err := pack.Open(tmpFile.Name())
    if err != nil {
        http.Error(w, "Invalid pack", 400)
        return
    }
    defer p.Close()

    results, err := p.VerifyAllAttestations(r.Context(), verifier)
    if err != nil {
        http.Error(w, "Verification failed", 403)
        return
    }

    // Process verified pack...
    json.NewEncoder(w).Encode(map[string]any{
        "status":  "verified",
        "signers": results,
    })
}

Build and sign in a CI/CD pipeline

package main

import (
    "context"
    "log"
    "os"

    "github.com/locktivity/epack/pack/builder"
    "github.com/locktivity/epack/sign"
    "github.com/locktivity/epack/sign/sigstore"
)

func main() {
    ctx := context.Background()

    // Build the pack
    b := builder.New(os.Getenv("GITHUB_REPOSITORY"))
    b.AddSource("github-actions", os.Getenv("GITHUB_ACTION"))

    // Add all artifacts from directory
    entries, _ := os.ReadDir("./artifacts")
    for _, e := range entries {
        if !e.IsDir() {
            b.AddFile("artifacts/"+e.Name(), "./artifacts/"+e.Name())
        }
    }

    packPath := "evidence.epack"
    if err := b.Build(packPath); err != nil {
        log.Fatal("build failed:", err)
    }

    // Sign with OIDC token from GitHub Actions
    signer, err := sigstore.NewSigner(ctx, sigstore.Options{
        OIDC: &sigstore.OIDCOptions{
            Token: os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN"),
        },
    })
    if err != nil {
        log.Fatal("signer failed:", err)
    }

    if err := sign.SignPackFile(ctx, packPath, signer); err != nil {
        log.Fatal("signing failed:", err)
    }

    log.Println("Pack built and signed successfully")
}

Build an audit comparison tool

package main

import (
    "fmt"
    "github.com/locktivity/epack/pack"
    "github.com/locktivity/epack/pack/diff"
)

func comparePacks(oldPath, newPath string) {
    old, err := pack.Open(oldPath)
    if err != nil {
        fmt.Printf("Cannot open %s: %v\n", oldPath, err)
        return
    }
    defer old.Close()

    new, err := pack.Open(newPath)
    if err != nil {
        fmt.Printf("Cannot open %s: %v\n", newPath, err)
        return
    }
    defer new.Close()

    result := diff.Packs(old, new)

    fmt.Printf("Comparing %s β†’ %s\n\n", oldPath, newPath)

    if result.IsIdentical() {
        fmt.Println("βœ“ Packs are identical")
        return
    }

    if len(result.Added) > 0 {
        fmt.Println("Added:")
        for _, a := range result.Added {
            fmt.Printf("  + %s\n", a.Path)
        }
    }

    if len(result.Removed) > 0 {
        fmt.Println("Removed:")
        for _, r := range result.Removed {
            fmt.Printf("  - %s\n", r.Path)
        }
    }

    if len(result.Changed) > 0 {
        fmt.Println("Changed:")
        for _, c := range result.Changed {
            fmt.Printf("  ~ %s\n", c.Path)
            fmt.Printf("    Before: %s\n", c.OldDigest[:16])
            fmt.Printf("    After:  %s\n", c.NewDigest[:16])
        }
    }
}

CLI vs Library

Choose the right tool for your use case.

⌨️

Use the CLI when...

  • Running in CI/CD pipelines
  • Writing shell scripts
  • Quick one-off operations
  • You need JSON output for other tools
  • Non-Go environments
CLI Reference β†’
πŸ“¦

Use the library when...

  • Building a Go application
  • You need custom verification logic
  • Processing packs in a web service
  • Building collectors or tools
  • You want type-safe error handling
Building Tools β†’

Ready to build?

Check out the source code for examples and tests. Questions? Open an issue.

We built Evidence Packs in the open because portable, verifiable assurance is a problem bigger than any one vendor.