param( [Parameter(Mandatory=$false)] [string]$Hostname = $env:COMPUTERNAME, [Parameter(Mandatory=$false)] [int]$ValidityYears = 2 ) # SECURITY: Validate hostname matches actual machine name to prevent cert for wrong host $actualHostname = [System.Net.Dns]::GetHostName() if ($Hostname -ne $actualHostname) { Write-Warning "Hostname mismatch detected!" Write-Warning " Requested: $Hostname" Write-Warning " Actual machine: $actualHostname" Write-Warning "Using actual hostname to prevent certificate mismatch." $Hostname = $actualHostname } # Generate self-signed cert for this endpoint $cert = New-SelfSignedCertificate ` -Subject "CN=$Hostname" ` -DnsName $Hostname ` -CertStoreLocation "Cert:\LocalMachine\My" ` -KeyExportPolicy NonExportable ` -KeyUsage DigitalSignature, KeyEncipherment ` -NotAfter (Get-Date).AddYears($ValidityYears) ` -HashAlgorithm SHA256 # SECURITY WARNING: Grant Users group read access to the private key so the tray # (running as interactive user) can use it when shelling out to shopdbagent.exe --once. # The service already works because it runs as SYSTEM. # # RISK: This grants ALL interactive users on the machine read access to the private # key, which means any user with console/RDP access can extract the key and impersonate # this endpoint. Only acceptable in environments where ALL interactive users are trusted. # # MITIGATION OPTIONS: # 1. Best: Remove tray dependency on cert (tray calls service API, service makes HTTP) # 2. Acceptable: Grant only to specific user account running the tray # 3. Document: Only deploy in environments where all console users are trusted operators try { $rsaKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert) $keyPath = $rsaKey.Key.UniqueName # CNG keys are in SystemKeys, not MachineKeys $keyFile = Join-Path "$env:ProgramData\Microsoft\Crypto\SystemKeys" $keyPath if (-not (Test-Path $keyFile)) { $keyFile = Join-Path "$env:ProgramData\Microsoft\Crypto\Keys" $keyPath } if (-not (Test-Path $keyFile)) { $keyFile = Join-Path "$env:ProgramData\Microsoft\Crypto\RSA\MachineKeys" $keyPath } if (Test-Path $keyFile) { # Grant Read + Execute to BUILTIN\Users $acl = Get-Acl $keyFile $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( "BUILTIN\Users", "Read,ReadAndExecute", "Allow") $acl.AddAccessRule($rule) Set-Acl -Path $keyFile -AclObject $acl Write-Host " Granted Users group access to private key" } else { Write-Host " WARNING: Could not find private key file to set permissions" } } catch { Write-Host " WARNING: Could not set private key permissions: $($_.Exception.Message)" } Write-Host "Generated certificate:" Write-Host " Thumbprint: $($cert.Thumbprint)" Write-Host " Subject: $($cert.Subject)" Write-Host " Expires: $($cert.NotAfter)" # Compute SHA1 fingerprint to match nginx $ssl_client_fingerprint variable # SECURITY NOTE: Nginx only provides SHA1 via $ssl_client_fingerprint variable. # SHA1 is deprecated for cryptographic signing but remains acceptable for certificate # fingerprinting in air-gapped environments with: # - Short-lived certificates (2 years max) # - Certificate revocation support (cert_revoked_at column) # - No external CA trust (self-signed only) # Future migration to SHA256 requires: nginx module for SHA256, or IIS deployment # which exposes full PEM cert for server-side hashing. $certBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert) $sha1 = [System.Security.Cryptography.SHA1]::Create() $fingerprintBytes = $sha1.ComputeHash($certBytes) $fingerprint = ($fingerprintBytes | ForEach-Object { $_.ToString("x2") }) -join "" Write-Host "" Write-Host "=========================================" Write-Host "CERTIFICATE GENERATED SUCCESSFULLY" Write-Host "=========================================" Write-Host "Fingerprint (SHA1): $fingerprint" Write-Host "Length: 40 hex chars (nginx limitation)" Write-Host "" Write-Host "Register this certificate in ShopDB database:" Write-Host "" Write-Host " UPDATE assets" Write-Host " SET client_cert_fingerprint = '$fingerprint'," Write-Host " cert_issued_at = CURRENT_TIMESTAMP," Write-Host " cert_expires_at = '$($cert.NotAfter.ToString("yyyy-MM-dd HH:mm:ss"))'," Write-Host " cert_revoked_at = NULL," Write-Host " cert_revocation_reason = NULL" Write-Host " WHERE assetnumber = '(REPLACE_WITH_ASSET_NUMBER)';" Write-Host "" Write-Host "To revoke this certificate later (compromised/lost endpoint):" Write-Host "" Write-Host " UPDATE assets" Write-Host " SET cert_revoked_at = CURRENT_TIMESTAMP," Write-Host " cert_revocation_reason = 'Compromised - TICKET-123'" Write-Host " WHERE client_cert_fingerprint = '$fingerprint';" Write-Host "" Write-Host "=========================================" # Update agent config $configPath = "C:\ProgramData\ShopDB\config.json" if (Test-Path $configPath) { $config = Get-Content $configPath | ConvertFrom-Json $config | Add-Member -NotePropertyName clientCertThumbprint -NotePropertyValue $cert.Thumbprint -Force $config | ConvertTo-Json | Set-Content $configPath Write-Host "" Write-Host "Updated $configPath with thumbprint" } else { Write-Host "" Write-Host "WARNING: $configPath not found - agent may not be configured" } # Output for automation Write-Host "" Write-Host "---MACHINE-READABLE---" Write-Host "THUMBPRINT=$($cert.Thumbprint)" Write-Host "FINGERPRINT=$fingerprint" Write-Host "HOSTNAME=$Hostname" Write-Host "EXPIRES=$($cert.NotAfter.ToString('yyyy-MM-dd HH:mm:ss'))"