**Approach (brief)**- Parse three CLI args: inputs.json, outputs.json, private.pem.- Unmarshal into typed slices (struct field order ensures deterministic JSON key order).- Build provenance struct with timestamp and BUILDER_ID.- Marshal deterministically (structs, no maps), sign SHA-256 of the JSON bytes with RSA private key, produce base64 signature, print final JSON.go
package main
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"time"
)
// FileDigest represents a file path and its SHA256 digest.
// Use struct (not map) so encoding/json emits keys in declaration order for deterministic output.
type FileDigest struct {
Path string `json:"path"`
SHA256 string `json:"sha256"`
}
// Attestation is the unsigned payload.
// Field order here controls JSON key ordering for determinism.
type Attestation struct {
BuilderID string `json:"builder_id"`
Timestamp string `json:"timestamp"`
Inputs []FileDigest `json:"inputs"`
Outputs []FileDigest `json:"outputs"`
}
// Envelope is the final JSON containing attestation and signature.
type Envelope struct {
Attestation Attestation `json:"attestation"`
Signature string `json:"signature"` // base64
}
func must(err error) {
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
func readJSONFile(path string, v interface{}) {
b, err := ioutil.ReadFile(path)
must(err)
must(json.Unmarshal(b, v))
}
func parsePrivateKey(pemPath string) *rsa.PrivateKey {
b, err := ioutil.ReadFile(pemPath)
must(err)
block, _ := pem.Decode(b)
if block == nil {
fmt.Fprintln(os.Stderr, "failed to parse PEM")
os.Exit(1)
}
// Try PKCS1 then PKCS8
if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {
return key
}
pk, err := x509.ParsePKCS8PrivateKey(block.Bytes)
must(err)
rk, ok := pk.(*rsa.PrivateKey)
if !ok {
fmt.Fprintln(os.Stderr, "not an RSA private key")
os.Exit(1)
}
return rk
}
func main() {
if len(os.Args) != 4 {
fmt.Fprintf(os.Stderr, "usage: %s inputs.json outputs.json private.pem\n", os.Args[0])
os.Exit(1)
}
inputPath, outputPath, keyPath := os.Args[1], os.Args[2], os.Args[3]
var inputs, outputs []FileDigest
readJSONFile(inputPath, &inputs)
readJSONFile(outputPath, &outputs)
builder := os.Getenv("BUILDER_ID")
if builder == "" {
builder = "unknown"
}
att := Attestation{
BuilderID: builder,
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
Inputs: inputs,
Outputs: outputs,
}
// Marshal the attestation deterministically:
// - Use struct types (no maps) so encoding/json preserves field order.
// - Disable HTML escaping for stability.
attBytes, err := json.Marshal(att)
must(err)
// Hash and sign
h := sha256.Sum256(attBytes)
priv := parsePrivateKey(keyPath)
sig, err := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, h[:])
must(err)
env := Envelope{
Attestation: att,
Signature: base64.StdEncoding.EncodeToString(sig),
}
// Marshal final envelope (deterministic for same reasons)
enc := json.NewEncoder(os.Stdout)
enc.SetEscapeHTML(false)
must(enc.Encode(env))
}
Notes:- Determinism: avoid maps; use struct declaration order to control JSON key order. For stricter determinism across Go versions, one could canonicalize (e.g., RFC 8785) but that's beyond stdlib-only constraints.- Signing uses RSA PKCS#1 v1.5 over SHA-256; adjust if another algorithm is required.