feat(cmd/scm): add manifest sign and verify commands
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
48d1eb22b0
commit
e73809cf8d
3 changed files with 240 additions and 0 deletions
|
|
@ -7,6 +7,8 @@
|
|||
// - compile: Compile .core/manifest.yaml into core.json
|
||||
// - index: Build marketplace index from repository directories
|
||||
// - export: Export a compiled manifest as JSON to stdout
|
||||
// - sign: Sign .core/manifest.yaml with an ed25519 private key
|
||||
// - verify: Verify a manifest signature with an ed25519 public key
|
||||
package scm
|
||||
|
||||
import (
|
||||
|
|
@ -39,4 +41,6 @@ func AddScmCommands(root *cli.Command) {
|
|||
addCompileCommand(scmCmd)
|
||||
addIndexCommand(scmCmd)
|
||||
addExportCommand(scmCmd)
|
||||
addSignCommand(scmCmd)
|
||||
addVerifyCommand(scmCmd)
|
||||
}
|
||||
|
|
|
|||
137
cmd/scm/cmd_sign_verify.go
Normal file
137
cmd/scm/cmd_sign_verify.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package scm
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/hex"
|
||||
|
||||
"dappco.re/go/core/io"
|
||||
"dappco.re/go/core/scm/manifest"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
)
|
||||
|
||||
func addSignCommand(parent *cli.Command) {
|
||||
var (
|
||||
dir string
|
||||
signKey string
|
||||
)
|
||||
|
||||
cmd := &cli.Command{
|
||||
Use: "sign",
|
||||
Short: "Sign manifest.yaml with a private key",
|
||||
Long: "Read .core/manifest.yaml, attach an ed25519 signature, and write the signed manifest back to disk.",
|
||||
RunE: func(cmd *cli.Command, args []string) error {
|
||||
return runSign(dir, signKey)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&dir, "dir", "d", ".", "Project root directory")
|
||||
cmd.Flags().StringVar(&signKey, "sign-key", "", "Hex-encoded ed25519 private key")
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
}
|
||||
|
||||
func runSign(dir, signKeyHex string) error {
|
||||
if signKeyHex == "" {
|
||||
return cli.Err("sign key is required")
|
||||
}
|
||||
|
||||
medium, err := io.NewSandboxed(dir)
|
||||
if err != nil {
|
||||
return cli.WrapVerb(err, "open", dir)
|
||||
}
|
||||
|
||||
m, err := manifest.Load(medium, ".")
|
||||
if err != nil {
|
||||
return cli.WrapVerb(err, "load", "manifest")
|
||||
}
|
||||
|
||||
keyBytes, err := hex.DecodeString(signKeyHex)
|
||||
if err != nil {
|
||||
return cli.WrapVerb(err, "decode", "sign key")
|
||||
}
|
||||
if len(keyBytes) != ed25519.PrivateKeySize {
|
||||
return cli.Err("sign key must be %d bytes when decoded", ed25519.PrivateKeySize)
|
||||
}
|
||||
|
||||
if err := manifest.Sign(m, ed25519.PrivateKey(keyBytes)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := manifest.MarshalYAML(m)
|
||||
if err != nil {
|
||||
return cli.WrapVerb(err, "marshal", "manifest")
|
||||
}
|
||||
|
||||
if err := medium.Write(".core/manifest.yaml", string(data)); err != nil {
|
||||
return cli.WrapVerb(err, "write", ".core/manifest.yaml")
|
||||
}
|
||||
|
||||
cli.Blank()
|
||||
cli.Print(" %s %s\n", successStyle.Render("signed"), valueStyle.Render(m.Code))
|
||||
cli.Print(" %s %s\n", dimStyle.Render("output:"), valueStyle.Render(".core/manifest.yaml"))
|
||||
cli.Blank()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addVerifyCommand(parent *cli.Command) {
|
||||
var (
|
||||
dir string
|
||||
publicKey string
|
||||
)
|
||||
|
||||
cmd := &cli.Command{
|
||||
Use: "verify",
|
||||
Short: "Verify manifest signature with a public key",
|
||||
Long: "Read .core/manifest.yaml and verify its ed25519 signature against a public key.",
|
||||
RunE: func(cmd *cli.Command, args []string) error {
|
||||
return runVerify(dir, publicKey)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&dir, "dir", "d", ".", "Project root directory")
|
||||
cmd.Flags().StringVar(&publicKey, "public-key", "", "Hex-encoded ed25519 public key")
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
}
|
||||
|
||||
func runVerify(dir, publicKeyHex string) error {
|
||||
if publicKeyHex == "" {
|
||||
return cli.Err("public key is required")
|
||||
}
|
||||
|
||||
medium, err := io.NewSandboxed(dir)
|
||||
if err != nil {
|
||||
return cli.WrapVerb(err, "open", dir)
|
||||
}
|
||||
|
||||
m, err := manifest.Load(medium, ".")
|
||||
if err != nil {
|
||||
return cli.WrapVerb(err, "load", "manifest")
|
||||
}
|
||||
|
||||
keyBytes, err := hex.DecodeString(publicKeyHex)
|
||||
if err != nil {
|
||||
return cli.WrapVerb(err, "decode", "public key")
|
||||
}
|
||||
if len(keyBytes) != ed25519.PublicKeySize {
|
||||
return cli.Err("public key must be %d bytes when decoded", ed25519.PublicKeySize)
|
||||
}
|
||||
|
||||
valid, err := manifest.Verify(m, ed25519.PublicKey(keyBytes))
|
||||
if err != nil {
|
||||
return cli.WrapVerb(err, "verify", "manifest")
|
||||
}
|
||||
if !valid {
|
||||
return cli.Err("signature verification failed for %s", m.Code)
|
||||
}
|
||||
|
||||
cli.Blank()
|
||||
cli.Success("Signature verified")
|
||||
cli.Print(" %s %s\n", dimStyle.Render("code:"), valueStyle.Render(m.Code))
|
||||
cli.Blank()
|
||||
|
||||
return nil
|
||||
}
|
||||
99
cmd/scm/cmd_sign_verify_test.go
Normal file
99
cmd/scm/cmd_sign_verify_test.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package scm
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
filepath "dappco.re/go/core/scm/internal/ax/filepathx"
|
||||
os "dappco.re/go/core/scm/internal/ax/osx"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/io"
|
||||
"dappco.re/go/core/scm/manifest"
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRunSign_Good_WritesSignedManifest_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
coreDir := filepath.Join(dir, ".core")
|
||||
require.NoError(t, os.MkdirAll(coreDir, 0755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(coreDir, "manifest.yaml"), []byte(`
|
||||
code: signed-cli
|
||||
name: Signed CLI
|
||||
version: 1.0.0
|
||||
`), 0644))
|
||||
|
||||
pub, priv, err := ed25519.GenerateKey(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = runSign(dir, hex.EncodeToString(priv))
|
||||
require.NoError(t, err)
|
||||
|
||||
raw, err := io.Local.Read(filepath.Join(dir, ".core", "manifest.yaml"))
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := manifest.Parse([]byte(raw))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "signed-cli", m.Code)
|
||||
assert.NotEmpty(t, m.Sign)
|
||||
|
||||
valid, err := manifest.Verify(m, pub)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, valid)
|
||||
}
|
||||
|
||||
func TestRunVerify_Good_ValidSignature_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
coreDir := filepath.Join(dir, ".core")
|
||||
require.NoError(t, os.MkdirAll(coreDir, 0755))
|
||||
|
||||
pub, priv, err := ed25519.GenerateKey(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := &manifest.Manifest{
|
||||
Code: "verified-cli",
|
||||
Name: "Verified CLI",
|
||||
Version: "1.0.0",
|
||||
}
|
||||
require.NoError(t, manifest.Sign(m, priv))
|
||||
|
||||
data, err := manifest.MarshalYAML(m)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(coreDir, "manifest.yaml"), data, 0644))
|
||||
|
||||
err = runVerify(dir, hex.EncodeToString(pub))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAddScmCommands_Good_SignAndVerifyRegistered_Good(t *testing.T) {
|
||||
root := &cli.Command{Use: "root"}
|
||||
|
||||
AddScmCommands(root)
|
||||
|
||||
var scmCmd *cli.Command
|
||||
for _, cmd := range root.Commands() {
|
||||
if cmd.Name() == "scm" {
|
||||
scmCmd = cmd
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, scmCmd)
|
||||
|
||||
var signCmd *cli.Command
|
||||
var verifyCmd *cli.Command
|
||||
for _, cmd := range scmCmd.Commands() {
|
||||
switch cmd.Name() {
|
||||
case "sign":
|
||||
signCmd = cmd
|
||||
case "verify":
|
||||
verifyCmd = cmd
|
||||
}
|
||||
}
|
||||
require.NotNil(t, signCmd)
|
||||
require.NotNil(t, verifyCmd)
|
||||
assert.NotNil(t, signCmd.Flags().Lookup("sign-key"))
|
||||
assert.NotNil(t, verifyCmd.Flags().Lookup("public-key"))
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue