# Fabrique des licences signees. A LANCER CHEZ TOI, JAMAIS LIVRE. # # .\tools\licence.ps1 -Init cree la paire de cles (une seule fois) # et imprime la constante Delphi a coller # .\tools\licence.ps1 -For "a@b.com" -Name "Ami" fabrique une licence # # POURQUOI ECDSA P-256 ET PAS AUTRE CHOSE : .NET sur Windows EST CNG. La # signature que produit ce script (r||s brut, 64 octets) est exactement celle # qu'attend BCryptVerifySignature cote Delphi, et les coordonnees exportees ici # se rangent telles quelles dans un BCRYPT_ECCPUBLIC_BLOB. Aucun ASN.1 a lire # d'un cote ni de l'autre - c'est ce qui rend la verification courte. param( [switch]$Init, [string]$For, [string]$Name = '', [string]$Machine, [string]$KeyFile = "$PSScriptRoot\licence-private.key" ) $ErrorActionPreference = 'Stop' Add-Type -AssemblyName System.Security function ToB64Url([byte[]]$b) { [Convert]::ToBase64String($b).TrimEnd('=').Replace('+', '-').Replace('/', '_') } if ($Init) { if (Test-Path $KeyFile) { throw "$KeyFile existe deja. L'ecraser invaliderait TOUTES les licences deja emises. Deplace-le d'abord si c'est voulu." } $ecdsa = New-Object System.Security.Cryptography.ECDsaCng 256 $p = $ecdsa.ExportParameters($true) # La cle privee : D, plus le point public pour pouvoir re-signer sans le # recalculer. 96 octets, en hexa pour rester lisible dans un gestionnaire # de mots de passe - c'est la ou elle doit finir, pas dans ce dossier. $priv = [byte[]]($p.D + $p.Q.X + $p.Q.Y) [System.IO.File]::WriteAllText($KeyFile, ([BitConverter]::ToString($priv) -replace '-', '')) # BCRYPT_ECCPUBLIC_BLOB : magic ECS1 + longueur + X + Y. $blob = [byte[]]([BitConverter]::GetBytes([uint32]0x31534345) + [BitConverter]::GetBytes([uint32]32) + $p.Q.X + $p.Q.Y) '' 'Cle privee ecrite dans : ' + $KeyFile 'METS-LA A L ABRI ET SUPPRIME-LA D ICI. Qui la detient fabrique des licences.' '' '--- a coller dans PM.Licence.pas ---' '' 'const' ' LicencePublicKeyBlob: array[0..71] of Byte = (' for ($i = 0; $i -lt $blob.Length; $i += 12) { $row = ($blob[$i..([Math]::Min($i + 11, $blob.Length - 1))] | ForEach-Object { '$' + $_.ToString('X2') }) -join ', ' if ($i + 12 -lt $blob.Length) { ' ' + $row + ',' } else { ' ' + $row } } ' );' '' return } if (-not $For) { throw 'Donne -For (ou -Init pour creer la paire de cles).' } if (-not (Test-Path $KeyFile)) { throw "Pas de cle privee dans $KeyFile. Lance -Init, ou remets la cle mise a l'abri." } $hex = (Get-Content $KeyFile -Raw).Trim() $priv = [byte[]]::new($hex.Length / 2) for ($i = 0; $i -lt $priv.Length; $i++) { $priv[$i] = [Convert]::ToByte($hex.Substring($i * 2, 2), 16) } $p = New-Object System.Security.Cryptography.ECParameters $p.Curve = [System.Security.Cryptography.ECCurve]::CreateFromFriendlyName('nistP256') $p.D = $priv[0..31] $q = New-Object System.Security.Cryptography.ECPoint $q.X = $priv[32..63] $q.Y = $priv[64..95] $p.Q = $q $ecdsa = [System.Security.Cryptography.ECDsa]::Create($p) # LE LIAGE AU POSTE EST OBLIGATOIRE, ET IL N'Y A PLUS DE DEROGATION. # # Les licences flottantes (sans champ m) existaient ; l'exe les refuse # desormais, ce qui etait la seule facon d'annuler celle qui etait sortie - # un schema hors ligne n'a pas de revocation. En emettre une aujourd'hui # produirait une cle que l'application rejettera : autant ne pas pouvoir. # # L'empreinte est celle qu'affiche l'ecran d'activation sur le poste du # client : il faut donc qu'il ait installe l'application AVANT qu'on emette # sa licence. C'est le prix du liage. if (-not $Machine) { throw @' Il manque -Machine . Fais-toi envoyer l'empreinte affichee sur l'ecran d'activation du poste (format XXXX-XXXX-XXXX-XXXX), puis : .\licence.ps1 -For "a@b.com" -Name "Nom" -Machine "XXXX-XXXX-XXXX-XXXX" Une licence non liee n'est plus emettable : l'application la refuserait. '@ } # Charge utile courte : elle voyage dans la cle que l'utilisateur recopie. # Pas d'expiration - une date qu'on fait respecter avec l'horloge de la # machine du client ne prouve rien. A ajouter le jour ou on vend un abonnement. $payload = [ordered]@{ n = $Name; e = $For; d = (Get-Date).ToString('yyyy-MM-dd') } # Les tirets ne sont qu'une aide a la recopie ; Delphi compare le condense # nu, en minuscules. $payload.m = ($Machine -replace '[^0-9A-Fa-f]', '').ToLower() if ($payload.m.Length -ne 16) { throw "Empreinte attendue : 16 caracteres hexadecimaux (recu $($payload.m.Length))." } $json = ($payload | ConvertTo-Json -Compress) $bytes = [Text.Encoding]::UTF8.GetBytes($json) $sig = $ecdsa.SignData($bytes, [System.Security.Cryptography.HashAlgorithmName]::SHA256) $licence = (ToB64Url $bytes) + '.' + (ToB64Url $sig) '' 'Licence pour ' + $For + ' — liee au poste ' + $Machine '' $licence '' 'Verification locale : ' + $ecdsa.VerifyData($bytes, $sig, [System.Security.Cryptography.HashAlgorithmName]::SHA256) ''