package bundle import ( "archive/tar" "bytes" "compress/gzip" "crypto/ed25519" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io" "strings" ) // VerifiedContents contains only payloads covered by a verified signed manifest. type VerifiedContents struct { Manifest Manifest Files map[string][]byte } // VerifyGzippedTar verifies the unchanged signed manifest format with default limits. func VerifyGzippedTar(raw []byte, pub ed25519.PublicKey) (*VerifiedContents, error) { return VerifyGzippedTarWithLimits(raw, pub, Limits{}) } // VerifyGzippedTarWithLimits verifies the signature and hashes under explicit // compressed, expanded, per-entry and metadata budgets. func VerifyGzippedTarWithLimits(raw []byte, pub ed25519.PublicKey, limits Limits) (*VerifiedContents, error) { l, err := limits.Normalized() if err != nil { return nil, err } if int64(len(raw)) > l.MaxCompressedBytes { return nil, ErrLimit } if len(pub) != ed25519.PublicKeySize { return nil, fmt.Errorf("bundle: invalid public key size") } gr, err := gzip.NewReader(bytes.NewReader(raw)) if err != nil { return nil, err } defer func() { _ = gr.Close() }() decoded := &io.LimitedReader{R: gr, N: l.MaxExpandedBytes + 1} tr := tar.NewReader(decoded) var manifestRaw, sig []byte files := map[string][]byte{} seen := map[string]bool{} count := 0 var logicalBytes int64 for { hdr, err := tr.Next() if decoded.N == 0 { return nil, ErrLimit } if err == io.EOF { break } if err != nil { return nil, err } count++ if count > l.MaxEntries { return nil, ErrLimit } if err = validatePath(hdr.Name, l.MaxPathBytes); err != nil { return nil, err } name := hdr.Name folded := strings.ToLower(name) if seen[folded] { return nil, fmt.Errorf("bundle: duplicate tar entry %q", name) } seen[folded] = true if hdr.Typeflag != tar.TypeReg && hdr.Typeflag != tar.TypeRegA { return nil, fmt.Errorf("bundle: nonregular tar entry %q", name) } for key := range hdr.PAXRecords { if strings.HasPrefix(key, "GNU.sparse.") { return nil, fmt.Errorf("bundle: sparse entry %q is unsupported", name) } } max := l.MaxFileBytes if name == manifestName { max = l.MaxManifestBytes } if name == sigFileName { max = ed25519.SignatureSize } if hdr.Size < 0 || hdr.Size > max || hdr.Size >= decoded.N || hdr.Size > l.MaxExpandedBytes-logicalBytes { return nil, ErrLimit } logicalBytes += hdr.Size data, err := io.ReadAll(io.LimitReader(tr, max+1)) if err != nil { return nil, err } if int64(len(data)) > max || decoded.N == 0 { return nil, ErrLimit } switch name { case manifestName: manifestRaw = data case sigFileName: sig = data default: files[name] = data } } // tar EOF precedes the gzip trailer. Drain through the same byte budget so // checksums, extra gzip members and trailing decompression are also bounded. if _, err = io.Copy(io.Discard, decoded); err != nil { return nil, err } if decoded.N == 0 { return nil, ErrLimit } if len(manifestRaw) == 0 || len(sig) != ed25519.SignatureSize { return nil, fmt.Errorf("bundle: missing manifest or signature") } if !ed25519.Verify(pub, manifestRaw, sig) { return nil, fmt.Errorf("bundle: ed25519 signature mismatch") } var m Manifest if err = json.Unmarshal(manifestRaw, &m); err != nil { return nil, err } if len(m.Files) > l.MaxEntries-2 { return nil, ErrLimit } listed := map[string]bool{} for _, fe := range m.Files { if err = validatePath(fe.Path, l.MaxPathBytes); err != nil { return nil, err } if fe.Path == manifestName || fe.Path == sigFileName || listed[strings.ToLower(fe.Path)] { return nil, fmt.Errorf("bundle: duplicate or reserved manifest path %q", fe.Path) } listed[strings.ToLower(fe.Path)] = true body, ok := files[fe.Path] if !ok { return nil, fmt.Errorf("bundle: missing file %q", fe.Path) } h := sha256.Sum256(body) if fmt.Sprintf("%x", h[:]) != fe.SHA256 { return nil, fmt.Errorf("bundle: checksum mismatch for %q", fe.Path) } } if len(listed) != len(files) { return nil, fmt.Errorf("bundle: payload is not covered by manifest") } return &VerifiedContents{Manifest: m, Files: files}, nil } // ParsePublicKeyHex decodes a 32-byte Ed25519 public key from hexadecimal. func ParsePublicKeyHex(s string) (ed25519.PublicKey, error) { s = strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(s, "0x"), "0X")) raw, err := hex.DecodeString(s) if err != nil { return nil, fmt.Errorf("bundle: hex decode: %w", err) } if len(raw) != ed25519.PublicKeySize { return nil, fmt.Errorf("bundle: want %d-byte ed25519 public key", ed25519.PublicKeySize) } return ed25519.PublicKey(raw), nil } // PublicKeyFromManifestBase64 decodes the manifest key. Callers must still trust // an independently provisioned key when authenticating the bundle. func PublicKeyFromManifestBase64(m *Manifest) (ed25519.PublicKey, error) { if m.PublicKeyB64 == "" { return nil, fmt.Errorf("bundle: manifest missing public_key_base64") } raw, err := base64.StdEncoding.DecodeString(m.PublicKeyB64) if err != nil { return nil, err } if len(raw) != ed25519.PublicKeySize { return nil, fmt.Errorf("bundle: bad public key length") } return ed25519.PublicKey(raw), nil }