mirror of
https://github.com/gwdevhub/Daybreak.git
synced 2026-09-13 20:09:21 +00:00
* Sign windows binaries * Fix temp test * Remove temporary test --------- Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
246 lines
9.4 KiB
PowerShell
246 lines
9.4 KiB
PowerShell
# Authenticate SimplySign Desktop non-interactively.
|
|
#
|
|
# Certum's SimplySign cloud has no headless login: the certificate only reaches
|
|
# the Windows store after the GUI client authenticates. So we generate the
|
|
# current TOTP from the otpauth:// secret (CERTUM_OTP_URI) and paste the
|
|
# credentials into the login dialog. This needs an interactive desktop session,
|
|
# which GitHub-hosted Windows runners provide.
|
|
#
|
|
# Based on https://www.devas.life/how-to-automate-signing-your-windows-app-with-certum/
|
|
# and the refinements in blinkdisk's connect-simplysign.ps1.
|
|
|
|
param(
|
|
[string]$OtpUri = $env:CERTUM_OTP_URI,
|
|
[string]$UserId = $env:CERTUM_USERID,
|
|
[string]$ExePath = $env:CERTUM_EXE_PATH
|
|
)
|
|
|
|
if (-not $OtpUri) { Write-Host "ERROR: CERTUM_OTP_URI not provided"; exit 1 }
|
|
if (-not $UserId) { Write-Host "ERROR: CERTUM_USERID not provided"; exit 1 }
|
|
if (-not $ExePath) {
|
|
$ExePath = "C:\Program Files\Certum\SimplySign Desktop\SimplySignDesktop.exe"
|
|
}
|
|
|
|
Write-Host "=== SimplySign Desktop TOTP authentication ==="
|
|
if (-not (Test-Path $ExePath)) {
|
|
Write-Host "ERROR: SimplySign Desktop not found at $ExePath"
|
|
exit 1
|
|
}
|
|
|
|
# --- Parse the otpauth:// URI (works on PowerShell 5.1 and 7+) ---------------
|
|
$uri = [Uri]$OtpUri
|
|
try {
|
|
$q = [System.Web.HttpUtility]::ParseQueryString($uri.Query)
|
|
} catch {
|
|
$q = @{}
|
|
foreach ($part in $uri.Query.TrimStart('?') -split '&') {
|
|
$kv = $part -split '=', 2
|
|
if ($kv.Count -eq 2) { $q[$kv[0]] = [Uri]::UnescapeDataString($kv[1]) }
|
|
}
|
|
}
|
|
|
|
$Base32 = $q['secret']
|
|
$Digits = if ($q['digits']) { [int]$q['digits'] } else { 6 }
|
|
$Period = if ($q['period']) { [int]$q['period'] } else { 30 }
|
|
$Algorithm = if ($q['algorithm']) { $q['algorithm'].ToUpper() } else { 'SHA256' }
|
|
|
|
if (-not $Base32) { Write-Host "ERROR: otpauth URI has no 'secret'"; exit 1 }
|
|
if ($Algorithm -notin @('SHA1', 'SHA256', 'SHA512')) {
|
|
Write-Host "ERROR: unsupported TOTP algorithm: $Algorithm"
|
|
exit 1
|
|
}
|
|
|
|
# --- TOTP generator (RFC 6238), inline C# so there are no dependencies --------
|
|
Add-Type -Language CSharp @"
|
|
using System;
|
|
using System.Security.Cryptography;
|
|
|
|
public static class Totp
|
|
{
|
|
private const string B32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
|
|
private static byte[] Base32Decode(string s)
|
|
{
|
|
s = s.TrimEnd('=').ToUpperInvariant();
|
|
byte[] bytes = new byte[s.Length * 5 / 8];
|
|
int bitBuffer = 0, bitsLeft = 0, idx = 0;
|
|
foreach (char c in s)
|
|
{
|
|
int val = B32.IndexOf(c);
|
|
if (val < 0) throw new ArgumentException("Invalid Base32 char: " + c);
|
|
bitBuffer = (bitBuffer << 5) | val;
|
|
bitsLeft += 5;
|
|
if (bitsLeft >= 8) { bytes[idx++] = (byte)(bitBuffer >> (bitsLeft - 8)); bitsLeft -= 8; }
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
private static HMAC Hmac(string algorithm, byte[] key)
|
|
{
|
|
switch (algorithm.ToUpper())
|
|
{
|
|
case "SHA1": return new HMACSHA1(key);
|
|
case "SHA256": return new HMACSHA256(key);
|
|
case "SHA512": return new HMACSHA512(key);
|
|
default: throw new ArgumentException("Unsupported algorithm: " + algorithm);
|
|
}
|
|
}
|
|
|
|
public static string Now(string secret, int digits, int period, string algorithm)
|
|
{
|
|
byte[] key = Base32Decode(secret);
|
|
long counter = DateTimeOffset.UtcNow.ToUnixTimeSeconds() / period;
|
|
byte[] cnt = BitConverter.GetBytes(counter);
|
|
if (BitConverter.IsLittleEndian) Array.Reverse(cnt);
|
|
|
|
byte[] hash;
|
|
using (var h = Hmac(algorithm, key)) { hash = h.ComputeHash(cnt); }
|
|
|
|
int offset = hash[hash.Length - 1] & 0x0F;
|
|
int binary =
|
|
((hash[offset] & 0x7F) << 24) |
|
|
((hash[offset + 1] & 0xFF) << 16) |
|
|
((hash[offset + 2] & 0xFF) << 8) |
|
|
(hash[offset + 3] & 0xFF);
|
|
int otp = binary % (int)Math.Pow(10, digits);
|
|
return otp.ToString(new string('0', digits));
|
|
}
|
|
}
|
|
"@
|
|
|
|
function Find-UiByName($el, $walker, $name) {
|
|
$c = $walker.GetFirstChild($el)
|
|
while ($c) {
|
|
if ($c.Current.Name -eq $name) { return $c }
|
|
$r = Find-UiByName $c $walker $name
|
|
if ($r) { return $r }
|
|
$c = $walker.GetNextSibling($c)
|
|
}
|
|
return $null
|
|
}
|
|
|
|
# An outdated SimplySign build pops a modal "New version found - download?" box
|
|
# over the login form, which swallows the credential keystrokes. Decline it by
|
|
# clicking "No" (Invoke / legacy default action / click the element's point).
|
|
function Dismiss-UpdatePrompt {
|
|
try { Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes -ErrorAction Stop } catch { return $false }
|
|
if (-not ('Win32Mouse' -as [type])) {
|
|
Add-Type @"
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
public static class Win32Mouse {
|
|
[DllImport("user32.dll")] static extern bool SetCursorPos(int x, int y);
|
|
[DllImport("user32.dll")] static extern void mouse_event(uint f, uint x, uint y, uint d, int e);
|
|
public static void Click(int x, int y){ SetCursorPos(x,y); mouse_event(0x02,0,0,0,0); mouse_event(0x04,0,0,0,0); }
|
|
}
|
|
"@
|
|
}
|
|
$procIds = @((Get-Process -Name '*SimplySign*' -ErrorAction SilentlyContinue).Id)
|
|
$walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
|
|
$top = $walker.GetFirstChild([System.Windows.Automation.AutomationElement]::RootElement)
|
|
while ($top) {
|
|
if ($procIds -contains $top.Current.ProcessId) {
|
|
$no = Find-UiByName $top $walker "No"
|
|
if ($no) {
|
|
Write-Host "Update prompt detected; declining (clicking 'No')."
|
|
try { ($no.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern)).Invoke(); return $true } catch {}
|
|
try { ($no.GetCurrentPattern([System.Windows.Automation.LegacyIAccessiblePattern]::Pattern)).DoDefaultAction(); return $true } catch {}
|
|
try { $pt = $no.GetClickablePoint(); [Win32Mouse]::Click([int]$pt.X, [int]$pt.Y); return $true } catch { Write-Host "Click 'No' failed: $($_.Exception.Message)" }
|
|
}
|
|
}
|
|
$top = $walker.GetNextSibling($top)
|
|
}
|
|
return $false
|
|
}
|
|
|
|
# Start from a clean slate so a stale window/session can't swallow the keystrokes.
|
|
if ($existing = Get-Process -Name "SimplySignDesktop" -ErrorAction Ignore) {
|
|
Write-Host "Killing existing SimplySignDesktop process..."
|
|
$existing | Stop-Process -Force
|
|
Start-Sleep -Seconds 1
|
|
}
|
|
|
|
Write-Host "Launching SimplySign Desktop..."
|
|
$proc = Start-Process -FilePath $ExePath -PassThru
|
|
Start-Sleep -Seconds 3
|
|
|
|
$wshell = New-Object -ComObject WScript.Shell
|
|
|
|
Write-Host "Focusing the login window..."
|
|
$focused = $wshell.AppActivate($proc.Id)
|
|
if (-not $focused) { $focused = $wshell.AppActivate('SimplySign Desktop') }
|
|
for ($i = 0; (-not $focused) -and ($i -lt 10); $i++) {
|
|
Start-Sleep -Milliseconds 500
|
|
$focused = $wshell.AppActivate($proc.Id) -or $wshell.AppActivate('SimplySign Desktop')
|
|
}
|
|
if (-not $focused) {
|
|
Write-Host "ERROR: could not bring SimplySign Desktop to the foreground"
|
|
exit 1
|
|
}
|
|
|
|
# Decline the "new version available" modal if it's covering the login form.
|
|
if (Dismiss-UpdatePrompt) { Start-Sleep -Milliseconds 800 }
|
|
|
|
# Re-assert focus: dismissing the dialog can move the foreground window, and
|
|
# keystrokes sent to the wrong window are silently dropped.
|
|
$wshell.AppActivate($proc.Id) | Out-Null
|
|
$wshell.AppActivate('SimplySign Desktop') | Out-Null
|
|
Start-Sleep -Milliseconds 400
|
|
|
|
# Paste rather than type: SendKeys mangles characters like + ^ % ( ) and can
|
|
# drop characters; pasting delivers the exact string. Falls back to typing if
|
|
# the clipboard is unavailable.
|
|
function Set-Field([string]$text) {
|
|
try {
|
|
Set-Clipboard -Value $text -ErrorAction Stop
|
|
Start-Sleep -Milliseconds 150
|
|
$wshell.SendKeys("^v")
|
|
} catch {
|
|
Write-Host "Clipboard unavailable ($($_.Exception.Message)); typing instead."
|
|
$wshell.SendKeys($text)
|
|
}
|
|
Start-Sleep -Milliseconds 250
|
|
}
|
|
|
|
Write-Host "Injecting credentials..."
|
|
Set-Field $UserId
|
|
$wshell.SendKeys("{TAB}")
|
|
Start-Sleep -Milliseconds 200
|
|
|
|
# Generate the code right before sending it so it can't expire while we focus.
|
|
$otp = [Totp]::Now($Base32, $Digits, $Period, $Algorithm)
|
|
Set-Field $otp
|
|
Start-Sleep -Milliseconds 200
|
|
$wshell.SendKeys("{ENTER}")
|
|
|
|
# Don't leave the OTP / username sitting on the clipboard afterwards.
|
|
try { Set-Clipboard -Value " " -ErrorAction Stop } catch {}
|
|
|
|
Write-Host "Waiting for authentication to settle..."
|
|
Start-Sleep -Seconds 5
|
|
|
|
if (-not (Get-Process -Id $proc.Id -ErrorAction SilentlyContinue)) {
|
|
Write-Host "ERROR: SimplySign Desktop exited - authentication failed."
|
|
exit 1
|
|
}
|
|
|
|
# Confirm the login actually mounted the signing certificate into the store.
|
|
$thumb = if ($env:CERTUM_CERT_SHA1) { ($env:CERTUM_CERT_SHA1 -replace '\s', '').ToUpperInvariant() } else { $null }
|
|
Write-Host "Verifying the signing certificate reached the store..."
|
|
$deadline = (Get-Date).AddSeconds(60)
|
|
$found = $false
|
|
do {
|
|
$certs = Get-ChildItem Cert:\CurrentUser\My, Cert:\LocalMachine\My -ErrorAction SilentlyContinue
|
|
if ($thumb) { $found = [bool]($certs | Where-Object { $_.Thumbprint -eq $thumb }) }
|
|
else { $found = [bool]($certs | Where-Object { $_.Subject -like '*Certum*' -or $_.Issuer -like '*Certum*' }) }
|
|
if ($found) { break }
|
|
Start-Sleep -Seconds 3
|
|
} while ((Get-Date) -lt $deadline)
|
|
|
|
if ($found) {
|
|
Write-Host "=== Authentication complete: signing certificate is available. ==="
|
|
} else {
|
|
Write-Host "ERROR: signing certificate did not appear after authentication."
|
|
exit 1
|
|
}
|