478 lines
14 KiB
Go
478 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
|
)
|
|
|
|
func main() {
|
|
entries, err := os.ReadDir("fixtures/conformance")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
var results []string
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || filepath.Ext(entry.Name()) != ".tsv" {
|
|
continue
|
|
}
|
|
path := filepath.Join("fixtures/conformance", entry.Name())
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
lines := strings.Split(strings.TrimSuffix(string(data), "\n"), "\n")
|
|
if len(lines) == 0 || lines[0] != "id\tversion\tkind\tinput\texpected" {
|
|
panic("invalid fixture header")
|
|
}
|
|
for _, line := range lines[1:] {
|
|
fields := strings.Split(line, "\t")
|
|
if len(fields) != 5 {
|
|
panic("invalid fixture row")
|
|
}
|
|
actual := evaluate(fields[1], fields[2], fields[3])
|
|
if actual != fields[4] {
|
|
panic(fmt.Sprintf("%s: got %s want %s", fields[0], actual, fields[4]))
|
|
}
|
|
results = append(results, fields[0]+"\t"+actual)
|
|
}
|
|
}
|
|
fixtureHash := readFixtureHash()
|
|
fmt.Printf("Go conformance passed normalized=%s fixtures=%s\n", normalizedDigest(results), fixtureHash)
|
|
}
|
|
|
|
func evaluate(version, kind, input string) string {
|
|
parts := map[string]string{}
|
|
for _, item := range strings.Split(input, ";") {
|
|
pair := strings.SplitN(item, "=", 2)
|
|
if len(pair) == 2 {
|
|
parts[pair[0]] = pair[1]
|
|
}
|
|
}
|
|
switch kind {
|
|
case "device_proof_transcript":
|
|
serverID, serverErr := hex.DecodeString(parts["server_id"])
|
|
principalID, principalErr := hex.DecodeString(parts["principal_id"])
|
|
deviceID, deviceErr := hex.DecodeString(parts["device_id"])
|
|
challenge, challengeErr := hex.DecodeString(parts["challenge"])
|
|
expiry, expiryErr := strconv.ParseInt(parts["expiry_unix_ms"], 10, 64)
|
|
if serverErr != nil || principalErr != nil || deviceErr != nil || challengeErr != nil || expiryErr != nil {
|
|
return "invalid:fixture"
|
|
}
|
|
transcript, err := protocol.DeviceRegistrationProofTranscript(serverID, principalID, deviceID, challenge, expiry)
|
|
if err != nil {
|
|
return "invalid:device_proof"
|
|
}
|
|
return hex.EncodeToString(transcript)
|
|
case "version":
|
|
if input == "2" || input == "1" || input == "0" {
|
|
return "valid"
|
|
}
|
|
return "invalid:unsupported_version"
|
|
case "page":
|
|
limit, err := strconv.Atoi(parts["limit"])
|
|
value := protocol.PageInfo{Limit: int64(limit), NextCursor: parts["cursor"]}
|
|
if err == nil && value.Validate() == nil {
|
|
return "valid"
|
|
}
|
|
return "invalid:invalid_limit"
|
|
case "manifest":
|
|
forbidden := []string{"provider_url", "vm_address", "password", "private_key"}
|
|
for _, key := range forbidden {
|
|
if _, ok := parts[key]; ok {
|
|
return "invalid:forbidden_field"
|
|
}
|
|
}
|
|
value := protocol.ConnectionManifest{
|
|
Version: parts["version"], Purpose: parts["purpose"], SessionID: "session-1",
|
|
ReconnectSequence: 0,
|
|
Gateway: protocol.ManifestGateway{
|
|
ID: parts["gateway_id"], Addresses: []string{"gateway.control.test:443"}, PublicIdentity: parts["public_identity"],
|
|
},
|
|
Tunnel: protocol.ManifestTunnel{Versions: []string{parts["protocol"] + "/1"}, Features: []string{"control.v1"}},
|
|
Profile: protocol.ManifestProfile{ID: "standard", Bounds: protocol.ManifestBounds{MinimumKbps: 1, TargetKbps: 2, MaximumKbps: 3}},
|
|
Grant: protocol.GrantReference{OpaqueValue: parts["grant"], ExpiresAt: parts["expires_at"], Audience: parts["audience"]},
|
|
CorrelationID: "correlation-1",
|
|
}
|
|
if value.Validate() == nil {
|
|
return "valid"
|
|
}
|
|
return "invalid:invalid_manifest"
|
|
case "clipboard":
|
|
_, hasFile := parts["file"]
|
|
value := protocol.ClipboardText{Text: parts["text"], Encoding: parts["encoding"]}
|
|
if !hasFile && value.Validate() == nil {
|
|
return "valid"
|
|
}
|
|
return "invalid:unsupported_clipboard"
|
|
case "session_request":
|
|
if version != "2" {
|
|
return "invalid:unsupported_version"
|
|
}
|
|
if _, supplied := parts["policy_snapshot"]; supplied {
|
|
return "invalid:forbidden_field"
|
|
}
|
|
value := protocol.SessionRequest{
|
|
ClientDeviceID: parts["client_device_id"], DeviceKeyID: parts["device_key_id"],
|
|
PoolID: parts["pool_id"], IdempotencyKey: parts["idempotency_key"],
|
|
}
|
|
if value.Validate() == nil {
|
|
return "valid"
|
|
}
|
|
return "invalid:required"
|
|
case "browser_authenticated_session":
|
|
if _, hasDevice := parts["client_device_id"]; hasDevice {
|
|
return "invalid:forbidden_field"
|
|
}
|
|
if _, hasKey := parts["device_key_id"]; hasKey {
|
|
return "invalid:forbidden_field"
|
|
}
|
|
value := protocol.BrowserAuthenticatedSession{
|
|
Username: parts["username"], Provider: parts["provider"], Roles: []string{parts["roles"]},
|
|
Role: parts["role"],
|
|
}
|
|
if value.Validate() == nil {
|
|
return "valid"
|
|
}
|
|
return "invalid:invalid_session"
|
|
case "native_authenticated_session":
|
|
clientDeviceID, hasDevice := parts["client_device_id"]
|
|
deviceKeyID, hasKey := parts["device_key_id"]
|
|
if !hasDevice || !hasKey {
|
|
return "invalid:required"
|
|
}
|
|
value := protocol.NativeAuthenticatedSession{
|
|
Username: parts["username"], Provider: parts["provider"], Roles: []string{parts["roles"]}, Role: parts["role"],
|
|
NativeIdentity: protocol.NativeSessionIdentity{ClientDeviceID: clientDeviceID, DeviceKeyID: deviceKeyID},
|
|
}
|
|
if value.Validate() == nil {
|
|
return "valid"
|
|
}
|
|
return "invalid:invalid_session"
|
|
case "native_tunnel_credential":
|
|
value := protocol.NativeTunnelCredential{
|
|
ClientDeviceID: parts["client_device_id"], DeviceKeyID: parts["device_key_id"],
|
|
CertificateChainPem: parts["certificate_chain_pem"], TrustBundlePem: parts["trust_bundle_pem"],
|
|
ExpiresAt: parts["expires_at"],
|
|
}
|
|
if value.Validate() == nil {
|
|
return "valid"
|
|
}
|
|
return "invalid:invalid_credential"
|
|
case "event":
|
|
sequence, sequenceErr := strconv.ParseInt(parts["sequence"], 10, 64)
|
|
payloadBytes, payloadErr := strconv.Atoi(parts["payload_bytes"])
|
|
if parts["version"] != "1" {
|
|
return "invalid:unsupported_version"
|
|
}
|
|
if parts["after"] != "" && parts["earliest"] != "" {
|
|
after, afterErr := strconv.ParseInt(parts["after"], 10, 64)
|
|
earliest, earliestErr := strconv.ParseInt(parts["earliest"], 10, 64)
|
|
if afterErr == nil && earliestErr == nil && after > 0 && earliest > 0 && after < earliest-1 {
|
|
return "invalid:gap"
|
|
}
|
|
}
|
|
value := protocol.EventEnvelope{
|
|
EventID: "event-1", Sequence: sequence, Type: "broker.session.changed", Version: 1,
|
|
Resource: protocol.ResourceLink{Type: "broker_session", ID: "session-1", Version: 1},
|
|
OccurredAt: "2099-01-01T00:00:00Z", CorrelationID: parts["correlation_id"], Payload: map[string]any{},
|
|
}
|
|
if payloadErr != nil || payloadBytes > 16384 {
|
|
return "invalid:payload_limit"
|
|
}
|
|
if sequenceErr != nil || value.Validate() != nil {
|
|
return "invalid:required"
|
|
}
|
|
return "valid"
|
|
case "tunnel":
|
|
feature := parts["feature"]
|
|
registered := feature == "control.v1" || feature == "control.v2" || feature == "display.request.v1" || feature == "input.absolute.v1" || feature == "input.scroll.v1"
|
|
if (parts["offered"] == "2" || parts["offered"] == "1" || parts["offered"] == "0") && registered {
|
|
return "valid"
|
|
}
|
|
if !registered {
|
|
return "invalid:unsupported_feature"
|
|
}
|
|
return "invalid:unsupported_version"
|
|
case "datagram":
|
|
return classifyDatagram(parts["hex"])
|
|
case "gateway_input":
|
|
return classifyGatewayInput(parts["hex"])
|
|
case "gateway_feedback":
|
|
return classifyGatewayFeedback(parts["hex"])
|
|
case "gateway_clipboard":
|
|
if _, hasFile := parts["file"]; hasFile {
|
|
return "invalid:forbidden"
|
|
}
|
|
value := protocol.GatewayClipboardText{Direction: parts["direction"], Text: parts["text"], Encoding: parts["encoding"], LoopToken: parts["loop_token"]}
|
|
if value.Validate() != nil {
|
|
return "invalid:clipboard"
|
|
}
|
|
return "valid"
|
|
case "gateway_clipboard_audit":
|
|
if _, hasText := parts["text"]; hasText {
|
|
return "invalid:forbidden"
|
|
}
|
|
textBytes, err := strconv.ParseInt(parts["text_bytes"], 10, 64)
|
|
if err != nil {
|
|
return "invalid:clipboard_audit"
|
|
}
|
|
value := protocol.GatewayClipboardAudit{Version: "1", SessionID: "fixture-session", Direction: parts["direction"], Outcome: parts["outcome"], TextBytes: textBytes, Reason: parts["reason"]}
|
|
if value.Validate() != nil {
|
|
return "invalid:clipboard_audit"
|
|
}
|
|
return "valid"
|
|
default:
|
|
return "invalid:unknown_kind"
|
|
}
|
|
}
|
|
|
|
func decodeGatewayHex(encoded string) ([]byte, string) {
|
|
raw, err := hex.DecodeString(encoded)
|
|
if err != nil {
|
|
return nil, "invalid:hex"
|
|
}
|
|
return raw, ""
|
|
}
|
|
|
|
func classifyGatewayInput(encoded string) string {
|
|
raw, invalid := decodeGatewayHex(encoded)
|
|
if invalid != "" {
|
|
return invalid
|
|
}
|
|
if len(raw) < 6 {
|
|
return "invalid:truncated"
|
|
}
|
|
if string(raw[:4]) != "VGI1" {
|
|
return "invalid:magic"
|
|
}
|
|
kind, length := raw[4], int(raw[5])
|
|
if len(raw) != 6+length {
|
|
return "invalid:length"
|
|
}
|
|
body := raw[6:]
|
|
switch kind {
|
|
case 1:
|
|
if len(body) != 4 || body[0] > 1 || (body[2] == 0 && body[3] == 0) {
|
|
return "invalid:field"
|
|
}
|
|
case 2:
|
|
if len(body) != 3 {
|
|
return "invalid:length"
|
|
}
|
|
if body[0] > 1 || body[1] < 1 || body[1] > 5 {
|
|
return "invalid:field"
|
|
}
|
|
if body[2] != 0 {
|
|
return "invalid:reserved"
|
|
}
|
|
case 3:
|
|
if len(body) != 4 {
|
|
return "invalid:length"
|
|
}
|
|
case 4:
|
|
if len(body) < 1 || len(body) > 4 || !utf8.Valid(body) || utf8.RuneCount(body) != 1 {
|
|
return "invalid:utf8"
|
|
}
|
|
case 5:
|
|
if len(body) != 17 {
|
|
return "invalid:length"
|
|
}
|
|
if body[0] > 15 {
|
|
return "invalid:field"
|
|
}
|
|
if body[1] == 0 && body[2] == 0 {
|
|
for _, value := range body[3:] {
|
|
if value != 0 {
|
|
return "invalid:field"
|
|
}
|
|
}
|
|
}
|
|
case 6:
|
|
if len(body) != 8 {
|
|
return "invalid:length"
|
|
}
|
|
x, y := uint16(body[0])<<8|uint16(body[1]), uint16(body[2])<<8|uint16(body[3])
|
|
width, height := uint16(body[4])<<8|uint16(body[5]), uint16(body[6])<<8|uint16(body[7])
|
|
if width == 0 || height == 0 || x >= width || y >= height {
|
|
return "invalid:field"
|
|
}
|
|
case 7:
|
|
if len(body) != 4 {
|
|
return "invalid:length"
|
|
}
|
|
default:
|
|
return "invalid:kind"
|
|
}
|
|
return "valid"
|
|
}
|
|
|
|
func classifyGatewayFeedback(encoded string) string {
|
|
raw, invalid := decodeGatewayHex(encoded)
|
|
if invalid != "" {
|
|
return invalid
|
|
}
|
|
if len(raw) < 8 {
|
|
return "invalid:truncated"
|
|
}
|
|
if string(raw[:4]) != "VGF1" {
|
|
return "invalid:magic"
|
|
}
|
|
direction, kind := raw[4], raw[5]
|
|
if len(raw) != 8+(int(raw[6])<<8)+int(raw[7]) {
|
|
return "invalid:length"
|
|
}
|
|
if direction != 0 && direction != 1 {
|
|
return "invalid:direction"
|
|
}
|
|
body := raw[8:]
|
|
if direction == 0 {
|
|
if kind >= 0x10 && kind <= 0x12 {
|
|
return "invalid:direction"
|
|
}
|
|
switch kind {
|
|
case 1:
|
|
if len(body) == 0 {
|
|
return "valid"
|
|
}
|
|
case 2:
|
|
if validFECStatus(body) {
|
|
return "valid"
|
|
}
|
|
return "invalid:field"
|
|
case 3:
|
|
if len(body) == 0 {
|
|
return "valid"
|
|
}
|
|
default:
|
|
return "invalid:type"
|
|
}
|
|
return "invalid:length"
|
|
}
|
|
if kind == 1 || kind == 2 || kind == 3 {
|
|
return "invalid:direction"
|
|
}
|
|
switch kind {
|
|
case 0x10:
|
|
if len(body) == 4 {
|
|
return "valid"
|
|
}
|
|
return "invalid:length"
|
|
case 0x11:
|
|
if len(body) != 5 {
|
|
return "invalid:length"
|
|
}
|
|
if body[0] <= 15 {
|
|
return "valid"
|
|
}
|
|
case 0x12:
|
|
if len(body) != 1 {
|
|
return "invalid:length"
|
|
}
|
|
if body[0] <= 1 {
|
|
return "valid"
|
|
}
|
|
default:
|
|
return "invalid:type"
|
|
}
|
|
return "invalid:field"
|
|
}
|
|
|
|
func validFECStatus(body []byte) bool {
|
|
if len(body) != 21 || int(body[10])<<8|int(body[11]) == 0 || int(body[14])<<8|int(body[15]) > int(body[10])<<8|int(body[11]) || int(body[16])<<8|int(body[17]) > int(body[12])<<8|int(body[13]) || body[18] > 100 || body[20] == 0 || body[19] >= body[20] {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func classifyDatagram(encoded string) string {
|
|
raw, err := hex.DecodeString(encoded)
|
|
if err != nil {
|
|
return "invalid:hex"
|
|
}
|
|
if len(raw) < 3 {
|
|
return "invalid:truncated"
|
|
}
|
|
if string(raw[:2]) != "VD" {
|
|
return "invalid:magic"
|
|
}
|
|
if raw[2] != 1 && raw[2] != 2 {
|
|
return "invalid:unsupported_version"
|
|
}
|
|
headerBytes := 21
|
|
limits := map[byte]int{1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
|
|
if raw[2] == 2 {
|
|
headerBytes = 23
|
|
limits = map[byte]int{10: 1177, 11: 1177}
|
|
}
|
|
if len(raw) < headerBytes {
|
|
return "invalid:truncated"
|
|
}
|
|
limit, ok := limits[raw[3]]
|
|
if !ok {
|
|
return "invalid:unknown_channel"
|
|
}
|
|
if raw[4] != 0 {
|
|
return "invalid:flags"
|
|
}
|
|
fragmentIndex, fragmentCount := int(raw[17]), int(raw[18])
|
|
payloadOffset := 19
|
|
if raw[2] == 2 {
|
|
fragmentIndex = int(raw[17])<<8 | int(raw[18])
|
|
fragmentCount = int(raw[19])<<8 | int(raw[20])
|
|
payloadOffset = 21
|
|
if fragmentCount > 891 {
|
|
return "invalid:fragment_limit"
|
|
}
|
|
}
|
|
if fragmentCount == 0 || fragmentIndex >= fragmentCount {
|
|
return "invalid:fragment"
|
|
}
|
|
payloadLength := int(raw[payloadOffset])<<8 | int(raw[payloadOffset+1])
|
|
if payloadLength > limit {
|
|
return "invalid:payload_limit"
|
|
}
|
|
if len(raw) != headerBytes+payloadLength {
|
|
return "invalid:length_mismatch"
|
|
}
|
|
if raw[2] == 1 && len(raw) > 65536 || raw[2] == 2 && len(raw) > 1200 {
|
|
return "invalid:frame_limit"
|
|
}
|
|
return "valid"
|
|
}
|
|
|
|
func normalizedDigest(results []string) string {
|
|
const offset = uint64(14695981039346656037)
|
|
const prime = uint64(1099511628211)
|
|
value := offset
|
|
for _, result := range results {
|
|
for _, byteValue := range []byte(result + "\n") {
|
|
value ^= uint64(byteValue)
|
|
value *= prime
|
|
}
|
|
}
|
|
return fmt.Sprintf("%016x", value)
|
|
}
|
|
|
|
func readFixtureHash() string {
|
|
data, err := os.ReadFile("fixtures/manifest.json")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
var manifest struct {
|
|
CorpusSHA256 string `json:"corpus_sha256"`
|
|
}
|
|
if err := json.Unmarshal(data, &manifest); err != nil || len(manifest.CorpusSHA256) != sha256.Size*2 {
|
|
panic("invalid fixture manifest")
|
|
}
|
|
return manifest.CorpusSHA256
|
|
}
|