feat(protocol): bind tunnel admission to device proof
Verify Protocol / verify (push) Canceled after 0s
Verify Protocol / module (push) Successful in 2m11s

This commit is contained in:
sechmachine
2026-07-29 10:32:08 +07:00
parent be709724ba
commit 4a2772c053
10 changed files with 138 additions and 14 deletions
+25 -1
View File
@@ -8,10 +8,11 @@ import (
"fmt"
"io"
"reflect"
"strings"
"time"
)
const SchemaSHA256 = "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec"
const SchemaSHA256 = "6b8631bf2b2aa12b14d0bc4d136af39a632e85b3237dc5614469ba09d93f5fca"
const ProtocolVersion = "1.0.0"
const CurrentWireVersion = "1"
const NMinus1WireVersion = "0"
@@ -326,6 +327,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"`
}
@@ -3923,6 +3925,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 +3964,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 +4087,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": "6b8631bf2b2aa12b14d0bc4d136af39a632e85b3237dc5614469ba09d93f5fca"
}
Binary file not shown.
+15 -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 = "6b8631bf2b2aa12b14d0bc4d136af39a632e85b3237dc5614469ba09d93f5fca";
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";
@@ -1345,12 +1345,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 +1373,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 +1386,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)]
+18 -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 = "6b8631bf2b2aa12b14d0bc4d136af39a632e85b3237dc5614469ba09d93f5fca"
public let currentWireVersion = "1"
public let nMinus1WireVersion = "0"
public let nMinus2WireVersion = "-1"
@@ -1797,6 +1797,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 +1807,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 +1819,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 +1828,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 +1849,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 +1890,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") }