mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-27 05:27:00 +00:00
cmd: add Ed25519 key generation and Lua script signing
This commit is contained in:
+60
-23
@@ -46,35 +46,72 @@ func decodePrivilegedValue(value string, size int) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func verifyPrivilegedSignature(code []byte) (bool, error) {
|
func verifyPrivilegedSignature(code []byte) (bool, error) {
|
||||||
for len(code) != 0 {
|
_, end, value, found := privilegedSignatureLine(code)
|
||||||
line, rest, _ := bytes.Cut(code, []byte{'\n'})
|
if !found {
|
||||||
code = rest
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKey := privilegedKey.publicKey
|
||||||
|
if len(publicKey) == 0 {
|
||||||
|
return false, errors.New("Lua privileged signature requires --lua-privileged-pubkey")
|
||||||
|
}
|
||||||
|
signature, err := decodePrivilegedValue(string(bytes.TrimSpace(value)), ed25519.SignatureSize)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.New("invalid Lua privileged signature").Base(err)
|
||||||
|
}
|
||||||
|
digest := sha256.Sum256(bytes.TrimSpace(code[end:]))
|
||||||
|
if !ed25519.Verify(publicKey, digest[:], signature) {
|
||||||
|
return false, errors.New("Lua privileged signature verification failed")
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignPrivilegedScript signs the script body, then prepends or replaces the signature line.
|
||||||
|
func SignPrivilegedScript(code []byte, seed string) ([]byte, error) {
|
||||||
|
decoded, err := decodePrivilegedValue(seed, ed25519.SeedSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("invalid Ed25519 private key seed").Base(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
start, end, _, found := privilegedSignatureLine(code)
|
||||||
|
body := code
|
||||||
|
if found {
|
||||||
|
body = code[end:]
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := sha256.Sum256(bytes.TrimSpace(body))
|
||||||
|
signature := ed25519.Sign(ed25519.NewKeyFromSeed(decoded), digest[:])
|
||||||
|
header := []byte("-- @xray:privileged-signature=" + base64.RawURLEncoding.EncodeToString(signature))
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
if newline := bytes.IndexByte(code, '\n'); newline > 0 && code[newline-1] == '\r' {
|
||||||
|
header = append(header, '\r')
|
||||||
|
}
|
||||||
|
header = append(header, '\n')
|
||||||
|
return append(header, code...), nil
|
||||||
|
}
|
||||||
|
signed := make([]byte, 0, start+len(header)+len(code)-end)
|
||||||
|
signed = append(signed, code[:start]...)
|
||||||
|
signed = append(signed, header...)
|
||||||
|
return append(signed, code[end:]...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func privilegedSignatureLine(code []byte) (start, end int, value []byte, found bool) {
|
||||||
|
for rest := code; len(rest) != 0; {
|
||||||
|
start = len(code) - len(rest)
|
||||||
|
var line []byte
|
||||||
|
line, rest, _ = bytes.Cut(rest, []byte{'\n'})
|
||||||
line = bytes.TrimSuffix(line, []byte{'\r'})
|
line = bytes.TrimSuffix(line, []byte{'\r'})
|
||||||
if len(bytes.TrimSpace(line)) == 0 {
|
if len(bytes.TrimSpace(line)) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
end = start + len(line)
|
||||||
line, found := bytes.CutPrefix(bytes.TrimLeft(line, " \t"), []byte("--"))
|
line, found := bytes.CutPrefix(bytes.TrimLeft(line, " \t"), []byte("--"))
|
||||||
if !found {
|
if !found {
|
||||||
return false, nil
|
return 0, 0, nil, false
|
||||||
}
|
}
|
||||||
value, found := bytes.CutPrefix(bytes.TrimLeft(line, " \t"), []byte("@xray:privileged-signature="))
|
value, found = bytes.CutPrefix(bytes.TrimLeft(line, " \t"), []byte("@xray:privileged-signature="))
|
||||||
if !found {
|
return start, end, value, found
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
publicKey := privilegedKey.publicKey
|
|
||||||
if len(publicKey) == 0 {
|
|
||||||
return false, errors.New("Lua privileged signature requires --lua-privileged-pubkey")
|
|
||||||
}
|
|
||||||
signature, err := decodePrivilegedValue(string(bytes.TrimSpace(value)), ed25519.SignatureSize)
|
|
||||||
if err != nil {
|
|
||||||
return false, errors.New("invalid Lua privileged signature").Base(err)
|
|
||||||
}
|
|
||||||
digest := sha256.Sum256(bytes.TrimSpace(rest))
|
|
||||||
if !ed25519.Verify(publicKey, digest[:], signature) {
|
|
||||||
return false, errors.New("Lua privileged signature verification failed")
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
return false, nil
|
return 0, 0, nil, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package lua
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func privilegedSeed(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
seed, err := hex.DecodeString(privilegedTestSeed)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return base64.RawURLEncoding.EncodeToString(seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignPrivilegedScriptPreservesSource(t *testing.T) {
|
||||||
|
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||||
|
seed := privilegedSeed(t)
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name, prefix, header, body, newline string
|
||||||
|
}{
|
||||||
|
{name: "unsigned", body: privilegedFixtureBody, newline: "\n"},
|
||||||
|
{name: "unsigned boundary whitespace", body: " \t\n\n" + privilegedFixtureBody + " \t\n", newline: "\n"},
|
||||||
|
{name: "unsigned CRLF", body: strings.ReplaceAll(privilegedFixtureBody, "\n", "\r\n"), newline: "\r\n"},
|
||||||
|
{name: "replace invalid header", header: privilegedSignatureHeader + "%", body: "\n" + privilegedFixtureBody, newline: "\n"},
|
||||||
|
{name: "replace indented header", prefix: " \t\r\n\n", header: " \t--\t @xray:privileged-signature=% \t", body: "\r\n \t\r\n" + privilegedFixtureBody + "\r\n", newline: "\r\n"},
|
||||||
|
{name: "replace valid header", header: privilegedSignatureHeader + privilegedFixtureSignature, body: "\n" + privilegedFixtureBody, newline: "\n"},
|
||||||
|
{name: "empty script", newline: "\n"},
|
||||||
|
{name: "header without newline", header: privilegedSignatureHeader + "%"},
|
||||||
|
{name: "ordinary comment before marker", body: "-- ordinary comment\n" + privilegedSignatureHeader + "%\nvalue = 1", newline: "\n"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
code := []byte(tc.prefix + tc.header + tc.body)
|
||||||
|
original := bytes.Clone(code)
|
||||||
|
signed, err := SignPrivilegedScript(code, seed)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(code, original) {
|
||||||
|
t.Fatal("signing modified the input buffer")
|
||||||
|
}
|
||||||
|
body := tc.body
|
||||||
|
if tc.header != "" {
|
||||||
|
body = strings.TrimPrefix(body, tc.newline)
|
||||||
|
}
|
||||||
|
header := privilegedSignatureHeader + testSignature(t, body)
|
||||||
|
want := header + tc.newline + tc.body
|
||||||
|
if tc.header != "" {
|
||||||
|
want = tc.prefix + header + tc.body
|
||||||
|
}
|
||||||
|
if string(signed) != want {
|
||||||
|
t.Fatalf("signed source = %q, want %q", signed, want)
|
||||||
|
}
|
||||||
|
program, err := Compile(writeTestScript(t, string(signed)))
|
||||||
|
if err != nil || program == nil || !program.privileged {
|
||||||
|
t.Fatalf("signed source Compile = %v, %v; want privileged program", program, err)
|
||||||
|
}
|
||||||
|
again, err := SignPrivilegedScript(signed, seed)
|
||||||
|
if err != nil || !bytes.Equal(again, signed) {
|
||||||
|
t.Fatalf("resigning changed the source: %q, %v", again, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignPrivilegedScriptRejectsInvalidSeed(t *testing.T) {
|
||||||
|
seed := privilegedSeed(t)
|
||||||
|
for _, value := range []string{
|
||||||
|
"", "%", seed + "=",
|
||||||
|
base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.SeedSize-1)),
|
||||||
|
base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.PrivateKeySize)),
|
||||||
|
} {
|
||||||
|
if signed, err := SignPrivilegedScript([]byte(privilegedFixtureBody), value); signed != nil || err == nil {
|
||||||
|
t.Fatalf("SignPrivilegedScript with invalid seed = %q, %v; want rejection", signed, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -382,7 +382,6 @@ func TestProgramPrivilegedSignatureHeader(t *testing.T) {
|
|||||||
{"after code", "value = 1\n" + header + "\n" + privilegedFixtureBody, false},
|
{"after code", "value = 1\n" + header + "\n" + privilegedFixtureBody, false},
|
||||||
{"inside string", `value = "` + header + `"`, false},
|
{"inside string", `value = "` + header + `"`, false},
|
||||||
{"wrong marker", "-- xray:privileged-signature=" + privilegedFixtureSignature + "\n" + privilegedFixtureBody, false},
|
{"wrong marker", "-- xray:privileged-signature=" + privilegedFixtureSignature + "\n" + privilegedFixtureBody, false},
|
||||||
{"old PSK first", "-- @xray:privileged-psk=secret\n" + header + "\n" + privilegedFixtureBody, false},
|
|
||||||
{"invalid marker after comment", "-- ordinary comment\n" + privilegedSignatureHeader + "%\nvalue = 1", false},
|
{"invalid marker after comment", "-- ordinary comment\n" + privilegedSignatureHeader + "%\nvalue = 1", false},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
@@ -492,28 +491,6 @@ func TestProgramInlineSignaturesCannotAuthorize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProgramLegacyPSKCannotAuthorize(t *testing.T) {
|
|
||||||
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
|
||||||
t.Setenv("xray.lua.privileged.psk", "secret")
|
|
||||||
t.Setenv("XRAY_LUA_PRIVILEGED_PSK", "secret")
|
|
||||||
program, err := Compile(writeTestScript(t, "-- @xray:privileged-psk=secret\n"+privilegedFixtureBody))
|
|
||||||
if err != nil || program == nil || program.privileged {
|
|
||||||
t.Fatalf("old PSK authorization = %v, %v; want sandbox", program, err)
|
|
||||||
}
|
|
||||||
L, err := program.NewState(context.Background(), nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer L.Close()
|
|
||||||
if got := L.GetGlobal("os"); got != glua.LNil {
|
|
||||||
t.Fatalf("old PSK os = %v, want nil", got)
|
|
||||||
}
|
|
||||||
setTestPrivilegedPublicKey(t, "")
|
|
||||||
if program, err := Compile(writeTestScript(t, privilegedSignatureHeader+privilegedFixtureSignature+"\n"+privilegedFixtureBody)); program != nil || err == nil {
|
|
||||||
t.Fatalf("legacy environment substituted for public key: %v, %v", program, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProgramSignedDiagnostics(t *testing.T) {
|
func TestProgramSignedDiagnostics(t *testing.T) {
|
||||||
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||||
const invalidBody = "\n\nlocal = 1"
|
const invalidBody = "\n\nlocal = 1"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/xtls/xray-core/main/commands/all/api"
|
"github.com/xtls/xray-core/main/commands/all/api"
|
||||||
"github.com/xtls/xray-core/main/commands/all/base64"
|
"github.com/xtls/xray-core/main/commands/all/base64"
|
||||||
"github.com/xtls/xray-core/main/commands/all/convert"
|
"github.com/xtls/xray-core/main/commands/all/convert"
|
||||||
|
"github.com/xtls/xray-core/main/commands/all/lua"
|
||||||
"github.com/xtls/xray-core/main/commands/all/tls"
|
"github.com/xtls/xray-core/main/commands/all/tls"
|
||||||
"github.com/xtls/xray-core/main/commands/base"
|
"github.com/xtls/xray-core/main/commands/base"
|
||||||
)
|
)
|
||||||
@@ -14,9 +15,11 @@ func init() {
|
|||||||
api.CmdAPI,
|
api.CmdAPI,
|
||||||
base64.CmdBase64,
|
base64.CmdBase64,
|
||||||
convert.CmdConvert,
|
convert.CmdConvert,
|
||||||
|
lua.CmdLua,
|
||||||
tls.CmdTLS,
|
tls.CmdTLS,
|
||||||
cmdUUID,
|
cmdUUID,
|
||||||
cmdX25519,
|
cmdX25519,
|
||||||
|
cmdEd25519,
|
||||||
cmdWG,
|
cmdWG,
|
||||||
cmdMLDSA65,
|
cmdMLDSA65,
|
||||||
cmdMLKEM768,
|
cmdMLKEM768,
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package all
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/main/commands/base"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cmdEd25519 = &base.Command{
|
||||||
|
UsageLine: `{{.Exec}} ed25519 [-i "private key seed (Base64URL)"]`,
|
||||||
|
Short: "Generate an Ed25519 key pair",
|
||||||
|
Long: `
|
||||||
|
Generate an Ed25519 key pair, or restore it from a 32-byte private key seed
|
||||||
|
with -i. PrivateKey (seed) and PublicKey use unpadded Base64URL.
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
var inputEd25519 = cmdEd25519.Flag.String("i", "", "Ed25519 private key seed in unpadded Base64URL.")
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cmdEd25519.Run = executeEd25519
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeEd25519(cmd *base.Command, args []string) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
cmd.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
var privateKey ed25519.PrivateKey
|
||||||
|
if *inputEd25519 == "" {
|
||||||
|
_, key, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
base.Fatalf("failed to generate Ed25519 key: %s", err)
|
||||||
|
}
|
||||||
|
privateKey = key
|
||||||
|
} else {
|
||||||
|
encoding := base64.RawURLEncoding.Strict()
|
||||||
|
seed, err := encoding.DecodeString(*inputEd25519)
|
||||||
|
if err != nil || len(seed) != ed25519.SeedSize || encoding.EncodeToString(seed) != *inputEd25519 {
|
||||||
|
base.Fatalf("invalid Ed25519 private key seed: expected 32 bytes in unpadded Base64URL")
|
||||||
|
}
|
||||||
|
privateKey = ed25519.NewKeyFromSeed(seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := fmt.Printf("PrivateKey: %s\nPublicKey: %s\n",
|
||||||
|
base64.RawURLEncoding.EncodeToString(privateKey.Seed()),
|
||||||
|
base64.RawURLEncoding.EncodeToString(privateKey.Public().(ed25519.PublicKey))); err != nil {
|
||||||
|
base.Fatalf("failed to write Ed25519 keys: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package lua
|
||||||
|
|
||||||
|
import "github.com/xtls/xray-core/main/commands/base"
|
||||||
|
|
||||||
|
// CmdLua holds the Lua script commands.
|
||||||
|
var CmdLua = &base.Command{
|
||||||
|
UsageLine: "{{.Exec}} lua",
|
||||||
|
Short: "Manage Lua scripts",
|
||||||
|
Long: "{{.Exec}} {{.LongName}} provides tools for local Lua scripts.",
|
||||||
|
Commands: []*base.Command{
|
||||||
|
cmdSign,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package lua
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
xlua "github.com/xtls/xray-core/common/lua"
|
||||||
|
"github.com/xtls/xray-core/main/commands/base"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cmdSign = &base.Command{
|
||||||
|
UsageLine: "{{.Exec}} lua sign [-key seed] [-o output-file] <input-file>",
|
||||||
|
Short: "Sign a local Lua script for privileged execution",
|
||||||
|
Long: `
|
||||||
|
Sign a local Lua file using a 32-byte Ed25519 private key seed in unpadded
|
||||||
|
Base64URL. Use {{.Exec}} ed25519 to generate a key pair.
|
||||||
|
Write back to the input file, or use -o to write another file (- for stdout).
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
signKey = cmdSign.Flag.String("key", "", "Ed25519 private key seed in unpadded Base64URL.")
|
||||||
|
signOutput = cmdSign.Flag.String("o", "", "Output file, or - for stdout. Default: input file.")
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cmdSign.Run = executeSign
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeSign(cmd *base.Command, args []string) {
|
||||||
|
if len(args) != 1 {
|
||||||
|
cmd.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Stat(args[0])
|
||||||
|
if err != nil {
|
||||||
|
base.Fatalf("failed to inspect Lua file: %s", err)
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
base.Fatalf("Lua input is not a regular file: %s", args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
code, err := os.ReadFile(args[0])
|
||||||
|
if err != nil {
|
||||||
|
base.Fatalf("failed to read Lua file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signed, err := xlua.SignPrivilegedScript(code, *signKey)
|
||||||
|
if err != nil {
|
||||||
|
base.Fatalf("failed to sign Lua file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *signOutput == "-" {
|
||||||
|
if _, err := os.Stdout.Write(signed); err != nil {
|
||||||
|
base.Fatalf("failed to write signed Lua file: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
output := *signOutput
|
||||||
|
if output == "" {
|
||||||
|
output = args[0]
|
||||||
|
}
|
||||||
|
if err := writeSignedFile(output, signed, info.Mode().Perm()); err != nil {
|
||||||
|
base.Fatalf("failed to write signed Lua file: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeSignedFile(path string, code []byte, mode os.FileMode) error {
|
||||||
|
if info, err := os.Stat(path); err == nil {
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
return &os.PathError{Op: "write", Path: path, Err: os.ErrInvalid}
|
||||||
|
}
|
||||||
|
mode = info.Mode().Perm()
|
||||||
|
path, err = filepath.EvalSymlinks(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.CreateTemp(filepath.Dir(path), ".xray-lua-sign-*")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(file.Name())
|
||||||
|
defer file.Close()
|
||||||
|
if _, err := file.Write(code); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.Chmod(mode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Rename(file.Name(), path)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user