Skip to content

Commit 5b763cc

Browse files
authored
Merge pull request renpy#6742 from bkats/ecsign
Replaces ecdsa python library with OpenSSL (resolves renpy#6686)
2 parents e483572 + 4dbba01 commit 5b763cc

File tree

14 files changed

+982
-77
lines changed

14 files changed

+982
-77
lines changed

launcher/game/distribute.rpy

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1719,14 +1719,12 @@ fix_dlc("renios", "renios")
17191719
f.write(update_data)
17201720

17211721
# Write the signed file.
1722-
import ecdsa
1723-
17241722
with open(self.find_update_pem(), "rb") as f:
1725-
signing_key = ecdsa.SigningKey.from_pem(f.read())
1723+
signing_key = renpy.ecsign.pem_to_der(f.read())
17261724

17271725
fn = renpy.fsencode(os.path.join(self.destination, "updates.ecdsa"))
17281726
with open(fn, "wb") as f:
1729-
f.write(signing_key.sign(update_data))
1727+
f.write(renpy.ecsign.sign_data(update_data, signing_key))
17301728

17311729
def find_update_pem(self):
17321730
if self.build['renpy']:
@@ -1735,14 +1733,12 @@ fix_dlc("renios", "renios")
17351733
return os.path.join(self.project.path, "update.pem")
17361734

17371735
def make_key_pem(self):
1738-
import ecdsa
1739-
17401736
with open(self.find_update_pem(), "rb") as f:
1741-
signing_key = ecdsa.SigningKey.from_pem(f.read())
1737+
signing_key = renpy.ecsign.pem_to_der(f.read())
17421738

17431739
key_pem = self.temp_filename("key.pem")
17441740
with open(key_pem, "wb") as f:
1745-
f.write(signing_key.verifying_key.to_pem())
1741+
f.write(renpy.ecsign.der_to_pem(renpy.ecsign.get_public_key_from_private(signing_key), "PUBLIC"))
17461742

17471743
def dump(self):
17481744
for k, v in sorted(self.file_lists.items()):

launcher/game/distribute_gui.rpy

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,8 @@ label add_update_pem:
272272
python hide:
273273
interface.info("You're trying to build an update, but an update.pem file doesn't exist.\n\nThis file is used to sign updates, and will be automatically created in your projects's base directory.\n\nYou'll need to back up update.pem and keep it safe.", cancel=Jump("build_distributions"))
274274

275-
import ecdsa
276-
key = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p).to_pem()
275+
import renpy.ecsign
276+
key = renpy.ecsign.der_to_pem(renpy.ecsign.generate_private_key(), "PRIVATE")
277277

278278
with open(os.path.join(project.current.path, "update.pem"), "wb") as f:
279279
f.write(key)

launcher/game/installer.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ def manifest(url, renpy=False, insecure=False):
450450
If true, verificaiton is disabled.
451451
"""
452452

453-
import ecdsa
453+
import renpy.ecsign
454454

455455
download(url, "temp:manifest.py")
456456

@@ -463,9 +463,9 @@ def manifest(url, renpy=False, insecure=False):
463463
with open(_path("temp:manifest.py.sig"), "rb") as f:
464464
sig = f.read()
465465

466-
key = ecdsa.VerifyingKey.from_pem(_renpy.exports.open_file("renpy_ecdsa_public.pem").read())
466+
key = renpy.ecsign.pem_to_der(_renpy.exports.open_file("renpy_ecdsa_public.pem").read())
467467

468-
if not key.verify(sig, manifest):
468+
if not renpy.ecsign.verify_data(manifest, key, sig):
469469
error(_("The manifest signature is not valid."))
470470
return
471471

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ dependencies = [
99
"six",
1010
"pefile",
1111
"requests",
12-
"ecdsa",
1312
"rsa",
1413
"setuptools",
1514
"cython",

renpy/common/00updater.rpy

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -817,8 +817,8 @@ init -1500 python in updater:
817817

818818
try:
819819

820-
import ecdsa
821-
verifying_key = ecdsa.VerifyingKey.from_pem(open(key, "rb").read())
820+
import renpy.ecsign
821+
verifying_key = renpy.ecsign.pem_to_der(open(key, "rb").read())
822822

823823
url = urlparse.urljoin(self.url, "updates.ecdsa")
824824
f = urlopen(url)
@@ -828,7 +828,7 @@ init -1500 python in updater:
828828
if not signature:
829829
break
830830

831-
if verifying_key.verify(signature, updates_json):
831+
if renpy.ecsign.verify_data(updates_json, verifying_key, signature):
832832
verified = True
833833

834834
self.log.write("Verified with ECDSA.\n")

renpy/ecsign.pyi

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright 2026 B.Kats and Tom Rothamel <pytom@bishoujo.us>
2+
#
3+
# Permission is hereby granted, free of charge, to any person
4+
# obtaining a copy of this software and associated documentation files
5+
# (the "Software"), to deal in the Software without restriction,
6+
# including without limitation the rights to use, copy, modify, merge,
7+
# publish, distribute, sublicense, and/or sell copies of the Software,
8+
# and to permit persons to whom the Software is furnished to do so,
9+
# subject to the following conditions:
10+
#
11+
# The above copyright notice and this permission notice shall be
12+
# included in all copies or substantial portions of the Software.
13+
#
14+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
22+
def generate_private_key() -> bytes | None:
23+
"""
24+
Generates a EC private key and return key in DER format
25+
"""
26+
27+
def sign_data(data : bytes, private_key : bytes) -> bytes:
28+
"""
29+
returns ecdsa signature for data using sha1 hash
30+
31+
`data`
32+
The data to sign
33+
34+
`private_key`
35+
The private key to use in DER format
36+
"""
37+
38+
def verify_data(data : bytes, public_key : bytes, sign : bytes) -> bool:
39+
"""
40+
verifies ecdsa signature for data using sha1 hash and returns result
41+
42+
`data`
43+
The data to sign
44+
45+
`public_key`
46+
The public key to use in DER format
47+
48+
`sign`
49+
The signature to verify
50+
"""
51+
52+
def get_public_key_from_private(private_key : bytes) -> bytes | None:
53+
"""
54+
returns public key in DER format from the private key
55+
56+
`private_key`
57+
The private key to use in DER format
58+
"""
59+
60+
def validate_private_key(private_key : bytes) -> bool:
61+
"""
62+
Validates if given key is a private key
63+
"""
64+
65+
def validate_public_key(public_key : bytes) -> bool:
66+
"""
67+
Validates if given key is a public key
68+
"""
69+
70+
def pem_to_der(pem : bytes | str) -> bytes:
71+
"""
72+
unpacks DER from a PEM file
73+
"""
74+
75+
def der_to_pem(der : bytes, name : str) -> bytes:
76+
"""
77+
packs a DER into a PEM file
78+
"""

renpy/ecsign.pyx

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Copyright 2026 B.Kats and Tom Rothamel <pytom@bishoujo.us>
2+
#
3+
# Permission is hereby granted, free of charge, to any person
4+
# obtaining a copy of this software and associated documentation files
5+
# (the "Software"), to deal in the Software without restriction,
6+
# including without limitation the rights to use, copy, modify, merge,
7+
# publish, distribute, sublicense, and/or sell copies of the Software,
8+
# and to permit persons to whom the Software is furnished to do so,
9+
# subject to the following conditions:
10+
#
11+
# The above copyright notice and this permission notice shall be
12+
# included in all copies or substantial portions of the Software.
13+
#
14+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
22+
23+
########################################
24+
## WARNING for when editing this file ##
25+
########################################
26+
27+
# This file contain functions that need to support async handling for the web build
28+
# Renaming or reordering any functions will break the async handling
29+
# The cython generated names for the following functions need to be listed
30+
# in the ASYNCIFY_ONLY list: (tasks/renpython.py; 2 names for each function)
31+
# - generate_private_key
32+
# - sign_data
33+
# - verify_data
34+
# - get_public_key_from_private
35+
# - validate_private_key
36+
# - validate_public_key
37+
38+
from libc.stdlib cimport free
39+
import base64
40+
41+
cdef extern from "ec_sign_core.h":
42+
int ECSign(const unsigned char *priv_key_der, size_t key_len, const char *data, size_t data_len, char *signature, size_t signature_len);
43+
int ECVerify(const unsigned char *public_key_der, size_t key_len, const char *data, size_t data_len, char *signature, size_t signature_len);
44+
45+
void ECGeneratePrivateKey(unsigned char **priv_key_der, size_t *priv_len);
46+
void ECGetPublicKeyFromPrivate(const unsigned char *priv_key_der, size_t priv_len, unsigned char **public_key_der, size_t *pub_len);
47+
48+
int ECValidateKey(int public, const unsigned char *key_der, size_t key_len);
49+
50+
def generate_private_key() -> bytes | None:
51+
cdef unsigned char* privkey = NULL
52+
cdef size_t privlen = 0
53+
54+
ECGeneratePrivateKey(&privkey, &privlen)
55+
56+
rv = None
57+
if privlen > 0 and privkey != NULL:
58+
rv = bytes(privkey[:privlen])
59+
60+
free(privkey);
61+
62+
return rv
63+
64+
def sign_data(data : bytes, private_key : bytes) -> bytes:
65+
sign = bytes(64)
66+
67+
if not ECSign(private_key, len(private_key), data, len(data), sign, len(sign)):
68+
raise Exception("Failed to sign data");
69+
70+
# print(" ".join("{:02x}".format(x) for x in sign))
71+
return sign
72+
73+
def verify_data(data : bytes, public_key : bytes, sign : bytes) -> bool:
74+
# print(" ".join("{:02x}".format(x) for x in sign))
75+
return ECVerify(public_key, len(public_key), data, len(data), sign, len(sign))
76+
77+
def get_public_key_from_private(private_key : bytes) -> bytes | None:
78+
cdef unsigned char* pubkey = NULL
79+
cdef size_t publen = 0
80+
81+
ECGetPublicKeyFromPrivate(private_key, len(private_key), &pubkey, &publen)
82+
83+
rv = None
84+
if publen > 0 and pubkey != NULL:
85+
rv = bytes(pubkey[:publen])
86+
87+
free(pubkey)
88+
89+
return rv
90+
91+
def validate_private_key(private_key : bytes) -> bool:
92+
return ECValidateKey(0, private_key, len(private_key))
93+
94+
def validate_public_key(public_key : bytes) -> bool:
95+
return ECValidateKey(1, public_key, len(public_key))
96+
97+
def _pem_lines(contents: bytes) -> typing.Iterator[bytes]:
98+
in_pem_part = False
99+
seen_pem_start = False
100+
101+
for line in contents.splitlines():
102+
line = line.strip()
103+
104+
# Skip empty lines
105+
if not line:
106+
continue
107+
108+
# Handle start marker
109+
if line.startswith(b'-----BEGIN'):
110+
if in_pem_part:
111+
raise ValueError('Seen start marker twice')
112+
113+
in_pem_part = True
114+
seen_pem_start = True
115+
continue
116+
117+
# Skip stuff before first marker
118+
if not in_pem_part:
119+
continue
120+
121+
# Handle end marker
122+
if in_pem_part and line.startswith(b'-----END'):
123+
in_pem_part = False
124+
break
125+
126+
# Load fields
127+
if b":" in line:
128+
continue
129+
130+
yield line
131+
132+
# Do some sanity checks
133+
if not seen_pem_start:
134+
raise ValueError('No PEM start marker found')
135+
136+
if in_pem_part:
137+
raise ValueError('No PEM end marker found')
138+
139+
def pem_to_der(pem : bytes | str) -> bytes:
140+
if isinstance(pem, str): # pragma: no branch
141+
pem = pem.encode()
142+
143+
d = b"".join(_pem_lines(pem))
144+
return base64.b64decode(d)
145+
146+
147+
def der_to_pem(der : bytes, name : str) -> bytes:
148+
b64 = base64.b64encode(der)
149+
lines = [("-----BEGIN %s KEY-----\n" % name).encode()]
150+
lines.extend(
151+
[b64[start : start + 76] + b"\n" for start in range(0, len(b64), 76)]
152+
)
153+
lines.append(("-----END %s KEY-----\n" % name).encode())
154+
return b"".join(lines)

0 commit comments

Comments
 (0)