diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8a6f9f6..3d6dbe7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,4 +1,6 @@ name: Build and deploy documentation +permissions: + contents: read on: push: diff --git a/.github/workflows/osv-scanner.yml b/.github/workflows/osv-scanner.yml new file mode 100644 index 0000000..bbca8cf --- /dev/null +++ b/.github/workflows/osv-scanner.yml @@ -0,0 +1,166 @@ +name: OSV Dependency Scan + +on: + push: + branches: [main] + paths: + - 'requirements.txt' + - 'dev-requirements.txt' + - '.github/workflows/osv-scanner.yml' + pull_request: + branches: [main] + paths: + - 'requirements.txt' + - 'dev-requirements.txt' + schedule: + # Run weekly on Monday at 00:00 UTC to catch new CVEs + - cron: '0 0 * * 1' + +permissions: + contents: read + security-events: write # Required for SARIF upload + actions: read # Required for OSV-Scanner action + +jobs: + osv-scan: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Prepare requirements for scanning + run: | + # Use Python to safely read files with correct encoding (handling BOMs automatically) + python3 -c " + import sys + + def process_file(filename): + try: + # Open with utf-8-sig to handle UTF-8 BOM, errors='replace' to handle binary/UTF-16 confusion + # Ideally we would detect encoding, but requirements files should be text + with open(filename, 'r', encoding='utf-8', errors='replace') as f: + content = f.read() + # If it looks like UTF-16 (null bytes), try re-reading + if '\x00' in content: + with open(filename, 'r', encoding='utf-16') as f2: + content = f2.read() + + for line in content.splitlines(): + line = line.strip() + if not line or line.startswith(('#', '-i', '--')): + continue + # Remove markers + line = line.split(';')[0].strip() + if line: + print(line) + except Exception as e: + print(f'Error processing {filename}: {e}', file=sys.stderr) + + process_file('requirements.txt') + process_file('dev-requirements.txt') + " | sort -u > combined-requirements.txt + + echo "Combined requirements file created:" + if [ -s combined-requirements.txt ]; then + echo " Total packages: $(wc -l < combined-requirements.txt)" + echo "" + echo "Package list (first 10):" + head -10 combined-requirements.txt + else + echo "Error: combined-requirements.txt is empty!" + echo "Debug: File listing:" + ls -l requirements.txt dev-requirements.txt + fi + echo "" + + - name: Install OSV-Scanner + run: | + curl -LO https://github.com/google/osv-scanner/releases/latest/download/osv-scanner_linux_amd64 + chmod +x osv-scanner_linux_amd64 + sudo mv osv-scanner_linux_amd64 /usr/local/bin/osv-scanner + + - name: Run OSV-Scanner + id: osv-scanner + continue-on-error: true + run: | + echo "Running OSV-Scanner dependency vulnerability check..." + echo "==================================================" + echo "" + + # Check if combined file has content + if [ ! -s combined-requirements.txt ]; then + echo "Error: combined-requirements.txt is empty!" + echo "Falling back to scanning requirements.txt only..." + LOCKFILE="requirements.txt" + else + LOCKFILE="combined-requirements.txt" + fi + + echo "Scanning: $LOCKFILE" + echo "" + + # Run scan and capture exit code + set +e + osv-scanner --lockfile="$LOCKFILE" + SCAN_EXIT_CODE=$? + set -e + + # Exit code 128 means no packages found, which should not happen + if [ $SCAN_EXIT_CODE -eq 128 ]; then + echo "" + echo "OSV-Scanner found no packages to scan" + echo "Debug: Contents of $LOCKFILE:" + cat "$LOCKFILE" + exit 1 + fi + + echo "" + echo "Generating SARIF report..." + osv-scanner --lockfile="$LOCKFILE" --format=sarif --output=results.sarif 2>&1 || { + echo "⚠️ SARIF generation encountered issues (possibly plugin warning)" + echo " Scan results are still valid - see output above" + } + + if [ -f results.sarif ] && [ -s results.sarif ]; then + echo "✓ SARIF file created successfully" + ls -lh results.sarif + fi + + # Return the original scan exit code + exit $SCAN_EXIT_CODE + + - name: Upload SARIF results to GitHub Security + if: always() && hashFiles('results.sarif') != '' + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: results.sarif + category: osv-scanner + + - name: Success - No vulnerabilities found + if: steps.osv-scanner.outcome == 'success' + run: | + echo "==========================================" + echo "✅ No Vulnerabilities Found" + echo "==========================================" + echo "" + echo "All Python dependencies (requirements.txt + dev-requirements.txt)" + echo "are free of known vulnerabilities." + echo "" + + - name: Check OSV-Scanner results + if: steps.osv-scanner.outcome == 'failure' + run: | + echo "==========================================" + echo "⚠️ Vulnerabilities Found in Dependencies" + echo "==========================================" + echo "" + echo "OSV-Scanner detected security vulnerabilities in your Python dependencies" + echo "(requirements.txt + dev-requirements.txt)." + echo "" + echo "Review the findings in the Security tab -> Code scanning alerts." + echo "" + echo "Note: This workflow is configured to pass even with findings" + echo "to allow for known unfixable or constrained vulnerabilities." + echo "" + diff --git a/.github/workflows/pylint_check.yml b/.github/workflows/pylint_check.yml new file mode 100644 index 0000000..e5011b7 --- /dev/null +++ b/.github/workflows/pylint_check.yml @@ -0,0 +1,53 @@ +name: Pylint Code Quality + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + pylint: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.13.7"] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + + - name: Install system dependencies for pyscard + run: | + sudo apt-get update -qq + sudo apt-get install -y swig libpcsclite-dev pcscd + + - name: Install Python dependencies + continue-on-error: true + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Install Pylint + run: | + pip install pylint + + - name: Run Pylint analysis + run: | + echo "Running Pylint code quality analysis..." + echo "========================================" + echo "" + pylint cryptnox_cli \ + --output-format=colorized \ + --fail-under=7.0 \ + --fail-on=error,fatal + diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 0000000..52733c7 --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,111 @@ +name: Semgrep Security Scan + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + # Run daily at 00:00 UTC to catch new vulnerabilities + - cron: '0 0 * * *' + +permissions: + contents: read + security-events: write # Required for SARIF upload + pull-requests: write # For PR comments + +jobs: + semgrep: + runs-on: ubuntu-latest + + # Skip scheduled runs on forks + if: (github.event_name != 'schedule') || (github.repository == 'cryptnox/cryptnox-cli') + + container: + # Official Semgrep Docker image + image: semgrep/semgrep + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Fix Git ownership in container + run: | + git config --global --add safe.directory /__w/cryptnox-cli/cryptnox-cli || true + git config --global --add safe.directory "$GITHUB_WORKSPACE" || true + + - name: Run Semgrep Security Scan + id: semgrep + continue-on-error: true + run: | + echo "Running Semgrep security analysis..." + echo "====================================" + echo "" + + # 1. Run Semgrep with text output to console (for logs) + # We use --error to ensure it fails if issues are found + semgrep scan \ + --config=p/python \ + --config=p/security-audit \ + --config=p/owasp-top-ten \ + --verbose \ + --metrics=off + + # Capture exit code from the text scan + EXIT_CODE=$? + + echo "" + echo "Generating SARIF report..." + + # 2. Generate SARIF report for GitHub Security + semgrep scan \ + --config=p/python \ + --config=p/security-audit \ + --config=p/owasp-top-ten \ + --sarif \ + --output=semgrep.sarif \ + --metrics=off || true + + # Verify SARIF file was created + if [ -f semgrep.sarif ]; then + echo "✓ SARIF file created successfully" + ls -lh semgrep.sarif + else + echo "⚠️ Warning: SARIF file not created" + fi + + # Return the exit code from the text scan + exit $EXIT_CODE + env: + SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} + + - name: Upload Semgrep SARIF to GitHub Security + if: always() && hashFiles('semgrep.sarif') != '' + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: semgrep.sarif + category: semgrep + + - name: Upload Semgrep reports as artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: semgrep-reports + path: semgrep.sarif + if-no-files-found: warn + + - name: Check Semgrep results + if: steps.semgrep.outcome == 'failure' + run: | + echo "⚠️ Semgrep found security issues" + echo "" + echo "Review the findings:" + echo " 1. Check the output above for details" + echo " 2. Go to Security -> Code scanning -> Semgrep category" + echo " 3. Download artifacts for full reports" + echo "" + echo "Note: Some findings may be false positives." + echo "Review each one carefully before taking action." + exit 1 + + diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..844d4cc --- /dev/null +++ b/.pylintrc @@ -0,0 +1,207 @@ +[MASTER] +# Use multiple processes to speed up Pylint +jobs=1 + +# Pickle collected data for later comparisons +persistent=yes + +# Discover python modules and packages in the file system subtree +recursive=yes + +# List of plugins (as comma separated values of python module names) to load +load-plugins= + +# Allow loading of arbitrary C extensions +unsafe-load-any-extension=no + +# Ignore patterns for file/directory matching +ignore=CVS,.git,__pycache__,build,dist,*.egg-info,venv,.venv,docs,cryptos + +[MESSAGES CONTROL] +# Disable specific warnings/errors that are not critical or conflict with project style +disable= + C0103, # invalid-name (allow short variable names in crypto/math contexts) + C0114, # missing-module-docstring + C0115, # missing-class-docstring + C0116, # missing-function-docstring + # Import related + C0411, # wrong-import-order (let isort handle it) + C0413, # wrong-import-position + # Design patterns + R0902, # too-many-instance-attributes + R0903, # too-few-public-methods + R0913, # too-many-arguments + R0914, # too-many-locals + # Other + W0511, # fixme (TODOs are ok) + W1203, # logging-fstring-interpolation + +[REPORTS] +# Set the output format +output-format=text + +# Activate the evaluation score +score=yes + +[BASIC] +# Good variable names which should always be accepted +good-names=i,j,k,x,y,z,ex,Run,_,id,db,pk,iv,kw + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,50}$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{1,50}$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{1,50}$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{1,50}$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{1,50}|(__.*__))$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,50}$ + +[FORMAT] +# Maximum number of characters on a single line +max-line-length=120 + +# Regexp for a line that is allowed to be longer than the limit +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required +indent-string=' ' + +# String used as indentation unit +indent-after-paren=4 + +[DESIGN] +# Maximum number of arguments for function / method (disabled above) +max-args=10 + +# Maximum number of locals for function / method body (disabled above) +max-locals=25 + +# Maximum number of return / yield for function / method body (disabled above) +max-returns=10 + +# Maximum number of branch for function / method body (disabled above) +max-branches=20 + +# Maximum number of statements in function / method body (disabled above) +max-statements=75 + +# Maximum number of parents for a class (inheritance) +max-parents=7 + +# Maximum number of attributes for a class (disabled above) +max-attributes=15 + +# Minimum number of public methods for a class (disabled above) +min-public-methods=0 + +# Maximum number of public methods for a class +max-public-methods=30 + +[IMPORTS] +# Deprecated modules which should not be used +deprecated-modules=optparse,imp,distutils + +# Allow wildcard imports from modules that define __all__ +allow-wildcard-with-all=no + +[CLASSES] +# List of method names used to declare (i.e. assign) instance attributes +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method +valid-metaclass-classmethod-first-arg=mcs + +[EXCEPTIONS] +# Exceptions that will emit a warning when caught +overgeneral-exceptions=builtins.BaseException,builtins.Exception + +[TYPECHECK] +# Ignore missing members accessed in mocking +ignored-modules= + cryptnox_sdk_py, + lazy_import, + appdirs, + tabulate, + ecdsa, + eth_account, + eth_typing, + eth_utils, + hexbytes, + web3, + requests, + boto3, + botocore, + cryptography, + winsdk, + stdiomask, + smartcard, + Cryptodome +# List of classes names for which member attributes should not be checked +ignored-classes=SQLObject,optparse.Values,thread._local,_thread._local + +# List of members which are set dynamically and missed by Pylint inference +generated-members= + +[SIMILARITIES] +# Minimum lines number of a similarity +min-similarity-lines=4 + +# Ignore comments when computing similarities +ignore-comments=yes + +# Ignore docstrings when computing similarities +ignore-docstrings=yes + +# Ignore imports when computing similarities +ignore-imports=yes + +[VARIABLES] +# Tells whether we should check for unused import in __init__ files +init-import=no + +# A regular expression matching the name of dummy variables +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# List of strings which can identify a callback function by name +callbacks=cb_,_cb + +[LOGGING] +# Format style used to check logging format string +logging-format-style=old + +[SPELLING] +# Spelling dictionary name +spelling-dict= + +# List of comma separated words that should not be checked +spelling-ignore-words= + +# A path to a file that contains private dictionary +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary +spelling-store-unknown-words=no diff --git a/.semgrep.yml b/.semgrep.yml new file mode 100644 index 0000000..e4493f4 --- /dev/null +++ b/.semgrep.yml @@ -0,0 +1,158 @@ +# Semgrep Configuration for Cryptnox CLI +# Security rules customized for smart card and cryptography code + +rules: + # Cryptography Security Rules + - id: weak-crypto-algorithm + pattern-either: + - pattern: hashlib.md5(...) + - pattern: hashlib.sha1(...) + - pattern: Crypto.Hash.MD5 + - pattern: Crypto.Hash.SHA1 + message: Weak cryptographic algorithm detected. Use SHA-256 or stronger. + languages: [python] + severity: WARNING + metadata: + category: security + cwe: "CWE-327: Use of a Broken or Risky Cryptographic Algorithm" + owasp: "A3:2017-Sensitive Data Exposure" + + - id: hardcoded-secret + pattern-either: + - pattern: | + $VAR = "..." + - pattern: | + $VAR = b"..." + pattern-regex: | + (password|passwd|pwd|secret|token|api_key|apikey|access_key|private_key|encryption_key)\s*=\s*["\'](?!<|{|\$).{8,}["\'] + message: Possible hardcoded secret detected. Use environment variables or secure storage. + languages: [python] + severity: ERROR + metadata: + category: security + cwe: "CWE-798: Use of Hard-coded Credentials" + owasp: "A2:2017-Broken Authentication" + + - id: insecure-random + pattern-either: + - pattern: random.random(...) + - pattern: random.randint(...) + - pattern: random.choice(...) + message: Using insecure random number generator. Use secrets module for cryptographic operations. + languages: [python] + severity: WARNING + metadata: + category: security + cwe: "CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator" + fix: secrets.SystemRandom() + + - id: dangerous-pickle-usage + pattern-either: + - pattern: pickle.loads(...) + - pattern: pickle.load(...) + message: Dangerous use of pickle. Can lead to arbitrary code execution if loading untrusted data. + languages: [python] + severity: ERROR + metadata: + category: security + cwe: "CWE-502: Deserialization of Untrusted Data" + owasp: "A8:2017-Insecure Deserialization" + + - id: eval-usage + pattern-either: + - pattern: eval(...) + - pattern: exec(...) + message: Use of eval() or exec() is dangerous and can lead to code injection. + languages: [python] + severity: ERROR + metadata: + category: security + cwe: "CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code" + + # Smart Card / PIN Security + - id: pin-in-logs + pattern-either: + - pattern: | + print(..., $PIN, ...) + - pattern: | + logging.$METHOD(..., $PIN, ...) + - pattern: | + logger.$METHOD(..., $PIN, ...) + pattern-regex: .*(pin|puk|password|secret).* + message: Possible PIN/PUK logging detected. Never log sensitive authentication data. + languages: [python] + severity: ERROR + metadata: + category: security + cwe: "CWE-532: Insertion of Sensitive Information into Log File" + + - id: sql-injection + pattern: | + $QUERY = "..." + $VAR + "..." + $CURSOR.execute($QUERY) + message: Possible SQL injection vulnerability. Use parameterized queries. + languages: [python] + severity: ERROR + metadata: + category: security + cwe: "CWE-89: SQL Injection" + owasp: "A1:2017-Injection" + + - id: unvalidated-input + pattern-either: + - pattern: | + $INPUT = input(...) + exec($INPUT) + - pattern: | + $INPUT = input(...) + eval($INPUT) + message: Executing unvalidated user input is extremely dangerous. + languages: [python] + severity: ERROR + metadata: + category: security + cwe: "CWE-20: Improper Input Validation" + + - id: http-without-timeout + pattern: requests.$METHOD(..., timeout=None, ...) + message: HTTP request without timeout can cause hanging connections. Always set a timeout. + languages: [python] + severity: WARNING + metadata: + category: security + cwe: "CWE-400: Uncontrolled Resource Consumption" + + - id: bare-except + pattern: | + try: + ... + except: + ... + message: Bare except clause catches all exceptions including KeyboardInterrupt. Be more specific. + languages: [python] + severity: WARNING + metadata: + category: security + cwe: "CWE-396: Declaration of Catch for Generic Exception" + + - id: assert-used + pattern: assert $CONDITION + message: Assert statements are removed in optimized bytecode (-O flag). Don't use for security checks. + languages: [python] + severity: WARNING + metadata: + category: security + cwe: "CWE-617: Reachable Assertion" + +# Paths to exclude from scanning +exclude: + - "*.pyc" + - "**/__pycache__/" + - "**/build/" + - "**/dist/" + - "**/*.egg-info/" + - "**/venv/" + - "**/.venv/" + - "**/docs/" + - "**/tests/" + - "**/.git/" diff --git a/cryptnox_cli/command/factory.py b/cryptnox_cli/command/factory.py index fc87272..ea21bd4 100644 --- a/cryptnox_cli/command/factory.py +++ b/cryptnox_cli/command/factory.py @@ -27,7 +27,7 @@ def command(data: Namespace, cards: CardManager = None) -> Command: 'btc', 'card_configuration', 'change_pin', 'change_puk', 'config', 'eth', 'history', 'info', 'initialize', 'seed', 'cards', 'server', 'reset', 'unlock_pin', 'user_key', 'transfer', 'get_xpub', - 'get_clearpubkey', 'decrypt' + 'get_clearpubkey', 'decrypt', 'manufacturer_certificate' ] for module_name in command_modules: diff --git a/cryptnox_cli/command/manufacturer_certificate.py b/cryptnox_cli/command/manufacturer_certificate.py new file mode 100644 index 0000000..efe8d15 --- /dev/null +++ b/cryptnox_cli/command/manufacturer_certificate.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +""" +Module containing command for retrieving and displaying manufacturer certificate +""" +import re +import cryptnox_sdk_py +from cryptography import x509 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec + +from .command import Command + +try: + import enums +except ImportError: + from .. import enums + + +class ManufacturerCertificate(Command): + """ + Command to retrieve and display the manufacturer certificate from the card + """ + _name = enums.Command.CERTIFICATE.value + + def _execute(self, card: cryptnox_sdk_py.Card) -> int: + print("Retrieving manufacturer certificate from the card...") + try: + # Check if the card object has the required method + if not hasattr(card, 'get_manufacturer_certificate'): + print("Error: The current card type does not support retrieving a manufacturer certificate.") + return -1 + + cert_raw = card.get_manufacturer_certificate(hexed=False) + if not cert_raw: + print("Error: Could not retrieve manufacturer certificate from the card.") + return -1 + + # Parse the certificate + cert = x509.load_der_x509_certificate(cert_raw) + + # Format and display the certificate + self._print_certificate_text(cert) + + return 0 + except Exception as e: + print(f"Error retrieving or processing manufacturer certificate: {e}") + return -1 + + def _print_certificate_text(self, cert: x509.Certificate): + """ + Prints the certificate in a human-readable format similar to openssl x509 -text + """ + print("Certificate Data:") + print(f" Version: {cert.version.value + 1} (0x{cert.version.value})") + print(" Serial Number:") + serial_hex = self._format_hex(cert.serial_number) + print(f" {serial_hex}") + sig_alg_name = cert.signature_algorithm_oid._name + sig_alg = re.sub(r'(with-)([a-z0-9]+)', lambda m: m.group(1) + m.group(2).upper(), sig_alg_name) + print(f" Signature Algorithm: {sig_alg}") + print(f" Issuer: {self._format_name(cert.issuer)}") + print(" Validity:") + # Use UTC for consistency as required by the GMT format in example + print(f" Not Before: {cert.not_valid_before_utc.strftime('%b %d %H:%M:%S %Y GMT')}") + print(f" Not After : {cert.not_valid_after_utc.strftime('%b %d %H:%M:%S %Y GMT')}") + print(f" Subject: {self._format_name(cert.subject)}") + + public_key = cert.public_key() + if isinstance(public_key, ec.EllipticCurvePublicKey): + print(" Subject Public Key Info:") + print(" Public Key Algorithm: id-ecPublicKey") + print(f" Public-Key: ({public_key.key_size} bit)") + print(" pub:") + pub_bytes = public_key.public_bytes( + encoding=serialization.Encoding.X962, + format=serialization.PublicFormat.UncompressedPoint + ) + pub_hex = ':'.join(f'{b:02x}' for b in pub_bytes) + self._print_indented_hex(pub_hex, 16) + print(f" ASN1 OID: {public_key.curve.name}") + if public_key.curve.name in ["prime256v1", "secp256r1"]: + print(" NIST CURVE: P-256") + + print(f"Signature Algorithm: {sig_alg}") + print("Signature Value:") + sig_hex = ':'.join(f'{b:02x}' for b in cert.signature) + self._print_indented_hex(sig_hex, 4) + + def _format_hex(self, value: int) -> str: + h = hex(value)[2:] + if len(h) % 2 != 0: + h = '0' + h + return ':'.join(h[i:i+2] for i in range(0, len(h), 2)) + + def _print_indented_hex(self, hex_str: str, indent: int): + parts = hex_str.split(':') + line_size = 15 + for i in range(0, len(parts), line_size): + chunk = parts[i:i+line_size] + line = ":".join(chunk) + if i + line_size < len(parts): + line += ":" + print(" " * indent + line) + + def _format_name(self, name: x509.Name) -> str: + short_names = { + "commonName": "CN", + "organizationName": "O", + "countryName": "C", + "localityName": "L", + "stateOrProvinceName": "ST", + "organizationalUnitName": "OU" + } + parts = [] + # Reverse to match common OpenSSL display order (Subject/Issuer) + for attribute in reversed(list(name)): + name_str = attribute.oid._name + short_name = short_names.get(name_str, name_str) + parts.append(f"{short_name}={attribute.value}") + return ", ".join(parts) diff --git a/cryptnox_cli/command/options/options.py b/cryptnox_cli/command/options/options.py index 28fce5e..b84fd7b 100644 --- a/cryptnox_cli/command/options/options.py +++ b/cryptnox_cli/command/options/options.py @@ -60,6 +60,7 @@ def add(parser, interactive: bool = False): _xpub_options(subparsers, interactive) _get_clearpubkey_options(subparsers, interactive) _decrypt_options(subparsers, interactive) + _cert_options(subparsers, interactive) if interactive: use_sub_parser = subparsers.add_parser("use", help="Change card to be used by default") @@ -300,3 +301,13 @@ def _decrypt_options(subparsers, interactive_mode): """ from ..decrypt import Decrypt Decrypt.add_options(subparsers) + + +def _cert_options(subparsers, _: bool): + """ + Add manufacturer certificate command options + + :param subparsers: Argument parser subparsers + """ + subparsers.add_parser(enums.Command.CERTIFICATE.value, + help="Retrieve and display the manufacturer certificate") diff --git a/cryptnox_cli/enums.py b/cryptnox_cli/enums.py index d578ae1..183f35d 100644 --- a/cryptnox_cli/enums.py +++ b/cryptnox_cli/enums.py @@ -34,3 +34,4 @@ class Command(Enum): UNLOCK_PIN = "unlock_pin" USER_KEY = "user_key" TRANSFER = "transfer" + CERTIFICATE = "cert" diff --git a/dev-requirements.txt b/dev-requirements.txt index 50a74be..51462e8 100644 Binary files a/dev-requirements.txt and b/dev-requirements.txt differ diff --git a/docs/cli.rst b/docs/cli.rst index 7c94558..2b4b9d0 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -91,6 +91,20 @@ Shows: - Public keys - Derivation information +cert +^^^^ + +Retrieve and display the manufacturer certificate from the card in a human-readable format. + +.. code-block:: bash + + cryptnox cert + +**Features:** + - Retrieves the full manufacturer certificate from the card + - Displays certificate details (Issuer, Subject, Validity, Public Key, Signature) + - Human-readable format similar to ``openssl x509 -text`` + Seed Management Commands ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/cryptnox_cli.command.rst b/docs/cryptnox_cli.command.rst index d8ce5f7..2778c33 100644 --- a/docs/cryptnox_cli.command.rst +++ b/docs/cryptnox_cli.command.rst @@ -136,6 +136,14 @@ cryptnox_cli.command.initialize module :undoc-members: :show-inheritance: +cryptnox_cli.command.manufacturer\_certificate module +---------------------------------------------------- + +.. automodule:: cryptnox_cli.command.manufacturer_certificate + :members: + :undoc-members: + :show-inheritance: + cryptnox_cli.command.reset module -------------------------------- diff --git a/requirements.txt b/requirements.txt index a89bd38..71c26ff 100644 Binary files a/requirements.txt and b/requirements.txt differ