chore: checkpoint 第一版
ci / verify (push) Canceled after 0s

This commit is contained in:
Codex Local
2026-09-06 17:04:11 +08:00
parent a8cdec98d1
commit 0c2151ace9
659 changed files with 185855 additions and 554 deletions
+166
View File
@@ -0,0 +1,166 @@
package hardwareprovision
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net/http"
"strings"
"time"
)
const defaultDynamicRegisterURL = "https://iot-cn-shanghai.iot.volces.com/2021-12-14/DynamicRegister?Action=DynamicRegister&Version=2021-12-14"
type RegisterInput struct {
InstanceID string
ProductKey string
DeviceName string
}
type RegisterResult struct {
DeviceSecret string
RTCAppID string
}
type Registrar interface {
Register(context.Context, RegisterInput) (RegisterResult, error)
}
type HTTPRegistrar struct {
endpoint string
productSecret string
client *http.Client
now func() time.Time
}
func NewHTTPRegistrar(endpoint, productSecret string, timeout time.Duration) (*HTTPRegistrar, error) {
endpoint = strings.TrimSpace(endpoint)
if endpoint == "" {
endpoint = defaultDynamicRegisterURL
}
if !strings.HasPrefix(endpoint, "https://") && !strings.HasPrefix(endpoint, "http://127.0.0.1:") && !strings.HasPrefix(endpoint, "http://localhost:") {
return nil, errors.New("Volcengine dynamic registration URL must use HTTPS")
}
if len([]byte(strings.TrimSpace(productSecret))) < 16 {
return nil, errors.New("Volcengine hardware product secret must contain at least 16 bytes")
}
if timeout <= 0 {
return nil, errors.New("Volcengine hardware registration timeout must be positive")
}
return &HTTPRegistrar{
endpoint: endpoint, productSecret: strings.TrimSpace(productSecret),
client: &http.Client{Timeout: timeout}, now: time.Now,
}, nil
}
func (r *HTTPRegistrar) Register(ctx context.Context, input RegisterInput) (RegisterResult, error) {
randomNumber, err := rand.Int(rand.Reader, big.NewInt(2_147_483_646))
if err != nil {
return RegisterResult{}, fmt.Errorf("generate dynamic registration nonce: %w", err)
}
randomValue := randomNumber.Int64() + 1
timestamp := r.now().UTC().UnixMilli()
content := fmt.Sprintf("auth_type=1&device_name=%s&random_num=%d&product_key=%s&timestamp=%d", input.DeviceName, randomValue, input.ProductKey, timestamp)
mac := hmac.New(sha256.New, []byte(r.productSecret))
_, _ = mac.Write([]byte(content))
signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
body, err := json.Marshal(map[string]any{
"InstanceID": input.InstanceID, "product_key": input.ProductKey, "device_name": input.DeviceName,
"random_num": randomValue, "timestamp": timestamp, "auth_type": 1, "signature": signature,
})
if err != nil {
return RegisterResult{}, fmt.Errorf("encode dynamic registration request: %w", err)
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, r.endpoint, strings.NewReader(string(body)))
if err != nil {
return RegisterResult{}, fmt.Errorf("create dynamic registration request: %w", err)
}
request.Header.Set("Accept", "application/json")
request.Header.Set("Content-Type", "application/json")
response, err := r.client.Do(request)
if err != nil {
return RegisterResult{}, fmt.Errorf("call Volcengine dynamic registration: %w", err)
}
defer response.Body.Close()
raw, err := io.ReadAll(io.LimitReader(response.Body, 64<<10))
if err != nil {
return RegisterResult{}, fmt.Errorf("read Volcengine dynamic registration response: %w", err)
}
var payload struct {
ResponseMetadata struct {
Error struct {
Code string `json:"Code"`
CodeN any `json:"CodeN"`
Message string `json:"Message"`
} `json:"Error"`
} `json:"ResponseMetadata"`
Result struct {
Payload string `json:"payload"`
RTCAppID string `json:"RTCAppID"`
} `json:"Result"`
Message string `json:"message"`
}
if err := json.Unmarshal(raw, &payload); err != nil {
return RegisterResult{}, fmt.Errorf("decode Volcengine dynamic registration response: HTTP %d", response.StatusCode)
}
if response.StatusCode < 200 || response.StatusCode >= 300 || payload.ResponseMetadata.Error.Code != "" || payload.ResponseMetadata.Error.CodeN != nil {
code := payload.ResponseMetadata.Error.Code
if code == "" {
code = fmt.Sprintf("HTTP_%d", response.StatusCode)
}
message := strings.TrimSpace(payload.ResponseMetadata.Error.Message)
if message == "" {
message = strings.TrimSpace(payload.Message)
}
if message == "" {
message = "Volcengine dynamic registration failed"
}
return RegisterResult{}, fmt.Errorf("%s: %s", code, message)
}
secret, err := decryptRegisteredDeviceSecret(payload.Result.Payload, r.productSecret)
if err != nil {
return RegisterResult{}, err
}
return RegisterResult{DeviceSecret: secret, RTCAppID: strings.TrimSpace(payload.Result.RTCAppID)}, nil
}
func decryptRegisteredDeviceSecret(encoded, productSecret string) (string, error) {
keyMaterial := []byte(productSecret)
if len(keyMaterial) < aes.BlockSize {
return "", errors.New("product secret is too short to decrypt device credential")
}
key := keyMaterial[:aes.BlockSize]
encrypted, err := base64.StdEncoding.DecodeString(strings.TrimSpace(encoded))
if err != nil || len(encrypted) == 0 || len(encrypted)%aes.BlockSize != 0 {
return "", errors.New("Volcengine dynamic registration returned an invalid device credential")
}
block, err := aes.NewCipher(key)
if err != nil {
return "", fmt.Errorf("create device credential decryptor: %w", err)
}
plain := make([]byte, len(encrypted))
cipher.NewCBCDecrypter(block, key).CryptBlocks(plain, encrypted)
padding := int(plain[len(plain)-1])
if padding < 1 || padding > aes.BlockSize || padding > len(plain) {
return "", errors.New("Volcengine device credential padding is invalid")
}
for _, value := range plain[len(plain)-padding:] {
if int(value) != padding {
return "", errors.New("Volcengine device credential padding is invalid")
}
}
secret := strings.TrimSpace(string(plain[:len(plain)-padding]))
if len(secret) < 16 || len(secret) > 4096 {
return "", errors.New("Volcengine device credential length is invalid")
}
return secret, nil
}
@@ -0,0 +1,89 @@
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) +
"&timestamp=" + 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)
}
+366
View File
@@ -0,0 +1,366 @@
package hardwareprovision
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"regexp"
"strings"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/deviceactivation"
"data-terminal-cloud/internal/deviceauth"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var (
ErrUnavailable = errors.New("hardware provider provisioning is unavailable")
ErrInvalid = errors.New("invalid hardware provider provisioning input")
ErrBindingMissing = errors.New("hardware agent binding is missing")
ErrBindingMismatch = errors.New("hardware agent product binding does not match platform provisioning configuration")
ErrHardwareMismatch = errors.New("hardware identity does not match the provisioned credential")
ErrActivationState = errors.New("device activation state does not allow provider provisioning")
)
var hardwareIDPattern = regexp.MustCompile(`^[a-zA-Z0-9_\-.:@]{1,200}$`)
type Options struct {
Enabled bool
InstanceID string
ProductKey string
BotID string
Transport string
MinimumSDKVersion string
ProductSecret string
CredentialMasterKey string
RegisterURL string
RegisterTimeout time.Duration
DeviceNamePrefix string
Registrar Registrar
}
type ProvisionInput struct {
HardwareID string `json:"hardware_id"`
}
type ProvisionResponse struct {
InstanceID string `json:"instance_id"`
ProductKey string `json:"product_key"`
DeviceName string `json:"device_name"`
HardwareID string `json:"hardware_id"`
DeviceSecret string `json:"device_secret"`
CredentialRef string `json:"credential_ref"`
CredentialVersion int64 `json:"credential_version"`
CredentialSource string `json:"credential_source"`
IssuedAt time.Time `json:"issued_at"`
}
type Service struct {
enabled bool
pool *pgxpool.Pool
audit *audit.Repository
registrar Registrar
aead cipher.AEAD
instanceID string
productKey string
botID string
transport string
minimumSDK string
deviceNamePrefix string
}
func NewService(pool *pgxpool.Pool, auditRepository *audit.Repository, options Options) (*Service, error) {
service := &Service{enabled: options.Enabled}
if !options.Enabled {
return service, nil
}
if pool == nil || auditRepository == nil {
return nil, errors.New("hardware provider provisioning requires database and audit repository")
}
service.instanceID = strings.TrimSpace(options.InstanceID)
service.productKey = strings.TrimSpace(options.ProductKey)
service.botID = strings.TrimSpace(options.BotID)
service.transport = strings.TrimSpace(options.Transport)
service.minimumSDK = strings.TrimSpace(options.MinimumSDKVersion)
service.deviceNamePrefix = strings.TrimSpace(options.DeviceNamePrefix)
if service.deviceNamePrefix == "" {
service.deviceNamePrefix = "dt-"
}
if service.instanceID == "" || len(service.instanceID) > 200 || service.productKey == "" || len(service.productKey) > 200 ||
service.botID == "" || len(service.botID) > 200 || (service.transport != "high_quality" && service.transport != "low_load") ||
service.minimumSDK == "" || len(service.minimumSDK) > 80 ||
!regexp.MustCompile(`^[a-zA-Z0-9_\-.:@]{1,32}$`).MatchString(service.deviceNamePrefix) {
return nil, errors.New("hardware provider instance, product defaults and device-name prefix are invalid")
}
key, err := hex.DecodeString(strings.TrimSpace(options.CredentialMasterKey))
if err != nil || len(key) != 32 {
return nil, errors.New("VOLC_HARDWARE_DEVICE_CREDENTIAL_MASTER_KEY must be 32 bytes encoded as 64 hexadecimal characters")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("create hardware provider credential cipher: %w", err)
}
service.aead, err = cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("create hardware provider credential AEAD: %w", err)
}
service.registrar = options.Registrar
if service.registrar == nil {
service.registrar, err = NewHTTPRegistrar(options.RegisterURL, options.ProductSecret, options.RegisterTimeout)
if err != nil {
return nil, err
}
}
service.pool = pool
service.audit = auditRepository
return service, nil
}
func (s *Service) Provision(ctx context.Context, identity deviceauth.Identity, requestID string, input ProvisionInput) (ProvisionResponse, error) {
if !s.enabled {
return ProvisionResponse{}, ErrUnavailable
}
hardwareID := strings.TrimSpace(input.HardwareID)
if !hardwareIDPattern.MatchString(hardwareID) {
return ProvisionResponse{}, ErrInvalid
}
tx, err := s.pool.Begin(ctx)
if err != nil {
return ProvisionResponse{}, fmt.Errorf("begin hardware provider provisioning: %w", err)
}
defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, identity.ID); err != nil {
return ProvisionResponse{}, fmt.Errorf("lock hardware provider provisioning: %w", err)
}
activationState, err := s.prepareActivation(ctx, tx, identity, requestID)
if err != nil {
return ProvisionResponse{}, err
}
if err := s.ensureProductBinding(ctx, tx, identity); err != nil {
return ProvisionResponse{}, err
}
existing, found, err := s.readExisting(ctx, tx, identity.ID)
if err != nil {
return ProvisionResponse{}, err
}
if found {
if existing.HardwareID != hardwareID {
return ProvisionResponse{}, ErrHardwareMismatch
}
if existing.InstanceID != s.instanceID || existing.ProductKey != s.productKey {
return ProvisionResponse{}, ErrBindingMismatch
}
secret, err := s.aead.Open(nil, existing.Nonce, existing.EncryptedSecret, credentialAAD(existing.CredentialRef, existing.KeyVersion, existing.InstanceID, existing.ProductKey, existing.DeviceName))
if err != nil {
return ProvisionResponse{}, errors.New("decrypt hardware provider device credential")
}
response := ProvisionResponse{InstanceID: existing.InstanceID, ProductKey: existing.ProductKey, DeviceName: existing.DeviceName,
HardwareID: existing.HardwareID, DeviceSecret: string(secret), CredentialRef: existing.CredentialRef,
CredentialVersion: existing.KeyVersion, CredentialSource: "platform_stored", IssuedAt: existing.IssuedAt}
if existing.Status == "SUSPENDED" {
if _, err := tx.Exec(ctx, `UPDATE hardware_agent_provider_credential SET status='ACTIVE',revoked_at=NULL,updated_at=now() WHERE device_id=$1::uuid AND status='SUSPENDED'`, identity.ID); err != nil {
return ProvisionResponse{}, fmt.Errorf("reactivate hardware provider credential: %w", err)
}
}
if activationState == deviceactivation.Provisioning {
if _, err := deviceactivation.Apply(ctx, tx, deviceactivation.Input{DeviceID: identity.ID, TenantID: identity.TenantID,
Target: deviceactivation.Active, Reason: "火山运行凭据已就绪", Source: "provider-provisioning", ActorType: "device",
ActorPublicID: &identity.PublicID, RequestID: requestID}); err != nil {
return ProvisionResponse{}, fmt.Errorf("complete provider activation: %w", err)
}
}
if err := s.recordAudit(ctx, tx, identity, requestID, "hardware_agent.provider_credential.resolve", response, true); err != nil {
return ProvisionResponse{}, err
}
if err := tx.Commit(ctx); err != nil {
return ProvisionResponse{}, fmt.Errorf("commit hardware provider credential resolution: %w", err)
}
return response, nil
}
var configuredProduct, configuredDeviceName string
err = tx.QueryRow(ctx, `
SELECT COALESCE(product_key,''),COALESCE(provider_device_ref,'')
FROM hardware_agent_device_binding
WHERE device_id=$1::uuid AND tenant_id=$2::uuid AND provider='volcengine' AND status='ACTIVE'
FOR UPDATE`, identity.ID, identity.TenantID).Scan(&configuredProduct, &configuredDeviceName)
if errors.Is(err, pgx.ErrNoRows) {
return ProvisionResponse{}, ErrBindingMissing
}
if err != nil {
return ProvisionResponse{}, fmt.Errorf("read hardware agent provider binding: %w", err)
}
if configuredProduct != "" && configuredProduct != s.productKey {
return ProvisionResponse{}, ErrBindingMismatch
}
deviceName := strings.TrimSpace(configuredDeviceName)
if deviceName == "" {
deviceName = s.deviceNamePrefix + strings.ReplaceAll(identity.PublicID, "-", "")
}
if len(deviceName) > 128 || !regexp.MustCompile(`^[a-zA-Z0-9_\-.:@]{1,128}$`).MatchString(deviceName) {
return ProvisionResponse{}, ErrInvalid
}
registered, err := s.registrar.Register(ctx, RegisterInput{InstanceID: s.instanceID, ProductKey: s.productKey, DeviceName: deviceName})
if err != nil {
return ProvisionResponse{}, fmt.Errorf("register hardware provider device: %w", err)
}
secret := []byte(strings.TrimSpace(registered.DeviceSecret))
if len(secret) < 16 || len(secret) > 4096 {
return ProvisionResponse{}, errors.New("hardware provider returned an invalid device credential")
}
credentialRef := "volc-device-secret://" + identity.PublicID
const keyVersion int64 = 1
nonce := make([]byte, s.aead.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return ProvisionResponse{}, fmt.Errorf("generate hardware provider credential nonce: %w", err)
}
encrypted := s.aead.Seal(nil, nonce, secret, credentialAAD(credentialRef, keyVersion, s.instanceID, s.productKey, deviceName))
issuedAt := time.Now().UTC()
_, err = tx.Exec(ctx, `
INSERT INTO hardware_agent_provider_credential(
tenant_id,device_id,credential_ref,instance_id,product_key,provider_device_name,hardware_id,
key_version,encrypted_secret,nonce,status,issued_at
) VALUES($1::uuid,$2::uuid,$3,$4,$5,$6,$7,$8,$9,$10,'ACTIVE',$11)`,
identity.TenantID, identity.ID, credentialRef, s.instanceID, s.productKey, deviceName, hardwareID,
keyVersion, encrypted, nonce, issuedAt)
if err != nil {
return ProvisionResponse{}, fmt.Errorf("store hardware provider device credential: %w", err)
}
_, err = tx.Exec(ctx, `
UPDATE hardware_agent_device_binding
SET product_key=$2,provider_device_ref=$3,credential_ref=$4,updated_at=now()
WHERE device_id=$1::uuid`, identity.ID, s.productKey, deviceName, credentialRef)
if err != nil {
return ProvisionResponse{}, fmt.Errorf("update hardware agent provider binding: %w", err)
}
response := ProvisionResponse{InstanceID: s.instanceID, ProductKey: s.productKey, DeviceName: deviceName, HardwareID: hardwareID,
DeviceSecret: string(secret), CredentialRef: credentialRef, CredentialVersion: keyVersion,
CredentialSource: "platform_dynamic_registration", IssuedAt: issuedAt}
if activationState == deviceactivation.Provisioning {
if _, err := deviceactivation.Apply(ctx, tx, deviceactivation.Input{DeviceID: identity.ID, TenantID: identity.TenantID,
Target: deviceactivation.Active, Reason: "火山动态注册与运行凭据已就绪", Source: "provider-provisioning", ActorType: "device",
ActorPublicID: &identity.PublicID, RequestID: requestID}); err != nil {
return ProvisionResponse{}, fmt.Errorf("complete provider activation: %w", err)
}
}
if err := s.recordAudit(ctx, tx, identity, requestID, "hardware_agent.provider_credential.provision", response, false); err != nil {
return ProvisionResponse{}, err
}
if err := tx.Commit(ctx); err != nil {
return ProvisionResponse{}, fmt.Errorf("commit hardware provider provisioning: %w", err)
}
return response, nil
}
// ensureProductBinding resolves the device runtime from the platform-owned
// product defaults. DynamicRegister only creates the provider device identity;
// it does not return a Bot ID, so devices must never supply or persist that
// routing choice themselves.
func (s *Service) ensureProductBinding(ctx context.Context, tx pgx.Tx, identity deviceauth.Identity) error {
var runtime string
if err := tx.QueryRow(ctx, `SELECT COALESCE(desired_config->>'voice_runtime','legacy_voicechat') FROM device_twin WHERE device_id=$1::uuid AND tenant_id=$2::uuid FOR UPDATE`, identity.ID, identity.TenantID).Scan(&runtime); errors.Is(err, pgx.ErrNoRows) {
return ErrBindingMissing
} else if err != nil {
return fmt.Errorf("read hardware agent runtime: %w", err)
}
if runtime != "volc_hardware_agent" {
return ErrBindingMissing
}
_, err := tx.Exec(ctx, `
INSERT INTO hardware_agent_device_binding(
tenant_id,device_id,provider,product_key,bot_id,transport,minimum_sdk_version,status
) VALUES($1::uuid,$2::uuid,'volcengine',$3,$4,$5,$6,'ACTIVE')
ON CONFLICT(device_id) DO UPDATE SET
product_key=EXCLUDED.product_key,bot_id=EXCLUDED.bot_id,transport=EXCLUDED.transport,
minimum_sdk_version=EXCLUDED.minimum_sdk_version,status='ACTIVE',updated_at=now()`,
identity.TenantID, identity.ID, s.productKey, s.botID, s.transport, s.minimumSDK)
if err != nil {
return fmt.Errorf("bind hardware agent product defaults: %w", err)
}
_, err = tx.Exec(ctx, `
UPDATE device_twin SET desired_revision=desired_revision+1,
desired_config=desired_config||jsonb_build_object(
'voice_runtime','volc_hardware_agent','hardware_agent_transport',$2::text,'minimum_sdk_version',$3::text
),updated_at=now()
WHERE device_id=$1::uuid AND (
COALESCE(desired_config->>'hardware_agent_transport','')<>$2 OR
COALESCE(desired_config->>'minimum_sdk_version','')<>$3
)`, identity.ID, s.transport, s.minimumSDK)
if err != nil {
return fmt.Errorf("publish hardware agent product defaults: %w", err)
}
return nil
}
type storedCredential struct {
Status string
CredentialRef string
InstanceID string
ProductKey string
DeviceName string
HardwareID string
KeyVersion int64
EncryptedSecret []byte
Nonce []byte
IssuedAt time.Time
}
func (s *Service) readExisting(ctx context.Context, tx pgx.Tx, deviceID string) (storedCredential, bool, error) {
var item storedCredential
err := tx.QueryRow(ctx, `
SELECT status,credential_ref,instance_id,product_key,provider_device_name,hardware_id,key_version,encrypted_secret,nonce,issued_at
FROM hardware_agent_provider_credential
WHERE device_id=$1::uuid AND provider='volcengine' AND status IN ('ACTIVE','SUSPENDED')
FOR UPDATE`, deviceID).Scan(&item.Status, &item.CredentialRef, &item.InstanceID, &item.ProductKey, &item.DeviceName, &item.HardwareID,
&item.KeyVersion, &item.EncryptedSecret, &item.Nonce, &item.IssuedAt)
if errors.Is(err, pgx.ErrNoRows) {
return storedCredential{}, false, nil
}
if err != nil {
return storedCredential{}, false, fmt.Errorf("read hardware provider device credential: %w", err)
}
return item, true, nil
}
func (s *Service) prepareActivation(ctx context.Context, tx pgx.Tx, identity deviceauth.Identity, requestID string) (deviceactivation.State, error) {
var stateText string
if err := tx.QueryRow(ctx, `SELECT activation_state FROM device WHERE id=$1::uuid AND tenant_id=$2::uuid FOR UPDATE`, identity.ID, identity.TenantID).Scan(&stateText); err != nil {
return "", fmt.Errorf("read device activation for provisioning: %w", err)
}
state, err := deviceactivation.Parse(stateText)
if err != nil {
return "", ErrActivationState
}
if state == deviceactivation.Bound {
result, err := deviceactivation.Apply(ctx, tx, deviceactivation.Input{DeviceID: identity.ID, TenantID: identity.TenantID,
Target: deviceactivation.Provisioning, Reason: "设备请求火山运行凭据", Source: "provider-provisioning", ActorType: "device",
ActorPublicID: &identity.PublicID, RequestID: requestID})
if err != nil {
return "", fmt.Errorf("start provider activation: %w", err)
}
return result.To, nil
}
if state != deviceactivation.Provisioning && state != deviceactivation.Active {
return "", ErrActivationState
}
return state, nil
}
func (s *Service) recordAudit(ctx context.Context, tx pgx.Tx, identity deviceauth.Identity, requestID, action string, response ProvisionResponse, reused bool) error {
return s.audit.Record(ctx, tx, audit.Event{TenantID: &identity.TenantID, ActorType: "device", ActorPublicID: &identity.PublicID,
Action: action, ResourceType: "device", ResourceID: &identity.PublicID, RequestID: requestID, Result: "success",
Detail: map[string]any{"provider": "volcengine", "product_key": response.ProductKey, "provider_device_name": response.DeviceName,
"credential_version": response.CredentialVersion, "reused": reused}})
}
func credentialAAD(reference string, version int64, instanceID, productKey, deviceName string) []byte {
return []byte(fmt.Sprintf("hardware-provider-credential-v1\n%s\n%d\n%s\n%s\n%s", reference, version, instanceID, productKey, deviceName))
}
@@ -0,0 +1,108 @@
package hardwareprovision
import (
"context"
"os"
"strings"
"sync"
"testing"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/deviceauth"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
type countingRegistrar struct {
mu sync.Mutex
calls int
}
func (r *countingRegistrar) Register(_ context.Context, _ RegisterInput) (RegisterResult, error) {
r.mu.Lock()
defer r.mu.Unlock()
r.calls++
return RegisterResult{DeviceSecret: "provider-device-secret-with-at-least-32-characters"}, nil
}
func TestProvisionPersistsEncryptedCredentialAndReusesRegistration(t *testing.T) {
databaseURL := os.Getenv("TEST_DATABASE_URL")
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
t.Fatal(err)
}
t.Cleanup(pool.Close)
deviceID := uuid.NewString()
devicePublicID := uuid.NewString()
const tenantID = "10000000-0000-0000-0000-000000000001"
_, err = pool.Exec(ctx, `
INSERT INTO device(id,public_id,tenant_id,serial_digest,model,status,credential_ref,activation_state,activation_reason)
VALUES($1::uuid,$2::uuid,$3::uuid,$4,'hardware-provision-test','registered',$5,'BOUND','integration-test')`,
deviceID, devicePublicID, tenantID, strings.Repeat("9", 64), "test://"+devicePublicID)
if err != nil {
t.Fatal(err)
}
_, err = pool.Exec(ctx, `
INSERT INTO device_twin(tenant_id,device_id,desired_revision,desired_config)
VALUES($1::uuid,$2::uuid,1,'{"voice_runtime":"volc_hardware_agent"}'::jsonb)`, tenantID, deviceID)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_, _ = pool.Exec(ctx, `DELETE FROM audit_event WHERE resource_id=$1::uuid`, devicePublicID)
_, _ = pool.Exec(ctx, `DELETE FROM hardware_agent_provider_credential WHERE device_id=$1::uuid`, deviceID)
_, _ = pool.Exec(ctx, `DELETE FROM hardware_agent_device_binding WHERE device_id=$1::uuid`, deviceID)
_, _ = pool.Exec(ctx, `DELETE FROM device_twin WHERE device_id=$1::uuid`, deviceID)
_, _ = pool.Exec(ctx, `DELETE FROM device_activation_transition WHERE device_id=$1::uuid`, deviceID)
_, _ = pool.Exec(ctx, `DELETE FROM device WHERE id=$1::uuid`, deviceID)
})
registrar := &countingRegistrar{}
service, err := NewService(pool, audit.NewRepository(), Options{
Enabled: true, InstanceID: "instance-1", ProductKey: "product-1", BotID: "bot-test", Transport: "low_load", MinimumSDKVersion: "1.1.0", ProductSecret: "unused-with-mock-registrar",
CredentialMasterKey: strings.Repeat("ab", 32), RegisterTimeout: time.Second, DeviceNamePrefix: "dt-", Registrar: registrar,
})
if err != nil {
t.Fatal(err)
}
identity := deviceauth.Identity{ID: deviceID, PublicID: devicePublicID, TenantID: tenantID, Status: "registered"}
first, err := service.Provision(ctx, identity, "request-1", ProvisionInput{HardwareID: "hardware-1"})
if err != nil {
t.Fatal(err)
}
second, err := service.Provision(ctx, identity, "request-2", ProvisionInput{HardwareID: "hardware-1"})
if err != nil {
t.Fatal(err)
}
if first.DeviceSecret != second.DeviceSecret || first.CredentialSource != "platform_dynamic_registration" || second.CredentialSource != "platform_stored" {
t.Fatalf("unexpected provisioning responses: first=%+v second=%+v", first, second)
}
registrar.mu.Lock()
calls := registrar.calls
registrar.mu.Unlock()
if calls != 1 {
t.Fatalf("registrar calls = %d, want 1", calls)
}
var activationState string
if err := pool.QueryRow(ctx, `SELECT activation_state FROM device WHERE id=$1::uuid`, deviceID).Scan(&activationState); err != nil || activationState != "ACTIVE" {
t.Fatalf("activation state=%s err=%v, want ACTIVE", activationState, err)
}
var encrypted []byte
var productKey, deviceName, credentialRef, botID, transport, minimumSDK string
if err := pool.QueryRow(ctx, `
SELECT pc.encrypted_secret,hb.product_key,hb.provider_device_ref,hb.credential_ref,hb.bot_id,hb.transport,hb.minimum_sdk_version
FROM hardware_agent_provider_credential pc JOIN hardware_agent_device_binding hb ON hb.device_id=pc.device_id
WHERE pc.device_id=$1::uuid`, deviceID).Scan(&encrypted, &productKey, &deviceName, &credentialRef, &botID, &transport, &minimumSDK); err != nil {
t.Fatal(err)
}
if strings.Contains(string(encrypted), first.DeviceSecret) || productKey != "product-1" || deviceName != first.DeviceName || credentialRef != first.CredentialRef || botID != "bot-test" || transport != "low_load" || minimumSDK != "1.1.0" {
t.Fatalf("credential was not stored/bound safely")
}
}