feat: authenticate Windows command launcher and signal helper
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
package windows
|
||||
|
||||
// The launcher wire format is deliberately private to one service/launcher
|
||||
// pair. It is not a second public protocol: the channel name is random, the
|
||||
// generation is bound into every frame, and every frame is length- and
|
||||
// checksum-validated before its payload is decoded.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
launcherMagic uint32 = 0x52564c31 // RVL1
|
||||
launcherVersion uint16 = 1
|
||||
launcherHeaderBytes = 4 + 2 + 2 + 16 + 4 + sha256.Size
|
||||
launcherMaxFrameBytes = 4 << 20
|
||||
)
|
||||
|
||||
type launcherFrameKind uint16
|
||||
|
||||
const (
|
||||
launcherFrameHello launcherFrameKind = iota + 1
|
||||
launcherFrameLaunch
|
||||
launcherFrameShellPrepared
|
||||
launcherFrameRelease
|
||||
launcherFrameReleased
|
||||
launcherFrameAbort
|
||||
launcherFrameSignalRequest
|
||||
launcherFrameSignalResult
|
||||
)
|
||||
|
||||
type launcherFrame struct {
|
||||
Kind launcherFrameKind
|
||||
Generation [16]byte
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func marshalLauncherFrame(frame launcherFrame) ([]byte, error) {
|
||||
if frame.Kind == 0 || frame.Kind > launcherFrameSignalResult {
|
||||
return nil, errors.New("launcher frame kind is invalid")
|
||||
}
|
||||
if uint64(len(frame.Payload)) > launcherMaxFrameBytes {
|
||||
return nil, fmt.Errorf("launcher frame payload exceeds %d bytes", launcherMaxFrameBytes)
|
||||
}
|
||||
encoded := make([]byte, launcherHeaderBytes+len(frame.Payload))
|
||||
binary.LittleEndian.PutUint32(encoded[0:4], launcherMagic)
|
||||
binary.LittleEndian.PutUint16(encoded[4:6], launcherVersion)
|
||||
binary.LittleEndian.PutUint16(encoded[6:8], uint16(frame.Kind))
|
||||
copy(encoded[8:24], frame.Generation[:])
|
||||
binary.LittleEndian.PutUint32(encoded[24:28], uint32(len(frame.Payload)))
|
||||
copy(encoded[launcherHeaderBytes:], frame.Payload)
|
||||
check := make([]byte, 28+len(frame.Payload))
|
||||
copy(check, encoded[:28])
|
||||
copy(check[28:], frame.Payload)
|
||||
digest := sha256.Sum256(check)
|
||||
copy(encoded[28:launcherHeaderBytes], digest[:])
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
func readLauncherFrame(reader io.Reader, expected [16]byte) (launcherFrame, error) {
|
||||
header := make([]byte, launcherHeaderBytes)
|
||||
if _, err := io.ReadFull(reader, header); err != nil {
|
||||
return launcherFrame{}, err
|
||||
}
|
||||
if binary.LittleEndian.Uint32(header[0:4]) != launcherMagic {
|
||||
return launcherFrame{}, errors.New("launcher frame magic mismatch")
|
||||
}
|
||||
if binary.LittleEndian.Uint16(header[4:6]) != launcherVersion {
|
||||
return launcherFrame{}, errors.New("launcher frame version mismatch")
|
||||
}
|
||||
kind := launcherFrameKind(binary.LittleEndian.Uint16(header[6:8]))
|
||||
if kind == 0 || kind > launcherFrameSignalResult {
|
||||
return launcherFrame{}, errors.New("launcher frame kind is invalid")
|
||||
}
|
||||
var generation [16]byte
|
||||
copy(generation[:], header[8:24])
|
||||
if generation != expected {
|
||||
return launcherFrame{}, errors.New("launcher frame generation mismatch")
|
||||
}
|
||||
length := binary.LittleEndian.Uint32(header[24:28])
|
||||
if length > launcherMaxFrameBytes {
|
||||
return launcherFrame{}, fmt.Errorf("launcher frame payload exceeds %d bytes", launcherMaxFrameBytes)
|
||||
}
|
||||
payload := make([]byte, int(length))
|
||||
if _, err := io.ReadFull(reader, payload); err != nil {
|
||||
return launcherFrame{}, err
|
||||
}
|
||||
check := make([]byte, 28+len(payload))
|
||||
copy(check, header[:28])
|
||||
copy(check[28:], payload)
|
||||
digest := sha256.Sum256(check)
|
||||
if string(digest[:]) != string(header[28:launcherHeaderBytes]) {
|
||||
return launcherFrame{}, errors.New("launcher frame checksum mismatch")
|
||||
}
|
||||
return launcherFrame{Kind: kind, Generation: generation, Payload: payload}, nil
|
||||
}
|
||||
|
||||
func readLauncherFrameContext(ctx context.Context, reader io.Reader, expected [16]byte) (launcherFrame, error) {
|
||||
result := make(chan struct {
|
||||
frame launcherFrame
|
||||
err error
|
||||
}, 1)
|
||||
go func() {
|
||||
frame, err := readLauncherFrame(reader, expected)
|
||||
result <- struct {
|
||||
frame launcherFrame
|
||||
err error
|
||||
}{frame: frame, err: err}
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return launcherFrame{}, ctx.Err()
|
||||
case value := <-result:
|
||||
return value.frame, value.err
|
||||
}
|
||||
}
|
||||
|
||||
func writeLauncherFrame(writer io.Writer, frame launcherFrame) error {
|
||||
encoded, err := marshalLauncherFrame(frame)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for len(encoded) > 0 {
|
||||
written, err := writer.Write(encoded)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if written <= 0 || written > len(encoded) {
|
||||
return errors.New("launcher frame writer made no progress")
|
||||
}
|
||||
encoded = encoded[written:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user