docs: design the EXE installer UI and code signing scope

- Design 14: WiX MSI + Burn with a C# BA host and WebView2 web UI,
  default screen catalog, installer-ui.json and the WebView2 fallback.
- Design 16: Smart App Control checks and what must be signed.
- Manual: beginner guide to Azure Artifact Signing.
- Link the decision from designs 09 and 10 and record it.

Refs #15

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jychoi4andClaude Opus 5.5 committed 2026-11-11 11:08:50 +09:00
1 parent aba06b3443
commit 7f5540f148
6 files changed
+672

No files matched your search

@@ -0,0 +1,29 @@
# EXE 설치 UI 설계 결정
2026-11-09. EXE 설치 파일의 화면을 자유롭게 꾸미는 것이 가장 중요한 요구로 정해져, 설치 UI 구조와 기본 화면 목록을 [14 EXE 설치 UI](../code-design/14-exe-installer-ui.md)로 정리했다. 설계 문서만 변경했으며 BA·웹 UI·Burn 번들 구현과 설치 시험은 범위 밖이다.
## 결정
- 설치 패키지는 WiX MSI를 Burn 번들(`setup.exe`)로 감싼다. WiX 버전과 OSMF 이용 조건은 채택 전에 확정한다.
- 설치 화면은 C#(.NET Framework 4.8) BA 호스트가 WebView2로 표시하는 HTML/CSS/TypeScript 웹 UI로 만든다.
- BA 호스트와 웹 UI는 공통으로 한 번 빌드·서명하고, 제품별 차이는 `installer-ui.json`과 자산으로만 전달한다.
- 기본 화면 카탈로그(신규 설치 7개, 유지 관리 2개, 상태 6개)를 제공한다. 1단계는 `welcome`, `license`, `installFolder`, `review`, `progress`, `finish`, `failure`, `cancelled`, `maintenance`, `uninstallConfirm` 10개다.
- WebView2가 없으면 같은 BA의 기본 네이티브 화면으로 설치한다. 무인 설치는 WebView2를 쓰지 않는다.
- [09 설치 과정과 UI 편집](../code-design/09-install-flow-and-ui.md)의 Rust/Tauri BA 방식과 WixStdBA 테마를 첫 구현으로 삼는 순서는 채택하지 않는다.
## 모델과 실행
- 프로젝트 모델의 `formats.exe.installerUi` 필드는 아직 추가하지 않았다. 구현 단계에서 `project-model-change` 절차로 반영한다.
- 웹 UI와 호스트는 정해진 메시지(`init`, `detected`, `validate`, `apply`, `progress`, `result` 등)만 주고받으며 호스트가 모든 입력을 다시 검증한다.
## 확인 결과
- 실행한 것: 문서 작성과 저장소 내부 링크 연결(09, 10, 설계 색인, 루트 README).
- 실행하지 않은 것: BA 구현, WebView2 연결, Burn 번들 빌드, VM 설치·업그레이드·제거·취소 시험, WebView2 미설치 PC 시험, 접근성 시험. 백로그 BL-08 검증 실험으로 수행한다.
## 근거
- [FireGiant — Out-of-process bootstrapper applications](https://docs.firegiant.com/wix/whatsnew/oopbas/)
- [FireGiant — WixStdBA](https://docs.firegiant.com/wix/tools/burn/wixstdba/)
- [FireGiant — Open Source Maintenance Fee](https://docs.firegiant.com/wix/osmf/)
- [Microsoft Learn — Distribute your app and the WebView2 Runtime](https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution)
@@ -2,6 +2,8 @@
2026-10-19 실행 범위 변경: 이 문서의 EXE 설치 과정·BA·HTML 페이지 편집은 후속 범위다. 첫 제품의 Tauri UI는 MSIX 제작 설정을 편집하며 설치 마법사를 생성하지 않는다. [현재 설계](12-msix-first-builder.md) 참조.
2026-11-09 결정: EXE 설치 UI는 C# BA 호스트와 WebView2 웹 UI로 만들고 기본 화면 카탈로그를 제공한다. 아래 "Rust/Tauri 사용자 정의 설치 UI" 절과 "기본 테마 편집 → 사용자 정의 BA" 구현 순서는 [14 EXE 설치 UI](14-exe-installer-ui.md)가 대체한다.
이 문서의 최종 사용자 설치 과정·마법사·Burn BA 커스터마이징은 **EXE 출력 전용**이다. MSIX는 Windows/Store가 설치 UI와 경로를 관리하며 제작기는 manifest의 이름·아이콘·지원 선언을 편집한다. 두 형식의 설정을 혼합하거나 미지원 단계를 조용히 생략하지 않는다. [범용 출력 계약](11-generic-exe-msix-architecture.md) 참조.
## 목표
@@ -8,6 +8,8 @@
현재 결과는 상호작용 시안과 구현 설계다. 실제 설치·백업·서명 검사·관리자 승격·재부팅은 실행하지 않으며 표시되는 경로·진행률·검증 결과는 예시다.
2026-11-09: 기본으로 제공할 화면 ID·숨김 규칙·1단계 화면 10개와 `installer-ui.json` 형식은 [14 EXE 설치 UI](14-exe-installer-ui.md)를 따른다. 아래 `prepare`·`apply`는 14번의 `filesInUse`·`progress`에 대응한다.
## 참고한 패턴
| 출처 | 확인한 특징 | EG-BIM에 적용한 설계 |
+245
View File
@@ -0,0 +1,245 @@
# EXE 설치 UI: WebView2 BA와 기본 화면
결정일: 2026-11-09. EXE 설치 파일(`setup.exe`)의 화면을 사용자가 자유롭게 꾸밀 수 있게 하는 구조와 기본으로 제공할 화면 목록을 정한다. 이 문서는 설계이며 BA·웹 UI 구현, Burn 연결, Windows 설치 시험은 수행하지 않았다. MSIX는 Windows가 설치 UI를 관리하므로 이 문서의 대상이 아니다.
[09 설치 과정과 UI 편집](09-install-flow-and-ui.md)의 단계·데이터 계약과 [10 설치 마법사 재설계](10-installer-wizard-redesign.md)의 모드별 흐름·실패 처리 원칙은 유지한다. 아래 결정이 두 문서와 다르면 이 문서를 따른다.
## 결정
| 항목 | 결정 |
| --- | --- |
| 설치 패키지 | WiX로 만든 MSI를 Burn 번들(`setup.exe`)로 감싼다. WiX 버전과 이용 조건(OSMF)은 채택 전에 확정한다. |
| 설치 UI | **사용자 정의 Burn BA**를 사용한다. BA 호스트는 C#(.NET Framework 4.8)로 만들고 화면은 **WebView2가 표시하는 HTML/CSS/TypeScript 웹 UI**로 만든다. |
| 제품별 차이 | BA 호스트와 웹 UI는 공통으로 한 번 빌드·서명한다. 제품마다 다른 화면 설정은 `installer-ui.json`과 이미지·약관 자산으로만 전달한다. |
| 기본 화면 | 아래 [기본 화면 카탈로그](#기본-화면-카탈로그)를 표준으로 제공한다. 제작자는 문구·자산·표시 여부·일부 순서를 편집한다. |
| WebView2가 없을 때 | 같은 BA 안의 단순한 네이티브 기본 화면(WPF)으로 설치를 진행한다. 인터넷이 되면 WebView2 웹 부트스트래퍼를 BA 선행 패키지로 먼저 설치할 수 있다. 고정 버전 런타임 포함은 기본값이 아니다. |
| 무인 설치 | `/quiet`·`/passive`에서는 WebView2를 만들지 않고 같은 검증과 기본값으로 설치한다. |
| 제외 | Rust/Tauri로 BA를 직접 구현하는 방식(09의 "Rust/Tauri 사용자 정의 설치 UI")과 WixStdBA 테마 편집을 첫 구현으로 삼는 순서는 채택하지 않는다. WiX가 C++·.NET BA API만 공식 제공하기 때문이다. |
## 구조
```text
setup.exe (Burn 번들, 서명)
├─ Burn 엔진 ........ 탐지 → 계획 → 적용, UAC 권한 상승, 롤백, 프로그램 추가/제거 등록
├─ BA 호스트 (C#, .NET Framework 4.8)
│ ├─ WebView2 ── 웹 UI 표시
│ ├─ 기본 네이티브 화면 ── WebView2를 쓸 수 없을 때
│ └─ 브리지 ── 웹 UI ↔ Burn 메시지 중계, 입력 재검증
├─ 웹 UI (Vite 빌드 결과: HTML/CSS/JS)
├─ installer-ui.json + 로고·이미지·약관
├─ 선행 프로그램 패키지
└─ 제품 MSI
```
WiX v5부터 Burn은 BA를 별도 실행 파일로 띄운다. BA 호스트는 WiX가 제공하는 .NET BA 라이브러리로 Burn과 통신한다. 실제 파일·레지스트리 변경은 Burn과 MSI가 수행하며 BA나 웹 UI가 `msiexec`를 따로 실행하지 않는다.
Installer Studio도 WebView2(Tauri)를 쓰므로 같은 웹 UI에 가짜 브리지를 연결해 제작 화면에서 미리보기를 제공한다. 미리보기는 시스템을 변경하지 않으며, 최종 판정은 실제 번들의 VM 시험으로 한다.
## 실행 흐름
1. Burn이 BA 호스트를 실행한다. 대화형 모드면 호스트가 창을 만들고 WebView2 사용 가능 여부를 확인한다.
2. 웹 UI는 번들 안의 파일만 가상 호스트(`https://installer.local/`)로 읽는다. 외부 주소 이동, 새 창, 다운로드는 차단한다.
3. Burn 탐지 결과(설치 여부, 설치된 버전, 기능 상태, 선행 프로그램 필요 여부)를 웹 UI에 보내고, 웹 UI는 신규 설치·업그레이드·유지 관리 중 맞는 흐름을 연다.
4. 사용자가 `review`에서 시작을 누르면 웹 UI가 `apply` 메시지를 보낸다. 호스트는 경로 규칙, 허용 기능, 약관 동의를 다시 검사한 뒤 Burn 변수를 설정하고 계획·적용을 요청한다.
5. Burn의 진행·오류·재부팅·취소 이벤트를 웹 UI에 전달한다. 적용이 시작된 뒤에는 선택값과 계획을 바꾸지 않는다.
6. 완료 화면의 "앱 실행"은 원래 사용자 권한으로 실행한다.
## 브리지 메시지
웹 UI는 아래 형식의 메시지만 보낼 수 있다. 호스트는 알 수 없는 `type`, 형식이 틀린 값, 현재 상태에서 허용되지 않는 요청을 거부한다.
| 방향 | `type` | 내용 |
| --- | --- | --- |
| 호스트 → UI | `init` | `installer-ui.json`, 선택 언어, 실행 모드(대화형) |
| 호스트 → UI | `detected` | 설치 상태(`absent`/`older`/`same`/`newer`), 설치된 버전·경로, 기능 상태, 필요한 선행 프로그램, 차단 사유 |
| 호스트 → UI | `validation` | 입력 검증 결과(필드별 오류 코드와 문구) |
| 호스트 → UI | `progress` | 전체 진행률, 현재 패키지·작업 이름 |
| 호스트 → UI | `filesInUse` | 사용 중인 프로그램 목록 |
| 호스트 → UI | `result` | `succeeded`/`failed`/`cancelled`, 재부팅 필요 여부, 오류 코드, 로그 경로 |
| UI → 호스트 | `validate` | 설치 폴더, 기능 선택 등 입력 검증 요청 |
| UI → 호스트 | `browseFolder` | 폴더 선택 대화상자 요청. 결과 경로도 호스트가 검증 |
| UI → 호스트 | `apply` | 동작(`install`/`modify`/`repair`/`uninstall`), 설치 폴더, 기능, 추가 작업, 약관 동의 |
| UI → 호스트 | `cancel` | 취소 요청. 처리 결과는 `result`로 확인 |
| UI → 호스트 | `retryFilesInUse` / `closeApplications` | 사용 중인 프로그램 다시 확인. 강제 종료는 기본 동작이 아니다. |
| UI → 호스트 | `launchApp`, `openLink`, `restart`, `close` | 완료 후 작업. `openLink`는 `installer-ui.json`에 등록된 주소만 허용 |
## 기본 화면 카탈로그
모든 화면은 공통 틀(상단 로고·제품명·단계 표시, 본문, 하단 이전/다음·실행/취소 버튼)을 쓴다. 언어는 한국어·영어를 기본으로 하고 시스템 언어로 자동 선택한다.
### 신규 설치 흐름
| 순서 | 화면 ID | 내용 | 숨김 |
| --- | --- | --- | --- |
| 1 | `welcome` | 제품명·버전·로고, 안내 문구. 이전 버전이 있으면 업그레이드 안내 | 가능 |
| 2 | `license` | 약관 전문과 동의. 동의해야 다음으로 진행 | 약관이 없을 때만 |
| 3 | `installFolder` | 기본 설치 폴더, 찾아보기, 필요·사용 가능 공간 | 가능. 숨기면 기본 경로 |
| 4 | `features` | 기능 트리와 용량. 필수 기능은 해제 불가 | 가능. 숨기면 기본 선택 |
| 5 | `tasks` | 바탕화면 바로가기, 파일 연결 등 선택 작업 | 가능. 숨기면 기본값 |
| 6 | `review` | 경로, 기능, 선행 프로그램, 필요한 권한 요약과 시작 버튼 | 불가 |
| 7 | `progress` | 현재 작업, 전체 진행률, 취소 | 불가 |
### 유지 관리 흐름
이미 설치된 PC에서 `setup.exe`를 실행하거나 프로그램 추가/제거에서 변경을 선택할 때 사용한다.
| 화면 ID | 내용 | 다음 화면 |
| --- | --- | --- |
| `maintenance` | 수정, 복구, 제거 선택 | 수정: `features` → `review`, 복구·제거: `review` 또는 `uninstallConfirm` |
| `uninstallConfirm` | 제거할 항목과 남는 항목(사용자 도면·설정·백업) 안내, 확인 | `progress` |
### 공통 상태 화면
조건에 따라 표시하며 숨길 수 없다.
| 화면 ID | 조건 | 내용 |
| --- | --- | --- |
| `blocked` | 시작 전 검사 실패 | 더 높은 버전 설치됨, 지원하지 않는 OS, 디스크 부족 등 원인과 조치 |
| `filesInUse` | 설치할 파일을 쓰는 프로그램이 있음 | 저장 후 종료 안내, 다시 확인 |
| `finish` | 성공 | 결과 요약, 앱 실행 선택, 링크 |
| `rebootRequired` | 성공했지만 재부팅 필요 | 이유, 지금/나중에 선택. 기본은 나중에 |
| `failure` | 오류 | 실패 단계, 기존 제품 상태, 로그 위치, 다음 조치 |
| `cancelled` | 사용자 취소 | 취소 시점과 엔진이 확인한 실제 상태 |
### 편집 규칙
- 화면을 숨겨도 그 단계의 작업은 기본값으로 실행한다. 숨김은 표시 여부만 바꾼다.
- `license`는 약관이 지정되어 있으면 숨길 수 없다. 숨김을 동의로 처리하지 않는다. 무인 설치의 동의 전달은 별도 인자로 정한다.
- 순서 변경은 `welcome`~`tasks` 사이의 선택 화면에만 허용한다. `review` 이후와 상태 화면은 고정한다.
- 표시하는 입력은 실제 설치 정의와 연결되어야 한다. 예를 들어 `tasks`의 바로가기 항목은 MSI 바로가기 정의가 있어야 하며, 연결이 없으면 빌드 오류로 처리한다.
| 대상 | 편집 가능 항목 |
| --- | --- |
| 모든 화면 | 제목, 설명, 다국어 문구, 로고, 강조 색, 밝기 테마 |
| `welcome` | 대표 이미지, 안내 문구 |
| `license` | 약관 파일(RTF/TXT/HTML), 동의 문구 |
| `installFolder` | 기본 경로(프로젝트 `formats.exe.installRoot`), 경로 변경 허용 여부 |
| `features` | 기능 이름·설명·기본 선택·필수 여부 |
| `tasks` | 항목 표시, 기본 선택 상태 |
| `finish` | 앱 실행 표시와 기본 선택, 링크 목록 |
## installer-ui.json
빌드 시 프로젝트 설정에서 생성해 번들에 넣는 파일이다. 제작기에서는 버전 메타파일의 `installerUi`로 편집하며([제작기 설치화면 편집](#제작기-설치화면-편집-2026-11-09)), 이 절은 번들에 넣는 생성 결과의 형식을 정한다. 자산 경로는 프로젝트 폴더 기준의 안전한 상대 경로만 허용한다.
```json
{
"schemaVersion": 1,
"template": { "id": "standard-wizard", "version": "1.0.0" },
"theme": {
"logo": "assets/logo.png",
"welcomeImage": "assets/welcome.png",
"accentColor": "#7257C5",
"colorScheme": "dark"
},
"locales": { "default": "ko-KR", "available": ["ko-KR", "en-US"] },
"strings": {
"ko-KR": { "welcome.title": "EG-BIM 설치를 시작합니다" },
"en-US": { "welcome.title": "Welcome to EG-BIM Setup" }
},
"pages": [
{ "id": "welcome", "visible": true },
{ "id": "license", "visible": true, "file": { "ko-KR": "assets/license.ko.rtf", "en-US": "assets/license.en.rtf" } },
{ "id": "installFolder", "visible": true, "allowChange": true },
{ "id": "features", "visible": false },
{ "id": "tasks", "visible": true },
{ "id": "review" },
{ "id": "progress" }
],
"finish": {
"launchApp": { "visible": true, "defaultChecked": true },
"links": [{ "label": "release.notes", "url": "https://example.com/egbim/release-notes" }]
}
}
```
- 알 수 없는 필드, 카탈로그에 없는 화면 ID, 숨길 수 없는 화면의 `visible: false`, 허용 범위 밖의 순서는 빌드 전에 거부한다.
- 상태 화면은 `pages`에 적지 않는다. 문구만 `strings`로 바꾼다.
- `url`은 `https`만 허용하고 `openLink` 요청은 이 목록과 비교한다.
- 빌드 스냅샷에는 `installer-ui.json`과 모든 자산의 SHA-256을 기록한다. 서명한 `setup.exe`의 화면을 외부 파일로 바꾸지 않는다.
## 제작기 설치화면 편집 (2026-11-09)
설치 구성 › **설치화면** 탭에서 버전마다 설치 단계·화면 문구·모양·스크립트를 편집한다. 값은 버전 메타파일(`.isver`)의 `installerUi`에 저장하고, 버전을 추가하면 함께 복사하며, 잠긴 버전은 읽기 전용이다. 이 필드가 없는 기존 버전 파일은 아래 표준 마법사로 연다. 빌드 시 이 값에서 `installer-ui.json`과 MSI 사용자 지정 동작을 만든다(EXE 빌드와 함께 후속 구현).
| 항목 | 규칙 |
| --- | --- |
| 표준 마법사 | `welcome`, `installFolder`, `review`, `progress`, `finish` 다음에 `maintenance`, `uninstallConfirm`, `failure`, `cancelled`. 이 9개는 지울 수 없다. |
| 단계 추가 | `license`(한 번만, 약관 파일 필수), `info`(안내 화면: 제목·설명), `input`(입력 화면). 추가한 단계는 `review` 바로 앞에 들어가며 삭제할 수 있다. |
| 순서 | `welcome`·`license`·`installFolder`·`info`·`input`만 `review` 앞에서 순서를 바꾼다. `review`·`progress`·`finish`와 유지 관리·결과 화면은 고정이다(`InvalidPageOrder`). |
| 숨김 | `welcome`·`installFolder`·`info`·`input`만 숨긴다. 숨겨도 그 단계는 기본값으로 진행한다(`PageCannotBeHidden`). |
| 문구 | 제목·설명은 서식 문자열이며 `[ProductName]` 같은 속성문자를 쓴다. 정의되지 않은 속성은 `UnknownProperty`. |
| 입력 화면 | 항목마다 저장할 사용자 속성(프로젝트 설정 › 속성문자), 이름, 형식(`text`·`checkbox`·`choice`), 기본값, 선택 목록, 필수 여부를 둔다. 같은 속성을 두 항목이 쓰지 않는다(`DuplicateInputProperty`). 입력값은 레지스트리·바로가기·스크립트 인수에서 `[속성]`으로 쓴다. |
| 모양 | 밝기(`dark`/`light`), 강조 색(`#RRGGBB`), 로고(프로젝트 폴더 기준 PNG·JPG·SVG). 약관 파일은 RTF·TXT·HTML. 파일이 없으면 `MissingAsset`. |
| 설치 폴더 | `fixedFolder`를 켜면 기본 설치 폴더를 바꿀 수 없다. |
### 스크립트
| 항목 | 규칙 |
| --- | --- |
| 종류 | `powershell`(.ps1), `cmd`(.cmd·.bat), `exe`(.exe). 확장자가 다르면 `ScriptKindMismatch`. 화면에 스크립트 본문을 입력하지 않는다. |
| 파일 | 프로젝트 소스(alias) 폴더 안의 `/` 경로. 설치 파일과 별도로 번들에 포함하므로 설치되지 않는 파일도 쓸 수 있다. 없으면 `MissingScriptFile`. 빌드 스냅샷에 SHA-256을 기록한다. |
| 실행 시점 | `beforeFiles`(설치: 파일 복사 전), `afterFiles`(설치: 파일 복사 후), `beforeUninstall`(제거: 파일 삭제 전)은 MSI 지연 사용자 지정 동작으로 설치 엔진 권한(관리자)으로 실행한다. `afterFinish`(완료 후)는 BA가 완료 화면 뒤에 로그인 사용자 권한으로 실행한다. |
| 인수 | 한 줄에 하나씩 배열로 전달하고 명령 문자열로 합치지 않는다. PowerShell은 `powershell.exe -NoProfile -ExecutionPolicy Bypass -File <파일> <인수...>`, CMD는 `cmd.exe /d /c <파일> <인수...>` 형태로 실행하며 인수는 따옴표 규칙에 맞게 하나씩 이스케이프한다. 각 인수는 서식 문자열이다. |
| 실패 처리 | `fail`은 0이 아닌 종료 코드·제한 시간 초과에서 설치를 중단하고 설치 엔진의 변경을 되돌린다. 스크립트가 직접 바꾼 내용은 되돌리지 않는다. `ignore`는 로그에 남기고 계속한다. `afterFinish`는 `ignore`만 허용한다(`ScriptFailureAfterFinish`). |
| 제한 시간 | 1~3600초(기본 600). 시간이 지나면 프로세스 트리를 종료한다. |
| 로그 | 스크립트 이름, 시점, 종료 코드, 소요 시간을 설치 로그에 남긴다. 출력에 비밀 값을 쓰지 않도록 안내한다. |
패치 버전도 같은 스크립트를 가질 수 있으며 패치 적용 시 실행된다. 스크립트 실행은 Windows VM 설치·제거·패치 시험 전에는 검증된 것으로 표시하지 않는다.
## WebView2 가용성
Windows 11과 대부분의 Windows 10에는 WebView2 Evergreen 런타임이 있지만 LTSC, Server, 업데이트가 막힌 오프라인 PC에는 없을 수 있다.
| 상황 | 동작 |
| --- | --- |
| WebView2 있음 | 웹 UI 표시 |
| 없음 + 인터넷 사용 가능 | 번들 설정이 허용하면 WebView2 웹 부트스트래퍼를 BA 선행 패키지로 설치한 뒤 웹 UI 표시. 실패하면 기본 네이티브 화면 |
| 없음 + 오프라인 | 기본 네이티브 화면으로 같은 흐름 진행 |
| 무인 설치 | WebView2를 사용하지 않음 |
기본 네이티브 화면은 같은 카탈로그의 필수 화면(`welcome`, `license`, `installFolder`, `review`, `progress`, 상태 화면)과 같은 검증을 제공하며 디자인 편집은 문구·로고로 제한한다. WebView2 사용자 데이터 폴더는 실행마다 임시 폴더에 만들고 종료 시 삭제한다.
## 보안
- 웹 UI는 번들 안 정적 파일만 사용한다. 원격 스크립트, 외부 CDN, `eval`을 쓰지 않고 CSP는 `default-src 'self'`로 둔다.
- 릴리스 빌드는 개발자 도구, 상황 메뉴, 확대·축소 단축키를 끈다.
- 웹 UI는 파일·레지스트리·프로세스에 직접 접근하지 않는다. 모든 요청은 정해진 메시지로 호스트를 거치고 호스트가 다시 검증한다.
- BA와 웹 UI는 일반 사용자 권한으로 실행한다. 권한 상승은 Burn이 관리하며 UAC 화면은 꾸미지 않는다.
- 로그와 메시지에 토큰·비밀번호를 넣지 않는다.
## 1단계 범위와 검증 실험
첫 구현은 신규 설치·업그레이드·제거와 실패·취소를 확인할 수 있는 화면 10개로 한다.
`welcome`, `license`, `installFolder`, `review`, `progress`, `finish`, `failure`, `cancelled`, `maintenance`, `uninstallConfirm`
`features`, `tasks`, `filesInUse`, `rebootRequired`, `blocked`는 MSI 기능·바로가기·파일 연결을 프로젝트 모델에 넣는 다음 단계에서 추가한다.
본 구현 전에 백로그 BL-08의 검증 실험을 VM에서 수행한다. MSI 1개와 선행 프로그램 1개를 담은 Burn 번들에 C# 호스트와 웹 UI를 붙이고 아래를 확인한다.
- 신규 설치, 같은 UpgradeCode의 상위 버전 업그레이드, 제거, 복구
- 설치 폴더 변경과 잘못된 경로 거부
- 진행률 표시, 적용 중 취소와 롤백 결과 표시
- 실패 화면과 로그 위치
- `/quiet` 무인 설치에서 WebView2 미사용
- WebView2가 없는 PC에서 기본 네이티브 화면으로 설치 완료
- 고해상도(DPI), 키보드 이동, 스크린 리더 읽기
실험 결과는 `src/tests/verification-YYYY-MM-DD-<slug>.json`에 실행·미실행 항목을 모두 기록한다. 실험 전에는 이 구조를 검증된 것으로 표시하지 않는다.
## 미결정 사항
- WiX 버전(v6 또는 v7)과 OSMF 이용 조건 수용 여부
- 지원 대상 PC 범위(Windows 10 LTSC·Server·오프라인 PC 포함 여부)와 WebView2 웹 부트스트래퍼 사용 허용 여부
- 약관 파일 형식과 무인 설치의 동의 전달 방식
- 기본 지원 언어 목록
- 기본 네이티브 화면을 WPF로 할지, 같은 호스트의 최소 Win32 화면으로 할지
## 근거
- [FireGiant — Out-of-process bootstrapper applications](https://docs.firegiant.com/wix/whatsnew/oopbas/): WiX v5부터 BA를 별도 실행 파일로 실행, C++·.NET BA API 제공
- [FireGiant — WixStdBA](https://docs.firegiant.com/wix/tools/burn/wixstdba/): 표준 BA 테마의 지원 범위
- [Microsoft Learn — Distribute your app and the WebView2 Runtime](https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution): Evergreen·고정 버전 런타임 배포 방식
- [Microsoft Learn — Overview of WebView2 features and APIs](https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/overview-features-apis): 가상 호스트 매핑, 웹 메시지 통신
- [Inno Setup — WizardStyle](https://jrsoftware.org/ishelp/topic_setup_wizardstyle.htm), [Revenera — Defining InstallShield Prerequisites](https://docs.revenera.com/installshield/helplibrary/IHelpSetPrereqCustom.htm): 기본 화면 구성 참고
@@ -0,0 +1,128 @@
# 16 스마트 앱 컨트롤 검사 대상과 서명 범위
2026-11-09 작성. 스마트 앱 컨트롤(Smart App Control, 이하 SAC)이 켜진 PC에서 설치한 제품이 동작하려면 무엇을 서명하고 무엇을 배포하지 말아야 하는지 정리한 참조 문서다. 서명 시스템(EXE 출력의 서명 단계, [05 릴리스 생성](05-release-pipeline.md))을 설계할 때 입력으로 쓴다. 이 문서는 조사 결과이며 구현이나 SAC 실제 시험 통과를 뜻하지 않는다. 정책 결정은 별도 변경 기록으로 남긴다.
## SAC 판정 순서
1. Microsoft 클라우드 평판 서비스가 안전 여부를 확신할 수 있으면 그 판정대로 허용하거나 차단한다. 유효한 서명이 있어도 악성 판정이면 차단된다.
2. 확신할 수 없으면 서명을 본다. 유효한 서명이 있으면 허용하고, 서명이 없거나 유효하지 않으면 차단한다.
3. 앱별·파일별 예외를 둘 수 없다. 사용자 선택지는 SAC를 끄는 것뿐이다. 최근 업데이트부터 끈 뒤 다시 켤 수 있다.
SmartScreen은 진입 실행 파일 하나만 평판을 보지만, SAC는 운영체제 로더와 스크립트 엔진이 불러오는 **모든 코드**를 검사한다. 서명된 설치기가 서명 없는 코드를 풀어 놓는 방식은 통과하지 못한다.
## 서명 요건
| 요건 | 내용 |
| --- | --- |
| 신뢰 루트 | Microsoft 신뢰 루트 프로그램(Trusted Root Program)에 속한 루트로 이어져야 한다. 사용자나 관리자가 직접 등록한 루트(개발용 인증서 포함)는 인정되지 않는다. |
| 다이제스트 | SHA-256 이상을 쓴다(`signtool sign /fd sha256`). |
| 키 알고리즘 | RSA를 쓴다. ECC는 지원되지만 호환성을 위해 피한다. |
| 타임스탬프 | SAC 자체 요건으로 명시된 자료는 없다. 인증서 만료 뒤에도 서명이 유효하도록 반드시 붙인다(`/tr <TSA URL> /td sha256`). |
| 서명 후 불변 | 서명 뒤 리소스·버전 정보를 수정하면 서명이 깨진다. 모든 후처리는 서명 전에 끝낸다. |
| 스크립트 서명 | `WinVerifyTrust`로 검증하며, 서명 루트가 대상 PC의 신뢰 루트 저장소에 있어야 한다. 공인 루트는 이 조건을 충족한다. |
## 검사 대상 분류
### A. 실행 코드(PE) — 반드시 서명
확장자가 아니라 파일 헤더(`MZ`와 PE 서명)로 판별한다. 운영체제 로더는 확장자와 관계없이 이미지를 불러올 때 검사한다.
- Microsoft가 명시한 확장자: `.exe`, `.com`, `.dll`, `.rll`, `.ocx`, `.sys`
- MotW 차단 목록에도 있는 PE 확장자: `.cpl`, `.drv`, `.scr`, `.vxd`
- 제품 고유 확장자 예(EG-BIM): `.irx`, `.tx`, `.txv`, `.txr`
- .NET 어셈블리(관리 코드 DLL)도 PE이므로 같은 기준으로 서명한다.
### B. Windows Installer — 서명
- `.msi`, `.msp`: 서명한다. MSI가 설치 중 임시 폴더에 푸는 사용자 지정 작업(Custom Action) DLL·EXE도 A와 같은 기준으로 미리 서명한다.
- `.mst`: App Control의 검사 대상이다. 서명 방법과 필요 여부는 확인이 필요하다.
### C. 패키지 — 패키지 서명
- `.appx`, `.appxbundle`, `.msix`, `.msixbundle`: 패키지 서명으로 처리한다. 패키지 안의 PE를 개별 서명해야 하는지는 SAC 시험으로 확인한다.
### D. 스크립트(App Control 연동 스크립트 호스트) — 서명하거나 배포하지 않음
| 호스트 | 확장자 | 서명이 없을 때 |
| --- | --- | --- |
| PowerShell | `.ps1`, `.psm1`, `.psd1` | 차단되지 않고 제한 언어 모드(Constrained Language Mode)로 실행된다. 기능이 조용히 실패할 수 있다. 진입 스크립트가 불러오는 모듈과 dot-source 파일도 모두 서명해야 한다. |
| Windows Script Host(`wscript.exe`, `cscript.exe`) | `.vbs`, `.js`, `.wsf` | 차단된다. |
| Windows Script Host(인코딩 스크립트) | `.vbe`, `.jse` | 배포하지 않는다. |
### E. 사용 금지
- `.hta`(`mshta.exe`): 스크립트 검사가 켜진 App Control 정책이 하나라도 있으면 모든 실행이 차단된다. SAC 환경에서도 같다고 보고 배포하지 않는다.
- MSXML을 통한 스크립트 실행(XSL 스크립트 등): 위와 같은 이유로 쓰지 않는다.
### F. 직접 통제 밖 — 서명 대상 아님, 단 실행되는 하위 프로그램은 검사됨
- `.bat`, `.cmd`(`cmd.exe`): App Control이 직접 통제하지 않으며 서명할 수도 없다. 배치 파일이 실행하는 EXE와 DLL은 검사된다.
- 앱이 스스로 해석하는 데이터·스크립트(LISP, CUI, DCL, 도면, 이미지 등): 검사 대상이 아니다.
- App Control과 연동하지 않는 타사 스크립트 엔진(Python, Java 등): 엔진 실행 파일은 A 기준으로 검사되지만, 그 엔진이 실행하는 스크립트는 통제되지 않는다.
### G. 인터넷에서 받은 파일(MotW)에 대한 차단 목록
웹 표식(Mark of the Web, MotW)이 붙은 아래 확장자는 열기 자체가 차단되고 무시 선택지가 없다. 목록은 고정이며 레지스트리로 바꿀 수 없다. 2026-04 기준 자료이므로 서명 시스템에는 갱신 가능한 데이터로 둔다.
`.appref-ms`, `.appx`, `.appxbundle`, `.bat`, `.chm`, `.cmd`, `.com`, `.cpl`, `.dll`, `.drv`, `.gadget`, `.hta`, `.iso`, `.js`, `.jse`, `.lnk`, `.msc`, `.msp`, `.ocx`, `.pif`, `.ppkg`, `.printerexport`, `.ps1`, `.rdp`, `.reg`, `.scf`, `.scr`, `.settingcontent-ms`, `.sys`, `.url`, `.vb`, `.vbe`, `.vbs`, `.vhd`, `.vhdx`, `.vxd`, `.wcx`, `.website`, `.wsf`, `.wsh`
배포와 업데이트에 적용하는 규칙:
- 배포물은 서명된 `.exe` 또는 `.msi`로 직접 내려받게 한다. `.iso`, `.vhd`, `.vhdx`로 감싸지 않는다.
- 업데이트가 받는 파일에 이 목록의 확장자를 쓰지 않는다. 패치는 서명된 설치 파일 형태로 받아 실행한다.
- MotW를 지워서 차단을 피하는 방식은 쓰지 않는다.
- 설치기가 디스크에 쓴 파일에는 보통 MotW가 붙지 않는다. 따라서 설치된 `.chm`, `.bat` 등은 이 목록의 영향을 받지 않는 것으로 본다. 이 판단은 SAC 시험으로 확인한다.
## 서명 시스템 반영 요구사항
1. **PE 판별**: 수집한 모든 파일을 헤더로 검사해 PE를 찾는다. 확장자 목록만으로 판정하지 않는다([05 릴리스 생성](05-release-pipeline.md)의 기존 원칙).
2. **확장자 분류 데이터**: B~G 분류를 코드에 고정하지 않고 버전이 붙은 데이터로 둔다. 분류 결과를 서명 보고서에 기록한다.
3. **파일별 정책**: 기존 `sign-owned`, `verify-vendor`, `not-applicable`을 유지한다. 서명 없는 타사 PE(오픈소스 등)를 처리할 정책은 결정이 필요하다.
4. **거부 규칙**: 정식 빌드에서 아래를 발견하면 설명과 함께 거부한다.
- 서명할 수 없거나 서명되지 않은 PE
- D 분류의 서명 없는 스크립트
- E 분류 파일
- 배포물 형식이 G 목록에 해당하는 경우
오류 코드는 구현 시 정하고 `src/README.md`에 적는다.
5. **서명 검증**: 서명 후 모든 대상 파일을 다시 검증하고 아래 항목을 기록한다. 허용할 루트 인증서의 지문은 계획에 고정한다.
- 다이제스트가 SHA-256 이상인지
- 체인과 루트 지문
- 타임스탬프 유무
- 서명자
6. **실행 중 생성 코드 점검표**: 정적 검사로 찾을 수 없으므로, 제품마다 아래 질문을 확인 항목으로 둔다.
- 업데이트가 내려받는 파일은 무엇인가
- 자체 압축 해제 EXE를 만드는 기능이 있는가
- 임시 폴더에 DLL을 풀어서 쓰는가
- 실행 중에 코드를 컴파일해 디스크에 저장하는가
## EG-BIM 조사 결과
2026-11-09 이 PC에 설치된 EG-BIM(`C:\Program Files\Baron\EG-BIM`)을 읽기 전용으로 조사했다. SAC를 켠 실제 실행 시험은 하지 않았다.
| 항목 | 결과 |
| --- | --- |
| PE 파일 | 965개. 서명 유효 620개, 서명 없음 344개, 판정 오류 1개(`IntelliConvert.com`) |
| 바론 서명 파일 | 6개(`Grimi.exe`, `Kkureomi.exe`, `LoadFile.exe`, `EGBIM.Drawer.Splash.exe`, `Nyng.PlotWindow.exe`, `PlotStyleTableEditor.exe`) |
| 바론 인증서 | GlobalSign GCC R45 CodeSigning CA 2020 발급, 루트 GlobalSign Code Signing Root R45(`AuthRoot` 저장소에 있음), 2026-08-10~2027-08-11, 온라인 폐기 확인 통과, 타임스탬프 있음 |
| 서명 없는 PE 확장자 | `.dll` 294, `.irx` 31, `.exe` 9, `.tx`·`.txv`·`.txr` 10, `.com` 1 |
| 그 밖의 SAC 관련 파일 | `.chm` 9개, `.bat` 1개(`Nyng.bat`). `.ps1`, `.vbs`, `.js`, `.hta`, `.msi`는 없음 |
| 실행 중 생성 코드 후보 | `Extractor.exe`(자체 압축 해제 스텁으로 보임), `BaronSoftware.Update.dll`(업데이트) |
## SAC 시험 항목(미실행)
SAC가 켜진 Windows 11 VM 스냅샷에서 실행하고 `CodeIntegrity/Operational`(3076, 3077, 3089)과 `AppLocker/MSI and Script`(8028, 8029, 8037, 8038) 이벤트를 수집한다.
- 모든 PE를 서명한 빌드의 설치·실행·주요 기능·업데이트·제거
- `.irx`, `.tx` 등 비표준 확장자 PE가 검사·차단되는지
- 서명된 `.dll`, `.msp`에 MotW가 붙었을 때 G 목록 차단이 서명으로 풀리는지
- MSIX 내부 PE를 개별 서명하지 않아도 되는지
- 설치된 `.chm`, `.bat`이 영향받지 않는지
- 첫 실행과 두 번째 실행의 시작 시간 차이
## 근거
- [What is Smart App Control? — Microsoft Support](https://support.microsoft.com/en-us/topic/what-is-smart-app-control-285ea03d-fa88-4d56-882e-6698afdb7003): 판정 순서, 예외 불가, 재활성화
- [App Control for Business feature availability — Microsoft Learn](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/feature-availability): 검사 대상 파일 형식
- [Understand App Control script enforcement — Microsoft Learn](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/design/script-enforcement): 스크립트 호스트별 동작, `cmd.exe` 비통제, 이벤트 ID
- [Smart App Control — text/plain(Eric Lawrence, Microsoft Defender 소속 개인 블로그, 2026-04-28)](https://textslashplain.com/2026/04/28/smart-app-control/): MotW 차단 목록, 서명 요건. 공식 문서가 아니므로 시험으로 확인한다.
@@ -0,0 +1,266 @@
# 처음 시작하는 Azure 아티팩트 서명(Artifact Signing) 운영 서명
대상: Installer Studio로 만든 MSIX를 **사내 시험을 넘어 실제 사용자 PC에 배포**하려는 담당자. 자체 서명 테스트 인증서 대신 공인 신뢰 서명을 처음 준비하는 사람 기준으로 쓴다.
확인일: **2026-11-03, 한국 시간**. Microsoft Learn 한국어·영어 문서(2026-10-08 갱신판)를 확인해 작성했으며 실제 계정 생성·신원 확인·서명을 수행한 기록은 아니다. 메뉴 이름은 **Azure 포털 한국어 화면 기준**으로 적고 영문을 괄호로 병기한다. Learn 한국어 페이지는 기계 번역이므로 실제 포털 표기가 약간 다를 수 있다. 2026년에 서비스 이름이 **Trusted Signing → Artifact Signing(아티팩트 서명)**으로 바뀌었다. 포털 검색, 역할 이름, winget 패키지 ID에는 옛 이름이 남아 있으므로 두 이름을 같은 것으로 읽는다.
## 이 방식이 하는 일
Azure가 인증서 발급·보관·갱신을 모두 맡고, 우리 PC에서는 SignTool이 Azure에 서명 요청만 보낸다. 인증서 개인 키는 Azure HSM 안에 있어 USB 토큰이나 PFX 파일이 없다. 인증서는 3일마다 자동 교체되지만 **주체(Subject: CN, O, L, S, C)는 신원 확인 결과로 고정**되므로 MSIX의 Publisher가 바뀌지 않는다.
```text
Azure 구독 준비 → 리소스 공급자 등록 → 아티팩트 서명 계정 만들기
→ 조직 ID 유효성 검사(1~20영업일) → 인증서 프로필 만들기 → 역할 할당
→ 서명 PC에 클라이언트 도구 설치 → metadata.json 작성 → az login → signtool로 MSIX 서명
→ 서명 검증 → Installer Studio Publisher를 인증서 주체로 맞춤
```
## 용어 대응표
| 포털 한국어 표기 | 영어 | 뜻 |
| --- | --- | --- |
| 아티팩트 서명 계정 | Artifact Signing account | 서명 서비스의 최상위 리소스 |
| ID 유효성 검사 | Identity validation | 회사(또는 개인) 신원 확인 요청 |
| 인증서 프로필 | Certificate profile | 실제 인증서가 발급되는 단위 |
| 공용 / 프라이빗 | Public / Private | 공개 배포용 / 사내 전용 |
| 액세스 제어(IAM) | Access control (IAM) | 역할 부여 메뉴 |
| 아티팩트 서명 ID 검증 도구 | Artifact Signing Identity Verifier | 신원 확인 요청 권한 |
| 아티팩트 서명 인증서 프로필 서명자 | Artifact Signing Certificate Profile Signer | 서명 실행 권한 |
| 인증서 제목(주체) 미리 보기 | Certificate subject preview | 인증서에 들어갈 CN·O 문자열 |
## 시작 전 확인
| 항목 | 내용 |
| --- | --- |
| 지원 국가 | 공용 신뢰(Public Trust) 인증서는 미국, 캐나다, EU, 영국, 호주, 뉴질랜드, 일본, **한국**, 싱가포르, 스위스, 노르웨이, 이스라엘의 **조직**에 제공한다. 개인 개발자는 미국·캐나다만 가능하다. 한국 법인은 조직으로 진행한다. |
| Azure 지역 | 계정은 지원 지역에만 만들 수 있다. **Korea Central**이 포함되어 있고 엔드포인트는 `https://krc.codesigning.azure.net`이다. |
| 비용 | 기본(Basic)은 월 5,000회, 프리미엄(Premium)은 월 100,000회 서명을 포함한다. 금액은 가격 페이지의 계산기에서 확인한다(제3자 자료 기준 월 약 10달러·100달러). **계정을 만든 날부터 과금되고 일할 계산이 없다.** 신원 확인이 끝나기 전에도 요금이 나간다. |
| 대상 Windows | Windows 10 1809 이상, Windows 11, Windows Server 2016 이상이 루트를 신뢰한다. 더 오래된 PC는 지원 범위에서 제외한다. |
| SmartScreen | 서명해도 즉시 신뢰되지 않는다. 설치 실적이 쌓이기 전에는 경고가 뜰 수 있으며 정상 동작이다. |
| 필요한 사람 | Azure 구독 소유자(역할 부여), 회사 대표로 개인 신원 확인을 할 담당자(신분증과 스마트폰), 서명을 실행할 개발자. |
## 1단계. Azure 구독과 테넌트 준비
1. [Azure 포털](https://portal.azure.com/)에 회사 계정으로 로그인한다. 회사 Microsoft Entra 테넌트가 없으면 먼저 만든다.
2. **구독(Subscriptions)**이 있어야 한다. 없으면 구독을 만들고 결제 수단을 등록한다. 조직 신원 확인에는 청구 계정 유형 조건이 없다(개인 확인에만 "개인(Individual)" 유형이 필요하다).
3. 검색 상자에 **구독**을 입력해 열고 대상 구독을 선택한다. 왼쪽 메뉴 **설정 → 리소스 공급자(Resource providers)**에서 `Microsoft.CodeSigning`을 찾아 줄임표(…) → **등록(Register)**을 누른다. 상태가 **등록됨(Registered)**으로 바뀌면 된다.
Azure CLI를 쓰면 같은 작업은 다음과 같다.
```powershell
az login
az account set -s <구독 ID>
az provider register --namespace "Microsoft.CodeSigning"
az extension add --name artifact-signing
```
## 2단계. 아티팩트 서명 계정 만들기
1. 포털 검색 상자에 **아티팩트 서명 계정(Artifact Signing Accounts)**을 입력해 열고 **만들기(Create)**를 누른다.
2. 입력값
- **구독(Subscription)**: 1단계의 구독
- **리소스 그룹(Resource group)**: **새로 만들기(Create new)**. 예: `rg-codesigning`
- **계정 이름(Account name)**: 3~24자 영숫자, 전 세계에서 유일, 문자로 시작. 예: `baronsigning`
- **지역(Region)**: **Korea Central**
- **가격 책정(Pricing)**: 처음에는 **기본(Basic)**
3. **검토 + 만들기(Review + create) → 만들기(Create)**. 완료되면 **리소스로 이동(Go to resource)**.
4. 계정 **개요(Overview)** 화면의 **계정 URI(Account URI)**를 적어 둔다. 뒤의 metadata.json에 들어간다.
```powershell
az group create --name rg-codesigning --location koreacentral
az artifact-signing create -n baronsigning -l koreacentral -g rg-codesigning --sku Basic
```
## 3단계. 역할 할당
계정 화면 왼쪽 메뉴 **액세스 제어(IAM)** → **추가(Add) → 역할 할당 추가(Add role assignment)**. **역할(Role)** 탭의 검색란에 "아티팩트 서명"을 입력하면 두 개의 사용자 지정 역할이 보인다. 역할을 고른 뒤 **구성원(Members)** 탭에서 사람 또는 앱을 선택하고 **검토 + 할당(Review + assign)**을 누른다.
| 역할 | 누구에게 | 용도 |
| --- | --- | --- |
| **아티팩트 서명 ID 검증 도구** (Artifact Signing Identity Verifier, 구 Trusted Signing Identity Verifier) | 신원 확인을 신청할 담당자 | 4단계 **새 ID** 버튼 활성화. 구독에 **읽기 권한자(Reader)** 이상이 함께 있어야 한다 |
| **아티팩트 서명 인증서 프로필 서명자** (Artifact Signing Certificate Profile Signer, 구 Trusted Signing Certificate Profile Signer) | 서명을 실행할 개발자 계정, 또는 CI용 앱 등록(서비스 주체) | 7단계 서명 호출 허용 |
계정과 인증서 프로필을 만드는 사람에게는 **기여자(Contributor)** 이상이 필요하다. 구독 **소유자(Owner)**라도 위 두 사용자 지정 역할이 없으면 해당 작업이 막힌다. 역할 반영에 몇 분 걸릴 수 있다.
### 여러 사람이 함께 서명할 때의 구조
서명자 역할을 사람마다 직접 주지 않고 **보안 그룹에 한 번만 주고 사람은 그룹에 넣고 뺀다.** 서명 PC에는 PFX나 비밀 값이 없으므로, 퇴사·이동 시 그룹에서 빼는 것만으로 서명이 즉시 막히고 인증서를 폐기할 필요가 없다.
1. **Microsoft Entra ID → 그룹(Groups) → 새 그룹(New group)**. 그룹 유형 **보안(Security)**, 이름 예 `sg-codesigning-signers`. 멤버십 유형은 **할당됨(Assigned)**.
2. 3단계의 **역할 할당 추가**에서 **구성원** 탭의 **액세스 할당 대상**을 "사용자, 그룹 또는 서비스 주체"로 두고 그룹을 선택한다. 역할은 **아티팩트 서명 인증서 프로필 서명자**.
3. 권한은 세 층으로 나눈다.
| 층 | 대상 | 역할 | 비고 |
| --- | --- | --- | --- |
| 소유 | 회사 대표 계정 1~2명 | 구독 **소유자** | 결제·역할 변경. 평소 서명에 쓰지 않는다 |
| 운영 | 서명 인프라 담당 1~2명 | 리소스 그룹 **기여자** + **ID 검증 도구** | 계정·프로필 생성, 신원 확인 갱신 |
| 서명 | `sg-codesigning-signers` 그룹 | **인증서 프로필 서명자** | 개발자 전원, CI용 앱 등록. 프로필을 바꾸거나 지울 수 없다 |
4. 사람마다 **자기 계정**으로 `az login`한다. 한 계정을 여럿이 공유하면 MFA·감사 기록·퇴사 처리가 모두 깨진다. 모든 서명자는 **같은 Entra 테넌트**에 있어야 하므로 대표 계정이 개인 Microsoft 계정의 기본 디렉터리라면 다른 사람을 **게스트 초대**해야 하고, 회사 도메인 테넌트가 있다면 그쪽에서 진행하는 것이 맞다.
5. CI 서버는 **앱 등록(App registrations)**으로 서비스 주체를 만들어 같은 그룹에 넣는다. GitHub Actions·Azure DevOps는 클라이언트 비밀 대신 **페더레이션 자격 증명(워크로드 ID)**을 쓰면 비밀 저장이 없다.
6. `metadata.json`은 비밀이 아니므로 저장소에 두고 공유한다. 팀·PC 구분이 필요하면 `"CorrelationId": "ci-main"`처럼 값을 넣어 서명 기록에서 구분한다.
7. 서명 횟수는 계정 단위로 합산된다. 기본(Basic) 월 5,000회를 모든 서명자와 CI가 함께 쓴다. CI가 커밋마다 서명하면 빠르게 소진되므로 릴리스 빌드에서만 서명하거나 **프리미엄**으로 올린다. 기본 계층은 **공용 신뢰 프로필을 1개만** 만들 수 있다. 같은 회사의 여러 제품은 주체가 같으므로 프로필 1개를 함께 쓰면 된다.
8. **서명 기록(Signing history)**은 서명자 역할이 볼 수 있고, 역할 변경은 **활동 로그(Activity log)**에 남는다. 역할 할당 변경에 경고 규칙을 걸어 두면 무단 추가를 감지할 수 있다.
## 4단계. 조직 ID 유효성 검사
가장 오래 걸리는 단계다. 처리 기간은 **영업일 기준 1~20일**이며 보완 서류 요청이 오면 더 길어진다. 여기서 입력한 회사명·주소가 그대로 인증서 주체가 되고 **MSIX Publisher**가 된다. 나중에 고치려면 ID 유효성 검사를 새로 해야 하고 앱 계열이 끊어지므로 처음부터 등기 서류와 똑같이 적는다.
1. 계정 화면 왼쪽 메뉴 **개체(Objects) → ID 유효성 검사(Identity validations)**를 연다.
2. 상단에서 **조직(Organization)**을 선택하고 **새 ID(New identity) → 공용(Public)**을 누른다.
3. **새 조직 유효성 검사(New organization validation)** 폼 입력값
| 항목(한국어 / 영어) | 적는 내용 |
| --- | --- |
| 조직 이름 / Organization Name | 법인 등기상의 정식 명칭. 영문 표기는 사업자등록증·영문 등기부의 표기와 일치 |
| 웹 사이트 URL / Website url | 회사가 소유한 도메인의 웹사이트 |
| 기본 이메일 / Primary Email | 회사 도메인의 실제 수신 가능한 메일. 확인 링크가 오며 7일 안에 열어야 한다 |
| 보조 이메일 / Secondary Email | 기본과 다른 같은 도메인 메일. 메일 그룹 가능 |
| 비즈니스 식별자 / Business Identifier | 사업자등록번호 또는 법인등록번호 |
| 판매자 ID / Seller ID | Microsoft Store 판매자만. 없으면 비움 |
| 도로명, 도시, 국가/지역, 주/도, 우편번호 | 법인 사업장 주소 |
| 이름 / First Name, 성 / Last Name | 회사를 대표해 **개인 신원 확인을 할 담당자**. 여권 등 신분증 표기와 똑같이 적는다 |
4. **인증서 제목 미리 보기(Certificate subject preview)**로 인증서에 들어갈 문자열을 미리 본다. 이 값이 나중의 Publisher다.
5. **만들기(Create)**. 상태가 **진행 중(In Progress)**이 된다.
6. 상태가 **작업 필요(Action Required)**로 바뀌면 담당자 개인 확인 차례다. 기본 이메일로도 링크가 온다.
1. 요청에 적은 이메일로 로그인해 링크를 연다.
2. **신뢰할 수 있는 ID 검증 도구를 통해 여기에서 확인 받기(Get verified here through our trusted ID-verifiers)**를 누르면 외부 확인 업체(AU10TIX) 화면으로 간다.
3. 이메일 PIN 확인 → 전화번호 입력 → **시작(Start)** → 노트북 화면의 QR 코드를 스마트폰으로 스캔.
4. 스마트폰에서 여권·운전면허증·신분증 촬영과 안면 확인을 진행한다. 플래시 없이, 평평한 곳에서, 잘리지 않게 찍는다.
5. **인증자 열기(Open Authenticator)**로 Microsoft Authenticator 앱에 확인된 ID(Verified ID)를 **추가(Add)**하고, 다시 브라우저 QR 코드를 스캔해 **공유(Share)**한다.
6. 브라우저에 **확인 성공(Verification Successful)**이 뜨면 포털 상태가 몇 분 뒤 **진행 중**으로 돌아가 조직 확인이 이어진다.
7. 서류 보완 메일이 오면 포털에서 업로드한다. 업로드 기회는 3번이다. 12개월 이내 발급, 만료가 2개월 이상 남은 서류여야 한다. 사업자등록증·법인등기부등본과 도메인 등록 증빙(회사명·도메인이 함께 적힌 것)을 준비해 둔다.
8. 상태가 **완료(Completed)**가 되면 다음 단계로 간다. **실패(Failed)**면 사유 메일을 확인하고 새 요청을 만든다.
## 5단계. 인증서 프로필 만들기
1. 계정 화면 **개체 → 인증서 프로필(Certificate profiles) → 만들기(Create)**에서 **공용 신뢰(Public Trust)**를 고른다.
2. **인증서 프로필 이름(Certificate Profile Name)**: 5~100자. 예: `baron-public-trust`
3. **인증서 유형(Certificate Type)**은 자동 입력, **프로그램 유형(Program Type)**은 **없음(None)** 그대로 둔다.
4. **유효성 검사된 CN 및 O(Verified CN and O)**에서 4단계에서 완료된 ID 유효성 검사를 선택한다. **거리 주소 포함(Include street address)**, **우편 번호 포함(Include postal code)**은 선택 사항이며 기본은 끈다.
5. **인증서 제목 미리 보기(Certificate Subject Preview)**에 최종 주체가 보인다. 이 문자열을 **그대로 복사해 보관**한다. 예: `CN=Baron Software Co., Ltd., O=Baron Software Co., Ltd., L=Seoul, S=Seoul, C=KR`
6. **만들기(Create)**.
CLI로는 ID 유효성 검사 화면에서 **ID 유효성 검사 ID**를 복사한 뒤 다음을 실행한다.
```powershell
az artifact-signing certificate-profile create -g rg-codesigning --account-name baronsigning -n baron-public-trust --profile-type PublicTrust --identity-validation-id <ID 유효성 검사 ID>
```
## 6단계. 서명 PC 준비
서명을 실행할 PC(개발 PC 또는 빌드 서버)에서 한다. Windows 10 1809 이상이어야 한다.
1. 관리자 PowerShell에서 클라이언트 도구를 설치한다. SignTool, .NET 8 런타임, 서명 플러그인(dlib)이 함께 들어온다.
```powershell
winget install -e --id Microsoft.Azure.ArtifactSigningClientTools
```
winget이 없는 서버는 MSI를 직접 받는다.
```powershell
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri "https://download.microsoft.com/download/70ad2c3b-761f-4aa9-a9de-e7405aa2b4c1/ArtifactSigningClientTools.msi" -OutFile .\ArtifactSigningClientTools.msi
Start-Process msiexec.exe -Wait -ArgumentList '/I ArtifactSigningClientTools.msi /quiet'
Remove-Item .\ArtifactSigningClientTools.msi
```
2. Windows SDK의 일반 SignTool은 이 플러그인과 호환되지 않는 버전일 수 있다. 설치된 도구 폴더의 SignTool을 쓴다. 기본 경로는 `C:\Program Files (x86)\Microsoft\ArtifactSigningClientTools\bin`이다.
3. `metadata.json`을 만든다. Endpoint는 2단계의 계정 URI와 같아야 하며 지역이 다르면 403 오류가 난다.
```json
{
"Endpoint": "https://krc.codesigning.azure.net",
"CodeSigningAccountName": "baronsigning",
"CertificateProfileName": "baron-public-trust"
}
```
4. Azure CLI를 설치하고 서명자 역할이 있는 계정으로 로그인한다. 사람이 서명하는 PC에서는 이것으로 충분하다.
```powershell
az login
```
빌드 서버에서는 **Microsoft Entra ID → 앱 등록(App registrations)**에서 앱을 만들어 3단계의 서명자 역할을 주고 환경 변수로 인증한다. 비밀 값은 소스나 프로젝트 파일에 넣지 않는다.
```powershell
$env:AZURE_TENANT_ID = "<테넌트 ID>"
$env:AZURE_CLIENT_ID = "<앱 등록 클라이언트 ID>"
$env:AZURE_CLIENT_SECRET = "<클라이언트 비밀>"
```
## 7단계. MSIX 서명
서명 전에 패키지의 Publisher가 5단계의 주체와 글자 단위로 같아야 한다. 다르면 SignTool이 `0x8007000B`로 거부한다.
```powershell
& "C:\Program Files (x86)\Microsoft\ArtifactSigningClientTools\bin\signtool.exe" sign /v /fd SHA256 `
/tr "http://timestamp.acs.microsoft.com" /td SHA256 `
/dlib "C:\Program Files (x86)\Microsoft\ArtifactSigningClientTools\bin\x64\Azure.CodeSigning.Dlib.dll" `
/dmdf .\metadata.json `
.\package.msix
```
- `/tr`과 `/td`의 타임스탬프는 **반드시** 넣는다. 인증서가 3일 뒤 만료되므로 타임스탬프가 없으면 그 뒤 서명 검증이 실패한다.
- 실패하면 `/debug`를 추가해 인증·인증서 체인 메시지를 본다.
- dlib 경로는 설치 버전에 따라 `bin\x64\` 또는 `bin\` 바로 아래일 수 있다. 폴더에서 `Azure.CodeSigning.Dlib.dll` 위치를 확인한다. 32비트 SignTool에는 x86 dlib를 쓴다.
서명 결과 확인:
```powershell
signtool verify /pa /v .\package.msix
Get-AuthenticodeSignature .\package.msix | Format-List Status, SignerCertificate, TimeStamperCertificate
```
`Status`가 Valid이고 SignerCertificate의 Subject가 5단계 값과 같으면 된다. 이 PC에 자체 서명 테스트 인증서를 신뢰 등록한 적이 있어도 운영 서명 검증과는 무관하다.
## 8단계. Installer Studio에 반영
1. 앱 정보 탭 **패키지 identity → Publisher**에 5단계에서 복사한 주체를 그대로 붙인다. 쉼표 뒤 공백까지 같아야 한다.
2. Identity Name은 바꾸지 않는다. Publisher가 바뀌면 PackageFamilyName이 달라져 기존 테스트 설치본과는 다른 앱이 된다. 테스트용 자체 서명본을 설치한 PC에서는 먼저 제거한다.
3. 현재 Installer Studio의 서명 모드는 `unsigned`와 `development`(자체 서명)만 있다. 운영 서명은 **미서명 모드로 빌드한 뒤 7단계 명령을 수동으로 실행**하는 방식으로 진행한다. 제작기 안에서 `/dlib` 호출을 지원하는 운영 서명 프로필은 백로그 BL-03 후속 작업이다.
4. 서명본은 제작기의 `releases/<id>` 폴더를 덮어쓰지 말고 별도 출력 폴더에 두고, 서명 로그와 `signtool verify` 결과를 함께 보관한다.
## 자주 막히는 곳
| 증상 | 확인할 것 |
| --- | --- |
| **새 ID** 버튼이 흐림 | 3단계 ID 검증 도구 역할이 없거나 구독 읽기 권한자가 없다 |
| 신원 확인 메일이 안 옴 | 기본 이메일이 외부 발신 링크를 받는지, 7일이 지나지 않았는지. 지났으면 새 요청 |
| 403 Forbidden / `SignerSign()` 실패 | metadata.json의 Endpoint 지역이 계정 지역과 다르거나 서명자 역할이 없다 |
| `0x8007000B` | Publisher와 인증서 주체 불일치 |
| dlib 로드 실패 | SDK 기본 SignTool을 쓴 경우. 클라이언트 도구 폴더의 SignTool 사용 |
| 서명 3일 뒤 검증 실패 | 타임스탬프 없이 서명함. 다시 서명 |
| 설치 PC에서 "신뢰할 수 없는 게시자" | Windows 10 1809 미만, 또는 루트 업데이트가 없는 격리 PC |
| 요금이 나가는데 아직 못 씀 | 정상. 계정 생성일부터 과금되므로 신원 확인 서류를 먼저 모은 뒤 계정을 만든다 |
## Key Vault + 공인 CA 방식 (대안)
아티팩트 서명을 쓸 수 없거나 특정 CA 인증서가 필요할 때의 방법이다. 비용은 CA 인증서 가격이 더해지고, 설정도 더 많다.
1. **키 자격 증명 모음(Key Vault)**을 **프리미엄(Premium)** 가격 책정 계층으로 만들고 **개체 → 인증서(Certificates) → 생성/가져오기(Generate/Import)**에서 **인증서 발급 방법(Method of Certificate Creation)**을 "생성", **인증 기관 유형(Type of Certificate Authority)**을 "통합되지 않은 CA에서 발급한 인증서(Certificate issued by a non-integrated CA)"로 고른다. 고급 정책에서 키 종류 RSA-HSM, 키 크기 3072 이상, **내보내기 가능한 프라이빗 키**는 아니오, 확장 키 사용(EKU)에 코드 서명 `1.3.6.1.5.5.7.3.3`을 넣는다.
2. 생성된 인증서 항목에서 **인증서 작업(Certificate Operation) → CSR 다운로드(Download CSR)**로 요청 파일을 받아 CA(DigiCert, Sectigo, GlobalSign 등)에 OV 코드 서명 인증서로 신청한다. CA가 조직 확인을 한다. **CA가 Azure Key Vault 키 증명을 받아 주는지 먼저 문의**한다. 2023년 이후 규정으로 HSM 보관 증명이 필요하다.
3. 발급된 인증서를 **서명된 요청 병합(Merge Signed Request)**으로 합친다.
4. 서명 계정에 **액세스 제어(IAM)**에서 **Key Vault 암호화 사용자(Key Vault Crypto User)**와 **Key Vault 인증서 사용자(Key Vault Certificate User)** 역할을 준다.
5. 서명 PC에 `dotnet tool install --global AzureSignTool`을 설치하고 서명한다.
```powershell
azuresigntool sign --azure-key-vault-url https://<vault>.vault.azure.net `
--azure-key-vault-certificate <인증서 이름> --azure-key-vault-managed-identity `
--timestamp-rfc3161 http://timestamp.digicert.com --timestamp-digest sha256 --file-digest sha256 `
.\package.msix
```
사람이 서명할 때는 `--azure-key-vault-managed-identity` 대신 `az login` 뒤 `--azure-key-vault-tenant-id`, `--azure-key-vault-client-id`, `--azure-key-vault-client-secret`(앱 등록) 조합을 쓴다.
6. 인증서 주체를 Publisher에 복사하는 8단계는 동일하다. 갱신 때 CA·회사 정보 표기를 바꾸지 않아야 주체가 유지된다.
## 근거
- [빠른 시작: 아티팩트 서명 설정 (한국어)](https://learn.microsoft.com/ko-kr/azure/artifact-signing/quickstart) — 한국어 메뉴명, 지원 국가, 지역·엔드포인트, ID 유효성 검사 절차, 인증서 프로필
- [자습서: 아티팩트 서명에서 역할 할당 (한국어)](https://learn.microsoft.com/ko-kr/azure/artifact-signing/tutorial-assign-roles) — 역할 이름과 권한 범위
- [Set up signing integrations to use Artifact Signing](https://learn.microsoft.com/en-us/azure/artifact-signing/how-to-signing-integrations) — 클라이언트 도구 설치, metadata.json, SignTool 명령, 타임스탬프
- [Sign your MSIX package - end-to-end guide](https://learn.microsoft.com/windows/msix/package/sign-msix-package-guide) — MSIX 운영 서명 절차, 지원 Windows 버전, SmartScreen 안내
- [Artifact Signing pricing](https://azure.microsoft.com/en-us/pricing/details/artifact-signing/) — Basic/Premium 포함 서명 횟수, 과금 시작 시점