ABOUT ME

-

Today
-
Total
-
  • Electron Code Signing (코드 서명)
    Study/Frontend 2023. 8. 25. 16:10
    반응형

     

    내 맥북에서는 잘 되는데 다른 사람 맥북에서는 실행이 안되네?
    윈도우에서 실행은 가능한데 SmartScreen filter에 걸리네?

     

    데스크탑 앱을 처음 만들어 보는 사람이라면 위와 같은 상황을 모두 한 번 쯤은 겪어봤을 것이다. 나 역시 그랬다 🥲

    이번에 Electron으로 데스크탑 앱을 만들어야했는데, 데스크탑 앱 개발에 대한 기본 지식이 부족한 상황이었다. electron-vite를 사용해서 개발 환경을 구축하는데는 무리가 없었지만, 역시나 예상대로 빌드와 배포를 하는데 엄청난 시간과 노력이 필요했다.

    위 문제를 해결하는데도 며칠 동안 리서치하며 해결방안을 찾아냈다. 위 문제의 정답은 코드 서명에 있었다.

     

    코드 서명이란?

    코드 서명(Code Signing)은 실행 파일과 스크립트에 디지털 서명을 하는 과정으로, 서명 이후에 코드가 변조되거나 손상되지 않음을 보장한다. 진위와 무결성 확인을 위해 암호화 해시를 사용한다. 코드 서명을 하지 않으면 각 운영 체제에서 보안 검사가 트리거되어 제대로 앱이 실행되지 않을 수 있다.

     

    코드 서명을 해야하는 이유

    (1) windows에서 앱 실행 방지 (2) macOS에서 앱 실행 방지 
    (3) 브라우저에서 실행 파일 다운로드 경고

     

    패키징을 완료한 앱을 사용자들에게 배포했을 때 위 화면과 같이 운영 체제 보안 검사를 트리거하지 않도록 앱에 서명해야 한다.  Windows와 macOS는 기본적으로 서명되지 않은 응용 프로그램의 다운로드 또는 실행을 방지한다. Windows에서는 인증서가 없거나 신뢰 수준이 낮은 경우 사용자가 앱을 실행하려고 할 때 보안 경고창이 표시된다. macOS에서 시스템은 변경 사항이 실수로 도입되었는지 또는 악성 코드에 의해 도입되었는지 여부에 관계없이 앱에 대한 모든 변경 사항을 감지할 수 있다. 서명되지 않은 앱을 배포할 수는 있지만 권장하지 않는다. macOS Catalina(버전 10.15)부터 사용자는 서명되지 않은 앱을 열려면 여러 수동 단계를 거쳐야 한다.

    그리고 코드 서명을 해야 하는 가장 큰 이유는 macOS의 경우 코드 서명을 하지 않으면 자동 업데이트가 불가능하다는 것이다. 진행중인 프로젝트에서 구현해야 할 기능 중에 electron 앱의 자동 업데이트 기능이 있기 때문에 코드 서명을 꼭 진행해야 했다.

     

     

    macOS 앱 코드 서명 하는 방법

     

    macOS에는 애플리케이션 배포를 위한 두 가지 보안 기술인 코드 서명(code signing)공증(notarization)이 있다. 코드 서명은 앱 작성자의 신원을 인증하고 배포 전에 변조되지 않았는지 확인하는 행위이고, 공증은 자동화된 malware 검사를 위해 앱을 Apple 서버로 보내는 추가 확인 단계이다.

    Mac App Store(MAS) 외부에 배포될 앱은 반드시 코드 서명이 되어있어야 한다. macOS 10.15(Catalina)부터 운영 체제 보안 검사를 비활성화하지 않고 사용자 컴퓨터에서 실행하려면 응용 프로그램에 코드 서명 및 공증이 모두 필요하다. Mac App Store(MAS)에 배포하는 앱은 MAS 제출 프로세스에 공증과 유사한 자동 검사가 포함되기 때문에 공증이 필요하지 않다.

     

    macOS 앱은 MAS에 배포하는 것과 MAS 외부에 배포하는 것으로 나뉘는데, 아래에서는 외부에 배포하는 것을 기준으로 macOS(darwin) 애플리케이션 파일(.app)에 코드 서명과 공증하는 방법을 안내하고자 한다.

     

    1. Developer ID Application certificate 생성

    macOS 앱용 코드 서명 인증서는 Apple Developer Program 멤버십을 구입하여 Apple을 통해서만 얻을 수 있다. 인증서 타입은 여러 가지가 있는데 Electron 앱에 코드 서명 및 공증까지 하려면 Developer ID Application certificate가 필요하다. Developer ID Application certificate는 Mac App Store 외부에서 배포되는 앱에 서명하는 용도로 사용한다. 만약 다른 타입의 인증서로 코드 서명을 하면 공증에 실패한다.


    https://www.rocketride.io/blog/macos-code-sign-notarize-electron-app

     

    How to code sign and notarize an electron app in 2022

    8 steps to make your Electron app ready for distribution on Mac OS

    www.rocketride.io

    인증서 생성 단계를 스크린샷과 함께 구체적으로 보고 싶다면 위 블로그를 참고하면 좋을 것 같다.

     

    1. Apple Developer Program 멤버십을 구입한다.
    2. 인증서 서명 요청(CSR/CertificateSigningRequest.certSigningRequest) 파일을 생성하기 위해 맥북에서 '키체인 접근' 실행 후 상단 메뉴에서 '인증서 지원 > 인증기관에서 인증서 요청'을 선택한다. 이 파일은 코드 서명에 필요한 인증서를 생성하는데 필요하다.
    3. 입력창이 뜨면 애플 개발자 계정 이메일 및 이름을 작성하고 요청 항목에 ‘디스크에 저장’을 선택한다.
    4. Developer ID Application 인증서를 생성하기 위해 애플 개발자 계정 사이트의 Certificates, IDs & Profiles 메뉴로 들어간다.
    5. Certificates 메뉴에서 파란색 플러스 아이콘을 클릭하고, 목록에서 Developer ID Application 인증서를 선택한다.
    6. 다음 스텝에서 'Profile Type: G2 Sub-CA (Xcode 11.4.1 or later)' 선택하고, 2번 단계에서 받았던 인증서 서명 요청 파일(CSR)을 파일 업로드 항목에서 업로드해서 인증서 생성을 요청한다.
    7. 인증서 파일(developerID_application.cer) 다운로드 후 키체인에 등록한다.
    8. 키체인에 등록이 완료되면 아래와 같은 화면을 키체인 접근에서 확인할 수 있다.
    9. 인증서가 잘 설치 됐는지 터미널에서  security find-identity -p codesigning -v 명령어를 입력해서 사용 가능한 코드 서명 인증서가 있는지 확인한다.

    키체인 등록 완료 화면
    터미널 명령어 입력해서 코드 서명 가능 인증서 있는지 체크

     

    2. electron-builder-config.js , entitlements.mac.plist 파일 작성

    /* eslint-disable node/no-unpublished-require */
    require('dotenv').config()
    
    const config = {
      appId: 'com.electron.app',
      productName: 'app',
      directories: {
        buildResources: 'build'
      },
      artifactName: '${productName}-${os}-${arch}-latest.${ext}',
      //...생략
      mac: {
        target: [
          {
            target: 'default',
            arch: ['arm64', 'x64']
          }
        ],
        notarize: {
          teamId: process.env.APPLE_TEAM_ID
        },
        category: 'public.app-category.utilities',
        hardenedRuntime: true,
        gatekeeperAssess: true,
        entitlements: 'build/entitlements.mac.plist',
        entitlementsInherit: 'build/entitlements.mac.plist',
      }
    }
    
    module.exports = config

    electron-builder-config,js 파일에 mac용 빌드 옵션 구성을 해준다.

    • hardendRuntime: macOS 앱에 대한 보안 보호 및 리소스 액세스를 관리하는 기능이고, 이것을 활성화해야 공증이 가능하다.
      참고링크1, 참고링크2
    • gatekeeperAssess: 이 항목을 true로 설정하면 코드 서명을 했는지 @electron/osx-sign 패키지에서 검증해준다.
    • entitlements: entitlements란 macOS 에서 실행 파일에 특정 기능(예: 카메라,마이크 등 장치에 대한 엑세스)을 부여하는 권한이다. 이 권한들은 코드 서명 시에 저장된다. 여기에는 권한을 작성한 파일의 경로를 작성한다. entitlementsInherit란 배포 번들에 대한 보안 설정을 상속하는 하위 권한에 대한 경로이다.
      참고링크1, 참고링크2참고링크3
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
      <dict>
        <key>com.apple.security.cs.allow-jit</key>
        <true/>
        <key>com.apple.security.cs.allow-dyld-environment-variables</key>
        <true/>
      </dict>
    </plist>

    build 폴더에 위와 같이 entitlements 권한을 작성한 entitlements.mac.plist 파일을 생성한다.

     

    3. build 실행 시 electron-builder 내부적으로 코드 서명 진행

    진행 중인 프로젝트에서는 electron-builder를 사용해서 패키징과 배포를 진행하고 있다. 이 도구를 사용하게 된 이유에 대해서는 자동 업데이트 글에서 더 상세하게 작성하도록 하겠다. 아무튼 electron-builder는 내부적으로 @electron/osx-sign 패키지를 사용해서 코드 서명을 진행한다. 빌드 타임에 macOS의 keychain에서 유효한 인증서를 찾으면, @electron/osx-sign을 사용해서 코드 서명을 진행한다.

    그리고 빌드 플랫폼에 따라 인증서를 자동으로 선택해 준다. mas일 경우 3rd Party Mac Developer Application: * (*) 인증서를 선택하고 darwin일 경우 Developer ID Application: * (*) 인증서를 선택해서 코드 서명을 한다.

    (참고 링크)

     

    "scripts": {
    	"build:mac": "electron-vite build && electron-builder --mac --config electron-builder-config.js"
     }

     

    package.json 에서 위와 같이 mac용 빌드 스크립트를 구성하고 빌드를 실행하면, electron-builder의 빌드 스크립트 중에 MacPackager.ts 스크립트 파일에서 코드 서명이 진행된다. 

     

     

     

     

    macOS 앱 공증하는 방법

     

    공증은 앱 심사가 아니다. Apple 공증(Notarizaion) 서비스 Developer ID 인증서로 서명한 소프트웨어에 악성 코드가 있는지 자동으로 스캔하고 보안 검사를 수행하는 시스템이다. 검사를 마치고 배포를 위해 내보낼 준비가 완료되면 Gatekeeper에서 소프트웨어가 공증을 받았음을 식별할 수 있도록 해당 소프트웨어에 티켓이 첨부된다.

    한마디로 Gatekeeper가 MAS 외부에서 배포되는 앱이 안전한지 체크하고 사용자를 악성 코드로부터 보호해 주는 역할을 한다.

    "앱스토어 외부에서 다운로드한 이 소프트웨어는 신뢰할 수 있습니다."라고 도장을 쾅쾅 찍어준다고 생각하면 된다.

     

    사용자가 소프트웨어를 처음 설치하거나 실행할 때 공증을 받았음을 식별할 수 있는 티켓이 있으면 Gatekeeper에 Apple이 소프트웨어를 공증했음을 알려준다. 그런 다음 Gatekeeper는 초기 실행 알림 창에 설명 정보를 배치하여 사용자가 앱 실행 여부에 대해 정보에 입각한 선택을 할 수 있도록 돕는다.

    공증이 완료된 실행파일을 처음 실행할 경우에는 아래와 같은 알림 창이 뜬다.

     

    1. ASP(App-Specific-Password) 생성

    App-Specific-Password는  공증에 사용하는 툴인 notarytool에 Apple 관련 자격 증명을 제공할 때 사용한다. 이것을 사용해서 공증하게 되면 Apple 이외의 개발자가 만든 앱에 안전하게 로그인할 수 있다. 

     

    1. https://appleid.apple.com/ 에 애플 개발자 계정으로 로그인
    2. App-Specific-Password 생성 후 복사
    3. env 파일에 Apple 관련 정보 입력
    APPLE_ID = 애플_개발자_계정
    APPLE_APP_SPECIFIC_PASSWORD = 생성된_App-Specific-Password 
    APPLE_TEAM_ID = 애플_개발자_멤버십_팀_ID

     

    2. electron-builder-config.js 파일에서 공증 옵션 추가

    공식 문서에서 공증에 대한 블로그 포스팅 링크를 제공하길래 처음에는 포스팅 그대로 따라했었다. afterSign Hook에 직접 작성한 notarize.js 스크립트를 실행하도록 했다. 근데 멀티 아키텍쳐로 빌드 스크립트를 구성해서 실행하는데 첫 번째 아키텍처는 afterSign Hook이 잘 실행되는데, 두 번째 아키텍처는 afterSign Hook이 돌지 않고, electron-builder 빌드 스크립트 중 MacPackager 스크립트 파일이 실행되는데 심지어 공증할 때 notarytool을 사용하는 것이 아니라 altool을 사용하는 것이었다.

     

    notarize:  module:app-builder-lib/out/options/macOptions.NotarizeOptions | Boolean | “undefined” - Options to use for @electron/notarize (ref: https://github.com/electron/notarize). Supports both legacy and notarytool notarization tools. Use false to explicitly disable

    이를 해결하기 위해 다시 공식 문서를 훑어보기 시작했고 Mac 빌드 옵션에 notarize 라는 것을 찾을 수 있었다. 하지만 옵션을 추가해도 공증 툴을 계속 altool을 사용하길래 원인이 무엇인지 파악하기 위해 electron-builder 의 MackPackager 스크립트 코드를 훑어보기 시작했다. 코드를 살펴보니 notarize 옵션을 추가할 때 반드시 teamId를 넣어야 notarytool이 실행되는 코드를 발견했다. 

     

    private generateNotarizeOptions(appPath: string, appleId: string, appleIdPassword: string): NotarizeOptions {
        const baseOptions = { appPath, appleId, appleIdPassword }
        const options = this.platformSpecificBuildOptions.notarize
        if (typeof options === "boolean") {
          return {
            ...baseOptions,
            tool: "legacy",
            appBundleId: this.appInfo.id,
          }
        }
        if (options?.teamId) { //🔥 코드 핵심
          return {
            ...baseOptions,
            tool: "notarytool",
            teamId: options.teamId,
          }
        }
        return {
          ...baseOptions,
          tool: "legacy",
          appBundleId: options?.appBundleId || this.appInfo.id,
          ascProvider: options?.ascProvider || undefined,
        }
      }

     

    위 코드를 보면 teamId가 옵션으로 들어와야지만 notarytool을 사용하는 것을 볼 수 있다.

     

    /* eslint-disable node/no-unpublished-require */
    require('dotenv').config()
    
    const config = {
      appId: 'com.electron.app',
      productName: 'app',
      directories: {
        buildResources: 'build'
      },
      artifactName: '${productName}-${os}-${arch}-latest.${ext}',
      //...생략
      mac: {
        target: [
          {
            target: 'default',
            arch: ['arm64', 'x64']
          }
        ],
        // 🔥 아래 옵션 추가
        notarize: {
          teamId: process.env.APPLE_TEAM_ID
        },
        category: 'public.app-category.utilities',
        hardenedRuntime: true,
        gatekeeperAssess: true,
        entitlements: 'build/entitlements.mac.plist',
        entitlementsInherit: 'build/entitlements.mac.plist',
      }
    }
    
    module.exports = config

     

     

    정리하자면 electron-builder에서 공증을 위해 내부적으로 @electron/notarize 패키지를 사용하고, 코드 서명 이후에 공증이 진행된다. 공증을 위해서는 mac 빌드 옵션에 위와 같이 notarize 옵션을 작성해주고 애플 개발자 멤버십 팀 id 를 입력해줘야한다.

     

     

    빌드 실행 후 위와 같은 로그가 뜨면 코드 서명과 공증이 모두 완료된 것이다!

     

    macOS 앱 코드 서명 및 공증 Trouble shooting

    CloudKit query for test.app ... failed due to "Record not found".
    Could not find base64 encoded ticket in response for ...
    The staple and validate action failed! Error 65.

    1. 다른 타입의 인증서(Apple Development)로 코드 서명 했을 때 에러 발생
    ➡️ 올바른 타입의 인증서(Developer ID Application)로 코드 서명하기

     

    Notarizing using the legacy altool system. The altool system will be disabled on November 1 2023. Please switch to the notarytool system before then.
    You can do this by setting "tool: notarytool" in your "@electron/notarize" options. Please note that the credentials options may be slightly different between tools.
    Error: Failed to upload app to Apple's notarization servers
    
    xcrun: error: unable to find utility "altool", not a developer tool or in PATH

     

    2. electron/notarize 패키지 버전이 낮을 때("^1.2.3”) default로 설정된 공증 툴이 ‘altool’ 이어서 에러 발생
    ➡️ 해결 방법1: 최신 버전(2.1.0)으로 업그레이드 (electron-builder 24.6.3 버전 말고 24.6.4 버전 사용 필요)

    ➡️ 해결 방법2: 공증 옵션 중 tool을 ‘notarize’로 명시

     

    3. 빌드된 실행파일의 아키텍처가 다를 경우 오류 발생

    ➡️ 아키텍처 별로 빌드할 수 있도록 수정

    mac: {
        target: [
          {
            target: 'default',
            arch: ['arm64', 'x64']
          }
        ],
    }

     

    4. 키체인에서 코드 서명에 사용할 인증서가 신뢰되지 않은 인증서라고 표시

    ➡️  Apple Worldwide Developer Relations Certification Authority를 설치

     

     


     

    macOS에서 Windows 앱 코드 서명하는 방법

     

    Windows 앱을 서명할 수 있는 인증서는  EV 코드 서명 인증서, 코드 서명 인증서 두 가지 타입이 있다. 일반적인 코드 서명 인증서는 앱을 설치할 때 경고를 표시하는데, 이 경고는 충분한 사용자가 앱을 설치하고 신뢰가 쌓이면 사라진다고 한다. 그러나 EV 코드 서명 인증서는 경고 없이 즉시 실행 가능하다. 아래에서는 EV 코드 서명 인증서를 활용해서 서명하는 방법을 작성했다.

     

    코드 서명 인증서 구매

    Windows Authenticode 코드 서명 인증서는 다양한 업체에서 구매할 수 있고, 가격도 다양하다. 일반적인 코드 서명 인증서는 신뢰도가 쌓일 때까지 경고창을 띄우기 때문에, 회사에서는 Digicert에서 EV 코드 서명 인증서를 구매했다.

     

    EV CodeSign 인증서

    EV(Extended Validation) Codesign 인증서는 Microsoft의 SmartScreen 경고 메시지를 즉시 해제할 수 있고, 엄격한 심사 프로세스 및 하드웨어 보안 요구 사항이 포함되어 있기 때문에 최종 사용자의 신뢰도를 높일 수 있는 인증서이다.

    하지만 가장 큰 단점은 코드 서명에 사용할 프라이빗키를 하드웨어 토큰 형식(ex. USB)으로 관리해야 한다는 것이다. 그래서 CI 프로세스에 사용하기 위해 키를 export 할 수가 없다. 

     

    Jsign을 사용해서 macOS에서 Windows 앱에 코드 서명하기

    Unix에서 Windows 앱 서명이 지원된다. 이를 달성하기 위한 여러 가지 방법이 있다. 기본적으로 PKCS 11을 사용하여 코드에 서명할 수 있는 애플리케이션이 필요하다. 여기서 사용할 애플리케이션은 Jsign이다.

    아래 electron-builder 공식 문서 내용을 참고해서 진행해보고자 한다.

     

    https://www.electron.build/tutorials/code-signing-windows-apps-on-unix#signing-windows-app-on-maclinux-using-jsign

     

    Sign a Windows app on macOS/Linux - electron-builder

    Sign a Windows app on macOS/Linux Info Described setup and configuration is required only if you have EV code signing certificate. The regular certificates supported out of the box. Signing Windows apps on Unix is supported. There are multiple methods to a

    www.electron.build

     

    1. 코드 서명을 위한 사전 작업

    Jsign 은 플랫폼 독립적인 Microsoft Authenticode의 Java 구현이며 Linux의 osslsigncode 및 Windows의 SignTool 또는 Unix 시스템의 Mono 개발 도구와 같은 기본 도구에 대한 대안을 제공한다.

    Jsign을 사용해서 코드 서명을 하려면 Java를 설치해야 한다. 그리고 USB 토큰을 읽어서 코드 서명을 진행할 수 있는 클라이언트 툴이 필요하다.

    1. safeNet Authentication Client 를 다운로드한다.
    2. SafeNet Authentication Client Tools를 실행한다.
    3. digicert USB 삽입 후 로그온을 진행해서 패스워드를 입력한다. 인증서의 패스워드는 반드시 비공개로 유지해야 한다.
    4. 루트 경로에 hardwareToken.cfg 파일 생성 후 아래 코드 작성한다.
    5. library 경로는 PKCS 11 module을 가진 library의 경로인지 체크해 보고 작성한다.
    6. Java를 설치(현재 14.0.2 설치) 하고 코드서명 툴인 Jsign 을 동일 경로에 설치한다.
    // hardwareToken.cfg 파일
    name = HardwareToken
    library = /Library/Frameworks/eToken.framework/Versions/A/libeToken.dylib
    slotListIndex = 0

     

    2. 루트 경로에 sign.js 파일 생성

    • CERTIFICATE_NAME은 SafeNet Authentication Client Tools 실행 후 Issued To 에 해당하는 이름 입력
    • TOKEN_PASSWORD는 EV Codesign 인증서의 패스워드 입력
    // eslint-disable-next-line node/no-unpublished-require
    require('dotenv').config()
    
    exports.default = async function (configuration) {
      // do not include passwords or other sensitive data in the file
      // rather create environment variables with sensitive data
    
      const CERTIFICATE_NAME = process.env.WINDOWS_SIGN_CERTIFICATE_NAME
      const TOKEN_PASSWORD = process.env.WINDOWS_SIGN_TOKEN_PASSWORD
    
      console.log('start sign')
    
      require('child_process').execSync(
        // your commande here ! For exemple and with JSign :
        `java -jar jsign-5.0.jar --keystore hardwareToken.cfg --storepass "${TOKEN_PASSWORD}" --storetype PKCS11 --tsaurl <http://timestamp.digicert.com> --alias "${CERTIFICATE_NAME}" "${configuration.path}"`,
        {
          stdio: 'inherit'
        }
      )
    }

     

    3. 빌드 옵션에 인증서 이름 작성 및 sign.js 파일 경로 추가

    https://www.electron.build/configuration/win#WindowsConfiguration-certificateSubjectName

     

    Any Windows Target - electron-builder

    Any Windows Target The top-level win key contains set of options instructing electron-builder on how it should build Windows targets. These options applicable for any Windows target. target = nsis String | TargetConfiguration - The target package type: lis

    www.electron.build

    • If you are using an EV Certificate, you need to provide win.certificateSubjectName in your electron-builder configuration.
    • certificateSubjectName String | “undefined” - The name of the subject of the signing certificate, which is often labeled with the field name issued to.

    위 문서에 따르면 아래와 같이 빌드 옵션에 인증서의 이름을 작성하고, sign.js 파일 경로를 추가한다.

    afterSign: build/notarize.js
    
    // 🔥 인증서 이름 작성 및 sign 스크립트 경로 추가
    win:
      executableName: sampleApp
      certificateSubjectName: CERT_NAME
      sign: ./sign.js

     

    4. windows 전용 빌드 스크립트 추가 및 실행

        "build:win": "electron-vite build && electron-builder --win --x64 --config",

    코드 서명을 완료하게 되면 위와 같은 로그가 뜬다. Adding Authenticode signature to ... 라는 문구가 나오면 코드 서명이 정상적으로 실행된 것으로 확인 가능하다. 그리고 앱을 실행해 보면 MS SmartScreen Filter에 걸리지 않고 정상적으로 실행된다.

    반응형

    댓글