v1.0.0: An insanely small ASN.1 codec for Node.js and Browsers
This commit is contained in:
parent
4555c003d7
commit
0c73f3fbbe
|
@ -1,3 +1,5 @@
|
||||||
|
*.gz
|
||||||
|
|
||||||
# ---> Node
|
# ---> Node
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
|
|
413
README.md
413
README.md
|
@ -1,3 +1,412 @@
|
||||||
# asn1.js
|
# @root/asn1
|
||||||
|
|
||||||
Lightweight, Zero-Dependency ASN.1 encoder and decoder in less than 200 lines of vanilla JavaScript
|
Built by [The Root Company](https://therootcompany.com)
|
||||||
|
for [Greenlock](https://greenlock.domains)
|
||||||
|
and [ACME.js](https://git.rootprojects.org/root/acme.js)
|
||||||
|
|
||||||
|
Lightweight, Zero-Dependency ASN.1 encoder and decoder for Node.js and Browsers,
|
||||||
|
in less than 300 lines of vanilla JavaScript
|
||||||
|
|
||||||
|
| 1.6k gzipped
|
||||||
|
| 4.2k minified
|
||||||
|
| 8.4k pretty
|
||||||
|
|
|
||||||
|
|
||||||
|
- [x] Zero External Dependencies
|
||||||
|
- [x] Universal Support
|
||||||
|
- [x] Node.js
|
||||||
|
- [x] Browsers
|
||||||
|
- [x] Vanilla JS
|
||||||
|
|
||||||
|
This ASN.1 codec is built for simplicity. It encodes into DER format
|
||||||
|
and decodes into a simple, classless Array of Arrays and values.
|
||||||
|
|
||||||
|
Most people don't actually want to work with ANS.1 directly,
|
||||||
|
but rather intend to work with pre-defined x509 schemas.
|
||||||
|
|
||||||
|
If you're **most people**, you're actually looking for one or more of these:
|
||||||
|
|
||||||
|
- [pem.js](https://git.rootprojects.org/root/pem.js)
|
||||||
|
- [x509.js](https://git.rootprojects.org/root/x509.js)
|
||||||
|
- [csr.js](https://git.rootprojects.org/root/csr.js)
|
||||||
|
- [keypairs.js](https://git.rootprojects.org/root/keypairs.js)
|
||||||
|
- [encoding.js](https://git.rootprojects.org/root/encoding.js)
|
||||||
|
|
||||||
|
Want to [contribute](#contributions)?
|
||||||
|
Need [commercial support](#commercial-support)?
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
ASN.1 DER consists values which have
|
||||||
|
|
||||||
|
- a type (2-bit class, 6-bit tag)
|
||||||
|
- a coded length
|
||||||
|
- zero or more values
|
||||||
|
|
||||||
|
Common types include:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
0x30 SEQUENCE
|
||||||
|
0x02 INTEGER*
|
||||||
|
0x03 BIT STRING**
|
||||||
|
0x04 OCTET STRING
|
||||||
|
0x05 NULL
|
||||||
|
0x06 OBJECT IDENTIFIER
|
||||||
|
0x0C UTF8String
|
||||||
|
0x16 IA5String (ASCII)
|
||||||
|
0x17 UTCTime
|
||||||
|
0x31 SET
|
||||||
|
0xA0 context-specific***
|
||||||
|
0xA3 context-specific***
|
||||||
|
```
|
||||||
|
|
||||||
|
<small>\* INTEGERS are always BigInt-encoded (a leading '00' for positive numbers with a 1 in the most-significant-bit position)</small>
|
||||||
|
|
||||||
|
<small>\*\*BIT STRINGS have a leading "bit mask" which, for all practical purposes, is actually _always_ '00'</small>
|
||||||
|
|
||||||
|
<small>\*\*\* See <https://stackoverflow.com/a/15071901/151312></small>
|
||||||
|
|
||||||
|
The core value in this library is that it:
|
||||||
|
|
||||||
|
- correctly sums the byte length of children elements
|
||||||
|
- correctly encodes BigInts
|
||||||
|
|
||||||
|
## Parser Usage
|
||||||
|
|
||||||
|
There are three options:
|
||||||
|
|
||||||
|
- `der` (required) - the input bytes as a buffer
|
||||||
|
- `json` (default) - returns hex strings for values, rather than buffers
|
||||||
|
- `verbose` - returns a more human-friendly object that is useful for debugging
|
||||||
|
|
||||||
|
```js
|
||||||
|
ASN1.parse({ der: `<Buffer>`, json: true, verbose: true });
|
||||||
|
```
|
||||||
|
|
||||||
|
Default (hex) output:
|
||||||
|
|
||||||
|
```js
|
||||||
|
[
|
||||||
|
'30',
|
||||||
|
[
|
||||||
|
['02', '01'],
|
||||||
|
['04', '2c8996...'],
|
||||||
|
['a0', [['06', '2a8648...']]],
|
||||||
|
['a1', [['03', '04bdd8...']]]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
Verbose output:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{ type: 48,
|
||||||
|
lengthSize: 0,
|
||||||
|
length: 119,
|
||||||
|
children:
|
||||||
|
[ { type: 2, lengthSize: 0, length: 1, value: <Buffer 01> },
|
||||||
|
{ type: 4,
|
||||||
|
lengthSize: 0,
|
||||||
|
length: 32,
|
||||||
|
value:
|
||||||
|
<Buffer 2c 89 96 ...>,
|
||||||
|
children: [] },
|
||||||
|
{ type: 160, lengthSize: 0, length: 10, children: [Array] },
|
||||||
|
{ type: 161, lengthSize: 0, length: 68, children: [Array] } ] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Packer Usage
|
||||||
|
|
||||||
|
You can use either of two syntaxes. One is much easier to read than the other.
|
||||||
|
|
||||||
|
Ironically, hex strings are used in place of buffers for efficiency.
|
||||||
|
|
||||||
|
```js
|
||||||
|
ASN1.Any(hexType, hexBytes1, hexBytes2, ...);
|
||||||
|
ASN1.UInt(hexBigInt);
|
||||||
|
ASN1.BitStr(hexBitStream);
|
||||||
|
```
|
||||||
|
|
||||||
|
In practice, you'll be cascading the objects into a final hex string:
|
||||||
|
|
||||||
|
```
|
||||||
|
// result is a hex-encoded DER
|
||||||
|
var der = hexToBuf(
|
||||||
|
ASN1.Any('30' // Sequence
|
||||||
|
, ASN1.UInt('01') // Integer (Version 1)
|
||||||
|
, ASN1.Any('04', '07CAD7...') // Octet String
|
||||||
|
, ASN1.Any('A0', '06082A...') // [0] Object ID (context-specific)
|
||||||
|
, ASN1.Any('A1', // [1] (context-specific value)
|
||||||
|
ASN1.BitStr('04BDD8...')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively you can pack either the sparse array or verbose object, using hex strings or buffers:
|
||||||
|
|
||||||
|
- `json` when set to true will return a hex-encoded DER rather than a DER buffer
|
||||||
|
|
||||||
|
```js
|
||||||
|
var buf = Uint8Array.from([0x01]);
|
||||||
|
|
||||||
|
ASN1.pack(
|
||||||
|
[
|
||||||
|
'30',
|
||||||
|
[
|
||||||
|
['02', buf],
|
||||||
|
['04', '07CAD7...'],
|
||||||
|
['A0', '06082A...'],
|
||||||
|
['A1', ['03', '04BDD8...']]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
{ json: false }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
var buf = Uint8Array.from([0x01]);
|
||||||
|
|
||||||
|
ASN1.pack(
|
||||||
|
{
|
||||||
|
type: 48,
|
||||||
|
children: [
|
||||||
|
{ type: 2, value: '01' },
|
||||||
|
{ type: 4, value: '2c 89 96 ...', children: [] },
|
||||||
|
{ type: 160, children: [...] },
|
||||||
|
{ type: 161, children: [...] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ json: false }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
# Install
|
||||||
|
|
||||||
|
This package contains both node-specific and browser-specific code,
|
||||||
|
and the `package.json#browser` field ensures that your package manager
|
||||||
|
will automatically choose the correct code for your environment.
|
||||||
|
|
||||||
|
## Node (and Webpack)
|
||||||
|
|
||||||
|
```js
|
||||||
|
npm install -g @root/asn1
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
var asn1 = require('@root/asn1');
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// just the packer
|
||||||
|
var asn1 = require('@root/asn1/packer');
|
||||||
|
|
||||||
|
// just the parser
|
||||||
|
var asn1 = require('@root/asn1/parser');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browsers (Vanilla JS)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="https://unpkg.com/@root/asn1/dist/asn1.all.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="https://unpkg.com/@root/asn1/dist/asn1.all.min.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
var ASN1 = window.ASN1;
|
||||||
|
```
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
## Decoding DER to JSON-ASN.1
|
||||||
|
|
||||||
|
```js
|
||||||
|
var PEM = require('@root/pem/packer');
|
||||||
|
var Enc = require('@root/encoding');
|
||||||
|
var ASN1 = require('@root/asn1/parser');
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
var pem = [
|
||||||
|
'-----BEGIN EC PRIVATE KEY-----',
|
||||||
|
'MHcCAQEEICyJlsaqkx2z9yx0H6rHA0lM3/7jXjxqn/VOhExHDuR6oAoGCCqGSM49',
|
||||||
|
'AwEHoUQDQgAEvdjQ3T6VBX82LIKDzepYgRsz3HgRwp83yPuonu6vqoshSQRe0Aye',
|
||||||
|
'mmdXUDX2wTZsmFSjhY9uroRiBbGZrigbKA==',
|
||||||
|
'-----END EC PRIVATE KEY-----'
|
||||||
|
].join('\n');
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
var der = PEM.parseBlock(pem).bytes;
|
||||||
|
var asn1 = ASN1.parse({ der: der, json: true, verbose: false });
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"30",
|
||||||
|
[
|
||||||
|
["02", "01"],
|
||||||
|
[
|
||||||
|
"04",
|
||||||
|
"2c8996c6aa931db3f72c741faac703494cdffee35e3c6a9ff54e844c470ee47a"
|
||||||
|
],
|
||||||
|
["a0", [["06", "2a8648ce3d030107"]]],
|
||||||
|
[
|
||||||
|
"a1",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"03",
|
||||||
|
"04bdd8d0dd3e95057f362c8283cdea58811b33dc7811c29f37c8fba89eeeafaa8b2149045ed00c9e9a67575035f6c1366c9854a3858f6eae846205b199ae281b28"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": 48,
|
||||||
|
"lengthSize": 0,
|
||||||
|
"length": 119,
|
||||||
|
"children": [
|
||||||
|
{ "type": 2, "lengthSize": 0, "length": 1, "value": "01" },
|
||||||
|
{
|
||||||
|
"type": 4,
|
||||||
|
"lengthSize": 0,
|
||||||
|
"length": 32,
|
||||||
|
"value": "2c8996c6aa931db3f72c741faac703494cdffee35e3c6a9ff54e844c470ee47a",
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 160,
|
||||||
|
"lengthSize": 0,
|
||||||
|
"length": 10,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": 6,
|
||||||
|
"lengthSize": 0,
|
||||||
|
"length": 8,
|
||||||
|
"value": "2a8648ce3d030107"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 161,
|
||||||
|
"lengthSize": 0,
|
||||||
|
"length": 68,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": 3,
|
||||||
|
"lengthSize": 0,
|
||||||
|
"length": 66,
|
||||||
|
"value": "04bdd8d0dd3e95057f362c8283cdea58811b33dc7811c29f37c8fba89eeeafaa8b2149045ed00c9e9a67575035f6c1366c9854a3858f6eae846205b199ae281b28",
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Encoding ASN.1 to DER
|
||||||
|
|
||||||
|
Here's an example of an SEC1-encoded EC P-256 Public/Private Keypair:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var ASN1 = require('@root/asn1/packer');
|
||||||
|
var Enc = require('@root/encoding');
|
||||||
|
var PEM = require('@root/pem/packer');
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 1.2.840.10045.3.1.7
|
||||||
|
// prime256v1 (ANSI X9.62 named elliptic curve)
|
||||||
|
var OBJ_ID_EC_256 = '06 08 2A8648CE3D030107';
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
var jwk = {
|
||||||
|
crv: 'P-256',
|
||||||
|
d: 'LImWxqqTHbP3LHQfqscDSUzf_uNePGqf9U6ETEcO5Ho',
|
||||||
|
kty: 'EC',
|
||||||
|
x: 'vdjQ3T6VBX82LIKDzepYgRsz3HgRwp83yPuonu6vqos',
|
||||||
|
y: 'IUkEXtAMnppnV1A19sE2bJhUo4WPbq6EYgWxma4oGyg',
|
||||||
|
kid: 'MnfJYyS9W5gUjrJLdn8ePMzik8ZJz2qc-VZmKOs_oCw'
|
||||||
|
};
|
||||||
|
var d = Enc.base64ToHex(jwk.d);
|
||||||
|
var x = Enc.base64ToHex(jwk.x);
|
||||||
|
var y = Enc.base64ToHex(jwk.y);
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
var der = Enc.hexToBuf(
|
||||||
|
ASN1.Any('30' // Sequence
|
||||||
|
, ASN1.UInt('01') // Integer (Version 1)
|
||||||
|
, ASN1.Any('04', d) // Octet String
|
||||||
|
, ASN1.Any('A0', OBJ_ID_EC_256) // [0] Object ID
|
||||||
|
, ASN1.Any('A1', // [1] Embedded EC/ASN1 public key
|
||||||
|
ASN1.BitStr('04' + x + y)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
var pem = PEM.packBlock({
|
||||||
|
type: 'EC PRIVATE KEY',
|
||||||
|
bytes: der
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
# Disabiguation
|
||||||
|
|
||||||
|
`ASN1.Any(typ, hexVal, ...)`
|
||||||
|
|
||||||
|
There was once an actual ASN.1 type with the literal name 'Any'.
|
||||||
|
It was deprecated in 1994 and the `Any` in this API simply means "give any value"
|
||||||
|
|
||||||
|
# Contributions
|
||||||
|
|
||||||
|
Did this project save you some time? Maybe make your day? Even save the day?
|
||||||
|
|
||||||
|
Please say "thanks" via Paypal or Patreon:
|
||||||
|
|
||||||
|
- Paypal: [\$5](https://paypal.me/rootprojects/5) | [\$10](https://paypal.me/rootprojects/10) | Any amount: <paypal@therootcompany.com>
|
||||||
|
- Patreon: <https://patreon.com/rootprojects>
|
||||||
|
|
||||||
|
Where does your contribution go?
|
||||||
|
|
||||||
|
[Root](https://therootcompany.com) is a collection of experts
|
||||||
|
who trust each other and enjoy working together on deep-tech,
|
||||||
|
Indie Web projects.
|
||||||
|
|
||||||
|
Our goal is to operate as a sustainable community.
|
||||||
|
|
||||||
|
Your contributions - both in code and _especially_ monetarily -
|
||||||
|
help to not just this project, but also our broader work
|
||||||
|
of [projects](https://rootprojects.org) that fuel the **Indie Web**.
|
||||||
|
|
||||||
|
Also, we chat on [Keybase](https://keybase.io)
|
||||||
|
in [#rootprojects](https://keybase.io/team/rootprojects)
|
||||||
|
|
||||||
|
# Commercial Support
|
||||||
|
|
||||||
|
Do you need...
|
||||||
|
|
||||||
|
- more features?
|
||||||
|
- bugfixes, on _your_ timeline?
|
||||||
|
- custom code, built by experts?
|
||||||
|
- commercial support and licensing?
|
||||||
|
|
||||||
|
Contact <aj@therootcompany.com> for support options.
|
||||||
|
|
||||||
|
# Legal
|
||||||
|
|
||||||
|
Copyright [AJ ONeal](https://coolaj86.com),
|
||||||
|
[Root](https://therootcompany.com) 2018-2019
|
||||||
|
|
||||||
|
MPL-2.0 |
|
||||||
|
[Terms of Use](https://therootcompany.com/legal/#terms) |
|
||||||
|
[Privacy Policy](https://therootcompany.com/legal/#privacy)
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# TODO convert to JS
|
||||||
|
cat parser.js packer.js > all.tmp.js
|
||||||
|
sed -i '' '/use strict/d' all.tmp.js
|
||||||
|
sed -i '' '/require/d' all.tmp.js
|
||||||
|
sed -i '' '/exports/d' all.tmp.js
|
||||||
|
|
||||||
|
echo ';(function () {' > dist/asn1.js
|
||||||
|
echo "'use strict';" >> dist/asn1.js
|
||||||
|
echo "var ASN1 = window.ASN1 = {};" >> dist/asn1.js
|
||||||
|
echo "var Enc = window.Encoding;" >> dist/asn1.js
|
||||||
|
cat all.tmp.js >> dist/asn1.js
|
||||||
|
rm all.tmp.js
|
||||||
|
echo '}());' >> dist/asn1.js
|
||||||
|
|
||||||
|
rm dist/*.gz
|
||||||
|
|
||||||
|
cat node_modules/@root/encoding/dist/encoding.all.js > all.js
|
||||||
|
cat dist/asn1.js >> all.js
|
||||||
|
uglifyjs dist/asn1.js > dist/asn1.min.js
|
||||||
|
gzip dist/asn1.min.js
|
||||||
|
uglifyjs dist/asn1.js > dist/asn1.min.js
|
||||||
|
|
||||||
|
mv all.js dist/asn1.all.js
|
||||||
|
uglifyjs dist/asn1.all.js > dist/asn1.all.min.js
|
||||||
|
gzip dist/asn1.all.min.js
|
||||||
|
uglifyjs dist/asn1.all.js > dist/asn1.all.min.js
|
|
@ -0,0 +1,518 @@
|
||||||
|
;(function () {
|
||||||
|
'use strict';
|
||||||
|
var Enc = window.Encoding = {};
|
||||||
|
|
||||||
|
|
||||||
|
// To Base64
|
||||||
|
|
||||||
|
Enc.bufToBase64 = function(u8) {
|
||||||
|
var bin = '';
|
||||||
|
u8.forEach(function(i) {
|
||||||
|
bin += String.fromCharCode(i);
|
||||||
|
});
|
||||||
|
return btoa(bin);
|
||||||
|
};
|
||||||
|
|
||||||
|
Enc.strToBase64 = function(str) {
|
||||||
|
return btoa(Enc.strToBin(str));
|
||||||
|
};
|
||||||
|
|
||||||
|
// From Base64
|
||||||
|
|
||||||
|
function _base64ToBin(b64) {
|
||||||
|
return atob(Enc.urlBase64ToBase64(b64));
|
||||||
|
}
|
||||||
|
|
||||||
|
Enc._base64ToBin = _base64ToBin;
|
||||||
|
|
||||||
|
Enc.base64ToBuf = function(b64) {
|
||||||
|
return Enc.binToBuf(_base64ToBin(b64));
|
||||||
|
};
|
||||||
|
|
||||||
|
Enc.base64ToStr = function(b64) {
|
||||||
|
return Enc.binToStr(_base64ToBin(b64));
|
||||||
|
};
|
||||||
|
|
||||||
|
// URL Safe Base64
|
||||||
|
|
||||||
|
Enc.urlBase64ToBase64 = function(u64) {
|
||||||
|
var r = u64 % 4;
|
||||||
|
if (2 === r) {
|
||||||
|
u64 += '==';
|
||||||
|
} else if (3 === r) {
|
||||||
|
u64 += '=';
|
||||||
|
}
|
||||||
|
return u64.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
};
|
||||||
|
|
||||||
|
Enc.base64ToUrlBase64 = function(b64) {
|
||||||
|
return b64
|
||||||
|
.replace(/\+/g, '-')
|
||||||
|
.replace(/\//g, '_')
|
||||||
|
.replace(/=/g, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
Enc.bufToUrlBase64 = function(buf) {
|
||||||
|
return Enc.base64ToUrlBase64(Enc.bufToBase64(buf));
|
||||||
|
};
|
||||||
|
|
||||||
|
Enc.strToUrlBase64 = function(str) {
|
||||||
|
return Enc.bufToUrlBase64(Enc.strToBuf(str));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// To Hex
|
||||||
|
|
||||||
|
Enc.bufToHex = function(u8) {
|
||||||
|
var hex = [];
|
||||||
|
var i, h;
|
||||||
|
var len = u8.byteLength || u8.length;
|
||||||
|
|
||||||
|
for (i = 0; i < len; i += 1) {
|
||||||
|
h = u8[i].toString(16);
|
||||||
|
if (2 !== h.length) {
|
||||||
|
h = '0' + h;
|
||||||
|
}
|
||||||
|
hex.push(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.join('').toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
Enc.numToHex = function(d) {
|
||||||
|
d = d.toString(16); // .padStart(2, '0');
|
||||||
|
if (d.length % 2) {
|
||||||
|
return '0' + d;
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
|
||||||
|
Enc.strToHex = function(str) {
|
||||||
|
return Enc._binToHex(Enc.strToBin(str));
|
||||||
|
};
|
||||||
|
|
||||||
|
Enc._binToHex = function(bin) {
|
||||||
|
return bin
|
||||||
|
.split('')
|
||||||
|
.map(function(ch) {
|
||||||
|
var h = ch.charCodeAt(0).toString(16);
|
||||||
|
if (2 !== h.length) {
|
||||||
|
h = '0' + h;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
// From Hex
|
||||||
|
|
||||||
|
Enc.hexToBuf = function(hex) {
|
||||||
|
var arr = [];
|
||||||
|
hex.match(/.{2}/g).forEach(function(h) {
|
||||||
|
arr.push(parseInt(h, 16));
|
||||||
|
});
|
||||||
|
return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr;
|
||||||
|
};
|
||||||
|
|
||||||
|
Enc.hexToStr = function(hex) {
|
||||||
|
return Enc.binToStr(_hexToBin(hex));
|
||||||
|
};
|
||||||
|
|
||||||
|
function _hexToBin(hex) {
|
||||||
|
return hex.replace(/([0-9A-F]{2})/gi, function(_, p1) {
|
||||||
|
return String.fromCharCode('0x' + p1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Enc._hexToBin = _hexToBin;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// to Binary String
|
||||||
|
|
||||||
|
Enc.bufToBin = function(buf) {
|
||||||
|
var bin = '';
|
||||||
|
// cannot use .map() because Uint8Array would return only 0s
|
||||||
|
buf.forEach(function(ch) {
|
||||||
|
bin += String.fromCharCode(ch);
|
||||||
|
});
|
||||||
|
return bin;
|
||||||
|
};
|
||||||
|
|
||||||
|
Enc.strToBin = function(str) {
|
||||||
|
// Note: TextEncoder might be faster (or it might be slower, I don't know),
|
||||||
|
// but it doesn't solve the double-utf8 problem and MS Edge still has users without it
|
||||||
|
var escstr = encodeURIComponent(str);
|
||||||
|
// replaces any uri escape sequence, such as %0A,
|
||||||
|
// with binary escape, such as 0x0A
|
||||||
|
var binstr = escstr.replace(/%([0-9A-F]{2})/g, function(_, p1) {
|
||||||
|
return String.fromCharCode('0x' + p1);
|
||||||
|
});
|
||||||
|
return binstr;
|
||||||
|
};
|
||||||
|
|
||||||
|
// to Buffer
|
||||||
|
|
||||||
|
Enc.binToBuf = function(bin) {
|
||||||
|
var arr = bin.split('').map(function(ch) {
|
||||||
|
return ch.charCodeAt(0);
|
||||||
|
});
|
||||||
|
return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr;
|
||||||
|
};
|
||||||
|
|
||||||
|
Enc.strToBuf = function(str) {
|
||||||
|
return Enc.binToBuf(Enc.strToBin(str));
|
||||||
|
};
|
||||||
|
|
||||||
|
// to Unicode String
|
||||||
|
|
||||||
|
Enc.binToStr = function(binstr) {
|
||||||
|
var escstr = binstr.replace(/(.)/g, function(m, p) {
|
||||||
|
var code = p
|
||||||
|
.charCodeAt(0)
|
||||||
|
.toString(16)
|
||||||
|
.toUpperCase();
|
||||||
|
if (code.length < 2) {
|
||||||
|
code = '0' + code;
|
||||||
|
}
|
||||||
|
return '%' + code;
|
||||||
|
});
|
||||||
|
|
||||||
|
return decodeURIComponent(escstr);
|
||||||
|
};
|
||||||
|
|
||||||
|
Enc.bufToStr = function(buf) {
|
||||||
|
return Enc.binToStr(Enc.bufToBin(buf));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base64 + Hex
|
||||||
|
|
||||||
|
Enc.base64ToHex = function(b64) {
|
||||||
|
return Enc.bufToHex(Enc.base64ToBuf(b64));
|
||||||
|
};
|
||||||
|
|
||||||
|
Enc.hexToBase64 = function(hex) {
|
||||||
|
return btoa(Enc._hexToBin(hex));
|
||||||
|
};
|
||||||
|
|
||||||
|
}());
|
||||||
|
;(function () {
|
||||||
|
'use strict';
|
||||||
|
var ASN1 = window.ASN1 = {};
|
||||||
|
var Enc = window.Encoding;
|
||||||
|
// Copyright 2018 AJ ONeal. All rights reserved
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Parser
|
||||||
|
//
|
||||||
|
|
||||||
|
// Although I've only seen 9 max in https certificates themselves,
|
||||||
|
// but each domain list could have up to 100
|
||||||
|
ASN1.ELOOPN = 102;
|
||||||
|
ASN1.ELOOP =
|
||||||
|
'uASN1.js Error: iterated over ' +
|
||||||
|
ASN1.ELOOPN +
|
||||||
|
'+ elements (probably a malformed file)';
|
||||||
|
// I've seen https certificates go 29 deep
|
||||||
|
ASN1.EDEEPN = 60;
|
||||||
|
ASN1.EDEEP =
|
||||||
|
'uASN1.js Error: element nested ' +
|
||||||
|
ASN1.EDEEPN +
|
||||||
|
'+ layers deep (probably a malformed file)';
|
||||||
|
// Container Types are Sequence 0x30, Container Array? (0xA0, 0xA1)
|
||||||
|
// Value Types are Boolean 0x01, Integer 0x02, Null 0x05, Object ID 0x06, String 0x0C, 0x16, 0x13, 0x1e Value Array? (0x82)
|
||||||
|
// Bit String (0x03) and Octet String (0x04) may be values or containers
|
||||||
|
// Sometimes Bit String is used as a container (RSA Pub Spki)
|
||||||
|
ASN1.CTYPES = [0x30, 0x31, 0xa0, 0xa1];
|
||||||
|
ASN1.VTYPES = [0x01, 0x02, 0x05, 0x06, 0x0c, 0x82];
|
||||||
|
ASN1.parseVerbose = function parseAsn1Helper(buf, opts) {
|
||||||
|
if (!opts) {
|
||||||
|
opts = {};
|
||||||
|
}
|
||||||
|
//var ws = ' ';
|
||||||
|
function parseAsn1(buf, depth, eager) {
|
||||||
|
if (depth.length >= ASN1.EDEEPN) {
|
||||||
|
throw new Error(ASN1.EDEEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = 2; // we know, at minimum, data starts after type (0) and lengthSize (1)
|
||||||
|
var asn1 = { type: buf[0], lengthSize: 0, length: buf[1] };
|
||||||
|
var child;
|
||||||
|
var iters = 0;
|
||||||
|
var adjust = 0;
|
||||||
|
var adjustedLen;
|
||||||
|
|
||||||
|
// Determine how many bytes the length uses, and what it is
|
||||||
|
if (0x80 & asn1.length) {
|
||||||
|
asn1.lengthSize = 0x7f & asn1.length;
|
||||||
|
// I think that buf->hex->int solves the problem of Endianness... not sure
|
||||||
|
asn1.length = parseInt(
|
||||||
|
Enc.bufToHex(buf.slice(index, index + asn1.lengthSize)),
|
||||||
|
16
|
||||||
|
);
|
||||||
|
index += asn1.lengthSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// High-order bit Integers have a leading 0x00 to signify that they are positive.
|
||||||
|
// Bit Streams use the first byte to signify padding, which x.509 doesn't use.
|
||||||
|
if (0x00 === buf[index] && (0x02 === asn1.type || 0x03 === asn1.type)) {
|
||||||
|
// However, 0x00 on its own is a valid number
|
||||||
|
if (asn1.length > 1) {
|
||||||
|
index += 1;
|
||||||
|
adjust = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adjustedLen = asn1.length + adjust;
|
||||||
|
|
||||||
|
//console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
|
||||||
|
|
||||||
|
function parseChildren(eager) {
|
||||||
|
asn1.children = [];
|
||||||
|
//console.warn('1 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', 0);
|
||||||
|
while (
|
||||||
|
iters < ASN1.ELOOPN &&
|
||||||
|
index < 2 + asn1.length + asn1.lengthSize
|
||||||
|
) {
|
||||||
|
iters += 1;
|
||||||
|
depth.length += 1;
|
||||||
|
child = parseAsn1(
|
||||||
|
buf.slice(index, index + adjustedLen),
|
||||||
|
depth,
|
||||||
|
eager
|
||||||
|
);
|
||||||
|
depth.length -= 1;
|
||||||
|
// The numbers don't match up exactly and I don't remember why...
|
||||||
|
// probably something with adjustedLen or some such, but the tests pass
|
||||||
|
index += 2 + child.lengthSize + child.length;
|
||||||
|
//console.warn('2 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', (2 + child.lengthSize + child.length));
|
||||||
|
if (index > 2 + asn1.lengthSize + asn1.length) {
|
||||||
|
if (!eager) {
|
||||||
|
console.error(JSON.stringify(asn1, ASN1._replacer, 2));
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
'Parse error: child value length (' +
|
||||||
|
child.length +
|
||||||
|
') is greater than remaining parent length (' +
|
||||||
|
(asn1.length - index) +
|
||||||
|
' = ' +
|
||||||
|
asn1.length +
|
||||||
|
' - ' +
|
||||||
|
index +
|
||||||
|
')'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
asn1.children.push(child);
|
||||||
|
//console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
|
||||||
|
}
|
||||||
|
if (index !== 2 + asn1.lengthSize + asn1.length) {
|
||||||
|
//console.warn('index:', index, 'length:', (2 + asn1.lengthSize + asn1.length));
|
||||||
|
throw new Error('premature end-of-file');
|
||||||
|
}
|
||||||
|
if (iters >= ASN1.ELOOPN) {
|
||||||
|
throw new Error(ASN1.ELOOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete asn1.value;
|
||||||
|
return asn1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse into types that are _always_ containers
|
||||||
|
if (-1 !== ASN1.CTYPES.indexOf(asn1.type)) {
|
||||||
|
return parseChildren(eager);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return types that are _always_ values
|
||||||
|
asn1.value = buf.slice(index, index + adjustedLen);
|
||||||
|
if (opts.json) {
|
||||||
|
asn1.value = Enc.bufToHex(asn1.value);
|
||||||
|
}
|
||||||
|
if (-1 !== ASN1.VTYPES.indexOf(asn1.type)) {
|
||||||
|
return asn1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For ambigious / unknown types, recurse and return on failure
|
||||||
|
// (and return child array size to zero)
|
||||||
|
try {
|
||||||
|
return parseChildren(true);
|
||||||
|
} catch (e) {
|
||||||
|
asn1.children.length = 0;
|
||||||
|
return asn1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var asn1 = parseAsn1(buf, []);
|
||||||
|
var len = buf.byteLength || buf.length;
|
||||||
|
if (len !== 2 + asn1.lengthSize + asn1.length) {
|
||||||
|
throw new Error(
|
||||||
|
'Length of buffer does not match length of ASN.1 sequence.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return asn1;
|
||||||
|
};
|
||||||
|
ASN1._toArray = function toArray(next, opts) {
|
||||||
|
var typ = opts.json ? Enc.numToHex(next.type) : next.type;
|
||||||
|
var val = next.value;
|
||||||
|
if (val) {
|
||||||
|
if ('string' !== typeof val && opts.json) {
|
||||||
|
val = Enc.bufToHex(val);
|
||||||
|
}
|
||||||
|
return [typ, val];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
typ,
|
||||||
|
next.children.map(function(child) {
|
||||||
|
return toArray(child, opts);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
ASN1.parse = function(opts) {
|
||||||
|
var opts2 = { json: false !== opts.json };
|
||||||
|
var verbose = ASN1.parseVerbose(opts.der, opts2);
|
||||||
|
if (opts.verbose) {
|
||||||
|
return verbose;
|
||||||
|
}
|
||||||
|
return ASN1._toArray(verbose, opts2);
|
||||||
|
};
|
||||||
|
ASN1._replacer = function(k, v) {
|
||||||
|
if ('type' === k) {
|
||||||
|
return '0x' + Enc.numToHex(v);
|
||||||
|
}
|
||||||
|
if (v && 'value' === k) {
|
||||||
|
return '0x' + Enc.bufToHex(v.data || v);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Packer
|
||||||
|
//
|
||||||
|
|
||||||
|
// Almost every ASN.1 type that's important for CSR
|
||||||
|
// can be represented generically with only a few rules.
|
||||||
|
function Any(/*type, hexstrings...*/) {
|
||||||
|
var args = Array.prototype.slice.call(arguments);
|
||||||
|
var typ = args.shift();
|
||||||
|
var str = args
|
||||||
|
.join('')
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
.toLowerCase();
|
||||||
|
var len = str.length / 2;
|
||||||
|
var lenlen = 0;
|
||||||
|
var hex = typ;
|
||||||
|
if ('number' === typeof hex) {
|
||||||
|
hex = Enc.numToHex(hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can't have an odd number of hex chars
|
||||||
|
if (len !== Math.round(len)) {
|
||||||
|
throw new Error('invalid hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first byte of any ASN.1 sequence is the type (Sequence, Integer, etc)
|
||||||
|
// The second byte is either the size of the value, or the size of its size
|
||||||
|
|
||||||
|
// 1. If the second byte is < 0x80 (128) it is considered the size
|
||||||
|
// 2. If it is > 0x80 then it describes the number of bytes of the size
|
||||||
|
// ex: 0x82 means the next 2 bytes describe the size of the value
|
||||||
|
// 3. The special case of exactly 0x80 is "indefinite" length (to end-of-file)
|
||||||
|
|
||||||
|
if (len > 127) {
|
||||||
|
lenlen += 1;
|
||||||
|
while (len > 255) {
|
||||||
|
lenlen += 1;
|
||||||
|
len = len >> 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lenlen) {
|
||||||
|
hex += Enc.numToHex(0x80 + lenlen);
|
||||||
|
}
|
||||||
|
return hex + Enc.numToHex(str.length / 2) + str;
|
||||||
|
}
|
||||||
|
ASN1.Any = Any;
|
||||||
|
|
||||||
|
// The Integer type has some special rules
|
||||||
|
ASN1.UInt = function UINT() {
|
||||||
|
var str = Array.prototype.slice.call(arguments).join('');
|
||||||
|
var first = parseInt(str.slice(0, 2), 16);
|
||||||
|
|
||||||
|
// If the first byte is 0x80 or greater, the number is considered negative
|
||||||
|
// Therefore we add a '00' prefix if the 0x80 bit is set
|
||||||
|
if (0x80 & first) {
|
||||||
|
str = '00' + str;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Any('02', str);
|
||||||
|
};
|
||||||
|
|
||||||
|
// The Bit String type also has a special rule
|
||||||
|
ASN1.BitStr = function BITSTR() {
|
||||||
|
var str = Array.prototype.slice.call(arguments).join('');
|
||||||
|
// '00' is a mask of how many bits of the next byte to ignore
|
||||||
|
return Any('03', '00' + str);
|
||||||
|
};
|
||||||
|
|
||||||
|
ASN1._toArray = function toArray(next, opts) {
|
||||||
|
var typ = opts.json ? Enc.numToHex(next.type) : next.type;
|
||||||
|
var val = next.value;
|
||||||
|
if (val) {
|
||||||
|
if ('string' !== typeof val && opts.json) {
|
||||||
|
val = Enc.bufToHex(val);
|
||||||
|
}
|
||||||
|
return [typ, val];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
typ,
|
||||||
|
next.children.map(function(child) {
|
||||||
|
return toArray(child, opts);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
ASN1._pack = function(arr) {
|
||||||
|
var typ = arr[0];
|
||||||
|
if ('number' === typeof arr[0]) {
|
||||||
|
typ = Enc.numToHex(arr[0]);
|
||||||
|
}
|
||||||
|
var str = '';
|
||||||
|
if (Array.isArray(arr[1])) {
|
||||||
|
arr[1].forEach(function(a) {
|
||||||
|
str += ASN1._pack(a);
|
||||||
|
});
|
||||||
|
} else if ('string' === typeof arr[1]) {
|
||||||
|
str = arr[1];
|
||||||
|
} else if (arr[1].byteLength) {
|
||||||
|
str = Enc.bufToHex(arr[1]);
|
||||||
|
} else {
|
||||||
|
throw new Error('unexpected array');
|
||||||
|
}
|
||||||
|
if ('03' === typ) {
|
||||||
|
return ASN1.BitStr(str);
|
||||||
|
} else if ('02' === typ) {
|
||||||
|
return ASN1.UInt(str);
|
||||||
|
} else {
|
||||||
|
return Any(typ, str);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO should this return a buffer?
|
||||||
|
ASN1.pack = function(asn1, opts) {
|
||||||
|
if (!opts) {
|
||||||
|
opts = {};
|
||||||
|
}
|
||||||
|
if (!Array.isArray(asn1)) {
|
||||||
|
asn1 = ASN1._toArray(asn1, { json: true });
|
||||||
|
}
|
||||||
|
var result = ASN1._pack(asn1);
|
||||||
|
if (opts.json) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return Enc.hexToBuf(result);
|
||||||
|
};
|
||||||
|
}());
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,319 @@
|
||||||
|
;(function () {
|
||||||
|
'use strict';
|
||||||
|
var ASN1 = window.ASN1 = {};
|
||||||
|
var Enc = window.Encoding;
|
||||||
|
// Copyright 2018 AJ ONeal. All rights reserved
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Parser
|
||||||
|
//
|
||||||
|
|
||||||
|
// Although I've only seen 9 max in https certificates themselves,
|
||||||
|
// but each domain list could have up to 100
|
||||||
|
ASN1.ELOOPN = 102;
|
||||||
|
ASN1.ELOOP =
|
||||||
|
'uASN1.js Error: iterated over ' +
|
||||||
|
ASN1.ELOOPN +
|
||||||
|
'+ elements (probably a malformed file)';
|
||||||
|
// I've seen https certificates go 29 deep
|
||||||
|
ASN1.EDEEPN = 60;
|
||||||
|
ASN1.EDEEP =
|
||||||
|
'uASN1.js Error: element nested ' +
|
||||||
|
ASN1.EDEEPN +
|
||||||
|
'+ layers deep (probably a malformed file)';
|
||||||
|
// Container Types are Sequence 0x30, Container Array? (0xA0, 0xA1)
|
||||||
|
// Value Types are Boolean 0x01, Integer 0x02, Null 0x05, Object ID 0x06, String 0x0C, 0x16, 0x13, 0x1e Value Array? (0x82)
|
||||||
|
// Bit String (0x03) and Octet String (0x04) may be values or containers
|
||||||
|
// Sometimes Bit String is used as a container (RSA Pub Spki)
|
||||||
|
ASN1.CTYPES = [0x30, 0x31, 0xa0, 0xa1];
|
||||||
|
ASN1.VTYPES = [0x01, 0x02, 0x05, 0x06, 0x0c, 0x82];
|
||||||
|
ASN1.parseVerbose = function parseAsn1Helper(buf, opts) {
|
||||||
|
if (!opts) {
|
||||||
|
opts = {};
|
||||||
|
}
|
||||||
|
//var ws = ' ';
|
||||||
|
function parseAsn1(buf, depth, eager) {
|
||||||
|
if (depth.length >= ASN1.EDEEPN) {
|
||||||
|
throw new Error(ASN1.EDEEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = 2; // we know, at minimum, data starts after type (0) and lengthSize (1)
|
||||||
|
var asn1 = { type: buf[0], lengthSize: 0, length: buf[1] };
|
||||||
|
var child;
|
||||||
|
var iters = 0;
|
||||||
|
var adjust = 0;
|
||||||
|
var adjustedLen;
|
||||||
|
|
||||||
|
// Determine how many bytes the length uses, and what it is
|
||||||
|
if (0x80 & asn1.length) {
|
||||||
|
asn1.lengthSize = 0x7f & asn1.length;
|
||||||
|
// I think that buf->hex->int solves the problem of Endianness... not sure
|
||||||
|
asn1.length = parseInt(
|
||||||
|
Enc.bufToHex(buf.slice(index, index + asn1.lengthSize)),
|
||||||
|
16
|
||||||
|
);
|
||||||
|
index += asn1.lengthSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// High-order bit Integers have a leading 0x00 to signify that they are positive.
|
||||||
|
// Bit Streams use the first byte to signify padding, which x.509 doesn't use.
|
||||||
|
if (0x00 === buf[index] && (0x02 === asn1.type || 0x03 === asn1.type)) {
|
||||||
|
// However, 0x00 on its own is a valid number
|
||||||
|
if (asn1.length > 1) {
|
||||||
|
index += 1;
|
||||||
|
adjust = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adjustedLen = asn1.length + adjust;
|
||||||
|
|
||||||
|
//console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
|
||||||
|
|
||||||
|
function parseChildren(eager) {
|
||||||
|
asn1.children = [];
|
||||||
|
//console.warn('1 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', 0);
|
||||||
|
while (
|
||||||
|
iters < ASN1.ELOOPN &&
|
||||||
|
index < 2 + asn1.length + asn1.lengthSize
|
||||||
|
) {
|
||||||
|
iters += 1;
|
||||||
|
depth.length += 1;
|
||||||
|
child = parseAsn1(
|
||||||
|
buf.slice(index, index + adjustedLen),
|
||||||
|
depth,
|
||||||
|
eager
|
||||||
|
);
|
||||||
|
depth.length -= 1;
|
||||||
|
// The numbers don't match up exactly and I don't remember why...
|
||||||
|
// probably something with adjustedLen or some such, but the tests pass
|
||||||
|
index += 2 + child.lengthSize + child.length;
|
||||||
|
//console.warn('2 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', (2 + child.lengthSize + child.length));
|
||||||
|
if (index > 2 + asn1.lengthSize + asn1.length) {
|
||||||
|
if (!eager) {
|
||||||
|
console.error(JSON.stringify(asn1, ASN1._replacer, 2));
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
'Parse error: child value length (' +
|
||||||
|
child.length +
|
||||||
|
') is greater than remaining parent length (' +
|
||||||
|
(asn1.length - index) +
|
||||||
|
' = ' +
|
||||||
|
asn1.length +
|
||||||
|
' - ' +
|
||||||
|
index +
|
||||||
|
')'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
asn1.children.push(child);
|
||||||
|
//console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
|
||||||
|
}
|
||||||
|
if (index !== 2 + asn1.lengthSize + asn1.length) {
|
||||||
|
//console.warn('index:', index, 'length:', (2 + asn1.lengthSize + asn1.length));
|
||||||
|
throw new Error('premature end-of-file');
|
||||||
|
}
|
||||||
|
if (iters >= ASN1.ELOOPN) {
|
||||||
|
throw new Error(ASN1.ELOOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete asn1.value;
|
||||||
|
return asn1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse into types that are _always_ containers
|
||||||
|
if (-1 !== ASN1.CTYPES.indexOf(asn1.type)) {
|
||||||
|
return parseChildren(eager);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return types that are _always_ values
|
||||||
|
asn1.value = buf.slice(index, index + adjustedLen);
|
||||||
|
if (opts.json) {
|
||||||
|
asn1.value = Enc.bufToHex(asn1.value);
|
||||||
|
}
|
||||||
|
if (-1 !== ASN1.VTYPES.indexOf(asn1.type)) {
|
||||||
|
return asn1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For ambigious / unknown types, recurse and return on failure
|
||||||
|
// (and return child array size to zero)
|
||||||
|
try {
|
||||||
|
return parseChildren(true);
|
||||||
|
} catch (e) {
|
||||||
|
asn1.children.length = 0;
|
||||||
|
return asn1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var asn1 = parseAsn1(buf, []);
|
||||||
|
var len = buf.byteLength || buf.length;
|
||||||
|
if (len !== 2 + asn1.lengthSize + asn1.length) {
|
||||||
|
throw new Error(
|
||||||
|
'Length of buffer does not match length of ASN.1 sequence.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return asn1;
|
||||||
|
};
|
||||||
|
ASN1._toArray = function toArray(next, opts) {
|
||||||
|
var typ = opts.json ? Enc.numToHex(next.type) : next.type;
|
||||||
|
var val = next.value;
|
||||||
|
if (val) {
|
||||||
|
if ('string' !== typeof val && opts.json) {
|
||||||
|
val = Enc.bufToHex(val);
|
||||||
|
}
|
||||||
|
return [typ, val];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
typ,
|
||||||
|
next.children.map(function(child) {
|
||||||
|
return toArray(child, opts);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
ASN1.parse = function(opts) {
|
||||||
|
var opts2 = { json: false !== opts.json };
|
||||||
|
var verbose = ASN1.parseVerbose(opts.der, opts2);
|
||||||
|
if (opts.verbose) {
|
||||||
|
return verbose;
|
||||||
|
}
|
||||||
|
return ASN1._toArray(verbose, opts2);
|
||||||
|
};
|
||||||
|
ASN1._replacer = function(k, v) {
|
||||||
|
if ('type' === k) {
|
||||||
|
return '0x' + Enc.numToHex(v);
|
||||||
|
}
|
||||||
|
if (v && 'value' === k) {
|
||||||
|
return '0x' + Enc.bufToHex(v.data || v);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Packer
|
||||||
|
//
|
||||||
|
|
||||||
|
// Almost every ASN.1 type that's important for CSR
|
||||||
|
// can be represented generically with only a few rules.
|
||||||
|
function Any(/*type, hexstrings...*/) {
|
||||||
|
var args = Array.prototype.slice.call(arguments);
|
||||||
|
var typ = args.shift();
|
||||||
|
var str = args
|
||||||
|
.join('')
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
.toLowerCase();
|
||||||
|
var len = str.length / 2;
|
||||||
|
var lenlen = 0;
|
||||||
|
var hex = typ;
|
||||||
|
if ('number' === typeof hex) {
|
||||||
|
hex = Enc.numToHex(hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can't have an odd number of hex chars
|
||||||
|
if (len !== Math.round(len)) {
|
||||||
|
throw new Error('invalid hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first byte of any ASN.1 sequence is the type (Sequence, Integer, etc)
|
||||||
|
// The second byte is either the size of the value, or the size of its size
|
||||||
|
|
||||||
|
// 1. If the second byte is < 0x80 (128) it is considered the size
|
||||||
|
// 2. If it is > 0x80 then it describes the number of bytes of the size
|
||||||
|
// ex: 0x82 means the next 2 bytes describe the size of the value
|
||||||
|
// 3. The special case of exactly 0x80 is "indefinite" length (to end-of-file)
|
||||||
|
|
||||||
|
if (len > 127) {
|
||||||
|
lenlen += 1;
|
||||||
|
while (len > 255) {
|
||||||
|
lenlen += 1;
|
||||||
|
len = len >> 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lenlen) {
|
||||||
|
hex += Enc.numToHex(0x80 + lenlen);
|
||||||
|
}
|
||||||
|
return hex + Enc.numToHex(str.length / 2) + str;
|
||||||
|
}
|
||||||
|
ASN1.Any = Any;
|
||||||
|
|
||||||
|
// The Integer type has some special rules
|
||||||
|
ASN1.UInt = function UINT() {
|
||||||
|
var str = Array.prototype.slice.call(arguments).join('');
|
||||||
|
var first = parseInt(str.slice(0, 2), 16);
|
||||||
|
|
||||||
|
// If the first byte is 0x80 or greater, the number is considered negative
|
||||||
|
// Therefore we add a '00' prefix if the 0x80 bit is set
|
||||||
|
if (0x80 & first) {
|
||||||
|
str = '00' + str;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Any('02', str);
|
||||||
|
};
|
||||||
|
|
||||||
|
// The Bit String type also has a special rule
|
||||||
|
ASN1.BitStr = function BITSTR() {
|
||||||
|
var str = Array.prototype.slice.call(arguments).join('');
|
||||||
|
// '00' is a mask of how many bits of the next byte to ignore
|
||||||
|
return Any('03', '00' + str);
|
||||||
|
};
|
||||||
|
|
||||||
|
ASN1._toArray = function toArray(next, opts) {
|
||||||
|
var typ = opts.json ? Enc.numToHex(next.type) : next.type;
|
||||||
|
var val = next.value;
|
||||||
|
if (val) {
|
||||||
|
if ('string' !== typeof val && opts.json) {
|
||||||
|
val = Enc.bufToHex(val);
|
||||||
|
}
|
||||||
|
return [typ, val];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
typ,
|
||||||
|
next.children.map(function(child) {
|
||||||
|
return toArray(child, opts);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
ASN1._pack = function(arr) {
|
||||||
|
var typ = arr[0];
|
||||||
|
if ('number' === typeof arr[0]) {
|
||||||
|
typ = Enc.numToHex(arr[0]);
|
||||||
|
}
|
||||||
|
var str = '';
|
||||||
|
if (Array.isArray(arr[1])) {
|
||||||
|
arr[1].forEach(function(a) {
|
||||||
|
str += ASN1._pack(a);
|
||||||
|
});
|
||||||
|
} else if ('string' === typeof arr[1]) {
|
||||||
|
str = arr[1];
|
||||||
|
} else if (arr[1].byteLength) {
|
||||||
|
str = Enc.bufToHex(arr[1]);
|
||||||
|
} else {
|
||||||
|
throw new Error('unexpected array');
|
||||||
|
}
|
||||||
|
if ('03' === typ) {
|
||||||
|
return ASN1.BitStr(str);
|
||||||
|
} else if ('02' === typ) {
|
||||||
|
return ASN1.UInt(str);
|
||||||
|
} else {
|
||||||
|
return Any(typ, str);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO should this return a buffer?
|
||||||
|
ASN1.pack = function(asn1, opts) {
|
||||||
|
if (!opts) {
|
||||||
|
opts = {};
|
||||||
|
}
|
||||||
|
if (!Array.isArray(asn1)) {
|
||||||
|
asn1 = ASN1._toArray(asn1, { json: true });
|
||||||
|
}
|
||||||
|
var result = ASN1._pack(asn1);
|
||||||
|
if (opts.json) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return Enc.hexToBuf(result);
|
||||||
|
};
|
||||||
|
}());
|
|
@ -0,0 +1 @@
|
||||||
|
(function(){"use strict";var ASN1=window.ASN1={};var Enc=window.Encoding;ASN1.ELOOPN=102;ASN1.ELOOP="uASN1.js Error: iterated over "+ASN1.ELOOPN+"+ elements (probably a malformed file)";ASN1.EDEEPN=60;ASN1.EDEEP="uASN1.js Error: element nested "+ASN1.EDEEPN+"+ layers deep (probably a malformed file)";ASN1.CTYPES=[48,49,160,161];ASN1.VTYPES=[1,2,5,6,12,130];ASN1.parseVerbose=function parseAsn1Helper(buf,opts){if(!opts){opts={}}function parseAsn1(buf,depth,eager){if(depth.length>=ASN1.EDEEPN){throw new Error(ASN1.EDEEP)}var index=2;var asn1={type:buf[0],lengthSize:0,length:buf[1]};var child;var iters=0;var adjust=0;var adjustedLen;if(128&asn1.length){asn1.lengthSize=127&asn1.length;asn1.length=parseInt(Enc.bufToHex(buf.slice(index,index+asn1.lengthSize)),16);index+=asn1.lengthSize}if(0===buf[index]&&(2===asn1.type||3===asn1.type)){if(asn1.length>1){index+=1;adjust=-1}}adjustedLen=asn1.length+adjust;function parseChildren(eager){asn1.children=[];while(iters<ASN1.ELOOPN&&index<2+asn1.length+asn1.lengthSize){iters+=1;depth.length+=1;child=parseAsn1(buf.slice(index,index+adjustedLen),depth,eager);depth.length-=1;index+=2+child.lengthSize+child.length;if(index>2+asn1.lengthSize+asn1.length){if(!eager){console.error(JSON.stringify(asn1,ASN1._replacer,2))}throw new Error("Parse error: child value length ("+child.length+") is greater than remaining parent length ("+(asn1.length-index)+" = "+asn1.length+" - "+index+")")}asn1.children.push(child)}if(index!==2+asn1.lengthSize+asn1.length){throw new Error("premature end-of-file")}if(iters>=ASN1.ELOOPN){throw new Error(ASN1.ELOOP)}delete asn1.value;return asn1}if(-1!==ASN1.CTYPES.indexOf(asn1.type)){return parseChildren(eager)}asn1.value=buf.slice(index,index+adjustedLen);if(opts.json){asn1.value=Enc.bufToHex(asn1.value)}if(-1!==ASN1.VTYPES.indexOf(asn1.type)){return asn1}try{return parseChildren(true)}catch(e){asn1.children.length=0;return asn1}}var asn1=parseAsn1(buf,[]);var len=buf.byteLength||buf.length;if(len!==2+asn1.lengthSize+asn1.length){throw new Error("Length of buffer does not match length of ASN.1 sequence.")}return asn1};ASN1._toArray=function toArray(next,opts){var typ=opts.json?Enc.numToHex(next.type):next.type;var val=next.value;if(val){if("string"!==typeof val&&opts.json){val=Enc.bufToHex(val)}return[typ,val]}return[typ,next.children.map(function(child){return toArray(child,opts)})]};ASN1.parse=function(opts){var opts2={json:false!==opts.json};var verbose=ASN1.parseVerbose(opts.der,opts2);if(opts.verbose){return verbose}return ASN1._toArray(verbose,opts2)};ASN1._replacer=function(k,v){if("type"===k){return"0x"+Enc.numToHex(v)}if(v&&"value"===k){return"0x"+Enc.bufToHex(v.data||v)}return v};function Any(){var args=Array.prototype.slice.call(arguments);var typ=args.shift();var str=args.join("").replace(/\s+/g,"").toLowerCase();var len=str.length/2;var lenlen=0;var hex=typ;if("number"===typeof hex){hex=Enc.numToHex(hex)}if(len!==Math.round(len)){throw new Error("invalid hex")}if(len>127){lenlen+=1;while(len>255){lenlen+=1;len=len>>8}}if(lenlen){hex+=Enc.numToHex(128+lenlen)}return hex+Enc.numToHex(str.length/2)+str}ASN1.Any=Any;ASN1.UInt=function UINT(){var str=Array.prototype.slice.call(arguments).join("");var first=parseInt(str.slice(0,2),16);if(128&first){str="00"+str}return Any("02",str)};ASN1.BitStr=function BITSTR(){var str=Array.prototype.slice.call(arguments).join("");return Any("03","00"+str)};ASN1._toArray=function toArray(next,opts){var typ=opts.json?Enc.numToHex(next.type):next.type;var val=next.value;if(val){if("string"!==typeof val&&opts.json){val=Enc.bufToHex(val)}return[typ,val]}return[typ,next.children.map(function(child){return toArray(child,opts)})]};ASN1._pack=function(arr){var typ=arr[0];if("number"===typeof arr[0]){typ=Enc.numToHex(arr[0])}var str="";if(Array.isArray(arr[1])){arr[1].forEach(function(a){str+=ASN1._pack(a)})}else if("string"===typeof arr[1]){str=arr[1]}else if(arr[1].byteLength){str=Enc.bufToHex(arr[1])}else{throw new Error("unexpected array")}if("03"===typ){return ASN1.BitStr(str)}else if("02"===typ){return ASN1.UInt(str)}else{return Any(typ,str)}};ASN1.pack=function(asn1,opts){if(!opts){opts={}}if(!Array.isArray(asn1)){asn1=ASN1._toArray(asn1,{json:true})}var result=ASN1._pack(asn1);if(opts.json){return result}return Enc.hexToBuf(result)}})();
|
|
@ -0,0 +1,8 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="./asn1.all.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,11 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var ASN1 = module.exports;
|
||||||
|
var packer = require('./packer.js');
|
||||||
|
var parser = require('./parser.js');
|
||||||
|
Object.keys(parser).forEach(function(key) {
|
||||||
|
ASN1[key] = parser[key];
|
||||||
|
});
|
||||||
|
Object.keys(packer).forEach(function(key) {
|
||||||
|
ASN1[key] = packer[key];
|
||||||
|
});
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "@root/asn1",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"requires": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@root/encoding": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@root/encoding/-/encoding-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ=="
|
||||||
|
},
|
||||||
|
"@root/pem": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@root/pem/-/pem-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-6iFwsbwm6YzWdfjogHzLTYkA1KWdeEkutVX2BBVfhyWoE9q0vp89G7mAcLIhi0QTRd199AMOacHWFq+gTyQkVA==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"name": "@root/asn1",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "VanillaJS, Lightweight, Zero-Dependency, ASN.1 encoder and decoder.",
|
||||||
|
"main": "index.js",
|
||||||
|
"browser": {
|
||||||
|
"./node/native.js": "./browser/native.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"*.js",
|
||||||
|
"node",
|
||||||
|
"browser",
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "node tests"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.rootprojects.org/root/asn1.js.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"ASN.1",
|
||||||
|
"asn1",
|
||||||
|
"x509",
|
||||||
|
"PEM"
|
||||||
|
],
|
||||||
|
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@root/pem": "^1.0.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@root/encoding": "^1.0.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var ASN1 = module.exports;
|
||||||
|
var Enc = require('@root/encoding/hex');
|
||||||
|
|
||||||
|
//
|
||||||
|
// Packer
|
||||||
|
//
|
||||||
|
|
||||||
|
// Almost every ASN.1 type that's important for CSR
|
||||||
|
// can be represented generically with only a few rules.
|
||||||
|
function Any(/*type, hexstrings...*/) {
|
||||||
|
var args = Array.prototype.slice.call(arguments);
|
||||||
|
var typ = args.shift();
|
||||||
|
var str = args
|
||||||
|
.join('')
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
.toLowerCase();
|
||||||
|
var len = str.length / 2;
|
||||||
|
var lenlen = 0;
|
||||||
|
var hex = typ;
|
||||||
|
if ('number' === typeof hex) {
|
||||||
|
hex = Enc.numToHex(hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can't have an odd number of hex chars
|
||||||
|
if (len !== Math.round(len)) {
|
||||||
|
throw new Error('invalid hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first byte of any ASN.1 sequence is the type (Sequence, Integer, etc)
|
||||||
|
// The second byte is either the size of the value, or the size of its size
|
||||||
|
|
||||||
|
// 1. If the second byte is < 0x80 (128) it is considered the size
|
||||||
|
// 2. If it is > 0x80 then it describes the number of bytes of the size
|
||||||
|
// ex: 0x82 means the next 2 bytes describe the size of the value
|
||||||
|
// 3. The special case of exactly 0x80 is "indefinite" length (to end-of-file)
|
||||||
|
|
||||||
|
if (len > 127) {
|
||||||
|
lenlen += 1;
|
||||||
|
while (len > 255) {
|
||||||
|
lenlen += 1;
|
||||||
|
len = len >> 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lenlen) {
|
||||||
|
hex += Enc.numToHex(0x80 + lenlen);
|
||||||
|
}
|
||||||
|
return hex + Enc.numToHex(str.length / 2) + str;
|
||||||
|
}
|
||||||
|
ASN1.Any = Any;
|
||||||
|
|
||||||
|
// The Integer type has some special rules
|
||||||
|
ASN1.UInt = function UINT() {
|
||||||
|
var str = Array.prototype.slice.call(arguments).join('');
|
||||||
|
var first = parseInt(str.slice(0, 2), 16);
|
||||||
|
|
||||||
|
// If the first byte is 0x80 or greater, the number is considered negative
|
||||||
|
// Therefore we add a '00' prefix if the 0x80 bit is set
|
||||||
|
if (0x80 & first) {
|
||||||
|
str = '00' + str;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Any('02', str);
|
||||||
|
};
|
||||||
|
|
||||||
|
// The Bit String type also has a special rule
|
||||||
|
ASN1.BitStr = function BITSTR() {
|
||||||
|
var str = Array.prototype.slice.call(arguments).join('');
|
||||||
|
// '00' is a mask of how many bits of the next byte to ignore
|
||||||
|
return Any('03', '00' + str);
|
||||||
|
};
|
||||||
|
|
||||||
|
ASN1._toArray = function toArray(next, opts) {
|
||||||
|
var typ = opts.json ? Enc.numToHex(next.type) : next.type;
|
||||||
|
var val = next.value;
|
||||||
|
if (val) {
|
||||||
|
if ('string' !== typeof val && opts.json) {
|
||||||
|
val = Enc.bufToHex(val);
|
||||||
|
}
|
||||||
|
return [typ, val];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
typ,
|
||||||
|
next.children.map(function(child) {
|
||||||
|
return toArray(child, opts);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
ASN1._pack = function(arr) {
|
||||||
|
var typ = arr[0];
|
||||||
|
if ('number' === typeof arr[0]) {
|
||||||
|
typ = Enc.numToHex(arr[0]);
|
||||||
|
}
|
||||||
|
var str = '';
|
||||||
|
if (Array.isArray(arr[1])) {
|
||||||
|
arr[1].forEach(function(a) {
|
||||||
|
str += ASN1._pack(a);
|
||||||
|
});
|
||||||
|
} else if ('string' === typeof arr[1]) {
|
||||||
|
str = arr[1];
|
||||||
|
} else if (arr[1].byteLength) {
|
||||||
|
str = Enc.bufToHex(arr[1]);
|
||||||
|
} else {
|
||||||
|
throw new Error('unexpected array');
|
||||||
|
}
|
||||||
|
if ('03' === typ) {
|
||||||
|
return ASN1.BitStr(str);
|
||||||
|
} else if ('02' === typ) {
|
||||||
|
return ASN1.UInt(str);
|
||||||
|
} else {
|
||||||
|
return Any(typ, str);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO should this return a buffer?
|
||||||
|
ASN1.pack = function(asn1, opts) {
|
||||||
|
if (!opts) {
|
||||||
|
opts = {};
|
||||||
|
}
|
||||||
|
if (!Array.isArray(asn1)) {
|
||||||
|
asn1 = ASN1._toArray(asn1, { json: true });
|
||||||
|
}
|
||||||
|
var result = ASN1._pack(asn1);
|
||||||
|
if (opts.json) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return Enc.hexToBuf(result);
|
||||||
|
};
|
|
@ -0,0 +1,189 @@
|
||||||
|
// Copyright 2018 AJ ONeal. All rights reserved
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var ASN1 = module.exports;
|
||||||
|
var Enc = require('@root/encoding/hex');
|
||||||
|
|
||||||
|
//
|
||||||
|
// Parser
|
||||||
|
//
|
||||||
|
|
||||||
|
// Although I've only seen 9 max in https certificates themselves,
|
||||||
|
// but each domain list could have up to 100
|
||||||
|
ASN1.ELOOPN = 102;
|
||||||
|
ASN1.ELOOP =
|
||||||
|
'uASN1.js Error: iterated over ' +
|
||||||
|
ASN1.ELOOPN +
|
||||||
|
'+ elements (probably a malformed file)';
|
||||||
|
// I've seen https certificates go 29 deep
|
||||||
|
ASN1.EDEEPN = 60;
|
||||||
|
ASN1.EDEEP =
|
||||||
|
'uASN1.js Error: element nested ' +
|
||||||
|
ASN1.EDEEPN +
|
||||||
|
'+ layers deep (probably a malformed file)';
|
||||||
|
// Container Types are Sequence 0x30, Container Array? (0xA0, 0xA1)
|
||||||
|
// Value Types are Boolean 0x01, Integer 0x02, Null 0x05, Object ID 0x06, String 0x0C, 0x16, 0x13, 0x1e Value Array? (0x82)
|
||||||
|
// Bit String (0x03) and Octet String (0x04) may be values or containers
|
||||||
|
// Sometimes Bit String is used as a container (RSA Pub Spki)
|
||||||
|
ASN1.CTYPES = [0x30, 0x31, 0xa0, 0xa1];
|
||||||
|
ASN1.VTYPES = [0x01, 0x02, 0x05, 0x06, 0x0c, 0x82];
|
||||||
|
ASN1.parseVerbose = function parseAsn1Helper(buf, opts) {
|
||||||
|
if (!opts) {
|
||||||
|
opts = {};
|
||||||
|
}
|
||||||
|
//var ws = ' ';
|
||||||
|
function parseAsn1(buf, depth, eager) {
|
||||||
|
if (depth.length >= ASN1.EDEEPN) {
|
||||||
|
throw new Error(ASN1.EDEEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = 2; // we know, at minimum, data starts after type (0) and lengthSize (1)
|
||||||
|
var asn1 = { type: buf[0], lengthSize: 0, length: buf[1] };
|
||||||
|
var child;
|
||||||
|
var iters = 0;
|
||||||
|
var adjust = 0;
|
||||||
|
var adjustedLen;
|
||||||
|
|
||||||
|
// Determine how many bytes the length uses, and what it is
|
||||||
|
if (0x80 & asn1.length) {
|
||||||
|
asn1.lengthSize = 0x7f & asn1.length;
|
||||||
|
// I think that buf->hex->int solves the problem of Endianness... not sure
|
||||||
|
asn1.length = parseInt(
|
||||||
|
Enc.bufToHex(buf.slice(index, index + asn1.lengthSize)),
|
||||||
|
16
|
||||||
|
);
|
||||||
|
index += asn1.lengthSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// High-order bit Integers have a leading 0x00 to signify that they are positive.
|
||||||
|
// Bit Streams use the first byte to signify padding, which x.509 doesn't use.
|
||||||
|
if (0x00 === buf[index] && (0x02 === asn1.type || 0x03 === asn1.type)) {
|
||||||
|
// However, 0x00 on its own is a valid number
|
||||||
|
if (asn1.length > 1) {
|
||||||
|
index += 1;
|
||||||
|
adjust = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adjustedLen = asn1.length + adjust;
|
||||||
|
|
||||||
|
//console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
|
||||||
|
|
||||||
|
function parseChildren(eager) {
|
||||||
|
asn1.children = [];
|
||||||
|
//console.warn('1 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', 0);
|
||||||
|
while (
|
||||||
|
iters < ASN1.ELOOPN &&
|
||||||
|
index < 2 + asn1.length + asn1.lengthSize
|
||||||
|
) {
|
||||||
|
iters += 1;
|
||||||
|
depth.length += 1;
|
||||||
|
child = parseAsn1(
|
||||||
|
buf.slice(index, index + adjustedLen),
|
||||||
|
depth,
|
||||||
|
eager
|
||||||
|
);
|
||||||
|
depth.length -= 1;
|
||||||
|
// The numbers don't match up exactly and I don't remember why...
|
||||||
|
// probably something with adjustedLen or some such, but the tests pass
|
||||||
|
index += 2 + child.lengthSize + child.length;
|
||||||
|
//console.warn('2 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', (2 + child.lengthSize + child.length));
|
||||||
|
if (index > 2 + asn1.lengthSize + asn1.length) {
|
||||||
|
if (!eager) {
|
||||||
|
console.error(JSON.stringify(asn1, ASN1._replacer, 2));
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
'Parse error: child value length (' +
|
||||||
|
child.length +
|
||||||
|
') is greater than remaining parent length (' +
|
||||||
|
(asn1.length - index) +
|
||||||
|
' = ' +
|
||||||
|
asn1.length +
|
||||||
|
' - ' +
|
||||||
|
index +
|
||||||
|
')'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
asn1.children.push(child);
|
||||||
|
//console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
|
||||||
|
}
|
||||||
|
if (index !== 2 + asn1.lengthSize + asn1.length) {
|
||||||
|
//console.warn('index:', index, 'length:', (2 + asn1.lengthSize + asn1.length));
|
||||||
|
throw new Error('premature end-of-file');
|
||||||
|
}
|
||||||
|
if (iters >= ASN1.ELOOPN) {
|
||||||
|
throw new Error(ASN1.ELOOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete asn1.value;
|
||||||
|
return asn1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse into types that are _always_ containers
|
||||||
|
if (-1 !== ASN1.CTYPES.indexOf(asn1.type)) {
|
||||||
|
return parseChildren(eager);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return types that are _always_ values
|
||||||
|
asn1.value = buf.slice(index, index + adjustedLen);
|
||||||
|
if (opts.json) {
|
||||||
|
asn1.value = Enc.bufToHex(asn1.value);
|
||||||
|
}
|
||||||
|
if (-1 !== ASN1.VTYPES.indexOf(asn1.type)) {
|
||||||
|
return asn1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For ambigious / unknown types, recurse and return on failure
|
||||||
|
// (and return child array size to zero)
|
||||||
|
try {
|
||||||
|
return parseChildren(true);
|
||||||
|
} catch (e) {
|
||||||
|
asn1.children.length = 0;
|
||||||
|
return asn1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var asn1 = parseAsn1(buf, []);
|
||||||
|
var len = buf.byteLength || buf.length;
|
||||||
|
if (len !== 2 + asn1.lengthSize + asn1.length) {
|
||||||
|
throw new Error(
|
||||||
|
'Length of buffer does not match length of ASN.1 sequence.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return asn1;
|
||||||
|
};
|
||||||
|
ASN1._toArray = function toArray(next, opts) {
|
||||||
|
var typ = opts.json ? Enc.numToHex(next.type) : next.type;
|
||||||
|
var val = next.value;
|
||||||
|
if (val) {
|
||||||
|
if ('string' !== typeof val && opts.json) {
|
||||||
|
val = Enc.bufToHex(val);
|
||||||
|
}
|
||||||
|
return [typ, val];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
typ,
|
||||||
|
next.children.map(function(child) {
|
||||||
|
return toArray(child, opts);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
ASN1.parse = function(opts) {
|
||||||
|
var opts2 = { json: false !== opts.json };
|
||||||
|
var verbose = ASN1.parseVerbose(opts.der, opts2);
|
||||||
|
if (opts.verbose) {
|
||||||
|
return verbose;
|
||||||
|
}
|
||||||
|
return ASN1._toArray(verbose, opts2);
|
||||||
|
};
|
||||||
|
ASN1._replacer = function(k, v) {
|
||||||
|
if ('type' === k) {
|
||||||
|
return '0x' + Enc.numToHex(v);
|
||||||
|
}
|
||||||
|
if (v && 'value' === k) {
|
||||||
|
return '0x' + Enc.bufToHex(v.data || v);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
};
|
|
@ -0,0 +1,123 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var ASN1 = require('../');
|
||||||
|
var Enc = require('@root/encoding');
|
||||||
|
var PEM = require('@root/pem');
|
||||||
|
|
||||||
|
// 1.2.840.10045.3.1.7
|
||||||
|
// prime256v1 (ANSI X9.62 named elliptic curve)
|
||||||
|
var OBJ_ID_EC_256 = '06 08 2A8648CE3D030107';
|
||||||
|
|
||||||
|
var jwk = {
|
||||||
|
crv: 'P-256',
|
||||||
|
d: 'LImWxqqTHbP3LHQfqscDSUzf_uNePGqf9U6ETEcO5Ho',
|
||||||
|
kty: 'EC',
|
||||||
|
x: 'vdjQ3T6VBX82LIKDzepYgRsz3HgRwp83yPuonu6vqos',
|
||||||
|
y: 'IUkEXtAMnppnV1A19sE2bJhUo4WPbq6EYgWxma4oGyg',
|
||||||
|
kid: 'MnfJYyS9W5gUjrJLdn8ePMzik8ZJz2qc-VZmKOs_oCw'
|
||||||
|
};
|
||||||
|
var d = Enc.base64ToHex(jwk.d);
|
||||||
|
var x = Enc.base64ToHex(jwk.x);
|
||||||
|
var y = Enc.base64ToHex(jwk.y);
|
||||||
|
|
||||||
|
var der = Enc.hexToBuf(
|
||||||
|
ASN1.Any(
|
||||||
|
'30', // Sequence
|
||||||
|
ASN1.UInt('01'), // Integer (Version 1)
|
||||||
|
ASN1.Any('04', d), // Octet String
|
||||||
|
ASN1.Any('A0', OBJ_ID_EC_256), // [0] Object ID
|
||||||
|
ASN1.Any(
|
||||||
|
'A1', // [1] Embedded EC/ASN1 public key
|
||||||
|
ASN1.BitStr('04' + x + y)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
var pem1 = PEM.packBlock({
|
||||||
|
type: 'EC PRIVATE KEY',
|
||||||
|
bytes: der
|
||||||
|
});
|
||||||
|
|
||||||
|
var expected = [
|
||||||
|
'-----BEGIN EC PRIVATE KEY-----',
|
||||||
|
'MHcCAQEEICyJlsaqkx2z9yx0H6rHA0lM3/7jXjxqn/VOhExHDuR6oAoGCCqGSM49',
|
||||||
|
'AwEHoUQDQgAEvdjQ3T6VBX82LIKDzepYgRsz3HgRwp83yPuonu6vqoshSQRe0Aye',
|
||||||
|
'mmdXUDX2wTZsmFSjhY9uroRiBbGZrigbKA==',
|
||||||
|
'-----END EC PRIVATE KEY-----'
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
if (pem1 !== expected) {
|
||||||
|
throw new Error('Did not correctly Cascade pack EC P-256 JWK to DER');
|
||||||
|
} else {
|
||||||
|
console.info('PASS: packed cascaded ASN1');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mix and match hex ints, hex strings, and byte arrays
|
||||||
|
var asn1Arr = [
|
||||||
|
'30', // Sequence
|
||||||
|
[
|
||||||
|
[0x02, '01'], // Integer (Version 1)
|
||||||
|
[0x04, Buffer.from(d, 'hex')], // Octet String
|
||||||
|
['a0', OBJ_ID_EC_256], // [0] Object ID
|
||||||
|
[
|
||||||
|
0xa1, // [1] Embedded EC/ASN1 public key
|
||||||
|
ASN1.BitStr('04' + x + y)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
var der2 = ASN1.pack(asn1Arr);
|
||||||
|
var pem2 = PEM.packBlock({
|
||||||
|
type: 'EC PRIVATE KEY',
|
||||||
|
bytes: der2
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pem2 !== expected) {
|
||||||
|
console.log(pem2);
|
||||||
|
console.log(expected);
|
||||||
|
throw new Error('Did not correctly Array pack EC P-256 JWK to DER');
|
||||||
|
} else {
|
||||||
|
console.info('PASS: packed array-style ASN1');
|
||||||
|
}
|
||||||
|
|
||||||
|
var block = PEM.parseBlock(expected);
|
||||||
|
var arr1 = ASN1.parse({ der: block.bytes });
|
||||||
|
//console.log(JSON.stringify(arr1));
|
||||||
|
var arr2 = ASN1.parse({ der: block.bytes, verbose: false, json: false });
|
||||||
|
var obj3 = ASN1.parse({ der: block.bytes, verbose: true, json: true });
|
||||||
|
//console.log(obj3);
|
||||||
|
|
||||||
|
function eq(b1, b2) {
|
||||||
|
if (b1.byteLength !== b2.byteLength) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return b1.every(function(b, i) {
|
||||||
|
return b === b2[i];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!eq(block.bytes, ASN1.pack(arr1))) {
|
||||||
|
throw new Error('packing hex array resulted in different bytes');
|
||||||
|
} else {
|
||||||
|
console.log('PASS: packs parsed (hex) array');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!eq(block.bytes, ASN1.pack(arr2))) {
|
||||||
|
throw new Error('packing array with bytes resulted in different bytes');
|
||||||
|
} else {
|
||||||
|
console.log('PASS: packs parsed array (with bytes)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!eq(block.bytes, ASN1.pack(obj3))) {
|
||||||
|
console.log(block.bytes.toString('hex'));
|
||||||
|
console.log(ASN1.pack(obj3));
|
||||||
|
throw new Error('packing verbose object resulted in different bytes');
|
||||||
|
} else {
|
||||||
|
console.log('PASS: packs parsed verbose object');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block.bytes.toString('hex') !== ASN1.pack(obj3, { json: true })) {
|
||||||
|
throw new Error('packing to hex resulted in different bytes');
|
||||||
|
} else {
|
||||||
|
console.log('PASS: packs as hex when json: true');
|
||||||
|
}
|
Loading…
Reference in New Issue