From e3e3caed6aa77067d389635c9577e3a62a4315db Mon Sep 17 00:00:00 2001 From: imbytecat Date: Thu, 19 Mar 2026 16:16:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(crypto):=20=E6=96=B0=E5=A2=9E=20RSA=20?= =?UTF-8?q?=E9=AA=8C=E7=AD=BE=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/crypto/src/index.ts | 1 + packages/crypto/src/rsa-signature.test.ts | 24 +++++++++++++++++++++++ packages/crypto/src/rsa-signature.ts | 19 ++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 packages/crypto/src/rsa-signature.test.ts create mode 100644 packages/crypto/src/rsa-signature.ts diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts index b8bd01b..d9c9b6c 100644 --- a/packages/crypto/src/index.ts +++ b/packages/crypto/src/index.ts @@ -4,3 +4,4 @@ export { hkdfSha256 } from './hkdf' export { hmacSha256, hmacSha256Base64 } from './hmac' export { generatePgpKeyPair, pgpSignDetached, pgpVerifyDetached, validatePgpPrivateKey } from './pgp' export { rsaOaepEncrypt } from './rsa-oaep' +export { rsaVerifySignature, validateRsaPublicKey } from './rsa-signature' diff --git a/packages/crypto/src/rsa-signature.test.ts b/packages/crypto/src/rsa-signature.test.ts new file mode 100644 index 0000000..481e16b --- /dev/null +++ b/packages/crypto/src/rsa-signature.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'bun:test' +import { constants, createSign, generateKeyPairSync } from 'node:crypto' +import { rsaVerifySignature, validateRsaPublicKey } from './rsa-signature' + +describe('rsaVerifySignature', () => { + it('verifies SHA256withRSA signatures over raw payload bytes', () => { + const { privateKey, publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }) + const payload = Buffer.from('eyJsaWNlbmNlX2lkIjoiTElDLTAwMSIsImV4cGlyZV90aW1lIjoiMjAyNy0wMy0xOSJ9', 'utf-8') + + const signer = createSign('RSA-SHA256') + signer.update(payload) + signer.end() + + const signature = signer.sign({ key: privateKey, padding: constants.RSA_PKCS1_PADDING }) + const publicKeyBase64 = publicKey.export({ format: 'der', type: 'spki' }).toString('base64') + + expect(rsaVerifySignature(payload, signature, publicKeyBase64)).toBe(true) + expect(rsaVerifySignature(Buffer.from(`${payload}x`, 'utf-8'), signature, publicKeyBase64)).toBe(false) + }) + + it('rejects malformed SPKI public keys', () => { + expect(() => validateRsaPublicKey('not-a-public-key')).toThrow() + }) +}) diff --git a/packages/crypto/src/rsa-signature.ts b/packages/crypto/src/rsa-signature.ts new file mode 100644 index 0000000..4fc2701 --- /dev/null +++ b/packages/crypto/src/rsa-signature.ts @@ -0,0 +1,19 @@ +import { constants, createPublicKey, verify } from 'node:crypto' + +const createSpkiPublicKey = (publicKeyBase64: string) => { + return createPublicKey({ + key: Buffer.from(publicKeyBase64, 'base64'), + format: 'der', + type: 'spki', + }) +} + +export const validateRsaPublicKey = (publicKeyBase64: string): void => { + createSpkiPublicKey(publicKeyBase64) +} + +export const rsaVerifySignature = (data: Uint8Array, signature: Uint8Array, publicKeyBase64: string): boolean => { + const publicKey = createSpkiPublicKey(publicKeyBase64) + + return verify('RSA-SHA256', data, { key: publicKey, padding: constants.RSA_PKCS1_PADDING }, signature) +}