Files
story/internal/hardwareprovision/service.go
T

367 lines
16 KiB
Go
Raw Normal View History

2026-09-06 17:04:11 +08:00
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))
}