Skip to content

Commit d554f02

Browse files
committed
update revoke
1 parent 5706ce7 commit d554f02

6 files changed

Lines changed: 347 additions & 15 deletions

File tree

.github/ISSUE_TEMPLATE/revoke.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ body:
2626
required: true
2727

2828
- type: input
29-
id: fingerprint
29+
id: serial_number
3030
attributes:
31-
label: Certificate Fingerprint
32-
description: The SHA-256 fingerprint of the certificate to revoke
33-
placeholder: "AA:BB:CC:DD:EE:FF:..."
31+
label: Certificate Serial Number
32+
description: The serial number of the certificate to revoke (found in your certificate issuance comment)
33+
placeholder: "1a2b3c4d5e6f7890abcdef1234567890"
3434
validations:
3535
required: true
3636

cert-manager.js

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,10 +262,111 @@ async function getCertificateStatistics (username, token, owner, repo) {
262262
return { total, active, expired, revoked }
263263
}
264264

265+
/**
266+
* 验证证书是否属于指定用户
267+
* @param {string} serialNumber - 证书序列号
268+
* @param {string} username - 声称拥有证书的用户名
269+
* @param {string} token - GitHub token
270+
* @param {string} owner - 仓库所有者
271+
* @param {string} repo - 仓库名
272+
* @returns {Promise<{isOwner: boolean, actualOwner?: string, issueNumber?: number}>}
273+
*/
274+
async function verifyCertificateOwnership (serialNumber, username, token, owner, repo) {
275+
try {
276+
console.log(`Verifying certificate ownership: serial=${serialNumber}, claimed user=${username}`)
277+
278+
const octokit = require('@actions/github').getOctokit(token)
279+
280+
// GraphQL 查询所有带有 approved 标签的已关闭 keyring issues
281+
const query = `
282+
query($owner: String!, $repo: String!) {
283+
repository(owner: $owner, name: $repo) {
284+
issues(
285+
first: 100
286+
filterBy: { states: CLOSED, labels: ["approved"] }
287+
orderBy: { field: CREATED_AT, direction: DESC }
288+
) {
289+
nodes {
290+
number
291+
title
292+
author {
293+
login
294+
}
295+
comments(first: 100) {
296+
nodes {
297+
body
298+
createdAt
299+
}
300+
}
301+
}
302+
}
303+
}
304+
}
305+
`
306+
307+
const result = await octokit.graphql(query, { owner, repo })
308+
const issues = result.repository.issues.nodes
309+
310+
// 搜索包含该序列号的 issue
311+
for (const issue of issues) {
312+
if (!issue.title.toLowerCase().includes('[keyring]')) continue
313+
314+
const certComment = issue.comments.nodes.find(c =>
315+
c.body &&
316+
c.body.includes('✅ Certificate successfully issued') &&
317+
c.body.includes(serialNumber)
318+
)
319+
320+
if (certComment) {
321+
const actualOwner = issue.author.login
322+
console.log(`Found certificate: serial=${serialNumber}, owner=${actualOwner}, issue=#${issue.number}`)
323+
324+
return {
325+
isOwner: actualOwner.toLowerCase() === username.toLowerCase(),
326+
actualOwner,
327+
issueNumber: issue.number
328+
}
329+
}
330+
}
331+
332+
console.log(`Certificate not found: serial=${serialNumber}`)
333+
return { isOwner: false }
334+
} catch (error) {
335+
console.error('Error verifying certificate ownership:', error)
336+
throw new Error(`Failed to verify certificate ownership: ${error.message}`)
337+
}
338+
}
339+
340+
/**
341+
* 检查用户是否是组织管理员
342+
* @param {string} username - 用户名
343+
* @param {string} token - GitHub token
344+
* @param {string} owner - 组织名称
345+
* @returns {Promise<boolean>}
346+
*/
347+
async function isOrgAdmin (username, token, owner) {
348+
try {
349+
const octokit = require('@actions/github').getOctokit(token)
350+
351+
const { data } = await octokit.rest.orgs.getMembershipForUser({
352+
org: owner,
353+
username
354+
})
355+
356+
// role 可以是 'admin' 或 'member'
357+
return data.role === 'admin'
358+
} catch (error) {
359+
console.error(`Error checking org admin status for ${username}:`, error.message)
360+
return false
361+
}
362+
}
363+
265364
module.exports = {
266365
checkExistingCertificate,
267366
fetchUserCertificateIssues,
268367
getCertificateStatistics,
269368
extractCertificateInfo,
270-
isCertificateRevoked
369+
isCertificateRevoked,
370+
verifyCertificateOwnership,
371+
isOrgAdmin
271372
}

index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const { context } = require('@actions/github')
33
const { setLabel, closeIssue, getIssue, getRepo, lockSpamIssue, orgBlockUser } = require('./github-utils')
44
const { recognizeTitle } = require('./utils')
55
const { handleKeyringIssue } = require('./keyring')
6+
const { handleRevokeIssue } = require('./revoke')
67

78
async function closeSpam (token, owner, repo, issueNo, username = '') {
89
await setLabel(token, owner, repo, issueNo, 'spam')
@@ -44,6 +45,11 @@ async function run () {
4445
if (prefixTag === 'keyring') {
4546
await handleKeyringIssue()
4647
}
48+
49+
// handle revoke issue
50+
if (prefixTag === 'revoke') {
51+
await handleRevokeIssue()
52+
}
4753
}
4854
} catch (error) {
4955
core.setFailed(error.message)

revoke.js

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
const { context } = require('@actions/github')
2+
const { getRepo, createComment, closeIssue, addLabel } = require('./github-utils')
3+
const { verifyCertificateOwnership, isOrgAdmin } = require('./cert-manager')
4+
5+
/**
6+
* 从 issue body 中提取序列号
7+
* @param {string} issueBody - Issue body 内容
8+
* @returns {string|null}
9+
*/
10+
function extractSerialNumber (issueBody) {
11+
if (!issueBody) return null
12+
13+
// 尝试匹配不同格式的序列号
14+
// 格式1: Serial Number: `xxxxx`
15+
const match1 = issueBody.match(/Serial.*?Number.*?`([0-9a-fA-F]+)`/i)
16+
if (match1) return match1[1]
17+
18+
// 格式2: serial_number: xxxxx (来自 GitHub issue template)
19+
const match2 = issueBody.match(/serial[_\s]*number[:]\s*([0-9a-fA-F]+)/i)
20+
if (match2) return match2[1]
21+
22+
// 格式3: 直接的序列号(32位或更长的十六进制)
23+
const match3 = issueBody.match(/\b([0-9a-fA-F]{32,})\b/)
24+
if (match3) return match3[1]
25+
26+
return null
27+
}
28+
29+
/**
30+
* 处理证书吊销请求
31+
*/
32+
async function handleRevokeIssue () {
33+
try {
34+
const token = process.env.REPO_TOKEN
35+
const { owner, repo } = getRepo()
36+
const issue = context.payload.issue
37+
38+
if (!issue) {
39+
console.log('Not an issue event')
40+
return
41+
}
42+
43+
const issueNumber = issue.number
44+
const issueTitle = issue.title
45+
const issueBody = issue.body || ''
46+
const requester = issue.user.login
47+
48+
// 检查是否是 revoke issue
49+
if (!issueTitle.toLowerCase().includes('[revoke]')) {
50+
console.log('Not a revoke issue')
51+
return
52+
}
53+
54+
console.log(`Processing revoke request from @${requester}`)
55+
56+
// 提取序列号
57+
const serialNumber = extractSerialNumber(issueBody)
58+
59+
if (!serialNumber) {
60+
console.log('No serial number found in issue body')
61+
await createComment(
62+
token,
63+
owner,
64+
repo,
65+
issueNumber,
66+
`❌ **吊销请求失败 / Revocation Request Failed**\n\n` +
67+
`无法从 Issue 中提取证书序列号。\n` +
68+
`Unable to extract certificate serial number from issue.\n\n` +
69+
`**请确保 Issue 包含以下信息 / Please ensure the issue contains:**\n` +
70+
`- \`Serial Number\`: \`1a2b3c4d5e6f7890...\`\n` +
71+
`- 或使用开发者门户自动填充 / Or use the Developer Portal to auto-fill\n\n` +
72+
`**提示 / Tip**: 序列号可以在您的证书签发评论中找到。\n` +
73+
`The serial number can be found in your certificate issuance comment.`
74+
)
75+
await closeIssue(token, owner, repo, issueNumber, false)
76+
return
77+
}
78+
79+
console.log(`Serial number extracted: ${serialNumber}`)
80+
81+
// 验证证书所有权
82+
console.log('Verifying certificate ownership...')
83+
const ownership = await verifyCertificateOwnership(
84+
serialNumber,
85+
requester,
86+
token,
87+
owner,
88+
repo
89+
)
90+
91+
if (!ownership.isOwner) {
92+
// 检查是否是组织管理员
93+
console.log('User is not the owner, checking admin status...')
94+
const isAdmin = await isOrgAdmin(requester, token, owner)
95+
96+
if (!isAdmin) {
97+
console.log('Permission denied: not owner and not admin')
98+
await createComment(
99+
token,
100+
owner,
101+
repo,
102+
issueNumber,
103+
`❌ **吊销请求被拒绝 / Revocation Request Denied**\n\n` +
104+
`@${requester},您无权吊销此证书。\n` +
105+
`You do not have permission to revoke this certificate.\n\n` +
106+
`**原因 / Reason**: 此证书不属于您 / This certificate does not belong to you\n\n` +
107+
(ownership.actualOwner
108+
? `- **证书所有者 / Certificate Owner**: @${ownership.actualOwner}\n` +
109+
`- **原始 Issue / Original Issue**: #${ownership.issueNumber}\n\n`
110+
: `- **状态 / Status**: 证书未找到或已被吊销 / Certificate not found or already revoked\n\n`) +
111+
`**说明 / Note**:\n` +
112+
`- 只有证书所有者可以吊销自己的证书\n` +
113+
`- Only the certificate owner can revoke their own certificate\n` +
114+
`- 组织管理员拥有吊销任何证书的特殊权限\n` +
115+
`- Organization admins have special permission to revoke any certificate`
116+
)
117+
await closeIssue(token, owner, repo, issueNumber, false)
118+
return
119+
}
120+
121+
console.log(`Admin override: @${requester} is an organization admin`)
122+
}
123+
124+
// 权限验证通过,执行吊销
125+
console.log('Permission granted, proceeding with revocation...')
126+
127+
// TODO: 在这里添加实际的 CRL 更新逻辑
128+
// await addToCRL(serialNumber, 'keyCompromise')
129+
130+
await createComment(
131+
token,
132+
owner,
133+
repo,
134+
issueNumber,
135+
`✅ **证书吊销成功 / Certificate Revoked Successfully**\n\n` +
136+
`- **序列号 / Serial Number**: \`${serialNumber}\`\n` +
137+
`- **吊销时间 / Revoked At**: ${new Date().toISOString()}\n` +
138+
`- **请求者 / Requested By**: @${requester}${ownership.isOwner ? '' : ' (Organization Admin)'}\n` +
139+
(ownership.actualOwner && !ownership.isOwner
140+
? `- **证书所有者 / Certificate Owner**: @${ownership.actualOwner}\n`
141+
: '') +
142+
(ownership.issueNumber ? `- **原始 Issue / Original Issue**: #${ownership.issueNumber}\n` : '') +
143+
`\n---\n\n` +
144+
`**重要提示 / Important Notes**:\n\n` +
145+
`1. ✅ 该证书已被添加到吊销列表 / The certificate has been added to the revocation list\n` +
146+
`2. ⚠️ 使用此证书签名的模块将不再被信任 / Modules signed with this certificate will no longer be trusted\n` +
147+
`3. 🔄 如需新证书,请创建新的 \`[keyring]\` issue / To get a new certificate, create a new \`[keyring]\` issue\n` +
148+
`4. 📋 吊销信息将在下次 CRL 更新时生效 / Revocation will take effect on the next CRL update\n\n` +
149+
`**安全建议 / Security Recommendations**:\n` +
150+
`- 如果私钥泄露,请立即停止使用该证书签名 / If the private key was compromised, stop using it immediately\n` +
151+
`- 使用新证书重新签名所有模块 / Re-sign all modules with your new certificate\n` +
152+
`- 保护好新的私钥,不要与任何人分享 / Protect your new private key, never share it with anyone`
153+
)
154+
155+
await addLabel(token, owner, repo, issueNumber, 'revoked')
156+
await closeIssue(token, owner, repo, issueNumber, true)
157+
158+
console.log(`Certificate ${serialNumber} revoked successfully`)
159+
} catch (error) {
160+
console.error('Error handling revoke issue:', error)
161+
console.error('Error stack:', error.stack)
162+
163+
try {
164+
const token = process.env.REPO_TOKEN
165+
const { owner, repo } = getRepo()
166+
const issue = context.payload.issue
167+
168+
if (issue) {
169+
await createComment(
170+
token,
171+
owner,
172+
repo,
173+
issue.number,
174+
`❌ **系统错误 / System Error**\n\n` +
175+
`处理吊销请求时发生错误。\n` +
176+
`An error occurred while processing the revocation request.\n\n` +
177+
`**错误信息 / Error**: ${error.message}\n\n` +
178+
`请联系管理员或在仓库中报告此问题。\n` +
179+
`Please contact an administrator or report this issue in the repository.`
180+
)
181+
}
182+
} catch (commentError) {
183+
console.error('Failed to post error comment:', commentError)
184+
}
185+
186+
throw error
187+
}
188+
}
189+
190+
module.exports = {
191+
handleRevokeIssue,
192+
extractSerialNumber
193+
}

0 commit comments

Comments
 (0)