mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-14 11:34:03 +00:00
Replace privileged PSK with Ed25519 signature verification
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"sync"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
var privilegedKey struct {
|
||||
sync.Once
|
||||
publicKey ed25519.PublicKey
|
||||
}
|
||||
|
||||
// SetPrivilegedPublicKey configures the trusted Ed25519 public key once at
|
||||
// startup, before compiling scripts. An empty value disables authorization.
|
||||
// Nonempty values must be unpadded Base64URL encodings of exactly 32 bytes.
|
||||
func SetPrivilegedPublicKey(value string) error {
|
||||
var publicKey ed25519.PublicKey
|
||||
if value != "" {
|
||||
decoded, err := decodePrivilegedValue(value, ed25519.PublicKeySize)
|
||||
if err != nil {
|
||||
return errors.New("invalid Lua privileged public key").Base(err)
|
||||
}
|
||||
publicKey = ed25519.PublicKey(decoded)
|
||||
}
|
||||
privilegedKey.Do(func() {
|
||||
privilegedKey.publicKey = publicKey
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodePrivilegedValue(value string, size int) ([]byte, error) {
|
||||
encoding := base64.RawURLEncoding.Strict()
|
||||
if len(value) != encoding.EncodedLen(size) {
|
||||
return nil, errors.New("expected an unpadded Base64URL encoding of ", size, " bytes")
|
||||
}
|
||||
decoded, err := encoding.DecodeString(value)
|
||||
if err != nil || len(decoded) != size || encoding.EncodeToString(decoded) != value {
|
||||
return nil, errors.New("expected an unpadded Base64URL encoding of ", size, " bytes")
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
func verifyPrivilegedSignature(code []byte) (bool, error) {
|
||||
for len(code) != 0 {
|
||||
line, rest, _ := bytes.Cut(code, []byte{'\n'})
|
||||
code = rest
|
||||
line = bytes.TrimSuffix(line, []byte{'\r'})
|
||||
if len(bytes.TrimSpace(line)) == 0 {
|
||||
continue
|
||||
}
|
||||
line, found := bytes.CutPrefix(bytes.TrimLeft(line, " \t"), []byte("--"))
|
||||
if !found {
|
||||
return false, nil
|
||||
}
|
||||
value, found := bytes.CutPrefix(bytes.TrimLeft(line, " \t"), []byte("@xray:privileged-signature="))
|
||||
if !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
|
||||
}
|
||||
+3
-26
@@ -32,8 +32,9 @@ func Compile(script string) (*Program, error) {
|
||||
if strings.HasPrefix(script, base64.GzipPrefix) || strings.HasPrefix(script, base64.Prefix) {
|
||||
code, err = base64.Decode(script)
|
||||
} else if name, err = platform.ResolveLuaFile(script); err == nil {
|
||||
code, err = os.ReadFile(name)
|
||||
privileged = hasPrivilegedPSK(code)
|
||||
if code, err = os.ReadFile(name); err == nil {
|
||||
privileged, err = verifyPrivilegedSignature(code)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -50,30 +51,6 @@ func Compile(script string) (*Program, error) {
|
||||
return &Program{proto: proto, privileged: privileged}, nil
|
||||
}
|
||||
|
||||
func hasPrivilegedPSK(code []byte) bool {
|
||||
expected := strings.TrimSpace(platform.NewEnvFlag(platform.LuaPrivilegedPSK).GetValue(func() string { return "" }))
|
||||
if expected == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for len(code) != 0 {
|
||||
line, rest, _ := bytes.Cut(code, []byte{'\n'})
|
||||
code = rest
|
||||
line = bytes.TrimSuffix(line, []byte{'\r'})
|
||||
if len(bytes.TrimSpace(line)) == 0 {
|
||||
continue
|
||||
}
|
||||
line, found := bytes.CutPrefix(bytes.TrimLeft(line, " \t"), []byte("--"))
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
key, found := bytes.CutPrefix(bytes.TrimLeft(line, " \t"), []byte("@xray:privileged-psk="))
|
||||
key = bytes.TrimSpace(key)
|
||||
return found && len(key) != 0 && string(key) == expected
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NewState creates a state, runs register, executes the program under ctx, and
|
||||
// runs validate. It removes the initialization context before returning a state
|
||||
// owned by the caller.
|
||||
|
||||
+339
-57
@@ -4,18 +4,21 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
"weak"
|
||||
|
||||
xbase64 "github.com/xtls/xray-core/common/base64"
|
||||
"github.com/xtls/xray-core/common/platform"
|
||||
glua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
@@ -160,7 +163,7 @@ func TestCompileInlineDiagnostics(t *testing.T) {
|
||||
}{
|
||||
{"function broken(", true, ""},
|
||||
{`error("initialization failed")`, false, ""},
|
||||
{"\n-- @xray:privileged-psk=secret\n\nerror('failure')", false, "[inline]:4: failure"},
|
||||
{"\n-- @xray:privileged-signature=invalid\n\nerror('failure')", false, "[inline]:4: failure"},
|
||||
} {
|
||||
program, err := Compile(xbase64.Prefix + base64.StdEncoding.EncodeToString([]byte(tc.code)))
|
||||
if tc.compileError {
|
||||
@@ -181,8 +184,8 @@ func TestCompileInlineDiagnostics(t *testing.T) {
|
||||
if err == nil || !strings.Contains(err.Error(), "[inline]") {
|
||||
t.Fatalf("Lua error = %v, want [inline] diagnostic", err)
|
||||
}
|
||||
if tc.wantMessage != "" && (!strings.Contains(err.Error(), tc.wantMessage) || strings.Contains(err.Error(), "secret")) {
|
||||
t.Fatalf("Lua error = %v, want %q without the PSK", err, tc.wantMessage)
|
||||
if tc.wantMessage != "" && !strings.Contains(err.Error(), tc.wantMessage) {
|
||||
t.Fatalf("Lua error = %v, want %q", err, tc.wantMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,55 +216,327 @@ func compileTestProgram(t *testing.T, code string) *Program {
|
||||
return program
|
||||
}
|
||||
|
||||
func TestProgramPrivilegedPSK(t *testing.T) {
|
||||
const header = "-- @xray:privileged-psk="
|
||||
for _, tc := range []struct {
|
||||
name, psk, code string
|
||||
privileged bool
|
||||
}{
|
||||
{"match", "secret", header + "secret", true},
|
||||
{"no comment space", "secret", "--@xray:privileged-psk=secret", true},
|
||||
{"comment whitespace", "secret", "-- \t @xray:privileged-psk=secret", true},
|
||||
{"mismatch", "secret", header + "other", false},
|
||||
{"both empty", "", header, false},
|
||||
{"missing header", "secret", "value = 1", false},
|
||||
{"empty header", "secret", header, false},
|
||||
{"blank lines and indentation", "secret", " \t\n\n \t" + header + "secret", true},
|
||||
{"CRLF", "secret", "\r\n" + header + "secret\r\nvalue = 1\r\n", true},
|
||||
{"key whitespace", " \tsecret \t", header + " secret ", true},
|
||||
{"whitespace-only keys", " \t", header + " \t", false},
|
||||
{"after comment", "secret", "-- ordinary comment\n" + header + "secret", false},
|
||||
{"after code", "secret", "value = 1\n" + header + "secret", false},
|
||||
{"inside string", "secret", `value = "-- @xray:privileged-psk=secret"`, false},
|
||||
{"wrong marker", "secret", "-- xray:privileged-psk=secret", false},
|
||||
{"later match", "secret", header + "other\n" + header + "secret", false},
|
||||
const privilegedSignatureHeader = "-- @xray:privileged-signature="
|
||||
|
||||
// RFC 8032 test-vector key. The fixture signature was independently computed
|
||||
// with Node's OpenSSL-backed crypto.sign(null, SHA256(body), privateKey), rather
|
||||
// than by the signing helper or the production verifier.
|
||||
const privilegedTestSeed = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"
|
||||
const privilegedTestPublicKey = "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
|
||||
const privilegedFixtureSignature = "rFbZtaaphe_69UEgSvfYwZcLZYHTXLFxdcukfEuLq6pzvmj8qtw6r60fD-7L5dwqSvnE7F1oAV0G_vHP3rLVBA"
|
||||
const privilegedFixtureDigest = "edef639ae641bacc9b76cea27473eed11369fbad65e223a87990864e899e578a"
|
||||
const privilegedFixtureBody = `-- Ordinary comments participate in the signed digest.
|
||||
value = 1
|
||||
|
||||
function HasPrivilegedLibraries()
|
||||
return io ~= nil and os ~= nil and debug ~= nil and channel ~= nil
|
||||
end`
|
||||
|
||||
func setTestPrivilegedPublicKey(t *testing.T, key string) {
|
||||
t.Helper()
|
||||
// Each test setup represents a new process with its own startup key.
|
||||
privilegedKey.Once = sync.Once{}
|
||||
privilegedKey.publicKey = nil
|
||||
t.Cleanup(func() {
|
||||
privilegedKey.Once = sync.Once{}
|
||||
privilegedKey.publicKey = nil
|
||||
})
|
||||
if err := SetPrivilegedPublicKey(key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeTestScript(t *testing.T, code string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "script.lua")
|
||||
if err := os.WriteFile(path, []byte(code), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func testSignature(t *testing.T, body string) string {
|
||||
t.Helper()
|
||||
seed, err := hex.DecodeString(privilegedTestSeed)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
digest := sha256.Sum256(bytes.TrimSpace([]byte(body)))
|
||||
return base64.RawURLEncoding.EncodeToString(ed25519.Sign(ed25519.NewKeyFromSeed(seed), digest[:]))
|
||||
}
|
||||
|
||||
func TestSetPrivilegedPublicKey(t *testing.T) {
|
||||
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||
path := writeTestScript(t, privilegedSignatureHeader+privilegedFixtureSignature+"\n"+privilegedFixtureBody)
|
||||
for _, tc := range []struct{ name, key string }{
|
||||
{"invalid base64", "%"},
|
||||
{"padding", privilegedTestPublicKey + "="},
|
||||
{"standard base64 alphabet", strings.ReplaceAll(privilegedTestPublicKey, "_", "/")},
|
||||
{"short key", base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.PublicKeySize-1))},
|
||||
{"long key", base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.PublicKeySize+1))},
|
||||
{"leading whitespace", " " + privilegedTestPublicKey},
|
||||
{"trailing whitespace", privilegedTestPublicKey + "\t"},
|
||||
{"embedded newline", privilegedTestPublicKey[:8] + "\n" + privilegedTestPublicKey[8:]},
|
||||
{"embedded carriage return", privilegedTestPublicKey[:8] + "\r" + privilegedTestPublicKey[8:]},
|
||||
{"nonzero unused bits", privilegedTestPublicKey[:len(privilegedTestPublicKey)-1] + "p"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Setenv(platform.LuaPrivilegedPSK, tc.psk)
|
||||
path := filepath.Join(t.TempDir(), "psk.lua")
|
||||
if err := os.WriteFile(path, []byte(tc.code), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
if err := SetPrivilegedPublicKey(tc.key); err == nil {
|
||||
t.Fatal("invalid public key was accepted")
|
||||
}
|
||||
for _, source := range []struct {
|
||||
name, script string
|
||||
privileged bool
|
||||
}{
|
||||
{"file", path, tc.privileged},
|
||||
{"base64", xbase64.Prefix + base64.StdEncoding.EncodeToString([]byte(tc.code)), false},
|
||||
{"gzip", xbase64.GzipPrefix + base64.StdEncoding.EncodeToString(compressSource(t, tc.code)), false},
|
||||
} {
|
||||
t.Run(source.name, func(t *testing.T) {
|
||||
program, err := Compile(source.script)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if program.privileged != source.privileged {
|
||||
t.Fatalf("privileged = %t, want %t", program.privileged, source.privileged)
|
||||
}
|
||||
})
|
||||
program, err := Compile(path)
|
||||
if err != nil || program == nil || !program.privileged {
|
||||
t.Fatalf("invalid setter changed the trusted key: Compile = %v, %v", program, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
setTestPrivilegedPublicKey(t, "")
|
||||
if program, err := Compile(path); program != nil || err == nil {
|
||||
t.Fatalf("signed script after disabling authorization = %v, %v; want rejection", program, err)
|
||||
}
|
||||
unsigned, err := Compile(writeTestScript(t, privilegedFixtureBody))
|
||||
if err != nil || unsigned == nil || unsigned.privileged {
|
||||
t.Fatalf("unsigned script with authorization disabled = %v, %v; want sandbox", unsigned, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrivilegedPublicKeyIsFixed(t *testing.T) {
|
||||
wrongKey := base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.PublicKeySize))
|
||||
for _, key := range []string{"", privilegedTestPublicKey} {
|
||||
name := "disabled"
|
||||
if key != "" {
|
||||
name = "configured"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
setTestPrivilegedPublicKey(t, key)
|
||||
path := writeTestScript(t, privilegedSignatureHeader+privilegedFixtureSignature+"\n"+privilegedFixtureBody)
|
||||
for _, replacement := range []string{privilegedTestPublicKey, "", wrongKey} {
|
||||
if err := SetPrivilegedPublicKey(replacement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
program, err := Compile(path)
|
||||
if key == "" {
|
||||
if program != nil || err == nil {
|
||||
t.Fatalf("disabled startup key changed: Compile = %v, %v", program, err)
|
||||
}
|
||||
} else if err != nil || program == nil || !program.privileged {
|
||||
t.Fatalf("trusted startup key changed: Compile = %v, %v", program, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgramPrivilegedSignatureFixture(t *testing.T) {
|
||||
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||
digest := sha256.Sum256([]byte(privilegedFixtureBody))
|
||||
if got := hex.EncodeToString(digest[:]); got != privilegedFixtureDigest {
|
||||
t.Fatalf("fixture SHA-256 = %s, want %s", got, privilegedFixtureDigest)
|
||||
}
|
||||
for _, signed := range []bool{false, true} {
|
||||
name, code := "unsigned", privilegedFixtureBody
|
||||
if signed {
|
||||
name, code = "signed", privilegedSignatureHeader+privilegedFixtureSignature+"\n"+code
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
program, err := Compile(writeTestScript(t, code))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if program.privileged != signed {
|
||||
t.Fatalf("privileged = %t, want %t", program.privileged, signed)
|
||||
}
|
||||
L, err := program.NewState(context.Background(), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer L.Close()
|
||||
if err := L.CallByParam(glua.P{Fn: L.GetGlobal("HasPrivilegedLibraries"), NRet: 1, Protect: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := L.Get(-1); got != glua.LBool(signed) {
|
||||
t.Fatalf("privileged libraries available = %v, want %t", got, signed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgramPrivilegedSignatureHeader(t *testing.T) {
|
||||
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||
header := privilegedSignatureHeader + privilegedFixtureSignature
|
||||
for _, tc := range []struct {
|
||||
name, code string
|
||||
privileged bool
|
||||
}{
|
||||
{"match", header + "\n" + privilegedFixtureBody, true},
|
||||
{"no comment space", "--@xray:privileged-signature=" + privilegedFixtureSignature + "\n" + privilegedFixtureBody, true},
|
||||
{"comment whitespace", "-- \t @xray:privileged-signature=" + privilegedFixtureSignature + "\n" + privilegedFixtureBody, true},
|
||||
{"signature whitespace", privilegedSignatureHeader + " \t" + privilegedFixtureSignature + " \t\n" + privilegedFixtureBody, true},
|
||||
{"blank lines and indentation", " \t\r\n\n \t" + header + "\n" + privilegedFixtureBody, true},
|
||||
{"CRLF header only", "\r\n" + header + "\r\n" + privilegedFixtureBody, true},
|
||||
{"body boundary whitespace", header + "\n \t\r\n\n" + privilegedFixtureBody + " \t\r\n\n", true},
|
||||
{"missing header", privilegedFixtureBody, false},
|
||||
{"after comment", "-- ordinary comment\n" + header + "\n" + privilegedFixtureBody, false},
|
||||
{"after code", "value = 1\n" + header + "\n" + privilegedFixtureBody, false},
|
||||
{"inside string", `value = "` + header + `"`, 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},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
program, err := Compile(writeTestScript(t, tc.code))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if program.privileged != tc.privileged {
|
||||
t.Fatalf("privileged = %t, want %t", program.privileged, tc.privileged)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgramRejectsInvalidPrivilegedSignatures(t *testing.T) {
|
||||
seed, err := hex.DecodeString(privilegedTestSeed)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wrongKey := base64.RawURLEncoding.EncodeToString(ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)).Public().(ed25519.PublicKey))
|
||||
bodySignature := base64.RawURLEncoding.EncodeToString(ed25519.Sign(ed25519.NewKeyFromSeed(seed), []byte(privilegedFixtureBody)))
|
||||
for _, tc := range []struct{ name, key, signature string }{
|
||||
{"missing key", "", privilegedFixtureSignature},
|
||||
{"wrong key", wrongKey, privilegedFixtureSignature},
|
||||
{"empty signature", privilegedTestPublicKey, ""},
|
||||
{"whitespace-only signature", privilegedTestPublicKey, " \t"},
|
||||
{"invalid base64", privilegedTestPublicKey, "%"},
|
||||
{"padding", privilegedTestPublicKey, privilegedFixtureSignature + "=="},
|
||||
{"standard base64 alphabet", privilegedTestPublicKey, strings.ReplaceAll(privilegedFixtureSignature, "_", "/")},
|
||||
{"short signature", privilegedTestPublicKey, base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.SignatureSize-1))},
|
||||
{"long signature", privilegedTestPublicKey, base64.RawURLEncoding.EncodeToString(make([]byte, ed25519.SignatureSize+1))},
|
||||
{"embedded carriage return", privilegedTestPublicKey, privilegedFixtureSignature[:8] + "\r" + privilegedFixtureSignature[8:]},
|
||||
{"nonzero unused bits", privilegedTestPublicKey, privilegedFixtureSignature[:len(privilegedFixtureSignature)-1] + "B"},
|
||||
{"damaged signature", privilegedTestPublicKey, "s" + privilegedFixtureSignature[1:]},
|
||||
{"signature over body instead of digest", privilegedTestPublicKey, bodySignature},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
setTestPrivilegedPublicKey(t, tc.key)
|
||||
program, err := Compile(writeTestScript(t, privilegedSignatureHeader+tc.signature+"\n"+privilegedFixtureBody))
|
||||
if program != nil || err == nil {
|
||||
t.Fatalf("Compile = %v, %v; want nil program and signature rejection", program, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||
code := privilegedSignatureHeader + "%\n" + privilegedSignatureHeader + privilegedFixtureSignature + "\n" + privilegedFixtureBody
|
||||
if program, err := Compile(writeTestScript(t, code)); program != nil || err == nil {
|
||||
t.Fatalf("later signature replaced invalid first signature: %v, %v", program, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgramSignedBodyTampering(t *testing.T) {
|
||||
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||
for _, tc := range []struct{ name, body string }{
|
||||
{"statement", strings.Replace(privilegedFixtureBody, "value = 1", "value = 2", 1)},
|
||||
{"comment", strings.Replace(privilegedFixtureBody, "Ordinary", "Modified", 1)},
|
||||
{"added comment", "-- added comment\n" + privilegedFixtureBody},
|
||||
{"indentation", strings.Replace(privilegedFixtureBody, " return", " return", 1)},
|
||||
{"internal spaces", strings.Replace(privilegedFixtureBody, "value = 1", "value = 1", 1)},
|
||||
{"internal trailing spaces", strings.Replace(privilegedFixtureBody, "value = 1\n", "value = 1 \t\n", 1)},
|
||||
{"removed blank line", strings.Replace(privilegedFixtureBody, "\n\n", "\n", 1)},
|
||||
{"added blank line", strings.Replace(privilegedFixtureBody, "\n\n", "\n\n\n", 1)},
|
||||
{"LF to CRLF", strings.ReplaceAll(privilegedFixtureBody, "\n", "\r\n")},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
code := privilegedSignatureHeader + privilegedFixtureSignature + "\n" + tc.body
|
||||
if program, err := Compile(writeTestScript(t, code)); program != nil || err == nil {
|
||||
t.Fatalf("tampered body Compile = %v, %v; want signature rejection", program, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
crlfBody := strings.ReplaceAll(privilegedFixtureBody, "\n", "\r\n")
|
||||
crlfSignature := testSignature(t, crlfBody)
|
||||
program, err := Compile(writeTestScript(t, privilegedSignatureHeader+crlfSignature+"\r\n"+crlfBody))
|
||||
if err != nil || program == nil || !program.privileged {
|
||||
t.Fatalf("correctly signed CRLF body = %v, %v; want privileged program", program, err)
|
||||
}
|
||||
if program, err := Compile(writeTestScript(t, privilegedSignatureHeader+crlfSignature+"\n"+privilegedFixtureBody)); program != nil || err == nil {
|
||||
t.Fatalf("CRLF to LF tampering = %v, %v; want signature rejection", program, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgramInlineSignaturesCannotAuthorize(t *testing.T) {
|
||||
for _, key := range []string{"", privilegedTestPublicKey} {
|
||||
setTestPrivilegedPublicKey(t, key)
|
||||
for _, signature := range []string{privilegedFixtureSignature, "%", ""} {
|
||||
code := privilegedSignatureHeader + signature + "\n" + privilegedFixtureBody
|
||||
for _, script := range []string{
|
||||
xbase64.Prefix + base64.StdEncoding.EncodeToString([]byte(code)),
|
||||
xbase64.GzipPrefix + base64.StdEncoding.EncodeToString(compressSource(t, code)),
|
||||
} {
|
||||
program, err := Compile(script)
|
||||
if err != nil || program == nil || program.privileged {
|
||||
t.Fatalf("inline signed source Compile = %v, %v; want sandbox without verification", program, err)
|
||||
}
|
||||
L, err := program.NewState(context.Background(), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := L.GetGlobal("os"); got != glua.LNil {
|
||||
L.Close()
|
||||
t.Fatalf("inline os = %v, want nil", got)
|
||||
}
|
||||
L.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||
const invalidBody = "\n\nlocal = 1"
|
||||
invalidPath := writeTestScript(t, "\n"+privilegedSignatureHeader+"%\n"+invalidBody)
|
||||
if program, err := Compile(invalidPath); program != nil || err == nil || !strings.Contains(err.Error(), "signature") {
|
||||
t.Fatalf("invalid signature and invalid syntax = %v, %v; want signature error before parsing", program, err)
|
||||
}
|
||||
validPath := writeTestScript(t, "\n"+privilegedSignatureHeader+testSignature(t, invalidBody)+"\n"+invalidBody)
|
||||
if program, err := Compile(validPath); program != nil || err == nil || !strings.Contains(err.Error(), validPath+" line:5(") {
|
||||
t.Fatalf("verified invalid syntax = %v, %v; want original source line 5", program, err)
|
||||
}
|
||||
const body = "\n\nerror('signed failure')"
|
||||
path := writeTestScript(t, "\n"+privilegedSignatureHeader+testSignature(t, body)+"\n"+body)
|
||||
program, err := Compile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
L, err := program.NewState(context.Background(), nil, nil)
|
||||
if L != nil {
|
||||
L.Close()
|
||||
t.Fatal("failing signed script returned a state")
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), path+":5: signed failure") {
|
||||
t.Fatalf("signed initialization error = %v; want original source line 5", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgramSandbox(t *testing.T) {
|
||||
@@ -444,9 +719,8 @@ func TestProgramSandboxClosesFailedInitialization(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProgramPrivilegedLibraries(t *testing.T) {
|
||||
t.Setenv(platform.LuaPrivilegedPSK, "secret")
|
||||
const code = `-- @xray:privileged-psk=secret
|
||||
assert(require("io") == io and require("os") == os)
|
||||
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||
const body = `assert(require("io") == io and require("os") == os)
|
||||
assert(require("debug") == debug and require("channel") == channel)
|
||||
assert(type(coroutine.create) == "function" and require("coroutine") == coroutine)
|
||||
assert(type(dofile) == "function" and type(loadfile) == "function")
|
||||
@@ -458,6 +732,7 @@ assert(assert(loadstring("return 42"))() == 42)
|
||||
local file = assert(io.open(testPath, "w"))
|
||||
file:write("privileged")
|
||||
file:close()`
|
||||
code := privilegedSignatureHeader + testSignature(t, body) + "\n" + body
|
||||
path := filepath.Join(t.TempDir(), "privileged.lua")
|
||||
if err := os.WriteFile(path, []byte(code), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -482,7 +757,7 @@ file:close()`
|
||||
if !tc.privileged {
|
||||
if L != nil {
|
||||
L.Close()
|
||||
t.Fatal("inline script with a matching PSK accessed privileged libraries")
|
||||
t.Fatal("inline script with a valid signature accessed privileged libraries")
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), "no preloaded module: io") {
|
||||
t.Fatalf("NewState error = %v, want io module unavailable", err)
|
||||
@@ -505,18 +780,18 @@ file:close()`
|
||||
}
|
||||
|
||||
func TestProgramPrivilegesAreCompileTimeSnapshot(t *testing.T) {
|
||||
setTestPrivilegedPublicKey(t, privilegedTestPublicKey)
|
||||
for _, privileged := range []bool{false, true} {
|
||||
name := "sandboxed"
|
||||
if privileged {
|
||||
name = "privileged"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
before, after := "other", "secret"
|
||||
const body = "value = 1"
|
||||
code := body
|
||||
if privileged {
|
||||
before, after = after, before
|
||||
code = privilegedSignatureHeader + testSignature(t, body) + "\n" + body
|
||||
}
|
||||
t.Setenv(platform.LuaPrivilegedPSK, before)
|
||||
const code = "-- @xray:privileged-psk=secret\nvalue = 1"
|
||||
path := filepath.Join(t.TempDir(), "snapshot.lua")
|
||||
if err := os.WriteFile(path, []byte(code), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -525,7 +800,14 @@ func TestProgramPrivilegesAreCompileTimeSnapshot(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv(platform.LuaPrivilegedPSK, after)
|
||||
// Source edits cannot change an already compiled Program's authorization.
|
||||
changed := privilegedSignatureHeader + testSignature(t, body) + "\n" + body
|
||||
if privileged {
|
||||
changed = body
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(changed), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check := func(L *glua.LState) {
|
||||
t.Helper()
|
||||
if got := L.GetGlobal("os").Type() == glua.LTTable; got != privileged {
|
||||
@@ -546,7 +828,7 @@ func TestProgramPrivilegesAreCompileTimeSnapshot(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if recompiled.privileged == privileged {
|
||||
t.Fatal("recompilation did not observe the changed environment")
|
||||
t.Fatal("recompilation did not observe the changed source")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ const (
|
||||
BrowserDialerAddress = "xray.browser.dialer"
|
||||
XUDPLog = "xray.xudp.show"
|
||||
XUDPBaseKey = "xray.xudp.basekey"
|
||||
LuaPrivilegedPSK = "xray.lua.privileged.psk"
|
||||
|
||||
TunFdKey = "xray.tun.fd"
|
||||
)
|
||||
|
||||
+13
-8
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/xtls/xray-core/common/cmdarg"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
clog "github.com/xtls/xray-core/common/log"
|
||||
"github.com/xtls/xray-core/common/lua"
|
||||
"github.com/xtls/xray-core/common/platform"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/main/commands/base"
|
||||
@@ -36,9 +37,8 @@ The -confdir=dir flag sets a dir with multiple json config
|
||||
The -format=json flag sets the format of config files.
|
||||
Default "auto".
|
||||
|
||||
The config root env object sets process environment variables after all config
|
||||
files are parsed. Variables needed to locate or parse config files must be set
|
||||
in the process environment before Xray starts.
|
||||
The --lua-privileged-pubkey=key flag sets the trusted Ed25519 public key
|
||||
for privileged local Lua scripts, encoded as unpadded Base64URL.
|
||||
|
||||
The -test flag tells Xray to test config files only,
|
||||
without launching the server.
|
||||
@@ -53,11 +53,12 @@ func init() {
|
||||
}
|
||||
|
||||
var (
|
||||
configFiles cmdarg.Arg // "Config file for Xray.", the option is customed type, parse in main
|
||||
configDir string
|
||||
dump = cmdRun.Flag.Bool("dump", false, "Dump merged config only, without launching Xray server.")
|
||||
test = cmdRun.Flag.Bool("test", false, "Test config file only, without launching Xray server.")
|
||||
format = cmdRun.Flag.String("format", "auto", "Format of input file.")
|
||||
configFiles cmdarg.Arg // "Config file for Xray.", the option is customed type, parse in main
|
||||
configDir string
|
||||
dump = cmdRun.Flag.Bool("dump", false, "Dump merged config only, without launching Xray server.")
|
||||
test = cmdRun.Flag.Bool("test", false, "Test config file only, without launching Xray server.")
|
||||
format = cmdRun.Flag.String("format", "auto", "Format of input file.")
|
||||
luaPrivilegedPublicKey = cmdRun.Flag.String("lua-privileged-pubkey", "", "Unpadded Base64URL Ed25519 public key for privileged local Lua scripts.")
|
||||
|
||||
/* We have to do this here because Golang's Test will also need to parse flag, before
|
||||
* main func in this file is run.
|
||||
@@ -214,6 +215,10 @@ func getConfigFormat() string {
|
||||
}
|
||||
|
||||
func startXray() (core.Server, error) {
|
||||
if err := lua.SetPrivilegedPublicKey(*luaPrivilegedPublicKey); err != nil {
|
||||
return nil, errors.New("failed to configure Lua privileged authorization").Base(err)
|
||||
}
|
||||
|
||||
configFiles := getConfigFilePath(true)
|
||||
|
||||
c, err := core.LoadConfig(getConfigFormat(), configFiles)
|
||||
|
||||
Reference in New Issue
Block a user