feat(licence): fabrique de licences signees ECDSA P-256

Script a lancer chez soi, jamais livre. -Init cree la paire et imprime
la constante Delphi du BCRYPT_ECCPUBLIC_BLOB ; -For emet une licence
payload.signature en base64url.

.NET sur Windows EST CNG : la signature produite (r||s brut, 64 octets)
est celle qu'attend BCryptVerifySignature, et les coordonnees exportees
se rangent telles quelles dans le blob. Aucun ASN.1 des deux cotes.

Verifie en rejouant l'import du blob par CngKey.Import : licence valide
acceptee, un octet retourne dans la charge utile la fait tomber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-09-05 14:36:09 +01:00
parent fc03c90c95
commit 2b9f3ca37d
2 changed files with 100 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
# 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]$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)
# 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') }
$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
''
$licence
''
'Verification locale : ' + $ecdsa.VerifyData($bytes, $sig, [System.Security.Cryptography.HashAlgorithmName]::SHA256)
''