feat: add development certificate signing

Closes #4
This commit is contained in:
jychoi4 committed 2026-11-04 16:14:00 +09:00
1 parent fdbc9a9d1b
commit 024f244f52
10 files changed
+910 -3

No files matched your search

+24
View File
@@ -51,6 +51,14 @@ enum Action {
output: PathBuf,
},
Schema,
Certificate {
#[arg(long)]
project: PathBuf,
#[arg(long, conflicts_with = "thumbprint")]
create: bool,
#[arg(long)]
thumbprint: Option<String>,
},
}
#[derive(Subcommand)]
enum PlanAction {
@@ -105,6 +113,22 @@ fn run(action: Action, cancel: &AtomicBool) -> Result<i32> {
&std::path::absolute(output)?,
)?)?,
Action::Schema => print(&schemars::schema_for!(package_domain::Project))?,
Action::Certificate {
project,
create,
thumbprint,
} => print(&infra::configure_developer_certificate(
&project,
if create {
"create"
} else if thumbprint.is_some() {
"select"
} else {
"list"
},
thumbprint.as_deref(),
cancel,
)?)?,
}
Ok(0)
}
+16
View File
@@ -0,0 +1,16 @@
export type DeveloperCertificate = { thumbprint: string; subject: string; notAfter: string; sha256: string; trustedPeople: boolean };
export type SigningReport = { certificate: DeveloperCertificate; trustVerification: string; cmsSignatureValid: boolean; winVerifyTrust: string };
const esc = (value: unknown) => String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]!);
export function signingStatus(signing: string, report?: SigningReport | null): string {
if (signing !== 'selfSignedDevelopment') return '미서명';
return report?.trustVerification === 'passed' ? '개발자 서명 완료 · 현재 PC 신뢰 확인' : '개발자 서명 완료 · 테스트 PC 신뢰 등록 필요';
}
export function renderSigningEditor(mode: string, thumbprint: string | null | undefined, certificates: DeveloperCertificate[], locked: boolean): string {
const disabled = locked ? 'disabled' : '';
return `<label class="field">서명 모드<select data-field="signing.mode" ${disabled}><option value="unsigned" ${mode === 'unsigned' ? 'selected' : ''}>미서명</option><option value="development" ${mode === 'development' ? 'selected' : ''}>개발자 서명 · 테스트용 자체 서명</option></select></label>
${mode === 'development' ? `<div class="state-card"><span class="badge amber">테스트 전용</span><h2>개발자 서명</h2><p>Publisher와 일치하는 자체 서명 인증서로 MSIX를 서명합니다. 인증서는 현재 Windows 사용자 저장소에 보관하며 같은 인증서를 다음 빌드에도 재사용합니다.</p><p>최초 사용 시 인증서를 생성하거나 기존 인증서를 선택하세요. 생성한 인증서는 3개월간 유효하며 개인 키는 내보내지 않습니다.</p></div>
<div class="actions"><button data-action="certificate-list" ${disabled}>인증서 목록 새로고침</button><button data-action="certificate-create" ${disabled}>테스트 인증서 생성</button></div>
<label class="field">사용 가능한 인증서<select id="developer-certificate" ${disabled}><option value="">인증서를 선택하세요</option>${certificates.map(c => `<option value="${esc(c.thumbprint)}" ${c.thumbprint === thumbprint ? 'selected' : ''}>${esc(c.subject)} · ${esc(c.notAfter)} 만료 · ${esc(c.thumbprint)}</option>`).join('')}</select></label><button data-action="certificate-select" ${disabled}>선택한 인증서 사용</button>
<div class="check-row"><span>저장된 인증서 지문</span><code>${esc(thumbprint || '선택되지 않음')}</code></div>
<div class="callout">테스트 PC에서 설치하려면 공개 인증서를 신뢰하도록 등록해야 합니다. 빌드 결과의 development-signing.cer를 확인한 후 Install-TestCertificate.ps1을 관리자 권한으로 실행하세요. 인증서 신뢰 등록은 자동으로 수행하지 않습니다.</div><p class="muted">현재 기능은 MSIX 패키지를 서명합니다. 내부 EXE/DLL 개별 서명, 공인 서명 및 타임스탬프는 포함하지 않습니다. Windows 개발자 모드와 별개이며 일반 사용자 배포용이 아닙니다.</p>` : '<div class="state-card"><h2>미서명 패키지</h2><p>패키지 생성과 내용 검증만 수행합니다. 설치 테스트에는 서명이 필요합니다.</p></div>'}`;
}
@@ -0,0 +1,20 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { renderSigningEditor, signingStatus } from '../src/signing-editor.ts';
test('signed packages keep trust status separate from signing', () => {
assert.equal(signingStatus('unsignedDevelopment'), '미서명');
assert.match(signingStatus('selfSignedDevelopment', { trustVerification: 'notTrusted' }), /신뢰 등록 필요/);
assert.match(signingStatus('selfSignedDevelopment', { trustVerification: 'passed' }), /현재 PC 신뢰 확인/);
assert.match(signingStatus('selfSignedDevelopment'), /신뢰 등록 필요/);
});
test('certificate selection escapes certificate data and locks actions during JSON editing', () => {
const certificate = { thumbprint: 'ABC', subject: '<img src=x>', notAfter: '2027-01-01', trustedPeople: false };
const html = renderSigningEditor('development', 'ABC', [certificate], true);
assert.ok(html.includes('value="ABC" selected'));
assert.ok(html.includes('&lt;img src=x&gt;'));
assert.ok(!html.includes('<img src=x>'));
assert.match(html, /data-action="certificate-create" disabled/);
assert.ok(!renderSigningEditor('unsigned', null, [], false).includes('data-action="certificate-create"'));
});