este ejemplo permite seleccionar un certificado digital válido de los instalados en el sistema Windows y obtener su thumbprint (huella digital).
Es una primera aproximación funcional que comparto, y también me gustaría que comentarais si disponéis de una forma mejor/alternativa de conseguir el Thumbprint de un certificado de la lista de instalados en el sistema.
/*
© 2025 Biel Maimó
*/
FUNCTION SelCert()
LOCAL oDlg, oBrw, aCerts, cThumb := "",nOpc, aSel:={},i,cFileCsv
cFileCsv:='liscer.csv'
FErase(cFileCsv)
Cert2Csv(cFileCsv)
aCerts := CsvToArrayOfHashes( cFileCsv, ";" )
IF Empty( aCerts )
MsgInfo( "CSV vacío o no encontrado" )
RETURN NIL
ENDIF
FOR i:=1 TO Len(aCerts)
AAdd(aSel,aCerts[i]["FriendlyName"])
NEXT
nOpc:=MsgList(aSel, 'Certificados',,,,,'&Seleccionar',,.T.,'&Cancelar')
IF nOpc!=0
?aCerts[nOpc]["FriendlyName"],aCerts[nOpc]["Thumbprint"]
cThumb:=aCerts[nOpc]["Thumbprint"]
ENDIF
RETURN cThumb
//------------------------------------------------------------------------------
FUNCTION Cert2Csv(cFileCsv)
LOCAL cPsExe,cScript,cCmd,cOut:='',cErr:='',nRc
cPSExe :=FindPS()
CrtPS1() //Crea cmd.ps1
cScript := "cmd.ps1"
cCmd := '"' + cPSExe + '" -NoProfile -ExecutionPolicy Bypass -File "' + cScript + '" -Path "'+cFileCsv+'"'
nRC := hb_processRun( cCmd, , @cOut, @cErr ) //hb_processRun( <cCommand>, [<cStdIn>], [<@cStdOut>], [<@cStdErr>], [<lDetach>] ) -> nResult
IF nRc!=0 //Error
? "RC:", nRC, "STDOUT >>>", cOut, "STDERR >>>", cErr
ELSE
FErase(cScript)
ENDIF
RETURN NIL
//-----------------------------------------------------------------------------
STATIC FUNCTION CsvToArrayOfHashes( cFile, cSep )
LOCAL c,aLines,aHeaders:={},aData:={},i, aTok, hRow
c := hb_MemoRead( cFile )
aLines := hb_ATokens( c, hb_eol() )
IF Empty( aLines )
RETURN {}
ENDIF
// Cabeceras
aTok := CsvSplit( aLines[1], cSep )
FOR i := 1 TO Len( aTok )
AAdd( aHeaders, AllTrim( StripQuotes( aTok[i] ) ) )
NEXT
// Filas
FOR i := 2 TO Len( aLines )
IF Empty( AllTrim( aLines[i] ) )
LOOP
ENDIF
aTok := CsvSplit( aLines[i], cSep )
hRow := {=>}
AEval( aHeaders, {|h, k| hRow[ h ] := iif( k <= Len( aTok ), StripQuotes( aTok[k] ), "" ) } )
AAdd( aData, hRow )
NEXT
RETURN aData
//------------------------------------------------------------------------------
STATIC FUNCTION CsvSplit( cLine, cSep )
LOCAL a := {}, cBuf := "", lInQ := .F., i, ch, q := '"'
FOR i := 1 TO Len( cLine )
ch := SubStr( cLine, i, 1 )
DO CASE
CASE ch == q
// (doble comilla dentro de campo: "" -> ")
IF lInQ .AND. i < Len( cLine ) .AND. SubStr( cLine, i+1, 1 ) == q
cBuf += q
i++
ELSE
lInQ := ! lInQ
ENDIF
CASE ch == cSep .AND. ! lInQ
AAdd( a, cBuf )
cBuf := ""
OTHERWISE
cBuf += ch
ENDCASE
NEXT
AAdd( a, cBuf )
RETURN a
//-----------------------------------------------------------------------------
STATIC FUNCTION StripQuotes( c )
c := AllTrim( c )
IF !Empty( c ) .AND. Left( c,1 ) == '"' .AND. Right( c,1 ) == '"'
c := SubStr( c,2, Len( c )-2 )
ENDIF
RETURN c
//------------------------------------------------------------------------------
FUNCTION FindPS()
LOCAL cWin,aTry,c
cWin := GetEnv( "WINDIR" )
aTry := { ;
cWin + "\Sysnative\WindowsPowerShell\v1.0\powershell.exe", ; // 32-bit app en OS 64-bit
cWin + "\System32\WindowsPowerShell\v1.0\powershell.exe", ;
cWin + "\SysWOW64\WindowsPowerShell\v1.0\powershell.exe", ;
"powershell.exe", ; // por si está en PATH
"pwsh.exe" } // PowerShell 7
FOR EACH c IN aTry
IF hb_FileExists( c )
RETURN c
ENDIF
NEXT
RETURN ""
//------------------------------------------------------------------------------
STATIC FUNCTION CrtPS1()
LOCAL cSrc
cSrc:='# © 2025 Biel Maimó'+hb_eol()
cSrc+='param('+ hb_eol()
cSrc+='[string]$Path = "Certs.csv" # valor por defecto si no se pasa parámetro'+ hb_eol()
cSrc+=')'+ hb_eol()
cSrc+='# Fecha actual'+ hb_eol()
cSrc+='$now = Get-Date'+ hb_eol()
cSrc+=''+ hb_eol()
cSrc+='Get-ChildItem Cert:\CurrentUser\My |'+ hb_eol()
cSrc+=' Where-Object {'+ hb_eol()
cSrc+=' # Debe tener clave privada'+ hb_eol()
cSrc+=' $_.HasPrivateKey -eq $true -and'+ hb_eol()
cSrc+=' # Estar dentro de fechas de validez'+ hb_eol()
cSrc+=' $_.NotBefore -le $now -and $_.NotAfter -ge $now -and'+ hb_eol()
cSrc+=' # EKU debe contener Firma digital o Code Signing o equivalente AEAT'+ hb_eol()
cSrc+=' ($_.EnhancedKeyUsageList |'+ hb_eol()
cSrc+=' Where-Object {'+ hb_eol()
cSrc+=' $_.Oid.Value -eq "1.3.6.1.5.5.7.3.3" -or # Code Signing'+ hb_eol()
cSrc+=' $_.Oid.Value -eq "1.3.6.1.4.1.5734.1.1" -or # AEAT sello'+ hb_eol()
cSrc+=' $_.FriendlyName -match "Firma" -or $_.FriendlyName -match "Digital Signature"'+ hb_eol()
cSrc+=' }'+ hb_eol()
cSrc+=' )'+ hb_eol()
cSrc+=' } |'+ hb_eol()
cSrc+=' Select-Object `'+ hb_eol()
cSrc+=' @{Name="Store";Expression={"CurrentUser\My"}},'+ hb_eol()
cSrc+=' Subject,'+ hb_eol()
cSrc+=' Issuer,'+ hb_eol()
cSrc+=' FriendlyName,'+ hb_eol()
cSrc+=' Thumbprint,'+ hb_eol()
cSrc+=' HasPrivateKey,'+ hb_eol()
cSrc+=' @{Name="NotBefore";Expression={ Get-Date $_.NotBefore -Format "yyyy-MM-dd HH:mm:ss" }},'+ hb_eol()
cSrc+=' @{Name="NotAfter";Expression={ Get-Date $_.NotAfter -Format "yyyy-MM-dd HH:mm:ss" }},'+ hb_eol()
cSrc+=' @{Name="EKUs";Expression={ ($_.EnhancedKeyUsageList | ForEach-Object { $_.Oid.Value }) -join "," }} |'+ hb_eol()
cSrc+=" Export-Csv -Path $path -NoTypeInformation -Encoding UTF8 -Delimiter ';'"+ hb_eol()
cSrc+=''+ hb_eol()
cSrc+='Write-Host "$path"'+ hb_eol()
hb_memowrit("cmd.ps1",cSrc)
RETURN NIL- Usa PowerShell para buscar certificados digitales en el almacén de Windows (CurrentUser\My) que sean válidos, tengan clave privada y sean aptos para firma digital o code signing.
- Guarda la información de los certificados en un archivo CSV (liscer.csv).
- Lee el archivo CSV y muestra una lista con los nombres descriptivos de los certificados en un cuadro de diálogo.
- El usuario selecciona un certificado de la lista.
- Devuelve el thumbprint del certificado elegido.
Cualquier mejora, duda o sugerencia es bienvenida.
https://henkobusiness.com/