diff --git a/.gitignore b/.gitignore index db1dfcd8e..3d9ae71cd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ **/__pycache__/** nxtscape-cli-access.json gclient.json +.env diff --git a/build/build.py b/build/build.py index cdb394a21..0dce288da 100755 --- a/build/build.py +++ b/build/build.py @@ -10,6 +10,22 @@ import click from pathlib import Path from typing import Optional +# Load .env file if it exists +def load_env_file(): + env_file = Path(__file__).parent.parent / '.env' + if env_file.exists(): + with open(env_file, 'r') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#'): + if '=' in line: + key, value = line.split('=', 1) + os.environ[key.strip()] = value.strip() + print(f"āœ“ Loaded environment from .env file") + +# Load .env file on import +load_env_file() + # Import shared components from context import BuildContext from utils import load_config, log_info, log_warning, log_error, log_success, IS_MACOS, IS_WINDOWS, IS_LINUX @@ -115,6 +131,7 @@ def build_main( gn_flags_file = None architectures = [arch] if arch else [] # Empty list if no arch specified universal = False + certificate_name = None # For Windows signing if config_file: config = load_config(config_file) log_info(f"šŸ“„ Loaded config from: {config_file}") @@ -156,6 +173,11 @@ def build_main( config_chromium_src = Path(config["paths"]["chromium_src"]) chromium_src = config_chromium_src log_info(f"šŸ“ Using Chromium source from config: {chromium_src}") + + # Get Windows signing certificate name from config + if IS_WINDOWS and "signing" in config and "certificate_name" in config["signing"]: + certificate_name = config["signing"]["certificate_name"] + log_info(f"šŸ” Using certificate for signing: {certificate_name}") # CLI takes precedence over config if chromium_src_dir: @@ -283,7 +305,11 @@ def build_main( log_info(f"\nšŸ” Signing {ctx.architecture} build...") if slack_notifications: notify_build_step(f"[{ctx.architecture}] Started signing") - sign(ctx) + # Pass certificate_name for Windows signing + if IS_WINDOWS: + sign(ctx, certificate_name) + else: + sign(ctx) if slack_notifications: notify_build_step(f"[{ctx.architecture}] Completed signing") diff --git a/build/config/release.windows.yaml b/build/config/release.windows.yaml index a939ba5f9..d00079c2b 100644 --- a/build/config/release.windows.yaml +++ b/build/config/release.windows.yaml @@ -1,18 +1,18 @@ # Nxtscape Windows Release Build Configuration build: type: release - architecture: x64 # Windows default architecture + architecture: x64 # Windows default architecture # No universal builds on Windows gn_flags: file: build/config/gn/flags.windows.release.gn steps: - clean: true - git_setup: true - apply_patches: true - build: true - sign: false # Windows signing requires certificate + clean: false + git_setup: false + apply_patches: false + build: false + sign: true # Enable signing with eSigner CKA package: true paths: @@ -23,16 +23,16 @@ paths: env: PYTHONPATH: scripts -# Signing configuration (optional - requires code signing certificate) +# Signing configuration (using eSigner CKA certificate in Windows store) signing: - # certificate_name: "Your Company Name" # Certificate subject name + certificate_name: "FELAFAX, INC." # Your certificate subject name from SSL.com # Or use environment variable: # require_env_vars: # - WINDOWS_CERTIFICATE_NAME # Notification settings notifications: - slack: true # Enable Slack notifications for release builds + slack: false # Enable Slack notifications for release builds # Build options build_options: diff --git a/build/modules/package_windows.py b/build/modules/package_windows.py index 4d4c1da33..942bc2fe4 100644 --- a/build/modules/package_windows.py +++ b/build/modules/package_windows.py @@ -4,6 +4,7 @@ Windows packaging module for Nxtscape Browser Based on ungoogled-chromium-windows packaging approach """ +import os import sys import shutil import zipfile @@ -161,21 +162,167 @@ def create_portable_zip(ctx: BuildContext) -> bool: def sign_binaries(ctx: BuildContext, certificate_name: Optional[str] = None) -> bool: - """Sign Windows binaries using signtool""" + """Sign Windows binaries using signtool or eSigner""" log_info("\nšŸ” Signing Windows binaries...") + # Check for signing method from environment or config + signing_method = os.environ.get('SIGNING_METHOD', 'signtool').lower() + + # Get paths to sign + build_output_dir = join_paths(ctx.chromium_src, ctx.out_dir) + + # List of binaries to sign + binaries_to_sign = [ + build_output_dir / "chrome.exe", + build_output_dir / "mini_installer.exe" + ] + + # Check which binaries exist + existing_binaries = [] + for binary in binaries_to_sign: + if binary.exists(): + existing_binaries.append(binary) + log_info(f"Found binary to sign: {binary.name}") + else: + log_warning(f"Binary not found: {binary}") + + if not existing_binaries: + log_error("No binaries found to sign") + return False + + # Use eSigner if configured + if signing_method == 'esigner' or os.environ.get('ESIGNER_USERNAME'): + return sign_with_esigner(existing_binaries) + + # Use PFX certificate if configured + if os.environ.get('PFX_PATH'): + return sign_with_pfx(existing_binaries) + + # Otherwise use traditional certificate store signing if not certificate_name: log_warning("No certificate specified, skipping signing") return True - # Get paths to sign - build_output_dir = join_paths(ctx.chromium_src, ctx.out_dir) - chrome_exe = build_output_dir / "chrome.exe" + return sign_with_certificate_store(existing_binaries, certificate_name) + + +def sign_with_esigner(binaries: List[Path]) -> bool: + """Sign binaries using SSL.com CodeSignTool""" + log_info("Using SSL.com CodeSignTool for signing...") - if not chrome_exe.exists(): - log_error(f"chrome.exe not found at: {chrome_exe}") + # Check for CodeSignTool from environment or default locations + codesigntool_path_str = os.environ.get('CODESIGNTOOL_PATH') + if codesigntool_path_str: + codesigntool_path = Path(codesigntool_path_str) + log_info(f"Using CodeSignTool from env: {codesigntool_path}") + else: + # Try default locations + codesigntool_path = Path("C:/src/BrowserOS/CodeSignTool-v1.3.2-windows/CodeSignTool.bat") + if not codesigntool_path.exists(): + codesigntool_path = Path("CodeSignTool.bat") + + if not codesigntool_path.exists(): + log_error(f"CodeSignTool.bat not found at: {codesigntool_path}") + log_error("Set CODESIGNTOOL_PATH in .env file or download from SSL.com") return False + # Check for required environment variables + username = os.environ.get('ESIGNER_USERNAME') + password = os.environ.get('ESIGNER_PASSWORD') + totp_secret = os.environ.get('ESIGNER_TOTP_SECRET') + credential_id = os.environ.get('ESIGNER_CREDENTIAL_ID') + + # Check if using TOTP secret or OTP + use_otp = os.environ.get('ESIGNER_USE_OTP', 'false').lower() == 'true' + + if use_otp: + # Prompt for OTP code + import getpass + otp_code = getpass.getpass("Enter your 6-digit OTP code from authenticator app: ") + if not otp_code or len(otp_code) != 6: + log_error("Invalid OTP code. Must be 6 digits.") + return False + else: + # Use TOTP secret + if not totp_secret: + log_error("Missing ESIGNER_TOTP_SECRET environment variable") + log_error("Either set ESIGNER_TOTP_SECRET or set ESIGNER_USE_OTP=true to enter OTP manually") + return False + + if not all([username, password]): + log_error("Missing required eSigner environment variables:") + log_error(" set ESIGNER_USERNAME=your-email") + log_error(" set ESIGNER_PASSWORD=your-password") + log_error(" set ESIGNER_TOTP_SECRET=your-totp-secret (or set ESIGNER_USE_OTP=true)") + log_error(" set ESIGNER_CREDENTIAL_ID=your-credential-id (optional)") + return False + + all_success = True + for binary in binaries: + try: + log_info(f"Signing {binary.name} with CodeSignTool...") + + # Build command + cmd = [ + str(codesigntool_path), + "sign", + "-username", username, + "-password", password, + "-input_file_path", str(binary), + "-output_dir_path", str(binary.parent), + "-override" # Override the input file after signing + ] + + # CodeSignTool doesn't support -otp flag, only -totp_secret + # For OTP, we need to convert it to a temporary TOTP secret + if use_otp: + # For manual OTP, we can't use it directly - need TOTP secret + log_warning("Note: CodeSignTool requires TOTP secret, not OTP code") + log_error("Please set ESIGNER_TOTP_SECRET in .env file") + log_error("You can find it in SSL.com dashboard under eSigner settings") + return False + else: + cmd.extend(["-totp_secret", totp_secret]) + + if credential_id: + cmd.extend(["-credential_id", credential_id]) + + # Note: Timestamp server is configured on SSL.com side automatically + + run_command(cmd) + log_success(f"{binary.name} signed successfully with CodeSignTool") + + except Exception as e: + log_error(f"Failed to sign {binary.name} with CodeSignTool: {e}") + all_success = False + + return all_success + + +def sign_with_pfx(binaries: List[Path]) -> bool: + """Sign binaries using PFX certificate file""" + pfx_path = os.environ.get('PFX_PATH') + if not pfx_path or not Path(pfx_path).exists(): + log_error(f"PFX certificate not found at: {pfx_path}") + return False + + log_info(f"Using PFX certificate: {pfx_path}") + + # Import the signing module + from .sign_with_esigner import sign_with_local_pfx + + all_success = True + for binary in binaries: + if not sign_with_local_pfx(str(binary), pfx_path): + all_success = False + + return all_success + + +def sign_with_certificate_store(binaries: List[Path], certificate_name: str) -> bool: + """Sign binaries using certificate from Windows certificate store""" + log_info(f"Using certificate from store: {certificate_name}") + # Check if signtool is available signtool_path = shutil.which("signtool") if not signtool_path: @@ -200,30 +347,35 @@ def sign_binaries(ctx: BuildContext, certificate_name: Optional[str] = None) -> log_error("signtool.exe not found. Please install Windows SDK.") return False - # Sign the main executable - try: - # Basic signing command - can be extended with timestamp server etc. - cmd = [ - signtool_path, - "sign", - "/n", certificate_name, # Certificate name - "/t", "http://timestamp.digicert.com", # Timestamp server - "/fd", "sha256", # File digest algorithm - str(chrome_exe) - ] - - run_command(cmd) - log_success("Binary signed successfully") - - # Verify signature - verify_cmd = [signtool_path, "verify", "/pa", str(chrome_exe)] - run_command(verify_cmd) - log_success("Signature verified successfully") - - return True - except Exception as e: - log_error(f"Failed to sign binary: {e}") - return False + # Sign each binary + all_success = True + for binary in binaries: + try: + log_info(f"Signing {binary.name}...") + + cmd = [ + signtool_path, + "sign", + "/n", certificate_name, # Certificate name + "/tr", "http://ts.ssl.com", # SSL.com timestamp server for eSigner + "/td", "sha256", # Timestamp digest algorithm + "/fd", "sha256", # File digest algorithm + str(binary) + ] + + run_command(cmd) + log_success(f"{binary.name} signed successfully") + + # Verify signature + verify_cmd = [signtool_path, "verify", "/pa", str(binary)] + run_command(verify_cmd) + log_success(f"{binary.name} signature verified successfully") + + except Exception as e: + log_error(f"Failed to sign {binary.name}: {e}") + all_success = False + + return all_success def package_universal(contexts: List[BuildContext]) -> bool: