Skip to content

Commit 5706ce7

Browse files
committed
check duplicated certificates
1 parent 74d9ecb commit 5706ce7

3 files changed

Lines changed: 403 additions & 1 deletion

File tree

cert-manager.js

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
const { getOctokit } = require('@actions/github')
2+
3+
/**
4+
* 使用 GraphQL 查询用户的证书和吊销记录
5+
* @param {string} token - GitHub token
6+
* @param {string} owner - 仓库所有者
7+
* @param {string} repo - 仓库名
8+
* @param {string} username - 用户名
9+
* @returns {Promise<{keyringIssues: Array, revokeIssues: Array}>}
10+
*/
11+
async function fetchUserCertificateIssues (token, owner, repo, username) {
12+
const octokit = getOctokit(token)
13+
14+
const query = `
15+
query($owner: String!, $repo: String!, $username: String!) {
16+
repository(owner: $owner, name: $repo) {
17+
keyringIssues: issues(
18+
first: 100
19+
filterBy: { createdBy: $username, states: CLOSED, labels: ["approved"] }
20+
orderBy: { field: CREATED_AT, direction: DESC }
21+
) {
22+
nodes {
23+
number
24+
title
25+
createdAt
26+
closedAt
27+
body
28+
labels(first: 10) {
29+
nodes {
30+
name
31+
}
32+
}
33+
comments(first: 100) {
34+
nodes {
35+
id
36+
body
37+
createdAt
38+
author {
39+
login
40+
}
41+
}
42+
}
43+
}
44+
}
45+
revokeIssues: issues(
46+
first: 100
47+
filterBy: { createdBy: $username, states: CLOSED, labels: ["revoked"] }
48+
orderBy: { field: CREATED_AT, direction: DESC }
49+
) {
50+
nodes {
51+
number
52+
title
53+
body
54+
createdAt
55+
closedAt
56+
}
57+
}
58+
}
59+
}
60+
`
61+
62+
try {
63+
const result = await octokit.graphql(query, {
64+
owner,
65+
repo,
66+
username
67+
})
68+
69+
return {
70+
keyringIssues: result.repository.keyringIssues.nodes || [],
71+
revokeIssues: result.repository.revokeIssues.nodes || []
72+
}
73+
} catch (error) {
74+
console.error('GraphQL query failed:', error)
75+
throw new Error(`Failed to fetch certificate issues: ${error.message}`)
76+
}
77+
}
78+
79+
/**
80+
* 从 issue 评论中提取证书信息
81+
* @param {Array} comments - Issue 评论列表
82+
* @returns {{serialNumber: string, fingerprint: string, issuedAt: Date, expiresAt: Date} | null}
83+
*/
84+
function extractCertificateInfo (comments) {
85+
// 查找证书签发评论
86+
const certComment = comments.find(c =>
87+
c.body &&
88+
c.body.includes('✅ Certificate successfully issued') &&
89+
c.body.includes('Serial Number')
90+
)
91+
92+
if (!certComment) {
93+
return null
94+
}
95+
96+
// 提取序列号
97+
const serialMatch = certComment.body.match(/Serial Number.*?`([^`]+)`/i)
98+
const serialNumber = serialMatch ? serialMatch[1] : null
99+
100+
// 提取指纹
101+
const fingerprintMatch = certComment.body.match(/Fingerprint \(SHA-256\).*?`([^`]+)`/i)
102+
const fingerprint = fingerprintMatch ? fingerprintMatch[1] : null
103+
104+
if (!serialNumber) {
105+
return null
106+
}
107+
108+
// 计算有效期(1年)
109+
const issuedAt = new Date(certComment.createdAt)
110+
const expiresAt = new Date(issuedAt.getTime() + 365 * 24 * 60 * 60 * 1000)
111+
112+
return {
113+
serialNumber,
114+
fingerprint,
115+
issuedAt,
116+
expiresAt
117+
}
118+
}
119+
120+
/**
121+
* 检查证书是否在吊销列表中
122+
* @param {string} serialNumber - 证书序列号
123+
* @param {Array} revokeIssues - 吊销 issue 列表
124+
* @returns {boolean}
125+
*/
126+
function isCertificateRevoked (serialNumber, revokeIssues) {
127+
return revokeIssues.some(issue =>
128+
issue.title.toLowerCase().includes('[revoke]') &&
129+
issue.body &&
130+
issue.body.includes(serialNumber)
131+
)
132+
}
133+
134+
/**
135+
* 检查用户是否已有有效证书
136+
* @param {string} username - GitHub 用户名
137+
* @param {string} token - GitHub token
138+
* @param {string} owner - 仓库所有者
139+
* @param {string} repo - 仓库名
140+
* @returns {Promise<{hasActiveCert: boolean, certificate?: object, issueNumber?: number}>}
141+
*/
142+
async function checkExistingCertificate (username, token, owner, repo) {
143+
try {
144+
console.log(`Checking existing certificates for user: ${username}`)
145+
146+
// 使用 GraphQL 获取用户的 issue
147+
const { keyringIssues, revokeIssues } = await fetchUserCertificateIssues(
148+
token,
149+
owner,
150+
repo,
151+
username
152+
)
153+
154+
console.log(`Found ${keyringIssues.length} keyring issues and ${revokeIssues.length} revoke issues`)
155+
156+
// 过滤出真正的 keyring issue(标题包含 [keyring])
157+
const validKeyringIssues = keyringIssues.filter(issue =>
158+
issue.title.toLowerCase().includes('[keyring]')
159+
)
160+
161+
if (validKeyringIssues.length === 0) {
162+
console.log('No keyring issues found for user')
163+
return { hasActiveCert: false }
164+
}
165+
166+
const now = new Date()
167+
168+
// 检查每个 keyring issue,从最新到最旧
169+
for (const issue of validKeyringIssues) {
170+
const certInfo = extractCertificateInfo(issue.comments.nodes)
171+
172+
if (!certInfo) {
173+
console.log(`Issue #${issue.number}: No certificate info found`)
174+
continue
175+
}
176+
177+
console.log(`Issue #${issue.number}: Found certificate ${certInfo.serialNumber}`)
178+
console.log(` Issued: ${certInfo.issuedAt.toISOString()}`)
179+
console.log(` Expires: ${certInfo.expiresAt.toISOString()}`)
180+
181+
// 检查是否过期
182+
if (now > certInfo.expiresAt) {
183+
console.log(` Status: EXPIRED`)
184+
continue
185+
}
186+
187+
// 检查是否被吊销
188+
const isRevoked = isCertificateRevoked(certInfo.serialNumber, revokeIssues)
189+
if (isRevoked) {
190+
console.log(` Status: REVOKED`)
191+
continue
192+
}
193+
194+
// 找到有效证书
195+
console.log(` Status: ACTIVE`)
196+
return {
197+
hasActiveCert: true,
198+
certificate: {
199+
serialNumber: certInfo.serialNumber,
200+
fingerprint: certInfo.fingerprint,
201+
issuedAt: certInfo.issuedAt.toISOString(),
202+
expiresAt: certInfo.expiresAt.toISOString()
203+
},
204+
issueNumber: issue.number
205+
}
206+
}
207+
208+
console.log('No active certificate found')
209+
return { hasActiveCert: false }
210+
} catch (error) {
211+
console.error('Error checking existing certificate:', error)
212+
// 如果检查失败,为了安全起见,假设没有证书(允许签发)
213+
// 但记录错误以便调试
214+
console.error('Certificate check failed, allowing issuance by default')
215+
return { hasActiveCert: false, error: error.message }
216+
}
217+
}
218+
219+
/**
220+
* 统计用户的证书情况
221+
* @param {string} username - GitHub 用户名
222+
* @param {string} token - GitHub token
223+
* @param {string} owner - 仓库所有者
224+
* @param {string} repo - 仓库名
225+
* @returns {Promise<{total: number, active: number, expired: number, revoked: number}>}
226+
*/
227+
async function getCertificateStatistics (username, token, owner, repo) {
228+
const { keyringIssues, revokeIssues } = await fetchUserCertificateIssues(
229+
token,
230+
owner,
231+
repo,
232+
username
233+
)
234+
235+
const validKeyringIssues = keyringIssues.filter(issue =>
236+
issue.title.toLowerCase().includes('[keyring]')
237+
)
238+
239+
let total = 0
240+
let active = 0
241+
let expired = 0
242+
let revoked = 0
243+
244+
const now = new Date()
245+
246+
for (const issue of validKeyringIssues) {
247+
const certInfo = extractCertificateInfo(issue.comments.nodes)
248+
if (!certInfo) continue
249+
250+
total++
251+
252+
const isRevoked = isCertificateRevoked(certInfo.serialNumber, revokeIssues)
253+
if (isRevoked) {
254+
revoked++
255+
} else if (now > certInfo.expiresAt) {
256+
expired++
257+
} else {
258+
active++
259+
}
260+
}
261+
262+
return { total, active, expired, revoked }
263+
}
264+
265+
module.exports = {
266+
checkExistingCertificate,
267+
fetchUserCertificateIssues,
268+
getCertificateStatistics,
269+
extractCertificateInfo,
270+
isCertificateRevoked
271+
}

keyring.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ const core = require('@actions/core')
22
const { context } = require('@actions/github')
33
const crypto = require('crypto')
44
const x509 = require('@peculiar/x509')
5-
const { getRepo, createComment, removeLabel, closeIssue, addLabel } = require('./github-utils')
5+
const { getRepo, createComment, removeLabel, closeIssue, addLabel, setLabel } = require('./github-utils')
66
const { fetchUserStats, evaluateUser, generateReport } = require('./rank')
7+
const { checkExistingCertificate } = require('./cert-manager')
78

89
// Set crypto provider for @peculiar/x509
910
x509.cryptoProvider.set(crypto.webcrypto)
@@ -299,6 +300,54 @@ async function handleKeyringIssue () {
299300
// Handle newly opened keyring issues
300301
if (action === 'opened') {
301302
console.log('Handling opened keyring issue')
303+
304+
// 检查用户是否已有有效证书
305+
console.log('Checking for existing certificates...')
306+
const existingCert = await checkExistingCertificate(username, token, owner, repo)
307+
308+
if (existingCert.hasActiveCert) {
309+
console.log('User already has an active certificate, rejecting request')
310+
const cert = existingCert.certificate
311+
await createComment(
312+
token,
313+
owner,
314+
repo,
315+
issueNumber,
316+
`⚠️ **证书申请被拒绝 / Certificate Request Rejected**\n\n` +
317+
`@${username},您已经拥有一个有效的开发者证书 / You already have an active developer certificate:\n\n` +
318+
`- **序列号 / Serial Number**: \`${cert.serialNumber}\`\n` +
319+
`- **指纹 / Fingerprint**: \`${cert.fingerprint || 'N/A'}\`\n` +
320+
`- **签发时间 / Issued**: ${new Date(cert.issuedAt).toLocaleDateString('zh-CN')}\n` +
321+
`- **过期时间 / Expires**: ${new Date(cert.expiresAt).toLocaleDateString('zh-CN')}\n` +
322+
`- **原始 Issue**: #${existingCert.issueNumber}\n\n` +
323+
`---\n\n` +
324+
`**策略 / Policy**: 每个开发者同一时间只能持有**一个**有效证书 / Each developer can only hold **ONE** active certificate at a time.\n\n` +
325+
`**选项 / Options**:\n` +
326+
`1. 等待当前证书过期后重新申请 / Wait for your current certificate to expire before reapplying\n` +
327+
`2. 如果需要更换证书,请先创建 \`[revoke]\` issue 吊销当前证书 / To replace your certificate, create a \`[revoke]\` issue first\n` +
328+
`3. 如果您丢失了私钥,请先创建 \`[revoke]\` issue,然后重新申请 / If you lost your private key, create a \`[revoke]\` issue first, then reapply\n\n` +
329+
`**安全提示 / Security Note**: 此策略可防止证书滥用和吊销绕过 / This policy prevents certificate abuse and revocation bypass.`
330+
)
331+
await setLabel(token, owner, repo, issueNumber, 'duplicate')
332+
await closeIssue(token, owner, repo, issueNumber, false)
333+
return
334+
}
335+
336+
if (existingCert.error) {
337+
console.log('Certificate check had errors, but proceeding with issuance')
338+
await createComment(
339+
token,
340+
owner,
341+
repo,
342+
issueNumber,
343+
`⚠️ **注意 / Note**: 无法完全验证您的证书历史记录,但申请将继续处理。\n\n` +
344+
`Unable to fully verify your certificate history, but your application will proceed.\n\n` +
345+
`错误信息 / Error: ${existingCert.error}`
346+
)
347+
} else {
348+
console.log('No active certificate found, proceeding with evaluation')
349+
}
350+
302351
await autoEvaluateDeveloper(token, owner, repo, issueNumber, username)
303352
return
304353
}

0 commit comments

Comments
 (0)