90 lines
3.2 KiB
Go
90 lines
3.2 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 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)
|
||
|
|
}
|