ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
120 lines
4.3 KiB
Go
120 lines
4.3 KiB
Go
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)
|
|
}
|