Compare commits

...
Author SHA1 Message Date
sechmachine 36f6edffca feat(protocol): include Apollo client identity in session work
Verify Protocol / verify (push) Canceled after 0s
Verify Protocol / module (push) Successful in 2m35s
2026-07-29 11:28:25 +07:00
sechmachine 357e5e0dbc feat(protocol): add gateway provider session work
Verify Protocol / verify (push) Canceled after 0s
Verify Protocol / module (push) Successful in 2m30s
2026-07-29 11:10:32 +07:00
sechmachine 4a2772c053 feat(protocol): bind tunnel admission to device proof
Verify Protocol / verify (push) Canceled after 0s
Verify Protocol / module (push) Successful in 2m11s
2026-07-29 10:32:08 +07:00
10 changed files with 653 additions and 14 deletions
+281 -1
View File
@@ -8,10 +8,11 @@ import (
"fmt"
"io"
"reflect"
"strings"
"time"
)
const SchemaSHA256 = "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec"
const SchemaSHA256 = "792abfb9cfe70e79911d499d76c009ab848713278bc240b88576df520580e480"
const ProtocolVersion = "1.0.0"
const CurrentWireVersion = "1"
const NMinus1WireVersion = "0"
@@ -239,6 +240,26 @@ type PageInfo struct {
NextCursor string `json:"next_cursor"`
}
type ProviderSessionWork struct {
Version string `json:"version"`
SessionID string `json:"session_id"`
GatewayID string `json:"gateway_id"`
ReconnectSequence int64 `json:"reconnect_sequence"`
ExpiresAt string `json:"expires_at"`
ProviderProfile string `json:"provider_profile"`
ProviderIdentity string `json:"provider_identity"`
PolicyVersionID string `json:"policy_version_id"`
ApplicationID string `json:"application_id"`
ClientID string `json:"client_id"`
ManagementHost string `json:"management_host"`
ManagementPort int64 `json:"management_port"`
StreamHost string `json:"stream_host"`
StreamPort int64 `json:"stream_port"`
ClientCertificatePem string `json:"client_certificate_pem"`
ClientPrivateKeyPem string `json:"client_private_key_pem"`
ServerCertificatePem string `json:"server_certificate_pem"`
}
type ProviderState struct {
Version string `json:"version"`
SessionID string `json:"session_id"`
@@ -326,6 +347,7 @@ type TunnelAdmissionRequest struct {
Grant string `json:"grant"`
ReconnectSequence int64 `json:"reconnect_sequence"`
ClientNonce string `json:"client_nonce"`
DeviceSignature string `json:"device_signature"`
Capabilities CapabilityProfile `json:"capabilities"`
}
@@ -2915,6 +2937,242 @@ func EncodePageInfo(value PageInfo) ([]byte, error) {
return json.Marshal(value)
}
func (v ProviderSessionWork) Validate() error {
var violations []FieldViolation
if v.Version == "" {
violations = append(violations, FieldViolation{Field: "version", Code: "required"})
}
if v.Version != "1" && v.Version != "" {
violations = append(violations, FieldViolation{Field: "version", Code: "invalid_value"})
}
if v.SessionID == "" {
violations = append(violations, FieldViolation{Field: "session_id", Code: "required"})
}
if len(v.SessionID) < 1 && v.SessionID != "" {
violations = append(violations, FieldViolation{Field: "session_id", Code: "min_length"})
}
if len(v.SessionID) > 128 {
violations = append(violations, FieldViolation{Field: "session_id", Code: "max_length"})
}
if v.GatewayID == "" {
violations = append(violations, FieldViolation{Field: "gateway_id", Code: "required"})
}
if len(v.GatewayID) < 1 && v.GatewayID != "" {
violations = append(violations, FieldViolation{Field: "gateway_id", Code: "min_length"})
}
if len(v.GatewayID) > 128 {
violations = append(violations, FieldViolation{Field: "gateway_id", Code: "max_length"})
}
if v.ReconnectSequence != 0 && v.ReconnectSequence < 0 {
violations = append(violations, FieldViolation{Field: "reconnect_sequence", Code: "minimum"})
}
if v.ExpiresAt == "" {
violations = append(violations, FieldViolation{Field: "expires_at", Code: "required"})
}
if len(v.ExpiresAt) > 64 {
violations = append(violations, FieldViolation{Field: "expires_at", Code: "max_length"})
}
if v.ExpiresAt != "" {
if parsed, err := time.Parse(time.RFC3339Nano, v.ExpiresAt); err != nil || parsed.UTC().Format(time.RFC3339Nano) != v.ExpiresAt {
violations = append(violations, FieldViolation{Field: "expires_at", Code: "invalid_time"})
}
}
if v.ProviderProfile == "" {
violations = append(violations, FieldViolation{Field: "provider_profile", Code: "required"})
}
if v.ProviderProfile != "apollo" && v.ProviderProfile != "" {
violations = append(violations, FieldViolation{Field: "provider_profile", Code: "invalid_value"})
}
if v.ProviderIdentity == "" {
violations = append(violations, FieldViolation{Field: "provider_identity", Code: "required"})
}
if len(v.ProviderIdentity) < 1 && v.ProviderIdentity != "" {
violations = append(violations, FieldViolation{Field: "provider_identity", Code: "min_length"})
}
if len(v.ProviderIdentity) > 256 {
violations = append(violations, FieldViolation{Field: "provider_identity", Code: "max_length"})
}
if v.PolicyVersionID == "" {
violations = append(violations, FieldViolation{Field: "policy_version_id", Code: "required"})
}
if len(v.PolicyVersionID) < 1 && v.PolicyVersionID != "" {
violations = append(violations, FieldViolation{Field: "policy_version_id", Code: "min_length"})
}
if len(v.PolicyVersionID) > 128 {
violations = append(violations, FieldViolation{Field: "policy_version_id", Code: "max_length"})
}
if v.ApplicationID == "" {
violations = append(violations, FieldViolation{Field: "application_id", Code: "required"})
}
if len(v.ApplicationID) < 1 && v.ApplicationID != "" {
violations = append(violations, FieldViolation{Field: "application_id", Code: "min_length"})
}
if len(v.ApplicationID) > 128 {
violations = append(violations, FieldViolation{Field: "application_id", Code: "max_length"})
}
if v.ClientID == "" {
violations = append(violations, FieldViolation{Field: "client_id", Code: "required"})
}
if len(v.ClientID) < 1 && v.ClientID != "" {
violations = append(violations, FieldViolation{Field: "client_id", Code: "min_length"})
}
if len(v.ClientID) > 128 {
violations = append(violations, FieldViolation{Field: "client_id", Code: "max_length"})
}
if v.ManagementHost == "" {
violations = append(violations, FieldViolation{Field: "management_host", Code: "required"})
}
if len(v.ManagementHost) < 1 && v.ManagementHost != "" {
violations = append(violations, FieldViolation{Field: "management_host", Code: "min_length"})
}
if len(v.ManagementHost) > 256 {
violations = append(violations, FieldViolation{Field: "management_host", Code: "max_length"})
}
if v.ManagementPort == 0 {
violations = append(violations, FieldViolation{Field: "management_port", Code: "required"})
}
if v.ManagementPort != 0 && v.ManagementPort < 1 {
violations = append(violations, FieldViolation{Field: "management_port", Code: "minimum"})
}
if v.ManagementPort > 65535 {
violations = append(violations, FieldViolation{Field: "management_port", Code: "maximum"})
}
if v.StreamHost == "" {
violations = append(violations, FieldViolation{Field: "stream_host", Code: "required"})
}
if len(v.StreamHost) < 1 && v.StreamHost != "" {
violations = append(violations, FieldViolation{Field: "stream_host", Code: "min_length"})
}
if len(v.StreamHost) > 256 {
violations = append(violations, FieldViolation{Field: "stream_host", Code: "max_length"})
}
if v.StreamPort == 0 {
violations = append(violations, FieldViolation{Field: "stream_port", Code: "required"})
}
if v.StreamPort != 0 && v.StreamPort < 1 {
violations = append(violations, FieldViolation{Field: "stream_port", Code: "minimum"})
}
if v.StreamPort > 65535 {
violations = append(violations, FieldViolation{Field: "stream_port", Code: "maximum"})
}
if v.ClientCertificatePem == "" {
violations = append(violations, FieldViolation{Field: "client_certificate_pem", Code: "required"})
}
if len(v.ClientCertificatePem) < 1 && v.ClientCertificatePem != "" {
violations = append(violations, FieldViolation{Field: "client_certificate_pem", Code: "min_length"})
}
if len(v.ClientCertificatePem) > 32768 {
violations = append(violations, FieldViolation{Field: "client_certificate_pem", Code: "max_length"})
}
if v.ClientPrivateKeyPem == "" {
violations = append(violations, FieldViolation{Field: "client_private_key_pem", Code: "required"})
}
if len(v.ClientPrivateKeyPem) < 1 && v.ClientPrivateKeyPem != "" {
violations = append(violations, FieldViolation{Field: "client_private_key_pem", Code: "min_length"})
}
if len(v.ClientPrivateKeyPem) > 32768 {
violations = append(violations, FieldViolation{Field: "client_private_key_pem", Code: "max_length"})
}
if v.ServerCertificatePem == "" {
violations = append(violations, FieldViolation{Field: "server_certificate_pem", Code: "required"})
}
if len(v.ServerCertificatePem) < 1 && v.ServerCertificatePem != "" {
violations = append(violations, FieldViolation{Field: "server_certificate_pem", Code: "min_length"})
}
if len(v.ServerCertificatePem) > 32768 {
violations = append(violations, FieldViolation{Field: "server_certificate_pem", Code: "max_length"})
}
if len(violations) > 0 {
return ValidationError{Violations: violations}
}
return nil
}
func DecodeProviderSessionWork(data []byte) (ProviderSessionWork, error) {
var value ProviderSessionWork
if len(data) > 1024*1024 {
return value, errors.New("protocol payload exceeds limit")
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(data, &fields); err != nil {
return value, err
}
if raw, ok := fields["application_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "application_id", Code: "required"}}}
}
if raw, ok := fields["client_certificate_pem"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "client_certificate_pem", Code: "required"}}}
}
if raw, ok := fields["client_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "client_id", Code: "required"}}}
}
if raw, ok := fields["client_private_key_pem"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "client_private_key_pem", Code: "required"}}}
}
if raw, ok := fields["expires_at"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "expires_at", Code: "required"}}}
}
if raw, ok := fields["gateway_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "gateway_id", Code: "required"}}}
}
if raw, ok := fields["management_host"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "management_host", Code: "required"}}}
}
if raw, ok := fields["management_port"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "management_port", Code: "required"}}}
}
if raw, ok := fields["policy_version_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "policy_version_id", Code: "required"}}}
}
if raw, ok := fields["provider_identity"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "provider_identity", Code: "required"}}}
}
if raw, ok := fields["provider_profile"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "provider_profile", Code: "required"}}}
}
if raw, ok := fields["reconnect_sequence"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "reconnect_sequence", Code: "required"}}}
}
if raw, ok := fields["server_certificate_pem"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "server_certificate_pem", Code: "required"}}}
}
if raw, ok := fields["session_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "session_id", Code: "required"}}}
}
if raw, ok := fields["stream_host"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "stream_host", Code: "required"}}}
}
if raw, ok := fields["stream_port"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "stream_port", Code: "required"}}}
}
if raw, ok := fields["version"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "version", Code: "required"}}}
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil {
return value, err
}
var trailing any
if err := decoder.Decode(&trailing); err != io.EOF {
if err == nil {
return value, errors.New("trailing JSON value")
}
return value, err
}
if err := value.Validate(); err != nil {
return value, err
}
return value, nil
}
func EncodeProviderSessionWork(value ProviderSessionWork) ([]byte, error) {
if err := value.Validate(); err != nil {
return nil, err
}
return json.Marshal(value)
}
func (v ProviderState) Validate() error {
var violations []FieldViolation
if v.Version == "" {
@@ -3923,6 +4181,15 @@ func (v TunnelAdmissionRequest) Validate() error {
if len(v.ClientNonce) > 128 {
violations = append(violations, FieldViolation{Field: "client_nonce", Code: "max_length"})
}
if v.DeviceSignature == "" {
violations = append(violations, FieldViolation{Field: "device_signature", Code: "required"})
}
if len(v.DeviceSignature) < 86 && v.DeviceSignature != "" {
violations = append(violations, FieldViolation{Field: "device_signature", Code: "min_length"})
}
if len(v.DeviceSignature) > 86 {
violations = append(violations, FieldViolation{Field: "device_signature", Code: "max_length"})
}
if reflect.DeepEqual(v.Capabilities, CapabilityProfile{}) {
violations = append(violations, FieldViolation{Field: "capabilities", Code: "required"})
}
@@ -3953,6 +4220,9 @@ func DecodeTunnelAdmissionRequest(data []byte) (TunnelAdmissionRequest, error) {
if raw, ok := fields["client_nonce"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "client_nonce", Code: "required"}}}
}
if raw, ok := fields["device_signature"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "device_signature", Code: "required"}}}
}
if raw, ok := fields["gateway_id"]; !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
return value, ValidationError{Violations: []FieldViolation{{Field: "gateway_id", Code: "required"}}}
}
@@ -4073,3 +4343,13 @@ func IntersectCapabilityProfiles(profiles ...CapabilityProfile) (CapabilityProfi
}
return selected, nil
}
func (v TunnelAdmissionRequest) DeviceAdmissionTranscript() []byte {
fields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf("%d", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, v.Capabilities.ClientDecode}
var transcript strings.Builder
transcript.WriteString("versevdi/tunnel-admission/v1")
for _, field := range fields {
fmt.Fprintf(&transcript, "%d:%s", len(field), field)
}
return []byte(transcript.String())
}
+2 -2
View File
@@ -12,7 +12,7 @@
"2"
]
},
"generator_sha256": "88535ecf2b1c926104b10b4ab56f1bc6298e1c3e75490e36ee8581a135bcded7",
"generator_sha256": "e9c6ee1541585fcb00dcc5e94a5a6d93dbe3a719a5c545f31e5eda268f2638ab",
"protocol_version": "1.0.0",
"schema_sha256": "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec"
"schema_sha256": "792abfb9cfe70e79911d499d76c009ab848713278bc240b88576df520580e480"
}
Binary file not shown.
+105 -3
View File
@@ -1,6 +1,6 @@
// Code generated by tools/generate.py; DO NOT EDIT.
#![allow(non_snake_case)]
pub const SCHEMA_SHA256: &str = "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec";
pub const SCHEMA_SHA256: &str = "792abfb9cfe70e79911d499d76c009ab848713278bc240b88576df520580e480";
pub const CURRENT_WIRE_VERSION: &str = "1";
pub const N_MINUS_1_WIRE_VERSION: &str = "0";
pub const N_MINUS_2_WIRE_VERSION: &str = "-1";
@@ -982,6 +982,96 @@ impl PageInfo {
pub fn nextCursor(&self) -> &String { &self.nextCursor }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderSessionWork {
version: String,
sessionId: String,
gatewayId: String,
reconnectSequence: i64,
expiresAt: String,
providerProfile: String,
providerIdentity: String,
policyVersionId: String,
applicationId: String,
clientId: String,
managementHost: String,
managementPort: i64,
streamHost: String,
streamPort: i64,
clientCertificatePem: String,
clientPrivateKeyPem: String,
serverCertificatePem: String,
}
impl ProviderSessionWork {
pub fn new(version: String, sessionId: String, gatewayId: String, reconnectSequence: i64, expiresAt: String, providerProfile: String, providerIdentity: String, policyVersionId: String, applicationId: String, clientId: String, managementHost: String, managementPort: i64, streamHost: String, streamPort: i64, clientCertificatePem: String, clientPrivateKeyPem: String, serverCertificatePem: String) -> Result<Self, ValidationError> {
let value = Self { version, sessionId, gatewayId, reconnectSequence, expiresAt, providerProfile, providerIdentity, policyVersionId, applicationId, clientId, managementHost, managementPort, streamHost, streamPort, clientCertificatePem, clientPrivateKeyPem, serverCertificatePem };
value.validate()?;
Ok(value)
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.version != "1" { return Err(ValidationError::new("version", "invalid_value")); }
if self.sessionId.is_empty() { return Err(ValidationError::new("session_id", "required")); }
if !self.sessionId.is_empty() && self.sessionId.len() < 1 { return Err(ValidationError::new("session_id", "min_length")); }
if self.sessionId.len() > 128 { return Err(ValidationError::new("session_id", "max_length")); }
if self.gatewayId.is_empty() { return Err(ValidationError::new("gateway_id", "required")); }
if !self.gatewayId.is_empty() && self.gatewayId.len() < 1 { return Err(ValidationError::new("gateway_id", "min_length")); }
if self.gatewayId.len() > 128 { return Err(ValidationError::new("gateway_id", "max_length")); }
if self.reconnectSequence < 0 { return Err(ValidationError::new("reconnect_sequence", "minimum")); }
if self.expiresAt.len() > 64 { return Err(ValidationError::new("expires_at", "max_length")); }
if self.providerProfile != "apollo" { return Err(ValidationError::new("provider_profile", "invalid_value")); }
if self.providerIdentity.is_empty() { return Err(ValidationError::new("provider_identity", "required")); }
if !self.providerIdentity.is_empty() && self.providerIdentity.len() < 1 { return Err(ValidationError::new("provider_identity", "min_length")); }
if self.providerIdentity.len() > 256 { return Err(ValidationError::new("provider_identity", "max_length")); }
if self.policyVersionId.is_empty() { return Err(ValidationError::new("policy_version_id", "required")); }
if !self.policyVersionId.is_empty() && self.policyVersionId.len() < 1 { return Err(ValidationError::new("policy_version_id", "min_length")); }
if self.policyVersionId.len() > 128 { return Err(ValidationError::new("policy_version_id", "max_length")); }
if self.applicationId.is_empty() { return Err(ValidationError::new("application_id", "required")); }
if !self.applicationId.is_empty() && self.applicationId.len() < 1 { return Err(ValidationError::new("application_id", "min_length")); }
if self.applicationId.len() > 128 { return Err(ValidationError::new("application_id", "max_length")); }
if self.clientId.is_empty() { return Err(ValidationError::new("client_id", "required")); }
if !self.clientId.is_empty() && self.clientId.len() < 1 { return Err(ValidationError::new("client_id", "min_length")); }
if self.clientId.len() > 128 { return Err(ValidationError::new("client_id", "max_length")); }
if self.managementHost.is_empty() { return Err(ValidationError::new("management_host", "required")); }
if !self.managementHost.is_empty() && self.managementHost.len() < 1 { return Err(ValidationError::new("management_host", "min_length")); }
if self.managementHost.len() > 256 { return Err(ValidationError::new("management_host", "max_length")); }
if self.managementPort < 1 { return Err(ValidationError::new("management_port", "minimum")); }
if self.managementPort > 65535 { return Err(ValidationError::new("management_port", "maximum")); }
if self.streamHost.is_empty() { return Err(ValidationError::new("stream_host", "required")); }
if !self.streamHost.is_empty() && self.streamHost.len() < 1 { return Err(ValidationError::new("stream_host", "min_length")); }
if self.streamHost.len() > 256 { return Err(ValidationError::new("stream_host", "max_length")); }
if self.streamPort < 1 { return Err(ValidationError::new("stream_port", "minimum")); }
if self.streamPort > 65535 { return Err(ValidationError::new("stream_port", "maximum")); }
if self.clientCertificatePem.is_empty() { return Err(ValidationError::new("client_certificate_pem", "required")); }
if !self.clientCertificatePem.is_empty() && self.clientCertificatePem.len() < 1 { return Err(ValidationError::new("client_certificate_pem", "min_length")); }
if self.clientCertificatePem.len() > 32768 { return Err(ValidationError::new("client_certificate_pem", "max_length")); }
if self.clientPrivateKeyPem.is_empty() { return Err(ValidationError::new("client_private_key_pem", "required")); }
if !self.clientPrivateKeyPem.is_empty() && self.clientPrivateKeyPem.len() < 1 { return Err(ValidationError::new("client_private_key_pem", "min_length")); }
if self.clientPrivateKeyPem.len() > 32768 { return Err(ValidationError::new("client_private_key_pem", "max_length")); }
if self.serverCertificatePem.is_empty() { return Err(ValidationError::new("server_certificate_pem", "required")); }
if !self.serverCertificatePem.is_empty() && self.serverCertificatePem.len() < 1 { return Err(ValidationError::new("server_certificate_pem", "min_length")); }
if self.serverCertificatePem.len() > 32768 { return Err(ValidationError::new("server_certificate_pem", "max_length")); }
Ok(())
}
pub fn version(&self) -> &String { &self.version }
pub fn sessionId(&self) -> &String { &self.sessionId }
pub fn gatewayId(&self) -> &String { &self.gatewayId }
pub fn reconnectSequence(&self) -> &i64 { &self.reconnectSequence }
pub fn expiresAt(&self) -> &String { &self.expiresAt }
pub fn providerProfile(&self) -> &String { &self.providerProfile }
pub fn providerIdentity(&self) -> &String { &self.providerIdentity }
pub fn policyVersionId(&self) -> &String { &self.policyVersionId }
pub fn applicationId(&self) -> &String { &self.applicationId }
pub fn clientId(&self) -> &String { &self.clientId }
pub fn managementHost(&self) -> &String { &self.managementHost }
pub fn managementPort(&self) -> &i64 { &self.managementPort }
pub fn streamHost(&self) -> &String { &self.streamHost }
pub fn streamPort(&self) -> &i64 { &self.streamPort }
pub fn clientCertificatePem(&self) -> &String { &self.clientCertificatePem }
pub fn clientPrivateKeyPem(&self) -> &String { &self.clientPrivateKeyPem }
pub fn serverCertificatePem(&self) -> &String { &self.serverCertificatePem }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderState {
version: String,
@@ -1345,12 +1435,13 @@ pub struct TunnelAdmissionRequest {
grant: String,
reconnectSequence: i64,
clientNonce: String,
deviceSignature: String,
capabilities: CapabilityProfile,
}
impl TunnelAdmissionRequest {
pub fn new(version: String, sessionId: String, gatewayId: String, audience: String, grant: String, reconnectSequence: i64, clientNonce: String, capabilities: CapabilityProfile) -> Result<Self, ValidationError> {
let value = Self { version, sessionId, gatewayId, audience, grant, reconnectSequence, clientNonce, capabilities };
pub fn new(version: String, sessionId: String, gatewayId: String, audience: String, grant: String, reconnectSequence: i64, clientNonce: String, deviceSignature: String, capabilities: CapabilityProfile) -> Result<Self, ValidationError> {
let value = Self { version, sessionId, gatewayId, audience, grant, reconnectSequence, clientNonce, deviceSignature, capabilities };
value.validate()?;
Ok(value)
}
@@ -1372,6 +1463,9 @@ impl TunnelAdmissionRequest {
if self.clientNonce.is_empty() { return Err(ValidationError::new("client_nonce", "required")); }
if !self.clientNonce.is_empty() && self.clientNonce.len() < 16 { return Err(ValidationError::new("client_nonce", "min_length")); }
if self.clientNonce.len() > 128 { return Err(ValidationError::new("client_nonce", "max_length")); }
if self.deviceSignature.is_empty() { return Err(ValidationError::new("device_signature", "required")); }
if !self.deviceSignature.is_empty() && self.deviceSignature.len() < 86 { return Err(ValidationError::new("device_signature", "min_length")); }
if self.deviceSignature.len() > 86 { return Err(ValidationError::new("device_signature", "max_length")); }
self.capabilities.validate().map_err(|_| ValidationError::new("capabilities", "invalid_object"))?;
Ok(())
}
@@ -1382,7 +1476,15 @@ impl TunnelAdmissionRequest {
pub fn grant(&self) -> &String { &self.grant }
pub fn reconnectSequence(&self) -> &i64 { &self.reconnectSequence }
pub fn clientNonce(&self) -> &String { &self.clientNonce }
pub fn deviceSignature(&self) -> &String { &self.deviceSignature }
pub fn capabilities(&self) -> &CapabilityProfile { &self.capabilities }
pub fn device_admission_transcript(&self) -> Vec<u8> {
let reconnect_sequence = self.reconnectSequence.to_string();
let fields = [&self.sessionId, &self.gatewayId, &self.audience, &self.grant, &reconnect_sequence, &self.clientNonce, &self.capabilities.transport, &self.capabilities.framing, &self.capabilities.media, &self.capabilities.audio, &self.capabilities.sourceRateControl, &self.capabilities.clientDecode];
let mut transcript = String::from("versevdi/tunnel-admission/v1");
for field in fields { transcript.push_str(&format!("{}:{}", field.as_bytes().len(), field)); }
transcript.into_bytes()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
+133 -3
View File
@@ -1,7 +1,7 @@
// Code generated by tools/generate.py; DO NOT EDIT.
import Foundation
public typealias JSONObject = [String: String]
public let schemaSHA256 = "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec"
public let schemaSHA256 = "792abfb9cfe70e79911d499d76c009ab848713278bc240b88576df520580e480"
public let currentWireVersion = "1"
public let nMinus1WireVersion = "0"
public let nMinus2WireVersion = "-1"
@@ -1310,6 +1310,121 @@ public struct PageInfo: Codable, Equatable {
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
}
public struct ProviderSessionWork: Codable, Equatable {
public let version: String
public let sessionId: String
public let gatewayId: String
public let reconnectSequence: Int64
public let expiresAt: String
public let providerProfile: String
public let providerIdentity: String
public let policyVersionId: String
public let applicationId: String
public let clientId: String
public let managementHost: String
public let managementPort: Int64
public let streamHost: String
public let streamPort: Int64
public let clientCertificatePem: String
public let clientPrivateKeyPem: String
public let serverCertificatePem: String
enum CodingKeys: String, CodingKey {
case version = "version"
case sessionId = "session_id"
case gatewayId = "gateway_id"
case reconnectSequence = "reconnect_sequence"
case expiresAt = "expires_at"
case providerProfile = "provider_profile"
case providerIdentity = "provider_identity"
case policyVersionId = "policy_version_id"
case applicationId = "application_id"
case clientId = "client_id"
case managementHost = "management_host"
case managementPort = "management_port"
case streamHost = "stream_host"
case streamPort = "stream_port"
case clientCertificatePem = "client_certificate_pem"
case clientPrivateKeyPem = "client_private_key_pem"
case serverCertificatePem = "server_certificate_pem"
}
public init(version: String, sessionId: String, gatewayId: String, reconnectSequence: Int64, expiresAt: String, providerProfile: String, providerIdentity: String, policyVersionId: String, applicationId: String, clientId: String, managementHost: String, managementPort: Int64, streamHost: String, streamPort: Int64, clientCertificatePem: String, clientPrivateKeyPem: String, serverCertificatePem: String) throws {
self.version = version
self.sessionId = sessionId
self.gatewayId = gatewayId
self.reconnectSequence = reconnectSequence
self.expiresAt = expiresAt
self.providerProfile = providerProfile
self.providerIdentity = providerIdentity
self.policyVersionId = policyVersionId
self.applicationId = applicationId
self.clientId = clientId
self.managementHost = managementHost
self.managementPort = managementPort
self.streamHost = streamHost
self.streamPort = streamPort
self.clientCertificatePem = clientCertificatePem
self.clientPrivateKeyPem = clientPrivateKeyPem
self.serverCertificatePem = serverCertificatePem
try validate()
}
public init(from decoder: Decoder) throws {
let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(version: try c.decode(String.self, forKey: .version), sessionId: try c.decode(String.self, forKey: .sessionId), gatewayId: try c.decode(String.self, forKey: .gatewayId), reconnectSequence: try c.decode(Int64.self, forKey: .reconnectSequence), expiresAt: try c.decode(String.self, forKey: .expiresAt), providerProfile: try c.decode(String.self, forKey: .providerProfile), providerIdentity: try c.decode(String.self, forKey: .providerIdentity), policyVersionId: try c.decode(String.self, forKey: .policyVersionId), applicationId: try c.decode(String.self, forKey: .applicationId), clientId: try c.decode(String.self, forKey: .clientId), managementHost: try c.decode(String.self, forKey: .managementHost), managementPort: try c.decode(Int64.self, forKey: .managementPort), streamHost: try c.decode(String.self, forKey: .streamHost), streamPort: try c.decode(Int64.self, forKey: .streamPort), clientCertificatePem: try c.decode(String.self, forKey: .clientCertificatePem), clientPrivateKeyPem: try c.decode(String.self, forKey: .clientPrivateKeyPem), serverCertificatePem: try c.decode(String.self, forKey: .serverCertificatePem))
}
public func validate() throws {
if self.version != "1" { throw ContractValidationError(field: "version", code: "invalid_value") }
if self.sessionId.isEmpty { throw ContractValidationError(field: "session_id", code: "required") }
if !self.sessionId.isEmpty && self.sessionId.utf8.count < 1 { throw ContractValidationError(field: "session_id", code: "min_length") }
if self.sessionId.utf8.count > 128 { throw ContractValidationError(field: "session_id", code: "max_length") }
if self.gatewayId.isEmpty { throw ContractValidationError(field: "gateway_id", code: "required") }
if !self.gatewayId.isEmpty && self.gatewayId.utf8.count < 1 { throw ContractValidationError(field: "gateway_id", code: "min_length") }
if self.gatewayId.utf8.count > 128 { throw ContractValidationError(field: "gateway_id", code: "max_length") }
if self.reconnectSequence < 0 { throw ContractValidationError(field: "reconnect_sequence", code: "minimum") }
if self.expiresAt.utf8.count > 64 { throw ContractValidationError(field: "expires_at", code: "max_length") }
if ISO8601DateFormatter().date(from: self.expiresAt) == nil { throw ContractValidationError(field: "expires_at", code: "invalid_time") }
if self.providerProfile != "apollo" { throw ContractValidationError(field: "provider_profile", code: "invalid_value") }
if self.providerIdentity.isEmpty { throw ContractValidationError(field: "provider_identity", code: "required") }
if !self.providerIdentity.isEmpty && self.providerIdentity.utf8.count < 1 { throw ContractValidationError(field: "provider_identity", code: "min_length") }
if self.providerIdentity.utf8.count > 256 { throw ContractValidationError(field: "provider_identity", code: "max_length") }
if self.policyVersionId.isEmpty { throw ContractValidationError(field: "policy_version_id", code: "required") }
if !self.policyVersionId.isEmpty && self.policyVersionId.utf8.count < 1 { throw ContractValidationError(field: "policy_version_id", code: "min_length") }
if self.policyVersionId.utf8.count > 128 { throw ContractValidationError(field: "policy_version_id", code: "max_length") }
if self.applicationId.isEmpty { throw ContractValidationError(field: "application_id", code: "required") }
if !self.applicationId.isEmpty && self.applicationId.utf8.count < 1 { throw ContractValidationError(field: "application_id", code: "min_length") }
if self.applicationId.utf8.count > 128 { throw ContractValidationError(field: "application_id", code: "max_length") }
if self.clientId.isEmpty { throw ContractValidationError(field: "client_id", code: "required") }
if !self.clientId.isEmpty && self.clientId.utf8.count < 1 { throw ContractValidationError(field: "client_id", code: "min_length") }
if self.clientId.utf8.count > 128 { throw ContractValidationError(field: "client_id", code: "max_length") }
if self.managementHost.isEmpty { throw ContractValidationError(field: "management_host", code: "required") }
if !self.managementHost.isEmpty && self.managementHost.utf8.count < 1 { throw ContractValidationError(field: "management_host", code: "min_length") }
if self.managementHost.utf8.count > 256 { throw ContractValidationError(field: "management_host", code: "max_length") }
if self.managementPort < 1 { throw ContractValidationError(field: "management_port", code: "minimum") }
if self.managementPort > 65535 { throw ContractValidationError(field: "management_port", code: "maximum") }
if self.streamHost.isEmpty { throw ContractValidationError(field: "stream_host", code: "required") }
if !self.streamHost.isEmpty && self.streamHost.utf8.count < 1 { throw ContractValidationError(field: "stream_host", code: "min_length") }
if self.streamHost.utf8.count > 256 { throw ContractValidationError(field: "stream_host", code: "max_length") }
if self.streamPort < 1 { throw ContractValidationError(field: "stream_port", code: "minimum") }
if self.streamPort > 65535 { throw ContractValidationError(field: "stream_port", code: "maximum") }
if self.clientCertificatePem.isEmpty { throw ContractValidationError(field: "client_certificate_pem", code: "required") }
if !self.clientCertificatePem.isEmpty && self.clientCertificatePem.utf8.count < 1 { throw ContractValidationError(field: "client_certificate_pem", code: "min_length") }
if self.clientCertificatePem.utf8.count > 32768 { throw ContractValidationError(field: "client_certificate_pem", code: "max_length") }
if self.clientPrivateKeyPem.isEmpty { throw ContractValidationError(field: "client_private_key_pem", code: "required") }
if !self.clientPrivateKeyPem.isEmpty && self.clientPrivateKeyPem.utf8.count < 1 { throw ContractValidationError(field: "client_private_key_pem", code: "min_length") }
if self.clientPrivateKeyPem.utf8.count > 32768 { throw ContractValidationError(field: "client_private_key_pem", code: "max_length") }
if self.serverCertificatePem.isEmpty { throw ContractValidationError(field: "server_certificate_pem", code: "required") }
if !self.serverCertificatePem.isEmpty && self.serverCertificatePem.utf8.count < 1 { throw ContractValidationError(field: "server_certificate_pem", code: "min_length") }
if self.serverCertificatePem.utf8.count > 32768 { throw ContractValidationError(field: "server_certificate_pem", code: "max_length") }
}
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
}
public struct ProviderState: Codable, Equatable {
public let version: String
public let sessionId: String
@@ -1797,6 +1912,7 @@ public struct TunnelAdmissionRequest: Codable, Equatable {
public let grant: String
public let reconnectSequence: Int64
public let clientNonce: String
public let deviceSignature: String
public let capabilities: CapabilityProfile
enum CodingKeys: String, CodingKey {
case version = "version"
@@ -1806,10 +1922,11 @@ public struct TunnelAdmissionRequest: Codable, Equatable {
case grant = "grant"
case reconnectSequence = "reconnect_sequence"
case clientNonce = "client_nonce"
case deviceSignature = "device_signature"
case capabilities = "capabilities"
}
public init(version: String, sessionId: String, gatewayId: String, audience: String, grant: String, reconnectSequence: Int64, clientNonce: String, capabilities: CapabilityProfile) throws {
public init(version: String, sessionId: String, gatewayId: String, audience: String, grant: String, reconnectSequence: Int64, clientNonce: String, deviceSignature: String, capabilities: CapabilityProfile) throws {
self.version = version
self.sessionId = sessionId
self.gatewayId = gatewayId
@@ -1817,6 +1934,7 @@ public struct TunnelAdmissionRequest: Codable, Equatable {
self.grant = grant
self.reconnectSequence = reconnectSequence
self.clientNonce = clientNonce
self.deviceSignature = deviceSignature
self.capabilities = capabilities
try validate()
}
@@ -1825,7 +1943,7 @@ public struct TunnelAdmissionRequest: Codable, Equatable {
let all = try decoder.container(keyedBy: AnyCodingKey.self)
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
let c = try decoder.container(keyedBy: CodingKeys.self)
try self.init(version: try c.decode(String.self, forKey: .version), sessionId: try c.decode(String.self, forKey: .sessionId), gatewayId: try c.decode(String.self, forKey: .gatewayId), audience: try c.decode(String.self, forKey: .audience), grant: try c.decode(String.self, forKey: .grant), reconnectSequence: try c.decode(Int64.self, forKey: .reconnectSequence), clientNonce: try c.decode(String.self, forKey: .clientNonce), capabilities: try c.decode(CapabilityProfile.self, forKey: .capabilities))
try self.init(version: try c.decode(String.self, forKey: .version), sessionId: try c.decode(String.self, forKey: .sessionId), gatewayId: try c.decode(String.self, forKey: .gatewayId), audience: try c.decode(String.self, forKey: .audience), grant: try c.decode(String.self, forKey: .grant), reconnectSequence: try c.decode(Int64.self, forKey: .reconnectSequence), clientNonce: try c.decode(String.self, forKey: .clientNonce), deviceSignature: try c.decode(String.self, forKey: .deviceSignature), capabilities: try c.decode(CapabilityProfile.self, forKey: .capabilities))
}
public func validate() throws {
@@ -1846,6 +1964,9 @@ public struct TunnelAdmissionRequest: Codable, Equatable {
if self.clientNonce.isEmpty { throw ContractValidationError(field: "client_nonce", code: "required") }
if !self.clientNonce.isEmpty && self.clientNonce.utf8.count < 16 { throw ContractValidationError(field: "client_nonce", code: "min_length") }
if self.clientNonce.utf8.count > 128 { throw ContractValidationError(field: "client_nonce", code: "max_length") }
if self.deviceSignature.isEmpty { throw ContractValidationError(field: "device_signature", code: "required") }
if !self.deviceSignature.isEmpty && self.deviceSignature.utf8.count < 86 { throw ContractValidationError(field: "device_signature", code: "min_length") }
if self.deviceSignature.utf8.count > 86 { throw ContractValidationError(field: "device_signature", code: "max_length") }
try self.capabilities.validate()
}
@@ -1884,6 +2005,15 @@ public struct VersionNegotiation: Codable, Equatable {
public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }
}
public extension TunnelAdmissionRequest {
func deviceAdmissionTranscript() -> Data {
let fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, capabilities.clientDecode]
var transcript = "versevdi/tunnel-admission/v1"
for field in fields { transcript += "\(field.utf8.count):\(field)" }
return Data(transcript.utf8)
}
}
public extension CapabilityProfile {
static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {
guard let selected = profiles.first else { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
+21
View File
@@ -102,6 +102,7 @@ message TunnelAdmissionRequest {
uint64 reconnect_sequence = 6;
string client_nonce = 7;
CapabilityProfile capabilities = 8;
string device_signature = 9;
}
message SessionAuthority {
@@ -116,6 +117,26 @@ message SessionAuthority {
string provider_identity = 9;
}
message ProviderSessionWork {
string version = 1;
string session_id = 2;
string gateway_id = 3;
uint64 reconnect_sequence = 4;
google.protobuf.Timestamp expires_at = 5;
string provider_profile = 6;
string provider_identity = 7;
string policy_version_id = 8;
string application_id = 9;
string management_host = 10;
uint32 management_port = 11;
string stream_host = 12;
uint32 stream_port = 13;
string client_certificate_pem = 14;
string client_private_key_pem = 15;
string server_certificate_pem = 16;
string client_id = 17;
}
message ChannelFrame {
string version = 1;
string flow_id = 2;
+26 -1
View File
@@ -406,7 +406,7 @@
"TunnelAdmissionRequest": {
"type": "object",
"additionalProperties": false,
"required": ["version", "session_id", "gateway_id", "audience", "grant", "reconnect_sequence", "client_nonce", "capabilities"],
"required": ["version", "session_id", "gateway_id", "audience", "grant", "reconnect_sequence", "client_nonce", "device_signature", "capabilities"],
"properties": {
"version": {"type": "string", "const": "1"},
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
@@ -415,6 +415,7 @@
"grant": {"type": "string", "minLength": 43, "maxLength": 256},
"reconnect_sequence": {"type": "integer", "minimum": 0},
"client_nonce": {"type": "string", "minLength": 16, "maxLength": 128},
"device_signature": {"type": "string", "minLength": 86, "maxLength": 86},
"capabilities": {"$ref": "#/$defs/CapabilityProfile"}
}
},
@@ -434,6 +435,30 @@
"provider_identity": {"type": "string", "minLength": 1, "maxLength": 256}
}
},
"ProviderSessionWork": {
"type": "object",
"additionalProperties": false,
"required": ["version", "session_id", "gateway_id", "reconnect_sequence", "expires_at", "provider_profile", "provider_identity", "policy_version_id", "application_id", "client_id", "management_host", "management_port", "stream_host", "stream_port", "client_certificate_pem", "client_private_key_pem", "server_certificate_pem"],
"properties": {
"version": {"type": "string", "const": "1"},
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
"reconnect_sequence": {"type": "integer", "minimum": 0},
"expires_at": {"type": "string", "format": "date-time", "maxLength": 64},
"provider_profile": {"type": "string", "const": "apollo"},
"provider_identity": {"type": "string", "minLength": 1, "maxLength": 256},
"policy_version_id": {"type": "string", "minLength": 1, "maxLength": 128},
"application_id": {"type": "string", "minLength": 1, "maxLength": 128},
"client_id": {"type": "string", "minLength": 1, "maxLength": 128},
"management_host": {"type": "string", "minLength": 1, "maxLength": 256},
"management_port": {"type": "integer", "minimum": 1, "maximum": 65535},
"stream_host": {"type": "string", "minLength": 1, "maxLength": 256},
"stream_port": {"type": "integer", "minimum": 1, "maximum": 65535},
"client_certificate_pem": {"type": "string", "minLength": 1, "maxLength": 32768},
"client_private_key_pem": {"type": "string", "minLength": 1, "maxLength": 32768},
"server_certificate_pem": {"type": "string", "minLength": 1, "maxLength": 32768}
}
},
"ChannelFrame": {
"type": "object",
"additionalProperties": false,
+39
View File
@@ -74,6 +74,35 @@ func TestCapabilityIntersectionRejectsNoOverlap(t *testing.T) {
}
}
func TestTunnelAdmissionRequiresDeviceSignature(t *testing.T) {
request := protocol.TunnelAdmissionRequest{
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway",
Grant: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_", ReconnectSequence: 0,
ClientNonce: "0123456789abcdef", Capabilities: protocol.CapabilityProfile{
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded",
SourceRateControl: "server", ClientDecode: "h264-opus",
},
}
if _, err := protocol.EncodeTunnelAdmissionRequest(request); err == nil {
t.Fatal("EncodeTunnelAdmissionRequest accepted an unsigned device admission")
}
}
func TestTunnelAdmissionTranscriptIsDomainSeparatedAndLengthDelimited(t *testing.T) {
request := protocol.TunnelAdmissionRequest{
Version: "1", SessionID: "session", GatewayID: "gateway", Audience: "audience",
Grant: strings.Repeat("g", 43), ReconnectSequence: 0, ClientNonce: strings.Repeat("n", 16),
DeviceSignature: strings.Repeat("s", 86), Capabilities: protocol.CapabilityProfile{
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded",
SourceRateControl: "server", ClientDecode: "h264-opus",
},
}
want := "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + strings.Repeat("g", 43) + "1:016:" + strings.Repeat("n", 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server9:h264-opus"
if got := string(request.DeviceAdmissionTranscript()); got != want {
t.Fatalf("DeviceAdmissionTranscript() = %q, want %q", got, want)
}
}
func TestSessionAuthorityRejectsProviderRoute(t *testing.T) {
valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","audience":"versevdi-gateway","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":"h264-opus"},"provider_profile":"apollo","provider_identity":"provider-1"}`
if _, err := protocol.DecodeSessionAuthority([]byte(valid)); err != nil {
@@ -83,3 +112,13 @@ func TestSessionAuthorityRejectsProviderRoute(t *testing.T) {
t.Fatal("session authority accepted a provider route")
}
}
func TestProviderSessionWorkIsStrictAndSessionBound(t *testing.T) {
valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","provider_profile":"apollo","provider_identity":"provider-1","policy_version_id":"policy-1","application_id":"42","client_id":"paired-client-1","management_host":"apollo.test","management_port":47990,"stream_host":"apollo.test","stream_port":47984,"client_certificate_pem":"certificate","client_private_key_pem":"private-key","server_certificate_pem":"server-certificate"}`
if _, err := protocol.DecodeProviderSessionWork([]byte(valid)); err != nil {
t.Fatalf("valid provider work rejected: %v", err)
}
if _, err := protocol.DecodeProviderSessionWork([]byte(strings.Replace(valid, `"application_id":"42"`, `"application_id":"42","management_password":"forbidden"`, 1))); err == nil {
t.Fatal("provider work accepted a management credential")
}
}
+30
View File
@@ -171,6 +171,7 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
"\"errors\"",
"\"fmt\"",
"\"reflect\"",
"\"strings\"",
"\"time\"",
")",
"",
@@ -255,6 +256,16 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
"}",
"",
])
out.extend([
"func (v TunnelAdmissionRequest) DeviceAdmissionTranscript() []byte {",
"\tfields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf(\"%d\", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, v.Capabilities.ClientDecode}",
"\tvar transcript strings.Builder",
"\ttranscript.WriteString(\"versevdi/tunnel-admission/v1\")",
"\tfor _, field := range fields { fmt.Fprintf(&transcript, \"%d:%s\", len(field), field) }",
"\treturn []byte(transcript.String())",
"}",
"",
])
# Use io.EOF in generated code without making every generated decoder depend on
# error-string comparison; replace the deliberately compact placeholder.
text = "\n".join(out).replace('"errors"\n"fmt"', '"errors"\n"fmt"\n\"io"')
@@ -395,6 +406,16 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
if prop_name not in required:
typ = f"Option<{typ}>"
out.append(f" pub fn {field}(&self) -> &{typ} {{ &self.{field} }}")
if name == "TunnelAdmissionRequest":
out.extend([
" pub fn device_admission_transcript(&self) -> Vec<u8> {",
" let reconnect_sequence = self.reconnectSequence.to_string();",
" let fields = [&self.sessionId, &self.gatewayId, &self.audience, &self.grant, &reconnect_sequence, &self.clientNonce, &self.capabilities.transport, &self.capabilities.framing, &self.capabilities.media, &self.capabilities.audio, &self.capabilities.sourceRateControl, &self.capabilities.clientDecode];",
" let mut transcript = String::from(\"versevdi/tunnel-admission/v1\");",
" for field in fields { transcript.push_str(&format!(\"{}:{}\", field.as_bytes().len(), field)); }",
" transcript.into_bytes()",
" }",
])
out.extend(["}", ""])
out.extend([
"pub fn intersect_capability_profiles(profiles: &[CapabilityProfile]) -> Result<CapabilityProfile, ValidationError> {",
@@ -517,6 +538,15 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
out.extend(swift_validation(definition))
out.extend([" }", "", " public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }", " public func encodeJSON() throws -> Data { try validate(); return try JSONEncoder().encode(self) }", "}", ""])
out.extend([
"public extension TunnelAdmissionRequest {",
" func deviceAdmissionTranscript() -> Data {",
" let fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, capabilities.clientDecode]",
" var transcript = \"versevdi/tunnel-admission/v1\"",
" for field in fields { transcript += \"\\(field.utf8.count):\\(field)\" }",
" return Data(transcript.utf8)",
" }",
"}",
"",
"public extension CapabilityProfile {",
" static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {",
" guard let selected = profiles.first else { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
+16 -4
View File
@@ -38,9 +38,13 @@ let capability = try CapabilityProfile(
let request = try TunnelAdmissionRequest(
version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience",
grant: String(repeating: "g", count: 43), reconnectSequence: 0,
clientNonce: String(repeating: "n", count: 16), capabilities: capability
clientNonce: String(repeating: "n", count: 16), deviceSignature: String(repeating: "s", count: 86), capabilities: capability
)
_ = request
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + String(repeating: "g", count: 43) + "1:016:" + String(repeating: "n", count: 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server9:h264-opus"
guard String(data: request.deviceAdmissionTranscript(), encoding: .utf8) == transcript else {
fatalError("unexpected device admission transcript")
}
let incompatible = try CapabilityProfile(
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
audio: "encoded", sourceRateControl: "server", clientDecode: "hevc-opus"
@@ -97,17 +101,25 @@ fn main() {
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
"encoded".into(), "server".into(), "h264-opus".into(),
).unwrap();
let request = TunnelAdmissionRequest::new(
"1".into(), "session".into(), "gateway".into(), "audience".into(),
"g".repeat(43), 0, "n".repeat(16), "s".repeat(86), capabilities.clone(),
).unwrap();
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:".to_string()
+ &"g".repeat(43) + "1:016:" + &"n".repeat(16)
+ "10:quic-tls1311:datagram-v17:encoded7:encoded6:server9:h264-opus";
assert_eq!(request.device_admission_transcript(), transcript.into_bytes());
assert!(TunnelAdmissionRequest::new(
"2".into(), "session".into(), "gateway".into(), "audience".into(),
"g".repeat(43), 0, "n".repeat(16), capabilities.clone(),
"g".repeat(43), 0, "n".repeat(16), "s".repeat(86), capabilities.clone(),
).is_err());
assert!(TunnelAdmissionRequest::new(
"0".into(), "session".into(), "gateway".into(), "audience".into(),
"g".repeat(43), 0, "n".repeat(16), capabilities.clone(),
"g".repeat(43), 0, "n".repeat(16), "s".repeat(86), capabilities.clone(),
).is_err());
assert!(TunnelAdmissionRequest::new(
"1".into(), "session".into(), "gateway".into(), "audience".into(),
"g".repeat(43), 0, "short".into(), capabilities.clone(),
"g".repeat(43), 0, "short".into(), "s".repeat(86), capabilities.clone(),
).is_err());
assert!(intersect_capability_profiles(&[capabilities.clone(), capabilities.clone()]).is_ok());
let incompatible = CapabilityProfile::new(