Skip to content

Commit

Permalink
process pem file in python while running on windows (#9)
Browse files Browse the repository at this point in the history
- add windows github actions check
- process pem files in python while it's running on windows
- add PR template
  • Loading branch information
j-z10 authored Mar 30, 2024
1 parent ac2ba9b commit 3b2b59f
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 49 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## Description (what + why)

## Checklist before requesting a review
- [ ] I have performed a self-review of my code
38 changes: 27 additions & 11 deletions .github/workflows/unittest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,38 +25,54 @@ jobs:
- name: Lint with flake8
run: |
poetry run flake8 . --count
test:
needs: pep8
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12"]
os: [ubuntu-latest, macos-latest]
python-version: ["3.12"]
gmssl-version: ["v3.1.1", "v3.1.0"]
os: [ubuntu-latest, macos-latest, windows-latest]

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install GmSSL dependencies
- name: Download GmSSL
run: |
git clone -b '${{ matrix.gmssl-version }}' --depth 1 https://github.com/guanzhi/GmSSL.git
- name: Install GmSSL on ubuntu and macos
if: ${{ startsWith(matrix.os, 'ubuntu') || startsWith(matrix.os, 'macos') }}
working-directory: ${{ github.workspace }}/GmSSL
run: |
git clone https://github.com/guanzhi/GmSSL.git
cd GmSSL && git checkout tags/v3.1.1
mkdir build && cd build && cmake ..
make && make test && sudo make install
mkdir build && cd build
cmake .. && make && make test && sudo make install
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
sudo ldconfig
elif [[ "$OSTYPE" == "darwin"* ]]; then
sudo update_dyld_shared_cache
fi
gmssl version
cd ../../
- name: Add nmake on windows
if: ${{ startsWith(matrix.os, 'windows') }}
uses: ilammy/msvc-dev-cmd@v1
- name: Install GmSSL on windows
if: ${{ startsWith(matrix.os, 'windows') }}
working-directory: ${{ github.workspace }}/GmSSL
run: |
"C:/Program Files/GmSSL/bin" | Out-File -FilePath $env:GITHUB_PATH -Append
mkdir build && cd build
cmake .. -G "NMake Makefiles"
nmake && nmake test && nmake install
- name: Check GmSSL version
run: gmssl version
- name: Install Python dependencies
run: |
python -m pip install poetry
poetry install --sync --with dev
poetry install --sync --without dev
- name: Test with pytest
run: |
poetry run pytest --cov=src --cov-report=xml
Expand All @@ -66,6 +82,6 @@ jobs:
env_vars: OS,PYTHON
fail_ci_if_error: true # optional (default = false)
flags: unittests
name: codecov-umbrella-python${{ matrix.python-version }}-${{ matrix.os }} # optional
name: codecov-umbrella-python${{ matrix.python-version }}-Gm-${{ matrix.gmssl-version }}-${{ matrix.os }} # optional
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true # optional (default = false)
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ pycryptodomex = "^3.20.0"
autopep8 = "^2.1.0"
isort = "^5.13.2"
flake8 = "^7.0.0"

[tool.poetry.group.test.dependencies]
pytest = "^8.1.1"
pytest-cov = "^5.0.0"

Expand Down
18 changes: 17 additions & 1 deletion src/pygmssl/_gm.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import sys
import warnings
from ctypes import cdll, c_char_p
from ctypes import cdll, c_char_p, c_void_p, c_int
from ctypes.util import find_library

if sys.platform == 'win32': # pragma: no cover
libc = cdll.LoadLibrary(find_library('msvcrt'))
win32 = True
else:
libc = cdll.LoadLibrary(find_library('c'))
win32 = False

libc.fopen.argtypes = [c_char_p, c_char_p]
libc.fopen.restype = c_void_p
libc.fclose.argtypes = [c_void_p]
libc.fclose.restype = c_int


libgm = find_library('gmssl')

Expand All @@ -17,3 +25,11 @@
else:
_gm = cdll.LoadLibrary(libgm)
_gm.gmssl_version_str.restype = c_char_p
_gm.sm2_private_key_info_encrypt_to_pem.argtypes = [c_void_p, c_char_p, c_void_p]
_gm.sm2_private_key_info_encrypt_to_pem.restype = c_int
_gm.sm2_private_key_info_decrypt_from_pem.argtypes = [c_void_p, c_char_p, c_void_p]
_gm.sm2_private_key_info_decrypt_from_pem.restype = c_int
_gm.sm2_public_key_info_from_pem.argtypes = [c_void_p, c_void_p]
_gm.sm2_public_key_info_from_pem.restype = c_int
_gm.sm2_public_key_info_to_pem.argtypes = [c_void_p, c_void_p]
_gm.sm2_public_key_info_to_pem.restype = c_int
143 changes: 113 additions & 30 deletions src/pygmssl/sm2.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from ctypes import byref, c_uint8, c_size_t, Structure, c_char_p, c_void_p
import base64
from ctypes import byref, c_uint8, c_size_t, Structure, c_char_p, pointer
import tempfile
import os

from Cryptodome.Util.asn1 import DerSequence

from ._gm import _gm, libc
from ._gm import _gm, libc, win32
from .sm3 import _SM3CTX

SM2_DEFAULT_ID = b'1234567812345678'
Expand Down Expand Up @@ -129,46 +131,127 @@ def decrypt(self, data: bytes) -> bytes:
_gm.sm2_decrypt(byref(self._sm2_key), byref(buff), len(data), byref(out), byref(length))
return bytes(out[:length.value])

def export_encrypted_private_key_to_pem(self, password: bytes) -> bytes:
with tempfile.NamedTemporaryFile(delete=False) as tmp_f:
libc.fopen.restype = c_void_p
fp = libc.fopen(tmp_f.name.encode('utf8'), 'wb')
assert _gm.sm2_private_key_info_encrypt_to_pem(byref(self._sm2_key), c_char_p(password), c_void_p(fp)) == 1
libc.fclose(c_void_p(fp))
with open(tmp_f.name, 'rb') as f:
def _export_encrypted_pri_to_der(self, password: bytes) -> bytes:
buff = (c_uint8 * 4096)()
length = c_size_t()
_gm.sm2_private_key_info_encrypt_to_der(byref(self._sm2_key), password, byref(pointer(buff)), byref(length))
return bytes(buff[:length.value])

def _export_pub_to_der(self) -> bytes:
buff = (c_uint8 * 4096)()
length = c_size_t()
_gm.sm2_public_key_info_to_der(byref(self._sm2_key), byref(pointer(buff)), byref(length))
return bytes(buff[:length.value])

def _nix_export_private_key_to_encrypted_pem(self, password: bytes) -> bytes:
with tempfile.NamedTemporaryFile(delete=False) as _tmp_f:
tmp_f_name = _tmp_f.name
fp = libc.fopen(tmp_f_name.encode('utf8'), b'wb')

assert _gm.sm2_private_key_info_encrypt_to_pem(byref(self._sm2_key), c_char_p(password), fp) == 1
libc.fclose(fp)
with open(tmp_f_name, 'rb') as f:
res = f.read()
return res

def export_public_key_to_pem(self) -> bytes:
with tempfile.NamedTemporaryFile(delete=False) as tmp_f:
libc.fopen.restype = c_void_p
fp = libc.fopen(tmp_f.name.encode('utf8'), 'wb')
assert _gm.sm2_public_key_info_to_pem(byref(self._sm2_key), c_void_p(fp)) == 1
libc.fclose(c_void_p(fp))
with open(tmp_f.name, 'rb') as f:
def _win_export_private_key_to_encrypted_pem(self, password: bytes) -> bytes:
der = self._export_encrypted_pri_to_der(password)
return self._pem_write(der, 'ENCRYPTED PRIVATE KEY')

def _nix_export_public_key_to_pem(self) -> bytes:
with tempfile.NamedTemporaryFile(delete=False) as _tmp_f:
tmp_f_name = _tmp_f.name
fp = libc.fopen(tmp_f_name.encode('utf8'), b'wb')
assert _gm.sm2_public_key_info_to_pem(byref(self._sm2_key), fp) == 1
libc.fclose(fp)
with open(tmp_f_name, 'rb') as f:
res = f.read()
return res

def _win_export_public_key_to_pem(self) -> bytes:
pub_der = self._export_pub_to_der()
return self._pem_write(pub_der, 'PUBLIC KEY')

def _pem_write(self, der: bytes, name: str) -> bytes:
data = base64.b64encode(der).decode('utf8')
prefix = f'-----BEGIN {name}-----'
suffix = f'-----END {name}-----'
tmp: list[str] = [prefix]
for i in range(0, len(data), 64):
chunk = data[i:i + 64]
tmp.append(chunk)
tmp.append(suffix)
return ''.join(_line + os.linesep for _line in tmp).encode('utf8')

@classmethod
def import_private_from_pem(cls, pem: bytes, password: bytes) -> 'SM2':
with tempfile.NamedTemporaryFile(delete=False) as tmp_f:
with open(tmp_f.name, 'wb') as f:
def _nix_import_private_key_from_encrypted_pem(cls, pem: bytes, password: bytes) -> 'SM2':
with tempfile.NamedTemporaryFile(delete=False) as _tmp_f:
tmp_f_name = _tmp_f.name
with open(tmp_f_name, 'wb') as f:
f.write(pem)
libc.fopen.restype = c_void_p
fp = libc.fopen(tmp_f.name.encode('utf8'), 'rb')
fp = libc.fopen(tmp_f_name.encode('utf8'), b'rb')
obj = SM2()
assert _gm.sm2_private_key_info_decrypt_from_pem(byref(obj._sm2_key), c_char_p(password), c_void_p(fp)) == 1
libc.fclose(c_void_p(fp))
assert _gm.sm2_private_key_info_decrypt_from_pem(byref(obj._sm2_key), c_char_p(password), fp) == 1
libc.fclose(fp)
return obj

@classmethod
def import_public_from_pem(cls, pem: bytes) -> 'SM2':
with tempfile.NamedTemporaryFile(delete=False) as tmp_f:
with open(tmp_f.name, 'wb') as f:
def _nix_import_public_key_from_pem(cls, pem: bytes) -> 'SM2':
with tempfile.NamedTemporaryFile(delete=False) as _tmp_f:
tmp_f_name = _tmp_f.name
with open(tmp_f_name, 'wb') as f:
f.write(pem)
libc.fopen.restype = c_void_p
fp = libc.fopen(tmp_f.name.encode('utf8'), 'rb')
fp = libc.fopen(tmp_f_name.encode('utf8'), b'rb')
obj = SM2()
assert _gm.sm2_public_key_info_from_pem(byref(obj._sm2_key), c_void_p(fp)) == 1
libc.fclose(c_void_p(fp))
assert _gm.sm2_public_key_info_from_pem(byref(obj._sm2_key), fp) == 1
libc.fclose(fp)
return obj

@staticmethod
def _pem_read(pem: str, name: str) -> bytes:
tmp = pem.splitlines()
prefix = f'-----BEGIN {name}-----'
suffix = f'-----END {name}-----'
assert tmp[0] == prefix
assert tmp[-1] == suffix
mid = ''.join(tmp[1:-1])
return base64.b64decode((mid + ('=' * (-len(mid) % 4))).encode())

@classmethod
def _win_import_private_key_from_encrypted_pem(cls, pem: bytes, password: bytes) -> 'SM2':
der_data = cls._pem_read(pem.decode('utf8'), 'ENCRYPTED PRIVATE KEY')
obj = SM2()
attr = (c_uint8 * 4096)()
attr_len = c_size_t()
p = pointer(attr)
buf = (c_uint8 * 4096)()
buf[:len(der_data)] = der_data
buflen = c_size_t(len(der_data))
cp = pointer(buf)
assert _gm.sm2_private_key_info_decrypt_from_der(byref(obj._sm2_key), byref(
p), byref(attr_len), password, byref(cp), byref(buflen)) == 1
assert buflen.value == 0
return obj

@classmethod
def _win_import_public_key_from_pem(cls, pem: bytes) -> 'SM2':
der_data = cls._pem_read(pem.decode('utf8'), 'PUBLIC KEY')
obj = SM2()
buf = (c_uint8 * 4096)()
buf[:len(der_data)] = der_data
vlen = c_size_t(len(der_data))
cp = pointer(buf)
assert _gm.sm2_public_key_info_from_der(byref(obj._sm2_key), byref(cp), byref(vlen)) == 1
assert vlen.value == 0
return obj

if win32:
export_public_key_to_pem = _win_export_public_key_to_pem
export_private_key_to_encrypted_pem = _win_export_private_key_to_encrypted_pem
import_public_key_from_pem = _win_import_public_key_from_pem
import_private_key_from_encrypted_pem = _win_import_private_key_from_encrypted_pem
else:
export_public_key_to_pem = _nix_export_public_key_to_pem
export_private_key_to_encrypted_pem = _nix_export_private_key_to_encrypted_pem
import_public_key_from_pem = _nix_import_public_key_from_pem
import_private_key_from_encrypted_pem = _nix_import_private_key_from_encrypted_pem
15 changes: 9 additions & 6 deletions tests/test_sm2.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,31 +73,34 @@ def test_010_sm2_sign_with_id_asn1(self):
self.assertFalse(self.k.verify(data + b'\x00', sig, id=b'test', asn1=True))
self.assertTrue(self.k.verify(data, sig, id=b'test', asn1=True))

def test_private_pem_export_and_import(self):
def test_100_private_pem_export_and_import(self):
password = b'test-123-456'
obj = SM2.generate_new_pair()
assert obj.pub_key != b'\x00' * 64
assert obj.pri_key != b'\x00' * 32
new_obj = SM2.import_private_from_pem(obj.export_encrypted_private_key_to_pem(password), password)
pem = obj.export_private_key_to_encrypted_pem(password)
new_obj = SM2.import_private_key_from_encrypted_pem(pem, password)
assert new_obj.pri_key != b'\x00' * 32
assert new_obj.pri_key == obj.pri_key

assert new_obj.pub_key != b'\x00' * 64
assert new_obj.pub_key == obj.pub_key

def test_pub_pem_export_and_import(self):
def test_101_pub_pem_export_and_import(self):
obj = SM2.generate_new_pair()
assert obj.pub_key != b'\x00' * 64
assert obj.pri_key != b'\x00' * 32
new_obj = SM2.import_public_from_pem(obj.export_public_key_to_pem())
pem = obj.export_public_key_to_pem()
new_obj = SM2.import_public_key_from_pem(pem)
assert new_obj.pri_key == b'\x00' * 32
assert new_obj.pub_key != b'\x00' * 64
assert new_obj.pub_key == obj.pub_key

def test_error_import_private_pem(self):
def test_102_error_import_private_pem(self):
password = b'test-123-456'
obj = SM2.generate_new_pair()
assert obj.pub_key != b'\x00' * 64
assert obj.pri_key != b'\x00' * 32
with self.assertRaises(Exception):
SM2.import_private_from_pem(obj.export_encrypted_private_key_to_pem(password), b'wrong-password')
pem = obj.export_private_key_to_encrypted_pem(password)
SM2.import_private_key_from_encrypted_pem(pem, b'wrong-password')

0 comments on commit 3b2b59f

Please sign in to comment.