Skip to content

Commit

Permalink
sm2 ciphertext signature support asn1
Browse files Browse the repository at this point in the history
  • Loading branch information
emmansun committed Apr 29, 2024
1 parent b40eec5 commit 8a5d572
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 75 deletions.
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module.exports = {
...require('./src/sm3'),
...require('./src/bytescodeHex'),
...require('./src/kdf'),
...require('./src/sm4'),
...require('./src/sm2')
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
],
"main": "index.js",
"scripts": {
"test": "standard && node test/asn1_test.js && node test/sm3_test.js && node test/sm4_test.js && node test/kdf_test.js && node test/sm2_test.js"
"test": "standard && node test/bn_patch_test.js && node test/asn1_test.js && node test/sm3_test.js && node test/sm4_test.js && node test/kdf_test.js && node test/sm2_test.js"
},
"devDependencies": {
"sjcl-with-all": "^1.0.8",
Expand Down
24 changes: 11 additions & 13 deletions src/asn1.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,16 @@ class Builder {
let lenLen, lenByte
if (length > 0x00fffffffe) {
throw new Error('pending ASN.1 child too long')
} else if (lenLen >= 0x1000000) {
} else if (length >= 0x1000000) {
lenLen = 5
lenByte = 0x80 | 0x04
} else if (lenLen >= 0x10000) {
} else if (length >= 0x10000) {
lenLen = 4
lenByte = 0x80 | 0x03
} else if (lenLen >= 0x100) {
} else if (length >= 0x100) {
lenLen = 3
lenByte = 0x80 | 0x02
} else if (lenLen >= 0x80) {
} else if (length >= 0x80) {
lenLen = 2
lenByte = 0x80 | 0x01
} else {
Expand All @@ -162,17 +162,15 @@ class Builder {
child.offset++
child.pendingLenLen = lenLen - 1
}
const l = length
if (child.pendingLenLen > 0) {
let l = length
for (let i = child.pendingLenLen - 1; i >= 0; i--) {
child.result.splice(child.offset, 0, l & 0xff)
l = l >>> 8
}

let l = length
for (let i = child.pendingLenLen - 1; i >= 0; i--) {
child.result.splice(child.offset, 0, l & 0xff)
l = l >>> 8
}
if (l !== 0) {
throw new Error(
`pending child length ${length} exceeds ${child.pendingLenLen}-byte lehgth profix`
`pending child length ${length} exceeds ${child.pendingLenLen}-byte length profix`
)
}
this.result = child.result
Expand Down Expand Up @@ -419,7 +417,7 @@ class Parser {
return { success: false }
}
let value = 0
for (let i = 0; i < value; i++) {
for (let i = 0; i < length; i++) {
value = (value << 8) | v[i]
}
return { success: true, value: value >>> 0 }
Expand Down
23 changes: 23 additions & 0 deletions src/bn_patch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
function patchBN (sjcl) {
sjcl.bn.prototype.toBytes = sjcl.bn.prototype.toBytes || function (l) {
this.fullReduce()
const result = []
const limbs = this.limbs
for (let i = limbs.length - 1; i >= 0; i--) {
result.push((limbs[i] >> 16) & 0xff)
result.push((limbs[i] >> 8) & 0xff)
result.push(limbs[i] & 0xff)
}
l = l || Math.ceil(this.bitLength() / 8)
if (l > result.length) {
result.splice(0, 0, ...Array(l - result.length).fill(0))
} else if (l < result.length) {
result.splice(0, result.length - l)
}
return result
}
}

module.exports = {
patchBN
}
32 changes: 32 additions & 0 deletions src/bytescodecHex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/** @fileOverview some bytes codec functions.
* @author Emman Sun
*/

function bindBytesCodecHex (sjcl) {
sjcl.bytescodec = sjcl.bytescodec || {}
sjcl.bytescodec.hex = sjcl.bytescodec.hex || {
fromBytes: function (arr) {
let res = ''
for (let i = 0; i < arr.length; i++) {
let hex = (arr[i] & 0xff).toString(16)
if (hex.length === 1) {
hex = '0' + hex
}
res += hex
}
return res
},
toBytes: function (hexStr) {
if (typeof hexStr !== 'string' || hexStr.length % 2 === 1) {
throw new Error('Invalid hex string')
}
const res = []
for (let i = 0; i < hexStr.length; i += 2) {
res.push(parseInt(hexStr.substr(i, 2), 16))
}
return res
}
}
}

module.exports = { bindBytesCodecHex }
137 changes: 113 additions & 24 deletions src/sm2.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
/** @fileOverview Low-level SM2 implementation.
* @author Emman Sun
*/
const { Builder, Parser } = require('./asn1')

function bindSM2 (sjcl) {
require('./bn_patch').patchBN(sjcl)
require('./bytescodecHex').bindBytesCodecHex(sjcl)
if (sjcl.ecc.curves.sm2p256v1) return

const sbp = sjcl.bn.pseudoMersennePrime
Expand Down Expand Up @@ -156,8 +159,24 @@ function bindSM2 (sjcl) {
/** SM2 sign hash function
* @param {bitArray} hash hash to sign.
* @param {int} paranoia paranoia for random number generation
* @param {string} type signature type, default asn1, also can use rs which means r||s
* @return {string} hex signature string
*/
signHash: function (hash, paranoia = 6) {
signHash: function (hash, paranoia = 6, type = 'asn1') {
const l = this._curve.r.bitLength()
const rs = this._signHashInternal(hash, paranoia)
if (type === 'asn1') {
const builder = new Builder()
builder.addASN1Sequence((b) => {
b.addASN1IntBytes(sjcl.codec.bytes.fromBits(rs.r.toBits(l)))
b.addASN1IntBytes(sjcl.codec.bytes.fromBits(rs.s.toBits(l)))
})
return sjcl.bytescodec.hex.fromBytes(builder.bytes())
}
return sjcl.codec.hex.fromBits(sjcl.bitArray.concat(rs.r.toBits(l), rs.s.toBits(l)))
},

_signHashInternal: function (hash, paranoia = 6) {
if (sjcl.bitArray.bitLength(hash) > this._curveBitLength) {
hash = sjcl.bitArray.clamp(hash, this._curveBitLength)
}
Expand All @@ -180,30 +199,64 @@ function bindSM2 (sjcl) {
if (s.equals(0)) {
throw new Error('sign failed, pls retry 3')
}
const l = R.bitLength()
return sjcl.bitArray.concat(r.toBits(l), s.toBits(l))
return { r, s }
},

/**
* Decrypt the ciphertext
* @param {bitArray} ciphertext The ciphertext to decrypt.
* @param {string} ciphertext The hex ciphertext to decrypt.
* @returns {bitArray} The plaintext.
* @throws {Error} If the decryption fails.
*/
decrypt: function (ciphertext) {
if (sjcl.bitArray.bitLength(ciphertext) <= (96 * 8)) {
if (typeof ciphertext !== 'string') {
throw new Error('invalid ciphertext')
}
const c1 = sjcl.bitArray.bitSlice(ciphertext, 0, 64 * 8)
const c3 = sjcl.bitArray.bitSlice(ciphertext, 64 * 8, 96 * 8)
const c2 = sjcl.bitArray.bitSlice(ciphertext, 96 * 8)
let c2, c3, point
if (ciphertext.startsWith('30')) {
// asn1 type
const input = new Parser(sjcl.bytescodec.hex.toBytes(ciphertext))
const xstr = {}
const ystr = {}
const c3str = {}
const c2str = {}
const inner = {}
const fail = !input.readASN1Sequence(inner) ||
!input.isEmpty() ||
!inner.out.readASN1IntBytes(xstr) ||
!inner.out.readASN1IntBytes(ystr) ||
!inner.out.readASN1OctetString(c3str) ||
!inner.out.readASN1OctetString(c2str) ||
!inner.out.isEmpty()
if (fail) {
throw new Error('decryption error')
}
c3 = sjcl.codec.bytes.toBits(c3str.out)
c2 = sjcl.codec.bytes.toBits(c2str.out)
const ECCPoint = sjcl.ecc.point
const CorruptException = sjcl.exception.corrupt
const BigInt = sjcl.bn
point = new ECCPoint(this._curve, new BigInt(`0x${sjcl.bytescodec.hex.fromBytes(xstr.out)}`), new BigInt(`0x${sjcl.bytescodec.hex.fromBytes(ystr.out)}`))
if (!point.isValid()) {
throw new CorruptException('not on the curve!')
}
point = point.mult(this._exponent)
} else {
if (ciphertext.startsWith('04')) {
ciphertext = ciphertext.substring(2)
}
ciphertext = sjcl.codec.hex.toBits(ciphertext)
point = this._curve.fromBits(sjcl.bitArray.bitSlice(ciphertext, 0, 64 * 8)).mult(this._exponent)
c3 = sjcl.bitArray.bitSlice(ciphertext, 64 * 8, 96 * 8)
c2 = sjcl.bitArray.bitSlice(ciphertext, 96 * 8)
}
const msgLen = sjcl.bitArray.bitLength(c2)

const point = this._curve.fromBits(c1).mult(this._exponent)

let plaintext = sjcl.misc.kdf(msgLen, point.toBits())
for (let i = 0; i < plaintext.length; i++) {
plaintext[i] ^= c2[i]
}
plaintext = sjcl.bitArray.clamp(plaintext, msgLen)

const SM3 = sjcl.hash.sm3
const hash = new SM3()
hash.update(point.x.toBits())
Expand Down Expand Up @@ -308,29 +361,52 @@ function bindSM2 (sjcl) {

/** SM2 verify function
* @param {String|bitArray} data The data used for hash
* @param {bitArray} rs signature bitArray.
* @param {string} signature the hex signature string
* @param {string} type the signature type, default asn1, also can use rs which means r||s.
* @param {String|bitArray} uid The uid used for ZA
* @returns {Boolean} verify result
*/
verify: function (msg, rs, uid) {
return this.verifyHash(this.hash(msg, uid), rs)
verify: function (msg, signature, type = 'asn1', uid) {
return this.verifyHash(this.hash(msg, uid), signature, type)
},

/**
* SM2 verify hash function
* @param {bitArray} hashValue The hash value.
* @param {bitArray} rs signature bitArray.
* @param {string} signature the hex signature string
* @param {string} type the signature type, default asn1, also can use rs which means r||s.
* @returns {Boolean} verify result
*/
verifyHash: function (hashValue, rs) {
if (sjcl.bitArray.bitLength(hashValue) > this._curveBitLength) {
hashValue = sjcl.bitArray.clamp(hashValue, this._curveBitLength)
verifyHash: function (hashValue, signature, type = 'asn1') {
if (typeof signature !== 'string') {
return false
}
const BigInt = sjcl.bn
let r, ss
if (type === 'asn1') {
const input = new Parser(sjcl.bytescodec.hex.toBytes(signature))
const c1 = {}
const c2 = {}
const inner = {}
const fail = !input.readASN1Sequence(inner) ||
!input.isEmpty() ||
!inner.out.readASN1IntBytes(c1) ||
!inner.out.readASN1IntBytes(c2) ||
!inner.out.isEmpty()
if (fail) {
return false
}
r = new BigInt(`0x${sjcl.bytescodec.hex.fromBytes(c1.out)}`)
ss = new BigInt(`0x${sjcl.bytescodec.hex.fromBytes(c2.out)}`)
} else {
if (this._curveBitLength !== signature.length << 1) {
return false
}
const l = signature.length / 2
r = new BigInt(`0x${signature.substring(0, l)}`)
ss = new BigInt(`0x${signature.substring(l)}`)
}
const w = sjcl.bitArray
const R = this._curve.r
const l = this._curveBitLength
const r = sjcl.bn.fromBits(w.bitSlice(rs, 0, l))
const ss = sjcl.bn.fromBits(w.bitSlice(rs, l, 2 * l))
if (r.equals(0) || ss.equals(0) || r.greaterEquals(R) || ss.greaterEquals(R)) {
return false
}
Expand All @@ -347,8 +423,10 @@ function bindSM2 (sjcl) {
* Encrypt message
* @param {String|bitArray} msg The data used for encryption
* @param {int} paranoia paranoia for random number generation
* @param {string} outputType the ciphertext type, default is asn1, also support c1c3c2
* @returns {string} hex string of ciphertext
*/
encrypt: function (msg, paranoia = 6) {
encrypt: function (msg, paranoia = 6, outputType = 'asn1') {
if (typeof msg === 'string') {
msg = sjcl.codec.utf8String.toBits(msg)
}
Expand All @@ -371,7 +449,18 @@ function bindSM2 (sjcl) {
hash.update(point.x.toBits())
hash.update(msg)
hash.update(point.y.toBits())
return sjcl.bitArray.concat(sjcl.bitArray.concat(c1.toBits(), hash.finalize()), ciphertext)

if (outputType === 'asn1') {
const builder = new Builder()
builder.addASN1Sequence((b) => {
b.addASN1IntBytes(c1.x.toBytes())
b.addASN1IntBytes(c1.y.toBytes())
b.addASN1OctetString(sjcl.codec.bytes.fromBits(hash.finalize()))
b.addASN1OctetString(sjcl.codec.bytes.fromBits(ciphertext))
})
return sjcl.bytescodec.hex.fromBytes(builder.bytes())
}
return `04${sjcl.codec.hex.fromBits(sjcl.bitArray.concat(sjcl.bitArray.concat(c1.toBits(), hash.finalize()), ciphertext))}`
},

getType: function () {
Expand Down
37 changes: 4 additions & 33 deletions test/asn1_test.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,8 @@
const test = require('tape')
const sjcl = require('sjcl-with-all')
require('../src/bytescodecHex').bindBytesCodecHex(sjcl)
const { Builder, Parser } = require('../src/asn1')

/**
* Convert byte array or Uint8Array to hex string
* @param {Uint8Array|Array} bytes byte array or Uint8Array
* @returns {string} hex string
*/
function toHex (bytes) {
const isUint8Array = bytes instanceof Uint8Array
if (!isUint8Array) {
bytes = Uint8Array.from(bytes)
}
return Array.prototype.map
.call(bytes, function (n) {
return (n < 16 ? '0' : '') + n.toString(16)
})
.join('')
}

/**
* Convert a hex string to a byte array.
*/
function hexToBytes (hexStr) {
if (typeof hexStr !== 'string' || hexStr.length % 2 === 1) {
throw new Error('Invalid hex string')
}
const bytes = []
for (let i = 0; i < hexStr.length; i += 2) {
bytes.push(0xff & parseInt(hexStr.substring(i, i + 2), 16))
}
return bytes
}

test('ASN1 builder basic', function (t) {
const builder = new Builder()
builder.addASN1Sequence((b) => {
Expand All @@ -49,14 +20,14 @@ test('ASN1 builder basic', function (t) {
})
})
t.equal(
toHex(builder.bytes()),
sjcl.bytescodec.hex.fromBytes(builder.bytes()),
'303c040301020304030405060206010203040506020700f102030405060304000708090101ff010100050030110206010203040506020700f10203040506'
)
t.end()
})

test('ASN1 parser basic', function (t) {
const input = new Parser(hexToBytes('303c040301020304030405060206010203040506020700f102030405060304000708090101ff010100050030110206010203040506020700f10203040506'))
const input = new Parser(sjcl.bytescodec.hex.toBytes('303c040301020304030405060206010203040506020700f102030405060304000708090101ff010100050030110206010203040506020700f10203040506'))
const c1 = {}
const c2 = {}
const c3 = {}
Expand Down

0 comments on commit 8a5d572

Please sign in to comment.