package hostevidence import ( "bytes" "context" "crypto/ed25519" "errors" "strings" "github.com/carlosframework/platform/internal/release" ) var ErrSignature = errors.New("hostevidence: invalid receipt signature") func message(key string, body []byte) ([]byte, error) { parts := strings.Split(key, "/") if len(parts) < 6 || parts[0] != "control" || parts[1] != "hosts" || !((len(parts) == 7 && parts[3] == "evidence") || (len(parts) == 6 && parts[3] == "serving-evidence")) || !strings.HasSuffix(key, ".json") || strings.ContainsAny(key, "\\\x00\r\n") { return nil, ErrSignature } for _, part := range parts { if part == "" || part == "." || part == ".." { return nil, ErrSignature } } out := append([]byte("carlos-host-stop-v1\x00"+key+"\x00"), body...) return out, nil } func Verify(pub ed25519.PublicKey, key string, body, signature []byte) error { wire, err := message(key, body) if err != nil || len(pub) != ed25519.PublicKeySize || len(signature) != ed25519.SignatureSize || !ed25519.Verify(pub, wire, signature) { return ErrSignature } return nil } func immutable(ctx context.Context, s release.Store, key string, body []byte) error { if _, err := s.Put(ctx, key, body, ""); err == nil { return nil } else if !errors.Is(err, release.ErrCASConflict) { return err } existing, _, err := s.Get(ctx, key) if err != nil { return err } if !bytes.Equal(existing, body) { return ErrSignature } return nil } func Publish(ctx context.Context, s release.Store, private ed25519.PrivateKey, key string, body []byte) error { wire, err := message(key, body) if err != nil || s == nil || len(private) != ed25519.PrivateKeySize { return ErrSignature } signature := ed25519.Sign(private, wire) if err = immutable(ctx, s, key, body); err != nil { return err } return immutable(ctx, s, key+".sig", signature) }