feat(manifest): add ed25519 signing and verification
Sign() computes signature over canonical YAML (excluding sign field), Verify() checks against public key. Tampered manifests are rejected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5e9f61acf4
commit
5157b80e18
2 changed files with 94 additions and 0 deletions
43
pkg/manifest/sign.go
Normal file
43
pkg/manifest/sign.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// signable returns the canonical bytes to sign (manifest without sign field).
|
||||
func signable(m *Manifest) ([]byte, error) {
|
||||
tmp := *m
|
||||
tmp.Sign = ""
|
||||
return yaml.Marshal(&tmp)
|
||||
}
|
||||
|
||||
// Sign computes the ed25519 signature and stores it in m.Sign (base64).
|
||||
func Sign(m *Manifest, priv ed25519.PrivateKey) error {
|
||||
msg, err := signable(m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("manifest.Sign: marshal: %w", err)
|
||||
}
|
||||
sig := ed25519.Sign(priv, msg)
|
||||
m.Sign = base64.StdEncoding.EncodeToString(sig)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify checks the ed25519 signature in m.Sign against the public key.
|
||||
func Verify(m *Manifest, pub ed25519.PublicKey) (bool, error) {
|
||||
if m.Sign == "" {
|
||||
return false, fmt.Errorf("manifest.Verify: no signature present")
|
||||
}
|
||||
sig, err := base64.StdEncoding.DecodeString(m.Sign)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("manifest.Verify: decode: %w", err)
|
||||
}
|
||||
msg, err := signable(m)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("manifest.Verify: marshal: %w", err)
|
||||
}
|
||||
return ed25519.Verify(pub, msg, sig), nil
|
||||
}
|
||||
51
pkg/manifest/sign_test.go
Normal file
51
pkg/manifest/sign_test.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSignAndVerify_Good(t *testing.T) {
|
||||
pub, priv, err := ed25519.GenerateKey(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := &Manifest{
|
||||
Code: "test-app",
|
||||
Name: "Test App",
|
||||
Version: "1.0.0",
|
||||
Layout: "HLCRF",
|
||||
Slots: map[string]string{"C": "main"},
|
||||
}
|
||||
|
||||
err = Sign(m, priv)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, m.Sign)
|
||||
|
||||
ok, err := Verify(m, pub)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestVerify_Bad_Tampered(t *testing.T) {
|
||||
pub, priv, _ := ed25519.GenerateKey(nil)
|
||||
m := &Manifest{Code: "test-app", Version: "1.0.0"}
|
||||
_ = Sign(m, priv)
|
||||
|
||||
m.Code = "evil-app" // tamper
|
||||
|
||||
ok, err := Verify(m, pub)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestVerify_Bad_Unsigned(t *testing.T) {
|
||||
pub, _, _ := ed25519.GenerateKey(nil)
|
||||
m := &Manifest{Code: "test-app"}
|
||||
|
||||
ok, err := Verify(m, pub)
|
||||
assert.Error(t, err)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue