package bundle import ( "archive/tar" "bytes" "compress/gzip" "crypto/ed25519" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io" "path" "strings" ) // VerifiedContents is the result of verifying a downloaded bundle on evobgp-node. type VerifiedContents struct { Manifest Manifest Files map[string][]byte } // VerifyGzippedTar checks Ed25519 signature over manifest.json and SHA-256 of each file. func VerifyGzippedTar(bundle []byte, pub ed25519.PublicKey) (*VerifiedContents, error) { gr, err := gzip.NewReader(bytes.NewReader(bundle)) if err != nil { return nil, err } defer func() { _ = gr.Close() }() var manifestRaw []byte var sig []byte files := make(map[string][]byte) tr := tar.NewReader(gr) for { hdr, err := tr.Next() if err == io.EOF { break } if err != nil { return nil, err } name := path.Clean(hdr.Name) if strings.HasPrefix(name, "..") { return nil, fmt.Errorf("bundle: illegal tar entry %q", hdr.Name) } data, err := io.ReadAll(tr) if err != nil { return nil, err } switch name { case manifestName: manifestRaw = data case sigFileName: sig = data default: files[name] = data } } if len(manifestRaw) == 0 || len(sig) == 0 { 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 } for _, fe := range m.Files { 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) } } return &VerifiedContents{Manifest: m, Files: files}, nil } // ParsePublicKeyHex decodes a 64-char hex Ed25519 public key (32 bytes). 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 uses the key embedded in the signed manifest (cross-check with expected pubkey optional). 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 }