package hardwareprovision import ( "context" "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "net/http" "net/http/httptest" "strconv" "testing" "time" "data-terminal-cloud/internal/deviceauth" ) func TestHTTPRegistrarSignsAndDecryptsDynamicRegistration(t *testing.T) { const productSecret = "1234567890abcdef-product-secret" const deviceSecret = "per-device-secret-with-at-least-32-characters" var received map[string]any server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if err := json.NewDecoder(request.Body).Decode(&received); err != nil { t.Fatalf("decode request: %v", err) } content := "auth_type=1&device_name=" + received["device_name"].(string) + "&random_num=" + strconv.FormatInt(int64(received["random_num"].(float64)), 10) + "&product_key=" + received["product_key"].(string) + "×tamp=" + strconv.FormatInt(int64(received["timestamp"].(float64)), 10) mac := hmac.New(sha256.New, []byte(productSecret)) _, _ = mac.Write([]byte(content)) if received["signature"] != base64.StdEncoding.EncodeToString(mac.Sum(nil)) { t.Fatal("dynamic registration signature did not match canonical fields") } if _, leaked := received["product_secret"]; leaked { t.Fatal("product secret must not be serialized into the provider request") } _ = json.NewEncoder(writer).Encode(map[string]any{"Result": map[string]any{ "payload": encryptDeviceSecretForTest(t, deviceSecret, productSecret), "RTCAppID": "rtc-app", }}) })) defer server.Close() registrar, err := NewHTTPRegistrar(server.URL, productSecret, time.Second) if err != nil { t.Fatalf("NewHTTPRegistrar() error = %v", err) } registrar.now = func() time.Time { return time.UnixMilli(1_700_000_000_000) } result, err := registrar.Register(context.Background(), RegisterInput{InstanceID: "instance-1", ProductKey: "product-1", DeviceName: "device-1"}) if err != nil { t.Fatalf("Register() error = %v", err) } if result.DeviceSecret != deviceSecret || result.RTCAppID != "rtc-app" { t.Fatalf("Register() result = %+v", result) } if received["InstanceID"] != "instance-1" || received["product_key"] != "product-1" || received["device_name"] != "device-1" { t.Fatalf("unexpected registration request: %+v", received) } } func TestDisabledProvisioningDoesNotRequireProductSecret(t *testing.T) { service, err := NewService(nil, nil, Options{Enabled: false}) if err != nil { t.Fatalf("NewService() error = %v", err) } _, err = service.Provision(context.Background(), deviceauth.Identity{}, "request", ProvisionInput{HardwareID: "device"}) if err != ErrUnavailable { t.Fatalf("Provision() error = %v, want ErrUnavailable", err) } } func TestHTTPRegistrarRTCAppIDVariantsAndMissingID(t *testing.T) { for _, field := range []string{"RTCAppID", "rtc_app_id", "missing"} { t.Run(field, func(t *testing.T) { const productSecret = "1234567890abcdef-product-secret" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { result := map[string]any{"payload": encryptDeviceSecretForTest(t, "fixture-device-secret-123456789", productSecret)} if field != "missing" { result[field] = "rtc-app" } _ = json.NewEncoder(w).Encode(map[string]any{"Result": result, "ResponseMetadata": map[string]any{"Error": map[string]any{"CodeN": 0}}}) })) defer server.Close() r, err := NewHTTPRegistrar(server.URL, productSecret, time.Second) if err != nil { t.Fatal(err) } out, err := r.Register(context.Background(), RegisterInput{"instance", "product", "device"}) if field == "missing" { if err == nil { t.Fatal("missing RTCAppID was accepted") } return } if err != nil || out.RTCAppID != "rtc-app" { t.Fatalf("app ID decoding failed: %v", err) } }) } } func encryptDeviceSecretForTest(t *testing.T, secret, productSecret string) string { t.Helper() key := []byte(productSecret)[:aes.BlockSize] block, err := aes.NewCipher(key) if err != nil { t.Fatal(err) } plain := []byte(secret) padding := aes.BlockSize - len(plain)%aes.BlockSize for index := 0; index < padding; index++ { plain = append(plain, byte(padding)) } encrypted := make([]byte, len(plain)) cipher.NewCBCEncrypter(block, key).CryptBlocks(encrypted, plain) return base64.StdEncoding.EncodeToString(encrypted) }