gomcp/internal/infrastructure/sbom/sbom.go

193 lines
5.3 KiB
Go

// Package sbom implements SEC-010 SBOM + Release Signing.
//
// Generates SPDX Software Bill of Materials and provides
// binary signing using Ed25519 (with Sigstore Cosign integration point).
//
// Usage:
//
// gen := sbom.NewGenerator("SENTINEL AI SOC", "2.1.0")
// gen.AddDependency("golang.org/x/crypto", "v0.21.0", "BSD-3-Clause")
// spdx, _ := gen.GenerateSPDX()
package sbom
import (
"crypto/ed25519"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"time"
)
// SPDXDocument is an SPDX 2.3 SBOM document.
type SPDXDocument struct {
SPDXVersion string `json:"spdxVersion"`
DataLicense string `json:"dataLicense"`
SPDXID string `json:"SPDXID"`
DocumentName string `json:"name"`
Namespace string `json:"documentNamespace"`
CreationInfo CreationInfo `json:"creationInfo"`
Packages []Package `json:"packages"`
Relationships []Relationship `json:"relationships,omitempty"`
}
// CreationInfo describes when and how the SBOM was created.
type CreationInfo struct {
Created string `json:"created"`
Creators []string `json:"creators"`
Comment string `json:"comment,omitempty"`
}
// Package is an SPDX package entry.
type Package struct {
SPDXID string `json:"SPDXID"`
Name string `json:"name"`
Version string `json:"versionInfo"`
Supplier string `json:"supplier,omitempty"`
License string `json:"licenseConcluded"`
DownloadURL string `json:"downloadLocation"`
Checksum string `json:"checksum,omitempty"` // SHA256:hex
}
// Relationship links packages.
type Relationship struct {
Element string `json:"spdxElementId"`
Type string `json:"relationshipType"`
Related string `json:"relatedSpdxElement"`
}
// ReleaseSignature is a signed release record.
type ReleaseSignature struct {
Binary string `json:"binary"`
Version string `json:"version"`
Hash string `json:"hash"` // SHA-256
Signature string `json:"signature"` // Ed25519 hex
KeyID string `json:"key_id"`
SignedAt string `json:"signed_at"`
}
// Generator produces SBOM documents.
type Generator struct {
productName string
version string
packages []Package
}
// NewGenerator creates an SBOM generator.
func NewGenerator(productName, version string) *Generator {
return &Generator{
productName: productName,
version: version,
}
}
// AddDependency adds a dependency to the SBOM.
func (g *Generator) AddDependency(name, version, license string) {
g.packages = append(g.packages, Package{
SPDXID: fmt.Sprintf("SPDXRef-%s", sanitizeID(name)),
Name: name,
Version: version,
License: license,
DownloadURL: fmt.Sprintf("https://pkg.go.dev/%s@%s", name, version),
})
}
// GenerateSPDX creates an SPDX 2.3 JSON document.
func (g *Generator) GenerateSPDX() (*SPDXDocument, error) {
doc := &SPDXDocument{
SPDXVersion: "SPDX-2.3",
DataLicense: "CC0-1.0",
SPDXID: "SPDXRef-DOCUMENT",
DocumentName: fmt.Sprintf("%s-%s", g.productName, g.version),
Namespace: fmt.Sprintf("https://sentinel.syntrex.pro/spdx/%s/%s", g.productName, g.version),
CreationInfo: CreationInfo{
Created: time.Now().UTC().Format(time.RFC3339),
Creators: []string{"Tool: sentinel-sbom-gen", "Organization: Syntrex"},
},
Packages: append([]Package{{
SPDXID: "SPDXRef-Product",
Name: g.productName,
Version: g.version,
License: "Proprietary",
DownloadURL: "https://github.com/syntrex-lab/gomcp",
}}, g.packages...),
}
// Add relationships.
for _, pkg := range g.packages {
doc.Relationships = append(doc.Relationships, Relationship{
Element: "SPDXRef-Product",
Type: "DEPENDS_ON",
Related: pkg.SPDXID,
})
}
return doc, nil
}
// ExportJSON serializes the SBOM to JSON.
func ExportJSON(doc *SPDXDocument) ([]byte, error) {
return json.MarshalIndent(doc, "", " ")
}
// SignRelease signs a binary for release verification.
func SignRelease(binaryPath, version string, privateKey ed25519.PrivateKey, keyID string) (*ReleaseSignature, error) {
hash, err := hashFile(binaryPath)
if err != nil {
return nil, fmt.Errorf("sbom: hash %s: %w", binaryPath, err)
}
hashBytes, _ := hex.DecodeString(hash)
sig := ed25519.Sign(privateKey, hashBytes)
return &ReleaseSignature{
Binary: binaryPath,
Version: version,
Hash: hash,
Signature: hex.EncodeToString(sig),
KeyID: keyID,
SignedAt: time.Now().UTC().Format(time.RFC3339),
}, nil
}
// VerifyRelease verifies a signed release.
func VerifyRelease(sig *ReleaseSignature, publicKey ed25519.PublicKey) bool {
hashBytes, err := hex.DecodeString(sig.Hash)
if err != nil {
return false
}
sigBytes, err := hex.DecodeString(sig.Signature)
if err != nil {
return false
}
return ed25519.Verify(publicKey, hashBytes, sigBytes)
}
// --- Helpers ---
func sanitizeID(name string) string {
result := make([]byte, 0, len(name))
for _, c := range name {
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' {
result = append(result, byte(c))
} else {
result = append(result, '-')
}
}
return string(result)
}
func hashFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}