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:
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에 적용한 설계 |
|
||||
|
||||
@@ -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 포함 서명 횟수, 과금 시작 시점
|
||||
Reference in new issue
Block a user