-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathkeyring.js
More file actions
459 lines (406 loc) · 16.4 KB
/
keyring.js
File metadata and controls
459 lines (406 loc) · 16.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
const core = require('@actions/core')
const { context } = require('@actions/github')
const crypto = require('crypto')
const x509 = require('@peculiar/x509')
const { getRepo, createComment, removeLabel, closeIssue, addLabel, setLabel } = require('./github-utils')
const { fetchUserStats, evaluateUser, generateReport } = require('./rank')
const { checkExistingCertificate } = require('./cert-manager')
// Set crypto provider for @peculiar/x509
x509.cryptoProvider.set(crypto.webcrypto)
/**
* Load Middle CA certificate and private key from environment variables
* Note: MIDDLE_CA_CERT should contain the full certificate chain (Middle CA + Root CA)
* @returns {{cert: x509.X509Certificate, privateKey: CryptoKey}}
*/
async function loadMiddleCA () {
const certPem = process.env.MIDDLE_CA_CERT
const keyPem = process.env.MIDDLE_CA_KEY
if (!certPem) {
throw new Error('Middle CA certificate not found: MIDDLE_CA_CERT environment variable not set')
}
if (!keyPem) {
throw new Error('Middle CA private key not found: MIDDLE_CA_KEY environment variable not set')
}
// Parse certificate
const cert = new x509.X509Certificate(certPem)
// Parse private key - convert Node.js KeyObject to CryptoKey
const nodeKey = crypto.createPrivateKey(keyPem)
const keyDer = nodeKey.export({ type: 'pkcs8', format: 'der' })
// Determine algorithm based on key type
const keyDetails = nodeKey.asymmetricKeyDetails
const namedCurve = keyDetails.namedCurve
const algorithm = {
name: 'ECDSA',
namedCurve: namedCurve === 'prime256v1' || namedCurve === 'secp256r1' ? 'P-256' : 'P-384'
}
const privateKey = await crypto.webcrypto.subtle.importKey(
'pkcs8',
keyDer,
algorithm,
true,
['sign']
)
return { cert, privateKey }
}
/**
* Extract Public Key from issue body
* @param {string} issueBody - Issue body content
* @returns {string|null} Extracted Public Key PEM or null
*/
function extractPublicKeyFromIssue (issueBody) {
const publicKeyBlockRegex = /-----BEGIN PUBLIC KEY-----([\s\S]*?)-----END PUBLIC KEY-----/
const match = issueBody.match(publicKeyBlockRegex)
if (match) {
return match[0]
}
return null
}
/**
* Generate a random serial number for certificate
* @returns {string} Serial number as hex string
*/
function generateSerialNumber () {
return crypto.randomBytes(16).toString('hex')
}
/**
* Calculate certificate fingerprint (SHA-256)
* @param {x509.X509Certificate} cert - Certificate
* @returns {string} Fingerprint in colon-separated hex format
*/
async function getCertificateFingerprint (cert) {
const thumbprint = await cert.getThumbprint(crypto.webcrypto)
const hex = Buffer.from(thumbprint).toString('hex')
// Format as XX:XX:XX:XX...
return hex.toUpperCase().match(/.{2}/g).join(':')
}
/**
* Issue developer certificate from public key
* @param {string} publicKeyPem - Public Key in PEM format
* @param {string} username - Developer's GitHub username
* @returns {Promise<{certPem: string, certChainPem: string, fingerprint: string, serialNumber: string}>}
*/
async function issueDeveloperCertificate (publicKeyPem, username) {
const { cert: caCert, privateKey: caKey } = await loadMiddleCA()
// Parse ECC public key (P-256/P-384 only)
let publicKey
let curveInfo
try {
// Parse public key using Node.js crypto
publicKey = crypto.createPublicKey(publicKeyPem)
// Get key details
const keyDetails = publicKey.asymmetricKeyDetails
if (!keyDetails) {
throw new Error('Unable to extract key details')
}
// Verify it's an EC key
if (publicKey.asymmetricKeyType !== 'ec') {
throw new Error(
`Unsupported key type (${publicKey.asymmetricKeyType}). Only ECC keys (P-256/P-384) are supported.`
)
}
// Verify curve is P-256 or P-384
const supportedCurves = {
'prime256v1': 'P-256',
'secp256r1': 'P-256',
'secp384r1': 'P-384'
}
const namedCurve = keyDetails.namedCurve
if (!supportedCurves[namedCurve]) {
throw new Error(
`Unsupported ECC curve (${namedCurve}). Only P-256 and P-384 curves are supported. ` +
'Please generate a new key pair with P-256 or P-384 from the Developer Portal.'
)
}
curveInfo = supportedCurves[namedCurve]
console.log('ECC Curve:', curveInfo)
console.log('Public key parsed successfully')
} catch (error) {
console.error('Failed to parse public key:', error.message)
if (error.message.includes('Unsupported')) {
throw error // Re-throw our custom errors
}
throw new Error(
'Invalid public key format. Please ensure you are submitting a valid ECC public key (P-256 or P-384). ' +
'Generate a proper key pair from the Developer Portal.'
)
}
// Convert public key to CryptoKey
const publicKeyDer = publicKey.export({ type: 'spki', format: 'der' })
const algorithm = {
name: 'ECDSA',
namedCurve: curveInfo
}
const cryptoPublicKey = await crypto.webcrypto.subtle.importKey(
'spki',
publicKeyDer,
algorithm,
true,
['verify']
)
// Determine hash algorithm: P-256 uses SHA-256, P-384 uses SHA-512
const hashAlgorithm = curveInfo === 'P-256' ? 'SHA-256' : 'SHA-512'
// Create certificate using @peculiar/x509
const cert = await x509.X509CertificateGenerator.create({
serialNumber: generateSerialNumber(),
subject: `CN=${username}, O=KernelSU Module Developers`,
issuer: caCert.subject,
notBefore: new Date(),
notAfter: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year
publicKey: cryptoPublicKey,
signingKey: caKey,
signingAlgorithm: {
name: 'ECDSA',
hash: hashAlgorithm
},
extensions: [
new x509.BasicConstraintsExtension(false, undefined, true), // cA: false, critical: true
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature, true), // critical: true
new x509.ExtendedKeyUsageExtension(['1.3.6.1.5.5.7.3.3']), // codeSigning
await x509.SubjectKeyIdentifierExtension.create(cryptoPublicKey),
await x509.AuthorityKeyIdentifierExtension.create(caCert)
]
})
const certPem = cert.toString('pem')
const fingerprint = await getCertificateFingerprint(cert)
const serialNumber = cert.serialNumber
// Build certificate chain (ensure proper newline between certificates)
const middleCaCertPem = process.env.MIDDLE_CA_CERT
const certChainPem = certPem.trimEnd() + '\n' + middleCaCertPem + '\n'
return {
certPem,
certChainPem,
fingerprint,
serialNumber
}
}
/**
* Automatically evaluate developer and take action
*/
async function autoEvaluateDeveloper (token, owner, repo, issueNumber, username) {
try {
console.log(`Auto-evaluating developer: ${username}`)
// 获取用户统计数据
const stats = await fetchUserStats(username, token)
console.log('Stats fetched:', stats)
// 评估用户
const evaluation = evaluateUser(stats)
console.log('Evaluation result:', evaluation)
// 生成报告
const report = generateReport(stats, evaluation)
// 所有提交自动通过,只附加rank信息
console.log('Creating approval comment with rank info...')
await createComment(
token,
owner,
repo,
issueNumber,
report + '\n\n---\n\n' +
'✅ **Certificate Request Auto-Approved**\n\n' +
`Your developer certificate request has been automatically approved.\n\n` +
`**Your GitHub Profile Rank**: ${evaluation.rank.level} (Top ${evaluation.rank.percentile.toFixed(1)}%)\n\n` +
'**Important reminders:**\n' +
'- ⚠️ Never share your private key (`.key.pem` file) with anyone\n' +
'- ✅ Only the public key (`.pub.pem` file) should be submitted in this issue\n' +
'- 📝 Make sure your public key is properly formatted between `-----BEGIN PUBLIC KEY-----` and `-----END PUBLIC KEY-----` markers\n\n' +
'Your certificate will be issued automatically. Please wait a moment...'
)
console.log('Adding approved label...')
await addLabel(token, owner, repo, issueNumber, 'approved')
console.log(`Auto-approved: ${username} (Rank: ${evaluation.rank.level})`)
} catch (error) {
console.error('Error in auto-evaluation:', error)
console.error('Error stack:', error.stack)
// 如果评估失败,仍然自动通过但不显示rank信息
console.log('Creating fallback approval comment...')
await createComment(
token,
owner,
repo,
issueNumber,
`Dear @${username},\n\n` +
'Thank you for submitting your public key to the KernelSU Developer Keyring!\n\n' +
'⚠️ **Unable to fetch your GitHub profile statistics.** This may be due to:\n' +
'- Private profile settings\n' +
'- API rate limits\n' +
'- Network issues\n\n' +
'Your request has been automatically approved without rank evaluation.\n\n' +
'**Important reminders:**\n' +
'- ⚠️ Never share your private key (`.key.pem` file) with anyone\n' +
'- ✅ Only the public key (`.pub.pem` file) should be submitted in this issue\n' +
'- 📝 Make sure your public key is properly formatted\n\n' +
'Your certificate will be issued automatically. Please wait a moment...'
)
console.log('Adding approved label (fallback)...')
await addLabel(token, owner, repo, issueNumber, 'approved')
console.log(`Auto-approved (no rank): ${username}`)
}
}
/**
* Handle developer public key submission in Issue
* Automatically issue certificate when core developer adds 'approved' label
*/
async function handleKeyringIssue () {
try {
const token = process.env.REPO_TOKEN
const { owner, repo } = getRepo()
const action = context.payload.action
const issue = context.payload.issue
if (!issue) {
console.log('Not an issue event')
return
}
const issueNumber = issue.number
const issueTitle = issue.title
const issueBody = issue.body || ''
const username = issue.user.login
// Check if this is a keyring issue
if (!issueTitle.toLowerCase().includes('[keyring]')) {
console.log('Not a keyring issue')
return
}
// Handle newly opened keyring issues
if (action === 'opened') {
console.log('Handling opened keyring issue')
// 检查用户是否已有有效证书
console.log('Checking for existing certificates...')
const existingCert = await checkExistingCertificate(username, token, owner, repo)
if (existingCert.hasActiveCert) {
console.log('User already has an active certificate, rejecting request')
const cert = existingCert.certificate
await createComment(
token,
owner,
repo,
issueNumber,
`⚠️ **证书申请被拒绝 / Certificate Request Rejected**\n\n` +
`@${username},您已经拥有一个有效的开发者证书 / You already have an active developer certificate:\n\n` +
`- **序列号 / Serial Number**: \`${cert.serialNumber}\`\n` +
`- **指纹 / Fingerprint**: \`${cert.fingerprint || 'N/A'}\`\n` +
`- **签发时间 / Issued**: ${new Date(cert.issuedAt).toLocaleDateString('zh-CN')}\n` +
`- **过期时间 / Expires**: ${new Date(cert.expiresAt).toLocaleDateString('zh-CN')}\n` +
`- **原始 Issue**: #${existingCert.issueNumber}\n\n` +
`---\n\n` +
`**策略 / Policy**: 每个开发者同一时间只能持有**一个**有效证书 / Each developer can only hold **ONE** active certificate at a time.\n\n` +
`**选项 / Options**:\n` +
`1. 等待当前证书过期后重新申请 / Wait for your current certificate to expire before reapplying\n` +
`2. 如果需要更换证书,请先创建 \`[revoke]\` issue 吊销当前证书 / To replace your certificate, create a \`[revoke]\` issue first\n` +
`3. 如果您丢失了私钥,请先创建 \`[revoke]\` issue,然后重新申请 / If you lost your private key, create a \`[revoke]\` issue first, then reapply\n\n` +
`**安全提示 / Security Note**: 此策略可防止证书滥用和吊销绕过 / This policy prevents certificate abuse and revocation bypass.`
)
await setLabel(token, owner, repo, issueNumber, 'duplicate')
await closeIssue(token, owner, repo, issueNumber, false)
return
}
if (existingCert.error) {
console.log('Certificate check had errors, but proceeding with issuance')
await createComment(
token,
owner,
repo,
issueNumber,
`⚠️ **注意 / Note**: 无法完全验证您的证书历史记录,但申请将继续处理。\n\n` +
`Unable to fully verify your certificate history, but your application will proceed.\n\n` +
`错误信息 / Error: ${existingCert.error}`
)
} else {
console.log('No active certificate found, proceeding with evaluation')
}
await autoEvaluateDeveloper(token, owner, repo, issueNumber, username)
return
}
// Handle labeled event (approval)
if (action !== 'labeled') {
console.log('Not a labeled or opened event, action:', action)
return
}
console.log('Handling labeled event')
const label = context.payload.label
console.log('Label:', label ? label.name : 'null')
if (!label || label.name !== 'approved') {
console.log('Not an approved label, skipping certificate issuance')
return
}
console.log('Approved label detected, proceeding with certificate issuance')
const approver = context.payload.sender
console.log('Approver:', approver.login)
const publicKeyPem = extractPublicKeyFromIssue(issueBody)
console.log('Public key extracted:', publicKeyPem ? 'Yes' : 'No')
if (!publicKeyPem) {
await createComment(
token,
owner,
repo,
issueNumber,
'⚠️ Unable to extract valid public key from Issue.\n\n' +
'Please ensure your Issue description contains a complete public key:\n' +
'```\n' +
'-----BEGIN PUBLIC KEY-----\n' +
'...\n' +
'-----END PUBLIC KEY-----\n' +
'```\n\n' +
'**How to generate a key pair:**\n' +
'1. Visit our [Developer Portal](https://developers.kernelsu.org)\n' +
'2. Use the "Generate Key" tab to create your private key and public key\n' +
'3. Submit the public key (NOT the private key) in this issue'
)
await removeLabel(token, owner, repo, issueNumber, 'approved')
return
}
let result
try {
result = await issueDeveloperCertificate(publicKeyPem, username)
} catch (err) {
await createComment(
token,
owner,
repo,
issueNumber,
`❌ Failed to issue certificate: ${err.message}\n\n` +
'Please check:\n' +
'- Public key format is valid\n' +
'- Middle CA certificate and private key are correctly configured in GitHub Secrets\n' +
'- Required secrets: `MIDDLE_CA_CERT`, `MIDDLE_CA_KEY`'
)
await removeLabel(token, owner, repo, issueNumber, 'approved')
return
}
await createComment(
token,
owner,
repo,
issueNumber,
`✅ Certificate successfully issued!\n\n` +
`- **User**: @${username}\n` +
`- **Serial Number**: \`${result.serialNumber}\`\n` +
`- **Fingerprint (SHA-256)**: \`${result.fingerprint}\`\n` +
`- **Issued by**: @${approver.login} (Core Developer)\n` +
`- **Valid for**: 1 year\n\n` +
`## Developer Certificate (with full certificate chain)\n\n` +
`Please save this certificate chain:\n\n` +
`\`\`\`\n${result.certChainPem}\n\`\`\`\n\n` +
`---\n\n` +
`**What's included:**\n` +
`- Your developer certificate\n` +
`- Middle CA certificate\n` +
`- Root CA certificate\n\n` +
`**Next Steps**:\n` +
`1. Download and save this certificate chain as \`${username}.cert.pem\`\n` +
`2. Keep your private key (\`${username}.key.pem\`) secure - never share it!\n` +
`3. Use your private key and certificate chain to sign KernelSU modules\n\n` +
`**For Module Users**:\n` +
`This certificate can be used to verify module signatures from @${username}.`
)
await closeIssue(token, owner, repo, issueNumber, true)
} catch (error) {
core.setFailed(error.message)
console.error('Error handling keyring issue:', error)
}
}
module.exports = {
handleKeyringIssue,
issueDeveloperCertificate,
extractPublicKeyFromIssue,
loadMiddleCA,
getCertificateFingerprint,
autoEvaluateDeveloper
}