Skip to content

Commit 781ec18

Browse files
committed
removed unused suffix from signature
1 parent 352c459 commit 781ec18

File tree

3 files changed

+323
-10
lines changed

3 files changed

+323
-10
lines changed

sodium-native.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,7 @@ declare module 'sodium-native' {
2323
export function crypto_auth_verify(output: Buffer, input: Buffer, key: Buffer): boolean
2424
export function crypto_sign(signedMessage: Buffer, message: Buffer, secretKey: Buffer): void
2525
export function crypto_sign_open(message: Buffer, signedMessage: Buffer, publicKey: Buffer): boolean
26+
export function crypto_sign_detached(signature: Buffer, message: Buffer, secretKey: Buffer): void
27+
export function crypto_sign_verify_detached(signature: Buffer, message: Buffer, publicKey: Buffer): boolean
2628
export function crypto_scalarmult(sharedSecret: Buffer, secretKey: Buffer, remotePublicKey: Buffer): void
2729
}

src/index.ts

Lines changed: 159 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,9 @@ export function setCustomStringifier(method: (input: unknown) => string, name: s
323323
* Returns a signature obtained by signing the input hash (hex string or buffer) with the sk string
324324
* @param input
325325
* @param sk
326+
* @param detached - If true (default), returns only the signature (64 bytes). If false, returns signature + message
326327
*/
327-
export function sign(input: hexstring | Buffer, sk: secretKey | Buffer): string {
328+
export function sign(input: hexstring | Buffer, sk: secretKey | Buffer, detached = true): string {
328329
let inputBuf: Buffer
329330
let skBuf: Buffer
330331
if (typeof input !== 'string') {
@@ -353,9 +354,65 @@ export function sign(input: hexstring | Buffer, sk: secretKey | Buffer): string
353354
throw new TypeError('Secret key string must be in hex format')
354355
}
355356
}
356-
const sig = Buffer.allocUnsafe(inputBuf.length + sodium.crypto_sign_BYTES)
357+
if (detached) {
358+
// Use detached signature
359+
const sig = Buffer.allocUnsafe(sodium.crypto_sign_BYTES)
360+
try {
361+
sodium.crypto_sign_detached(sig, inputBuf, skBuf)
362+
} catch (e) {
363+
throw new Error('Failed to sign input with provided secret key.')
364+
}
365+
return sig.toString('hex')
366+
} else {
367+
// Use non-detached signature (legacy behavior)
368+
const sig = Buffer.allocUnsafe(inputBuf.length + sodium.crypto_sign_BYTES)
369+
try {
370+
sodium.crypto_sign(sig, inputBuf, skBuf)
371+
} catch (e) {
372+
throw new Error('Failed to sign input with provided secret key.')
373+
}
374+
return sig.toString('hex')
375+
}
376+
}
377+
378+
/**
379+
* Returns a detached signature obtained by signing the input hash (hex string or buffer) with the sk string
380+
* @param input - The message to sign (hex string or buffer)
381+
* @param sk - The secret key
382+
* @returns Only the 64-byte signature as hex string
383+
*/
384+
export function signDetached(input: hexstring | Buffer, sk: secretKey | Buffer): string {
385+
let inputBuf: Buffer
386+
let skBuf: Buffer
387+
if (typeof input !== 'string') {
388+
if (Buffer.isBuffer(input)) {
389+
inputBuf = input
390+
} else {
391+
throw new TypeError('Input must be a hex string or buffer.')
392+
}
393+
} else {
394+
try {
395+
inputBuf = Buffer.from(input, 'hex')
396+
} catch (e) {
397+
throw new TypeError('Input string must be in hex format.')
398+
}
399+
}
400+
if (typeof sk !== 'string') {
401+
if (Buffer.isBuffer(sk)) {
402+
skBuf = sk
403+
} else {
404+
throw new TypeError('Secret key must be a hex string or buffer.')
405+
}
406+
} else {
407+
try {
408+
skBuf = Buffer.from(sk, 'hex')
409+
} catch (e) {
410+
throw new TypeError('Secret key string must be in hex format')
411+
}
412+
}
413+
const sig = Buffer.allocUnsafe(sodium.crypto_sign_BYTES)
357414
try {
358-
sodium.crypto_sign(sig, inputBuf, skBuf)
415+
sodium.crypto_sign_detached(sig, inputBuf, skBuf)
359416
} catch (e) {
360417
throw new Error('Failed to sign input with provided secret key.')
361418
}
@@ -368,9 +425,33 @@ export function sign(input: hexstring | Buffer, sk: secretKey | Buffer): string
368425
* @param obj
369426
* @param sk
370427
* @param pk
428+
* @param detached - If true (default), uses detached signature (64 bytes only). If false, uses non-detached
429+
* @returns the new signed object with the `sign` field. The original object is mutated as well.
430+
*/
431+
export function signObj(obj: object, sk: secretKey | Buffer, pk: publicKey | Buffer, detached = true): SignedObject {
432+
if (typeof obj !== 'object') {
433+
throw new TypeError('Input must be an object.')
434+
}
435+
// If it's an array, we don't want to try to sign it
436+
if (Array.isArray(obj)) {
437+
throw new TypeError('Input cannot be an array.')
438+
}
439+
const objStr = stringify(obj)
440+
const hashed = hash(objStr, 'buffer')
441+
const sig = sign(hashed, sk, detached)
442+
const signPk = Buffer.isBuffer(pk) ? bufferToHex(pk) : pk
443+
;(obj as SignedObject).sign = { owner: signPk, sig }
444+
return obj as SignedObject
445+
}
446+
447+
/**
448+
* Attaches a sign field to the input object using detached signatures
449+
* @param obj - The object to sign
450+
* @param sk - The secret key
451+
* @param pk - The public key
371452
* @returns the new signed object with the `sign` field. The original object is mutated as well.
372453
*/
373-
export function signObj(obj: object, sk: secretKey | Buffer, pk: publicKey | Buffer): SignedObject {
454+
export function signObjDetached(obj: object, sk: secretKey | Buffer, pk: publicKey | Buffer): SignedObject {
374455
if (typeof obj !== 'object') {
375456
throw new TypeError('Input must be an object.')
376457
}
@@ -380,7 +461,7 @@ export function signObj(obj: object, sk: secretKey | Buffer, pk: publicKey | Buf
380461
}
381462
const objStr = stringify(obj)
382463
const hashed = hash(objStr, 'buffer')
383-
const sig = sign(hashed, sk)
464+
const sig = signDetached(hashed, sk)
384465
const signPk = Buffer.isBuffer(pk) ? bufferToHex(pk) : pk
385466
;(obj as SignedObject).sign = { owner: signPk, sig }
386467
return obj as SignedObject
@@ -402,13 +483,59 @@ function verify(msg: string, sig: hexstring | Buffer, pk: publicKey | Buffer): b
402483
}
403484
const sigBuf = _ensureBuffer(sig)
404485
const pkBuf = _ensureBuffer(pk)
486+
487+
// Auto-detect signature type based on length
488+
// Detached signatures are exactly 64 bytes
489+
// Non-detached signatures are 64 bytes + message length
490+
const msgBuf = Buffer.from(msg, 'hex')
491+
const expectedNonDetachedLength = sodium.crypto_sign_BYTES + msgBuf.length
492+
493+
if (sigBuf.length === sodium.crypto_sign_BYTES) {
494+
// This is a detached signature
495+
try {
496+
return sodium.crypto_sign_verify_detached(sigBuf as Buffer, msgBuf, pkBuf as Buffer)
497+
} catch (e) {
498+
return false
499+
}
500+
} else if (sigBuf.length === expectedNonDetachedLength) {
501+
// This is a non-detached signature
502+
try {
503+
const opened = Buffer.allocUnsafe(sigBuf.length - sodium.crypto_sign_BYTES)
504+
sodium.crypto_sign_open(opened, sigBuf as Buffer, pkBuf as Buffer)
505+
const verified = opened.toString('hex')
506+
return verified === msg
507+
} catch (e) {
508+
return false
509+
}
510+
} else {
511+
// Invalid signature length
512+
throw new Error('Invalid signature length. Expected either detached (64 bytes) or non-detached signature.')
513+
}
514+
}
515+
516+
/**
517+
* Verifies a detached signature against a message
518+
* @param msg - The message that was signed (hex string)
519+
* @param sig - The detached signature (hex string or buffer)
520+
* @param pk - The public key to verify with
521+
* @returns true if the signature is valid, false otherwise
522+
*/
523+
export function verifyDetached(msg: string, sig: hexstring | Buffer, pk: publicKey | Buffer): boolean {
524+
if (typeof msg !== 'string') {
525+
throw new TypeError('Message to compare must be a string.')
526+
}
527+
const msgBuf = Buffer.from(msg, 'hex')
528+
const sigBuf = _ensureBuffer(sig)
529+
const pkBuf = _ensureBuffer(pk)
530+
531+
if (sigBuf.length !== sodium.crypto_sign_BYTES) {
532+
throw new Error('Invalid signature length for detached signature.')
533+
}
534+
405535
try {
406-
const opened = Buffer.allocUnsafe(sigBuf.length - sodium.crypto_sign_BYTES)
407-
sodium.crypto_sign_open(opened, sigBuf as Buffer, pkBuf as Buffer)
408-
const verified = opened.toString('hex')
409-
return verified === msg
536+
return sodium.crypto_sign_verify_detached(sigBuf as Buffer, msgBuf, pkBuf as Buffer)
410537
} catch (e) {
411-
throw new Error('Unable to verify provided signature with provided public key.')
538+
return false
412539
}
413540
}
414541

@@ -433,6 +560,28 @@ export function verifyObj(obj: SignedObject): boolean {
433560
return verify(objHash, obj.sign.sig, obj.sign.owner)
434561
}
435562

563+
/**
564+
* Verifies an object signed with detached signature
565+
* @param obj - The signed object to verify
566+
* @returns true if the signature is valid, false otherwise
567+
*/
568+
export function verifyObjDetached(obj: SignedObject): boolean {
569+
if (typeof obj !== 'object') {
570+
throw new TypeError('Input must be an object.')
571+
}
572+
if (!obj.sign || !obj.sign.owner || !obj.sign.sig) {
573+
throw new Error('Object must contain a sign field with the following data: { owner, sig }')
574+
}
575+
if (typeof obj.sign.owner !== 'string') {
576+
throw new TypeError('Owner must be a public key represented as a hex string.')
577+
}
578+
if (typeof obj.sign.sig !== 'string') {
579+
throw new TypeError('Signature must be a valid signature represented as a hex string.')
580+
}
581+
const objHash = hashObj(obj, true)
582+
return verifyDetached(objHash, obj.sign.sig, obj.sign.owner)
583+
}
584+
436585
/**
437586
* This function initialized the cryptographic hashing functions
438587
* @param key The HASH_KEY for initializing the cryptographic hashing functions
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import * as crypto from '../../src/index'
2+
3+
describe('Detached Signatures', () => {
4+
beforeAll(() => {
5+
crypto.init('69fa4195670576c0160d660c3be36556ff8d504725be8a59b5a96509e0c994bc')
6+
})
7+
8+
describe('signDetached and verifyDetached', () => {
9+
it('should create and verify a detached signature', () => {
10+
const keypair = crypto.generateKeypair()
11+
const message = 'Hello, World!'
12+
const messageHash = crypto.hash(message)
13+
14+
// Create detached signature
15+
const signature = crypto.signDetached(messageHash, keypair.secretKey)
16+
17+
// Verify signature is 64 bytes (128 hex chars)
18+
expect(signature.length).toBe(128)
19+
20+
// Verify the signature
21+
const isValid = crypto.verifyDetached(messageHash, signature, keypair.publicKey)
22+
expect(isValid).toBe(true)
23+
24+
// Verify with wrong message fails
25+
const wrongMessage = crypto.hash('Wrong message')
26+
const isInvalid = crypto.verifyDetached(wrongMessage, signature, keypair.publicKey)
27+
expect(isInvalid).toBe(false)
28+
})
29+
})
30+
31+
describe('signObjDetached and verifyObjDetached', () => {
32+
it('should sign and verify objects with detached signatures', () => {
33+
const keypair = crypto.generateKeypair()
34+
const testObj = {
35+
message: 'Test object',
36+
value: 42,
37+
timestamp: Date.now()
38+
}
39+
40+
// Sign with detached signature
41+
const signedObj = crypto.signObjDetached(testObj, keypair.secretKey, keypair.publicKey)
42+
43+
// Verify signature is 64 bytes
44+
expect(signedObj.sign.sig.length).toBe(128)
45+
46+
// Verify the signed object
47+
const isValid = crypto.verifyObjDetached(signedObj)
48+
expect(isValid).toBe(true)
49+
50+
// Tamper with object and verify it fails
51+
signedObj.value = 43
52+
const isInvalid = crypto.verifyObjDetached(signedObj)
53+
expect(isInvalid).toBe(false)
54+
})
55+
})
56+
57+
describe('Backward compatibility', () => {
58+
it('should verify both old and new signatures with auto-detection', () => {
59+
const keypair = crypto.generateKeypair()
60+
const message = 'Test message'
61+
const messageHash = crypto.hash(message)
62+
63+
// Create old-style (non-detached) signature
64+
const oldSignature = crypto.sign(messageHash, keypair.secretKey, false)
65+
// Old signature should be 96 bytes for 32-byte hash (64 + 32)
66+
expect(oldSignature.length).toBe(192)
67+
68+
// Create new-style (detached) signature
69+
const newSignature = crypto.sign(messageHash, keypair.secretKey, true)
70+
// New signature should be 64 bytes
71+
expect(newSignature.length).toBe(128)
72+
73+
// Both should verify successfully with verifyObj
74+
const testObj1 = { data: 'test' }
75+
const testObj2 = { data: 'test' }
76+
77+
// Sign with old style
78+
crypto.signObj(testObj1, keypair.secretKey, keypair.publicKey, false)
79+
expect(crypto.verifyObj(testObj1)).toBe(true)
80+
81+
// Sign with new style
82+
crypto.signObj(testObj2, keypair.secretKey, keypair.publicKey, true)
83+
expect(crypto.verifyObj(testObj2)).toBe(true)
84+
})
85+
86+
it('should handle mixed signature types correctly', () => {
87+
const keypair = crypto.generateKeypair()
88+
const testObj = { data: 'test data', id: 123 }
89+
90+
// Create both types of signatures
91+
const objOld = JSON.parse(JSON.stringify(testObj))
92+
const objNew = JSON.parse(JSON.stringify(testObj))
93+
94+
// Sign with old method
95+
crypto.signObj(objOld, keypair.secretKey, keypair.publicKey, false)
96+
97+
// Sign with new method
98+
crypto.signObj(objNew, keypair.secretKey, keypair.publicKey, true)
99+
100+
// Old signature should be longer
101+
expect(objOld.sign.sig.length).toBeGreaterThan(objNew.sign.sig.length)
102+
103+
// Both should verify with verifyObj (auto-detection)
104+
expect(crypto.verifyObj(objOld)).toBe(true)
105+
expect(crypto.verifyObj(objNew)).toBe(true)
106+
107+
// New detached verify should work only with detached signature
108+
expect(crypto.verifyObjDetached(objNew)).toBe(true)
109+
expect(() => crypto.verifyObjDetached(objOld)).toThrow()
110+
})
111+
})
112+
113+
describe('Sign function with detached parameter', () => {
114+
it('should produce different signature lengths based on detached parameter', () => {
115+
const keypair = crypto.generateKeypair()
116+
const message = crypto.hash('test')
117+
118+
// Default (undefined) should now use detached
119+
const defaultSig = crypto.sign(message, keypair.secretKey)
120+
expect(defaultSig.length).toBe(128) // 64 bytes sig only
121+
122+
// Explicit false should use non-detached
123+
const nonDetachedSig = crypto.sign(message, keypair.secretKey, false)
124+
expect(nonDetachedSig.length).toBe(192) // 96 bytes = 64 sig + 32 msg
125+
126+
// Explicit true should use detached
127+
const detachedSig = crypto.sign(message, keypair.secretKey, true)
128+
expect(detachedSig.length).toBe(128) // 64 bytes sig only
129+
})
130+
})
131+
132+
describe('Default behavior change', () => {
133+
it('sign() should default to detached signatures', () => {
134+
const keypair = crypto.generateKeypair()
135+
const message = crypto.hash('test message')
136+
137+
// Call without detached parameter
138+
const signature = crypto.sign(message, keypair.secretKey)
139+
140+
// Should produce 64-byte signature
141+
expect(signature.length).toBe(128) // 128 hex chars = 64 bytes
142+
143+
// Should verify with verifyDetached
144+
expect(crypto.verifyDetached(message, signature, keypair.publicKey)).toBe(true)
145+
})
146+
147+
it('signObj() should default to detached signatures', () => {
148+
const keypair = crypto.generateKeypair()
149+
const obj = { data: 'test', value: 123 }
150+
151+
// Call without detached parameter
152+
const signedObj = crypto.signObj(obj, keypair.secretKey, keypair.publicKey)
153+
154+
// Should produce 64-byte signature
155+
expect(signedObj.sign.sig.length).toBe(128)
156+
157+
// Should verify with both methods due to auto-detection
158+
expect(crypto.verifyObj(signedObj)).toBe(true)
159+
expect(crypto.verifyObjDetached(signedObj)).toBe(true)
160+
})
161+
})
162+
})

0 commit comments

Comments
 (0)