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.
What you'll need
Installation
BeginnerInstall 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.29Pin your dependencies to a specific version to ensure reproducible builds.
Quickstart
BeginnerGet started in under 30 seconds with these common operations.
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!") }
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) } }
Requirements
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 verificationgithub.com/sigstore/cosignβ Keyless signing
pack
core
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
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
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)
WithIssuer and
WithSubject, you're only checking that someone signed it.
pack/diff
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
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) }
sign
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
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) }
errors
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) }
Configuration
IntermediateConfigure 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
IntermediateTest 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
AdvancedOptimize 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
IntermediateKeep your integration secure.
Use WithIssuer() and WithSubject() to ensure packs come from trusted sources.
OIDC-based signing eliminates the need to manage and rotate private keys.
Use specific versions in go.mod to ensure reproducible builds.
Avoid WithInsecureSkipIdentityCheck() in productionβit accepts any valid signer.
If using key-based signing, load keys from secure storage or environment variables.
Always check error returnsβsilent failures can lead to security bypasses.
Real-world Scenarios
AdvancedComplete 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
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
Ready to build?
Check out the source code for examples and tests. Questions? Open an issue.
Also see
Initiated by
Locktivity
We built Evidence Packs in the open because portable, verifiable assurance is a problem bigger than any one vendor.