package hardwarelicense import ( "context" "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/hex" "errors" "fmt" "io" "regexp" "strings" "time" "data-terminal-cloud/internal/audit" "data-terminal-cloud/internal/deviceactivation" "data-terminal-cloud/internal/deviceauth" "data-terminal-cloud/internal/identity" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) var ( ErrInvalid = errors.New("invalid hardware license input") ErrAccessDenied = errors.New("hardware license access denied") ErrNotFound = errors.New("hardware license not found") ErrUnavailable = errors.New("no hardware license is available") ErrAssignment = errors.New("hardware license assignment mismatch") ErrExpired = errors.New("hardware license expired") ErrAcknowledgement = errors.New("hardware license acknowledgement invalid") ErrVersionConflict = errors.New("hardware license version conflict") ) var referencePattern = regexp.MustCompile(`^[a-zA-Z0-9_\-.:@/]{1,200}$`) type Service struct { pool *pgxpool.Pool audit *audit.Repository aead cipher.AEAD } type ImportInput struct { LicenseRef string Payload string InstanceID string ProductKey string ExpiresAt time.Time } type Item struct { ID string `json:"id"` LicenseRef string `json:"license_ref"` Provider string `json:"provider"` InstanceID string `json:"instance_id"` ProductKey string `json:"product_key"` Status string `json:"status"` AllocatedDeviceID *string `json:"allocated_device_id,omitempty"` AllocatedAt *time.Time `json:"allocated_at,omitempty"` ActivatedAt *time.Time `json:"activated_at,omitempty"` ExpiresAt time.Time `json:"expires_at"` LastErrorCode *string `json:"last_error_code,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` Version int64 `json:"version"` } type Delivery struct { LicenseRef string `json:"license_ref"` License string `json:"license"` Digest string `json:"digest"` ExpiresAt time.Time `json:"expires_at"` Status string `json:"status"` } type AcknowledgeInput struct { LicenseRef string Digest string Result string ErrorCode string } func NewService(pool *pgxpool.Pool, auditRepository *audit.Repository, masterKey string) (*Service, error) { if pool == nil || auditRepository == nil { return nil, errors.New("hardware license service dependencies are required") } key, err := hex.DecodeString(strings.TrimSpace(masterKey)) if err != nil || len(key) != 32 { return nil, errors.New("hardware license 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 license cipher: %w", err) } aead, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("create hardware license AEAD: %w", err) } return &Service{pool: pool, audit: auditRepository, aead: aead}, nil } func (s *Service) Import(ctx context.Context, principal identity.Principal, tenantPublicID, requestID string, input ImportInput) (Item, error) { input.LicenseRef = strings.TrimSpace(input.LicenseRef) input.Payload = strings.TrimSpace(input.Payload) input.InstanceID = strings.TrimSpace(input.InstanceID) input.ProductKey = strings.TrimSpace(input.ProductKey) if !referencePattern.MatchString(input.LicenseRef) || input.InstanceID == "" || len(input.InstanceID) > 200 || input.ProductKey == "" || len(input.ProductKey) > 200 || len(input.Payload) < 8 || len(input.Payload) > 65536 || !input.ExpiresAt.After(time.Now().UTC()) { return Item{}, ErrInvalid } tx, err := s.pool.Begin(ctx) if err != nil { return Item{}, err } defer tx.Rollback(ctx) access, err := requireAccess(ctx, tx, principal, tenantPublicID, true) if err != nil { return Item{}, err } digestBytes := sha256.Sum256([]byte(input.Payload)) digest := hex.EncodeToString(digestBytes[:]) nonce := make([]byte, s.aead.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return Item{}, err } encrypted := s.aead.Seal(nil, nonce, []byte(input.Payload), licenseAAD(input.LicenseRef, input.InstanceID, input.ProductKey, input.ExpiresAt)) var item Item err = tx.QueryRow(ctx, `INSERT INTO hardware_agent_license( tenant_id,provider,instance_id,product_key,license_ref,payload_digest,encrypted_payload,nonce,status,expires_at ) VALUES($1::uuid,'volcengine',$2,$3,$4,$5,$6,$7,'AVAILABLE',$8) RETURNING public_id::text,license_ref,provider,instance_id,product_key,status,NULL::text,allocated_at,activated_at,expires_at,last_error_code,created_at,updated_at,version`, access.tenantID, input.InstanceID, input.ProductKey, input.LicenseRef, digest, encrypted, nonce, input.ExpiresAt). Scan(&item.ID, &item.LicenseRef, &item.Provider, &item.InstanceID, &item.ProductKey, &item.Status, &item.AllocatedDeviceID, &item.AllocatedAt, &item.ActivatedAt, &item.ExpiresAt, &item.LastErrorCode, &item.CreatedAt, &item.UpdatedAt, &item.Version) if err != nil { return Item{}, fmt.Errorf("import hardware license: %w", err) } if err := s.audit.Record(ctx, tx, audit.Event{TenantID: &access.tenantID, ActorType: "user", ActorPublicID: &access.userPublicID, Action: "ops.hardware_license.import", ResourceType: "hardware_agent_license", ResourceID: &item.ID, RequestID: requestID, Result: "success", Detail: map[string]any{"license_ref": item.LicenseRef, "product_key": item.ProductKey, "expires_at": item.ExpiresAt, "payload_digest": digest}}); err != nil { return Item{}, err } if err := tx.Commit(ctx); err != nil { return Item{}, err } return item, nil } func (s *Service) List(ctx context.Context, principal identity.Principal, tenantPublicID string) ([]Item, error) { access, err := requireAccess(ctx, s.pool, principal, tenantPublicID, false) if err != nil { return nil, err } _, _ = s.pool.Exec(ctx, `UPDATE hardware_agent_license SET status='EXPIRED',updated_at=now(),version=version+1 WHERE tenant_id=$1::uuid AND expires_at<=now() AND status IN ('AVAILABLE','ALLOCATED','ACTIVE','SUSPENDED')`, access.tenantID) rows, err := s.pool.Query(ctx, `SELECT l.public_id::text,l.license_ref,l.provider,l.instance_id,l.product_key,l.status,d.public_id::text,l.allocated_at,l.activated_at,l.expires_at,l.last_error_code,l.created_at,l.updated_at,l.version FROM hardware_agent_license l LEFT JOIN device d ON d.id=l.allocated_device_id WHERE l.tenant_id=$1::uuid ORDER BY l.created_at DESC`, access.tenantID) if err != nil { return nil, err } defer rows.Close() items := make([]Item, 0) for rows.Next() { var item Item if err := rows.Scan(&item.ID, &item.LicenseRef, &item.Provider, &item.InstanceID, &item.ProductKey, &item.Status, &item.AllocatedDeviceID, &item.AllocatedAt, &item.ActivatedAt, &item.ExpiresAt, &item.LastErrorCode, &item.CreatedAt, &item.UpdatedAt, &item.Version); err != nil { return nil, err } items = append(items, item) } return items, rows.Err() } func (s *Service) Revoke(ctx context.Context, principal identity.Principal, tenantPublicID, licensePublicID, requestID string, expectedVersion int64) (Item, error) { return s.transitionInventory(ctx, principal, tenantPublicID, licensePublicID, requestID, expectedVersion, "REVOKED") } func (s *Service) Release(ctx context.Context, principal identity.Principal, tenantPublicID, licensePublicID, requestID string, expectedVersion int64) (Item, error) { return s.transitionInventory(ctx, principal, tenantPublicID, licensePublicID, requestID, expectedVersion, "AVAILABLE") } func (s *Service) transitionInventory(ctx context.Context, principal identity.Principal, tenantPublicID, licensePublicID, requestID string, expectedVersion int64, target string) (Item, error) { tx, err := s.pool.Begin(ctx) if err != nil { return Item{}, err } defer tx.Rollback(ctx) access, err := requireAccess(ctx, tx, principal, tenantPublicID, true) if err != nil { return Item{}, err } var internalID, status string var version int64 var deviceID *string if err := tx.QueryRow(ctx, `SELECT id::text,status,version,allocated_device_id::text FROM hardware_agent_license WHERE public_id=$1::uuid AND tenant_id=$2::uuid FOR UPDATE`, licensePublicID, access.tenantID).Scan(&internalID, &status, &version, &deviceID); errors.Is(err, pgx.ErrNoRows) { return Item{}, ErrNotFound } else if err != nil { return Item{}, err } if version != expectedVersion { return Item{}, ErrVersionConflict } if target == "AVAILABLE" && (status != "SUSPENDED" || deviceID == nil) { return Item{}, ErrAssignment } if target == "AVAILABLE" && deviceID != nil { var bound bool if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM device_binding WHERE device_id=$1::uuid AND unbound_at IS NULL)`, *deviceID).Scan(&bound); err != nil { return Item{}, err } if bound { return Item{}, ErrAssignment } } if target == "REVOKED" && status == "REVOKED" { return Item{}, ErrAssignment } if target == "REVOKED" && deviceID != nil { var activationState string if err := tx.QueryRow(ctx, `SELECT activation_state FROM device WHERE id=$1::uuid AND tenant_id=$2::uuid FOR UPDATE`, *deviceID, access.tenantID).Scan(&activationState); err != nil { return Item{}, err } if activationState == string(deviceactivation.Active) { if _, err := deviceactivation.Apply(ctx, tx, deviceactivation.Input{DeviceID: *deviceID, TenantID: access.tenantID, Target: deviceactivation.Provisioning, Reason: "硬件智能体License已吊销", Source: "license-revoke", ActorType: "user", ActorPublicID: &access.userPublicID, RequestID: requestID}); err != nil { return Item{}, err } } } var item Item query := `UPDATE hardware_agent_license SET status=$3, allocated_device_id=CASE WHEN $3='AVAILABLE' THEN NULL ELSE allocated_device_id END, allocated_at=CASE WHEN $3='AVAILABLE' THEN NULL ELSE allocated_at END, activated_at=CASE WHEN $3='AVAILABLE' THEN NULL ELSE activated_at END, last_error_code=CASE WHEN $3='AVAILABLE' THEN NULL ELSE last_error_code END, released_at=CASE WHEN $3='AVAILABLE' THEN now() ELSE released_at END, revoked_at=CASE WHEN $3='REVOKED' THEN now() ELSE revoked_at END, updated_at=now(),version=version+1 WHERE id=$1::uuid AND tenant_id=$2::uuid RETURNING public_id::text,license_ref,provider,instance_id,product_key,status,NULL::text,allocated_at,activated_at,expires_at,last_error_code,created_at,updated_at,version` if err := tx.QueryRow(ctx, query, internalID, access.tenantID, target).Scan(&item.ID, &item.LicenseRef, &item.Provider, &item.InstanceID, &item.ProductKey, &item.Status, &item.AllocatedDeviceID, &item.AllocatedAt, &item.ActivatedAt, &item.ExpiresAt, &item.LastErrorCode, &item.CreatedAt, &item.UpdatedAt, &item.Version); err != nil { return Item{}, err } if err := s.audit.Record(ctx, tx, audit.Event{TenantID: &access.tenantID, ActorType: "user", ActorPublicID: &access.userPublicID, Action: "ops.hardware_license." + strings.ToLower(target), ResourceType: "hardware_agent_license", ResourceID: &item.ID, RequestID: requestID, Result: "success", Detail: map[string]any{"license_ref": item.LicenseRef, "previous_status": status}}); err != nil { return Item{}, err } if err := tx.Commit(ctx); err != nil { return Item{}, err } return item, nil } func (s *Service) AcquireForDevice(ctx context.Context, tx pgx.Tx, tenantID, deviceID, instanceID, productKey string) (Delivery, error) { _, _ = tx.Exec(ctx, `UPDATE hardware_agent_license SET status='EXPIRED',updated_at=now(),version=version+1 WHERE tenant_id=$1::uuid AND expires_at<=now() AND status IN ('AVAILABLE','ALLOCATED','ACTIVE','SUSPENDED')`, tenantID) var internalID, ref, digest, status string var encrypted, nonce []byte var expires time.Time err := tx.QueryRow(ctx, `SELECT id::text,license_ref,payload_digest,status,encrypted_payload,nonce,expires_at FROM hardware_agent_license WHERE tenant_id=$1::uuid AND allocated_device_id=$2::uuid AND instance_id=$3 AND product_key=$4 AND status IN ('ALLOCATED','ACTIVE','SUSPENDED') FOR UPDATE`, tenantID, deviceID, instanceID, productKey).Scan(&internalID, &ref, &digest, &status, &encrypted, &nonce, &expires) if errors.Is(err, pgx.ErrNoRows) { err = tx.QueryRow(ctx, `SELECT id::text,license_ref,payload_digest,status,encrypted_payload,nonce,expires_at FROM hardware_agent_license WHERE tenant_id=$1::uuid AND allocated_device_id IS NULL AND instance_id=$2 AND product_key=$3 AND status='AVAILABLE' AND expires_at>now() ORDER BY expires_at,id FOR UPDATE SKIP LOCKED LIMIT 1`, tenantID, instanceID, productKey).Scan(&internalID, &ref, &digest, &status, &encrypted, &nonce, &expires) } if errors.Is(err, pgx.ErrNoRows) { return Delivery{}, ErrUnavailable } if err != nil { return Delivery{}, err } payload, err := s.aead.Open(nil, nonce, encrypted, licenseAAD(ref, instanceID, productKey, expires)) if err != nil { return Delivery{}, errors.New("decrypt hardware license") } if status != "ACTIVE" { status = "ALLOCATED" } if _, err := tx.Exec(ctx, `UPDATE hardware_agent_license SET allocated_device_id=$2::uuid,status=$3, allocated_at=COALESCE(allocated_at,now()),released_at=NULL,last_error_code=NULL,updated_at=now(),version=version+1 WHERE id=$1::uuid`, internalID, deviceID, status); err != nil { return Delivery{}, err } return Delivery{LicenseRef: ref, License: string(payload), Digest: digest, ExpiresAt: expires, Status: status}, nil } func (s *Service) Acknowledge(ctx context.Context, identity deviceauth.Identity, requestID string, input AcknowledgeInput) error { input.LicenseRef = strings.TrimSpace(input.LicenseRef) input.Digest = strings.ToLower(strings.TrimSpace(input.Digest)) input.Result = strings.ToUpper(strings.TrimSpace(input.Result)) input.ErrorCode = strings.TrimSpace(input.ErrorCode) if !referencePattern.MatchString(input.LicenseRef) || !regexp.MustCompile(`^[0-9a-f]{64}$`).MatchString(input.Digest) || (input.Result != "INSTALLED" && input.Result != "FAILED") || len(input.ErrorCode) > 100 { return ErrAcknowledgement } tx, err := s.pool.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) var id, publicID, digest, status string var expires time.Time if err := tx.QueryRow(ctx, `SELECT id::text,public_id::text,payload_digest,status,expires_at FROM hardware_agent_license WHERE tenant_id=$1::uuid AND allocated_device_id=$2::uuid AND license_ref=$3 FOR UPDATE`, identity.TenantID, identity.ID, input.LicenseRef).Scan(&id, &publicID, &digest, &status, &expires); errors.Is(err, pgx.ErrNoRows) { return ErrAssignment } else if err != nil { return err } if !expires.After(time.Now().UTC()) { _, _ = tx.Exec(ctx, `UPDATE hardware_agent_license SET status='EXPIRED',updated_at=now(),version=version+1 WHERE id=$1::uuid`, id) if err := tx.Commit(ctx); err != nil { return err } return ErrExpired } if digest != input.Digest { return ErrAcknowledgement } if input.Result == "FAILED" { _, err = tx.Exec(ctx, `UPDATE hardware_agent_license SET status='SUSPENDED',last_error_code=NULLIF($2,''),updated_at=now(),version=version+1 WHERE id=$1::uuid`, id, input.ErrorCode) if err != nil { return err } } else { _, err = tx.Exec(ctx, `UPDATE hardware_agent_license SET status='ACTIVE',activated_at=COALESCE(activated_at,now()),last_error_code=NULL,updated_at=now(),version=version+1 WHERE id=$1::uuid`, id) if err != nil { return err } var providerReady, bindingReady bool var activationState string if err := tx.QueryRow(ctx, `SELECT d.activation_state,EXISTS(SELECT 1 FROM hardware_agent_provider_credential pc WHERE pc.device_id=d.id AND pc.status='ACTIVE'),EXISTS(SELECT 1 FROM device_binding b WHERE b.device_id=d.id AND b.unbound_at IS NULL) FROM device d WHERE d.id=$1::uuid AND d.tenant_id=$2::uuid FOR UPDATE`, identity.ID, identity.TenantID).Scan(&activationState, &providerReady, &bindingReady); err != nil { return err } if !providerReady || !bindingReady { return ErrAssignment } if activationState == string(deviceactivation.Provisioning) { if _, err := deviceactivation.Apply(ctx, tx, deviceactivation.Input{DeviceID: identity.ID, TenantID: identity.TenantID, Target: deviceactivation.Active, Reason: "设备已安全保存并确认有效License", Source: "license-ack", ActorType: "device", ActorPublicID: &identity.PublicID, RequestID: requestID}); err != nil { return err } } } if err := s.audit.Record(ctx, tx, audit.Event{TenantID: &identity.TenantID, ActorType: "device", ActorPublicID: &identity.PublicID, Action: "hardware_agent.license.ack", ResourceType: "hardware_agent_license", ResourceID: &publicID, RequestID: requestID, Result: "success", Detail: map[string]any{"license_ref": input.LicenseRef, "result": input.Result, "error_code": input.ErrorCode}}); err != nil { return err } return tx.Commit(ctx) } type access struct{ tenantID, userPublicID, role string } type queryRow interface { QueryRow(context.Context, string, ...any) pgx.Row } func requireAccess(ctx context.Context, q queryRow, principal identity.Principal, tenantPublicID string, mutate bool) (access, error) { var a access err := q.QueryRow(ctx, `SELECT t.id::text,ua.public_id::text,ua.platform_role FROM tenant t JOIN user_account ua ON ua.tenant_id=t.id AND ua.status='active' AND ua.account_kind='operator' WHERE t.public_id=$1::uuid AND t.status='active' AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3`, tenantPublicID, principal.Issuer, principal.Subject).Scan(&a.tenantID, &a.userPublicID, &a.role) if errors.Is(err, pgx.ErrNoRows) { return access{}, ErrAccessDenied } if err != nil { return access{}, err } if mutate && a.role != "ops_operator" && a.role != "ops_admin" { return access{}, ErrAccessDenied } return a, nil } func licenseAAD(ref, instanceID, productKey string, expires time.Time) []byte { return []byte(fmt.Sprintf("hardware-license-v1\n%s\n%s\n%s\n%d", ref, instanceID, productKey, expires.UTC().Unix())) }