Skip to content

Commit ff56547

Browse files
Support code signing from Windows system certificate stores. (#58)
1 parent 7825edb commit ff56547

4 files changed

Lines changed: 172 additions & 27 deletions

File tree

spotfire/codesign.pyx

Lines changed: 127 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
"""Tools to apply Authenticode code signing signatures and timestamps to files using the native Microsoft APIs.
88
Only runs on Windows platforms."""
99

10+
cpdef enum CertificateStoreLocation:
11+
CURRENT_USER = 1
12+
LOCAL_MACHINE = 2
13+
1014
IF UNAME_SYSNAME == "Windows":
1115

1216
import os
@@ -32,11 +36,12 @@ IF UNAME_SYSNAME == "Windows":
3236
cpdef void codesign_file(filename,
3337
certificate,
3438
windows.LPCWSTR password,
35-
windows.LPCWSTR timestamp = NULL,
39+
timestamp = None,
3640
bint use_rfc3161 = False,
3741
bint use_sha256 = False) except *:
38-
"""Codesign a file using the Microsoft signing API found in mssign32.dll.
39-
42+
"""Codesign a file with the Microsoft signing API found in mssign32.dll using a certificate found in a PFX file
43+
or PKCS#12 container.
44+
4045
:param filename: the filename of the file to codesign
4146
:param certificate: the filename of the certificate file to codesign with
4247
:param password: the password used to unlock the certificate
@@ -46,13 +51,107 @@ IF UNAME_SYSNAME == "Windows":
4651
:param use_sha256: whether or not to use SHA-256 as the timestamping hash function. If ``True``, use SHA-256.
4752
If ``False``, use SHA-1.
4853
"""
54+
cdef wincrypt.CRYPT_DATA_BLOB cert_blob
55+
cdef windows.HANDLE cert_store = NULL
56+
cdef windows.LPCWSTR timestamp_wstr = NULL
57+
58+
try:
59+
# Sanity check arguments
60+
if not os.path.isfile(certificate):
61+
raise FileNotFoundError(f"No such file: '{certificate}'")
62+
63+
# Open the certificate file and convert it into an in-memory cert store
64+
with open(certificate, "rb") as cert:
65+
cert_data = cert.read()
66+
cert_blob.cbData = <windows.DWORD>len(cert_data)
67+
cert_blob.pbData = <char*>cert_data
68+
cert_store = wincrypt.PFXImportCertStore(&cert_blob, password, 0)
69+
if cert_store is NULL:
70+
cert_store = wincrypt.PFXImportCertStore(&cert_blob, _empty_wstring, 0)
71+
if cert_store is NULL:
72+
cert_store = wincrypt.PFXImportCertStore(&cert_blob, NULL, 0)
73+
if cert_store is NULL:
74+
raise CodesignError(f"Could not load certificate {certificate}; is the password correct?")
75+
76+
# Handle optional values
77+
if timestamp is not None:
78+
timestamp_pystr = str(timestamp)
79+
timestamp_wstr = timestamp_pystr
80+
81+
# Call the common signing code
82+
_codesign_file_core(filename, cert_store, wincrypt.CERT_FIND_ANY, NULL, timestamp_wstr,
83+
use_rfc3161, use_sha256)
84+
finally:
85+
if cert_store is not NULL:
86+
wincrypt.CertCloseStore(cert_store, wincrypt.CERT_CLOSE_STORE_CHECK_FLAG)
87+
88+
cpdef void codesign_file_from_store(filename,
89+
CertificateStoreLocation store_location,
90+
windows.LPCWSTR store_name,
91+
windows.LPCWSTR store_cn,
92+
timestamp = None,
93+
bint use_rfc3161 = False,
94+
bint use_sha256 = False) except *:
95+
"""Codesign a file with the Microsoft signing API found in mssign32.dll using a certificate found in a system
96+
certificate store.
97+
98+
:param filename: the filename of the file to codesign
99+
:param store_location: the location of the Windows certificate store to find the certificate to sign with in
100+
:param store_name: the name of the Windows certificate store to find the certificate to sign with in
101+
:param store_cn: a string specifying the subject common name (or a substring thereof) of the certificate to
102+
sign with
103+
:param timestamp: a URL of the timestamping service to timestamp the code signature with
104+
:param use_rfc3161: whether or not to use the RFC 3161 timestamping protocol. If ``True``, use RFC 3161.
105+
If ``False``, use Authenticode.
106+
:param use_sha256: whether or not to use SHA-256 as the timestamping hash function. If ``True``, use SHA-256.
107+
If ``False``, use SHA-1.
108+
"""
109+
cdef windows.HANDLE cert_store = NULL
110+
cdef windows.DWORD cert_location
111+
cdef windows.LPCWSTR timestamp_wstr = NULL
112+
113+
try:
114+
# Sanity check arguments
115+
if store_name is None or len(store_name) == 0:
116+
raise ValueError("System certificate store name is empty")
117+
if store_cn is None or len(store_cn) == 0:
118+
raise ValueError("System store certificate common name is empty")
119+
120+
# Open the system store
121+
if store_location == CertificateStoreLocation.CURRENT_USER:
122+
cert_location = wincrypt.CERT_SYSTEM_STORE_CURRENT_USER
123+
elif store_location == CertificateStoreLocation.LOCAL_MACHINE:
124+
cert_location = wincrypt.CERT_SYSTEM_STORE_LOCAL_MACHINE
125+
else:
126+
raise ValueError(f"Unknown local store location '{store_location}'")
127+
cert_store = wincrypt.CertOpenStore(wincrypt.CERT_STORE_PROV_SYSTEM_W, 0, 0, cert_location, store_name)
128+
if cert_store is NULL:
129+
raise CodesignError("Could not open system store")
130+
131+
# Handle optional values
132+
if timestamp is not None:
133+
timestamp_pystr = str(timestamp)
134+
timestamp_wstr = timestamp_pystr
135+
136+
# Call the common signing code
137+
_codesign_file_core(filename, cert_store, wincrypt.CERT_FIND_SUBJECT_STR_W, store_cn, timestamp_wstr,
138+
use_rfc3161, use_sha256)
139+
finally:
140+
if cert_store is not NULL:
141+
wincrypt.CertCloseStore(cert_store, wincrypt.CERT_CLOSE_STORE_CHECK_FLAG)
142+
143+
cdef void _codesign_file_core(filename,
144+
windows.HANDLE cert_store,
145+
windows.DWORD cert_find_type,
146+
windows.LPCWSTR cert_find_param,
147+
windows.LPCWSTR timestamp,
148+
bint use_rfc3161,
149+
bint use_sha256) except *:
49150
cdef windows.HANDLE mssign32_library = NULL
50151
cdef mssign32.SignerSignExType signer_sign_ex_fun
51152
cdef mssign32.SignerTimeStampType signer_time_stamp_fun
52153
cdef mssign32.SignerTimeStampEx2Type signer_time_stamp_ex2_fun
53154
cdef mssign32.SignerFreeSignerContextType signer_free_signer_context_fun
54-
cdef wincrypt.CRYPT_DATA_BLOB cert_blob
55-
cdef windows.HANDLE cert_store = NULL
56155
cdef const wincrypt.CERT_CONTEXT* cert_context = NULL
57156
cdef windows.DWORD key_spec, key_spec_len
58157
cdef mssign32.SIGNER_FILE_INFO signer_file_info
@@ -68,8 +167,6 @@ IF UNAME_SYSNAME == "Windows":
68167
# Sanity check arguments
69168
if not os.path.isfile(filename):
70169
raise FileNotFoundError(f"No such file: '{filename}'")
71-
if not os.path.isfile(certificate):
72-
raise FileNotFoundError(f"No such file: '{certificate}'")
73170
if use_sha256 and not use_rfc3161:
74171
raise ValueError("SHA-256 timestamping requires the RFC 3161 timestamping protocol")
75172

@@ -90,23 +187,10 @@ IF UNAME_SYSNAME == "Windows":
90187
if signer_free_signer_context_fun is NULL:
91188
raise CodesignError("Cannot find function 'SignerFreeSignerContext'")
92189

93-
# Open the certificate file and convert it into an in-memory cert store
94-
with open(certificate, "rb") as cert:
95-
cert_data = cert.read()
96-
cert_blob.cbData = <windows.DWORD>len(cert_data)
97-
cert_blob.pbData = <char*>cert_data
98-
cert_store = wincrypt.PFXImportCertStore(&cert_blob, password, 0)
99-
if cert_store is NULL:
100-
cert_store = wincrypt.PFXImportCertStore(&cert_blob, _empty_wstring, 0)
101-
if cert_store is NULL:
102-
cert_store = wincrypt.PFXImportCertStore(&cert_blob, NULL, 0)
103-
if cert_store is NULL:
104-
raise CodesignError(f"Could not load certificate {certificate}; is the password correct?")
105-
106-
# Extract the cert from the new cert store
190+
# Extract the cert from the store
107191
cert_context = wincrypt.CertFindCertificateInStore(cert_store,
108192
wincrypt.X509_ASN_ENCODING | wincrypt.PKCS_7_ASN_ENCODING,
109-
0, wincrypt.CERT_FIND_ANY, NULL, NULL)
193+
0, cert_find_type, cert_find_param, NULL)
110194
if cert_context is NULL:
111195
raise CodesignError("Could not get certificate from store")
112196
found_private_key = False
@@ -120,7 +204,7 @@ IF UNAME_SYSNAME == "Windows":
120204
else:
121205
cert_context = wincrypt.CertFindCertificateInStore(cert_store,
122206
wincrypt.X509_ASN_ENCODING | wincrypt.PKCS_7_ASN_ENCODING,
123-
0, wincrypt.CERT_FIND_ANY, NULL, cert_context)
207+
0, cert_find_type, cert_find_param, cert_context)
124208
if cert_context is NULL:
125209
raise CodesignError("Could not get certificate from store")
126210

@@ -182,15 +266,14 @@ IF UNAME_SYSNAME == "Windows":
182266
finally:
183267
if cert_context is not NULL:
184268
wincrypt.CertFreeCertificateContext(cert_context)
185-
if cert_store is not NULL:
186-
wincrypt.CertCloseStore(cert_store, wincrypt.CERT_CLOSE_STORE_CHECK_FLAG)
187269
if mssign32_library is not NULL:
188270
windows.FreeLibrary(mssign32_library)
189271

190272
ELSE:
191273

192274
def codesign_file(filename, certificate, password, timestamp = None, use_rfc3161 = False, use_sha256 = False):
193-
"""Codesign a file using the Microsoft signing API found in mssign32.dll.
275+
"""Codesign a file with the Microsoft signing API found in mssign32.dll using a certificate found in a PFX file
276+
or PKCS#12 container.
194277
195278
:param filename: the filename of the file to codesign
196279
:param certificate: the filename of the certificate file to codesign with
@@ -202,3 +285,21 @@ ELSE:
202285
If ``False``, use SHA-1.
203286
"""
204287
raise OSError("Codesigning not supported on non-Win32 platforms")
288+
289+
def codesign_file_from_store(filename, store_location, store_name, store_cn, timestamp = None, use_rfc3161 = False,
290+
use_sha256 = False):
291+
"""Codesign a file with the Microsoft signing API found in mssign32.dll using a certificate found in a system
292+
certificate store.
293+
294+
:param filename: the filename of the file to codesign
295+
:param store_location: the location of the Windows certificate store to find the certificate to sign with in
296+
:param store_name: the name of the Windows certificate store to find the certificate to sign with in
297+
:param store_cn: a string specifying the subject common name (or a substring thereof) of the certificate to
298+
sign with
299+
:param timestamp: a URL of the timestamping service to timestamp the code signature with
300+
:param use_rfc3161: whether or not to use the RFC 3161 timestamping protocol. If ``True``, use RFC 3161.
301+
If ``False``, use Authenticode.
302+
:param use_sha256: whether or not to use SHA-256 as the timestamping hash function. If ``True``, use SHA-256.
303+
If ``False``, use SHA-1.
304+
"""
305+
raise OSError("Codesigning not supported on non-Win32 platforms")

spotfire/spk.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,9 @@ def __init__(self):
620620
super().__init__()
621621
self.cert_file = None
622622
self.cert_password = None
623+
self.cert_store_machine = False
624+
self.cert_store_name = None
625+
self.cert_store_cn = None
623626
self.timestamp_url = None
624627
self.sha256 = False
625628
self._resources = []
@@ -673,7 +676,14 @@ def _build_payload(self, metadata: ElementTree.Element, module: ElementTree.Elem
673676
payload.writestr("module.xml", _et_to_bytes(module))
674677

675678
# Codesign the payload
676-
if self.cert_file:
679+
if self.cert_store_name and self.cert_store_cn:
680+
if self.cert_store_machine:
681+
store_location = codesign.LOCAL_MACHINE
682+
else:
683+
store_location = codesign.CURRENT_USER
684+
codesign.codesign_file_from_store(payload_dest, store_location, self.cert_store_name, self.cert_store_cn,
685+
self.timestamp_url, self.sha256, self.sha256)
686+
elif self.cert_file:
677687
codesign.codesign_file(payload_dest, self.cert_file, self.cert_password, self.timestamp_url, self.sha256,
678688
self.sha256)
679689

@@ -690,6 +700,14 @@ def _build_payload(self, metadata: ElementTree.Element, module: ElementTree.Elem
690700
argument("--cert", metavar="FILE", help="path to the certificate file to sign the package with (Analyst "
691701
"only)"),
692702
argument("--password", help="password for the certificate file (Analyst only)"),
703+
argument("--store-machine", action="store_true", help="Use the 'local machine' certificate store "
704+
"location to sign the package instead of the "
705+
"'current user' (Analyst only)"),
706+
argument("--store-name", metavar="STORE", help="name of the certificate store to find the certificate "
707+
"to sign the package in (Analyst only)"),
708+
argument("--store-cn", metavar="STRING", help="substring of the subject common name of the certificate "
709+
"in the certificate store to sign the package with (Analyst "
710+
"only)"),
693711
argument("--timestamp", metavar="URL", help="URL of a timestamping service to timestamp the package with "
694712
"(Analyst only)"),
695713
argument("--sha256", action="store_true", help="use SHA-256 for file and timestamp digests (Analyst only)")
@@ -713,6 +731,9 @@ def python(args, hook=None) -> None:
713731
package_builder.excludes = getattr(args, "exclude")
714732
package_builder.cert_file = getattr(args, "cert")
715733
package_builder.cert_password = getattr(args, "password")
734+
package_builder.cert_store_machine = getattr(args, "store_machine")
735+
package_builder.cert_store_name = getattr(args, "store_name")
736+
package_builder.cert_store_cn = getattr(args, "store_cn")
716737
package_builder.timestamp_url = getattr(args, "timestamp")
717738
package_builder.sha256 = getattr(args, "sha256")
718739
else:
@@ -774,6 +795,14 @@ def python(args, hook=None) -> None:
774795
argument("--cert", metavar="FILE", help="path to the certificate file to sign the package with (Analyst "
775796
"only)"),
776797
argument("--password", help="password for the certificate file (Analyst only)"),
798+
argument("--store-machine", action="store_true", help="Use the 'local machine' certificate store "
799+
"location to sign the package instead of the "
800+
"'current user' (Analyst only)"),
801+
argument("--store-name", metavar="STORE", help="name of the certificate store to find the certificate "
802+
"to sign the package in (Analyst only)"),
803+
argument("--store-cn", metavar="STRING", help="substring of the subject common name of the certificate "
804+
"in the certificate store to sign the package with (Analyst "
805+
"only)"),
777806
argument("--timestamp", metavar="URL", help="URL of a timestamping service to timestamp the package with "
778807
"(Analyst only)"),
779808
argument("--sha256", action="store_true", help="use SHA-256 for file and timestamp digests (Analyst only)")
@@ -789,6 +818,9 @@ def packages(args) -> None:
789818
package_builder = _CabPackageBuilder()
790819
package_builder.cert_file = getattr(args, "cert")
791820
package_builder.cert_password = getattr(args, "password")
821+
package_builder.cert_store_machine = getattr(args, "store_machine")
822+
package_builder.cert_store_name = getattr(args, "store_name")
823+
package_builder.cert_store_cn = getattr(args, "store_cn")
792824
package_builder.timestamp_url = getattr(args, "timestamp")
793825
package_builder.sha256 = getattr(args, "sha256")
794826
brand_subkey = "Analyst"

vendor/windows/__init__.pxd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
cdef extern from "<windows.h>" nogil:
22
# Types defined at https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types
33
ctypedef bint BOOL
4+
ctypedef const char* LPCSTR
45
ctypedef const Py_UNICODE* LPCWSTR
56
ctypedef unsigned long DWORD
67
ctypedef void* HANDLE
78
ctypedef int HRESULT
89
ctypedef Py_ssize_t INT_PTR
10+
ctypedef Py_ssize_t ULONG_PTR
911

1012
# Preprocessor definitions defined at https://docs.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values
1113
cdef enum:

vendor/windows/wincrypt.pxd

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ cdef extern from "<wincrypt.h>" nogil:
3535
# Function defined at https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-pfximportcertstore
3636
windows.HANDLE PFXImportCertStore(CRYPT_DATA_BLOB*, windows.LPCWSTR, windows.DWORD)
3737

38+
# Function defined at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certopenstore
39+
windows.HANDLE CertOpenStore(windows.LPCSTR, windows.DWORD, windows.ULONG_PTR, windows.DWORD, void*)
40+
41+
# Preprocessor definitions for CertOpenStore function
42+
cdef enum:
43+
CERT_SYSTEM_STORE_CURRENT_USER
44+
CERT_SYSTEM_STORE_LOCAL_MACHINE
45+
char* CERT_STORE_PROV_SYSTEM_W
46+
3847
# Function defined at https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certclosestore
3948
windows.BOOL CertCloseStore(windows.HANDLE, windows.DWORD)
4049

@@ -49,6 +58,7 @@ cdef extern from "<wincrypt.h>" nogil:
4958
# Preprocessor definitions for CertFindCertificateInStore
5059
cdef enum:
5160
CERT_FIND_ANY
61+
CERT_FIND_SUBJECT_STR_W
5262

5363
# Function defined at https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certgetcertificatecontextproperty
5464
windows.BOOL CertGetCertificateContextProperty(CERT_CONTEXT*, windows.DWORD, void*, windows.DWORD*)

0 commit comments

Comments
 (0)