111 lines
2.8 KiB
Go
111 lines
2.8 KiB
Go
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 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
|
|
}
|