405ac7fac4
C'etait la decision prise, et je l'avais mal lue : "pour le moment 1" repondait a la question 1, pas "une seule licence". PM.MachineId melange l'UUID de la carte mere (WMI) et la serie du volume systeme, puis n'en publie que 16 hexa : l'empreinte se recopie dans un courriel, l'UUID brut n'a pas a circuler. Un UUID nul ou tout a FF est rejete — VM et images clonees l'annoncent, et deux postes tomberaient alors sur la meme empreinte. Le champ m est compare APRES la signature. m absent = licence flottante, acceptee : seule la cle privee peut en emettre, et le champ ne peut pas etre retire sans casser la signature. Cote script -Machine est obligatoire sauf -Floating explicite. L'ecran d'activation affiche l'Installation ID : une licence liee ne peut plus etre emise avant l'installation. Verifie sur ce poste : liee a lui acceptee, liee a un autre refusee, flottante acceptee. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
132 lines
5.2 KiB
PowerShell
132 lines
5.2 KiB
PowerShell
# 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,
|
|
[switch]$Floating,
|
|
[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 <email> (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 LE DEFAUT, ET IL FAUT LE DIRE POUR Y COUPER.
|
|
#
|
|
# Une licence flottante s'active sur autant de machines qu'on veut : c'est
|
|
# parfois voulu (licence de site), jamais par distraction. -Floating l'exige
|
|
# explicitement, sinon l'oubli de -Machine produirait en silence la licence
|
|
# la plus permissive - exactement le defaut qu'on ne veut pas.
|
|
#
|
|
# 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 -and -not $Floating) {
|
|
throw @'
|
|
Il manque -Machine <empreinte>.
|
|
|
|
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"
|
|
|
|
Pour une licence NON liee, qui s'activera sur n'importe quelle machine,
|
|
demande-la explicitement avec -Floating.
|
|
'@
|
|
}
|
|
|
|
# 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') }
|
|
if ($Machine) {
|
|
# 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)
|
|
|
|
''
|
|
if ($Machine) { 'Licence pour ' + $For + ' — liee au poste ' + $Machine }
|
|
else { 'Licence pour ' + $For + ' — FLOTTANTE (toute machine)' }
|
|
''
|
|
$licence
|
|
''
|
|
'Verification locale : ' + $ecdsa.VerifyData($bytes, $sig, [System.Security.Cryptography.HashAlgorithmName]::SHA256)
|
|
''
|