Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14864602
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
19 KB
Subscribers
None
View Options
diff --git a/modules/ecash-lib/src/index.ts b/modules/ecash-lib/src/index.ts
index 7413a09bb..8af09db5c 100644
--- a/modules/ecash-lib/src/index.ts
+++ b/modules/ecash-lib/src/index.ts
@@ -1,30 +1,32 @@
// Copyright (c) 2024 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
export * from './consts.js';
export * from './ecc.js';
export * from './hash.js';
export * from './op.js';
export * from './opcode.js';
export * from './script.js';
export * from './messages.js';
export * from './mnemonic.js';
export * from './hdwallet.js';
export * from './address/address.js';
export * from './sigHashType.js';
export * from './tx.js';
export * from './txBuilder.js';
export * from './unsignedTx.js';
export * from './io/bytes.js';
export * from './io/hex.js';
export * from './io/int.js';
export * from './io/str.js';
export * from './io/varsize.js';
export * from './io/writer.js';
export * from './io/writerbytes.js';
export * from './io/writerlength.js';
export * from './token/alp.js';
export * from './token/common.js';
export * from './token/empp.js';
export * from './token/slp.js';
+
+export * as payment from './payment';
diff --git a/modules/ecash-lib/src/payment/asn1.test.ts b/modules/ecash-lib/src/payment/asn1.test.ts
new file mode 100644
index 000000000..ba53bafdd
--- /dev/null
+++ b/modules/ecash-lib/src/payment/asn1.test.ts
@@ -0,0 +1,134 @@
+// Copyright (c) 2025 The Bitcoin developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+import { expect } from 'chai';
+import * as tls from 'node:tls';
+
+import { asn1, x509 } from '.';
+import { Ecc } from '../ecc.js';
+import { toHex } from '../io/hex.js';
+import { sha256 } from '../hash.js';
+import '../initNodeJs.js';
+
+/*
+key.pem:
+openssl ecparam -genkey -out key.pem -name secp256k1
+
+-----BEGIN EC PARAMETERS-----
+BgUrgQQACg==
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MHQCAQEEIFzOgjk70LwSfTAWNAzwm2tZedMM30pwyMsaK8KkJAEHoAcGBSuBBAAK
+oUQDQgAE23jFaVYo7rulgXbZ5WaADFaKCzn8Lk6ZubOqabsXrqfM3JaKRrrPwyrk
+aXzvoFu5D28kF7OXDsomtTWH9h5Slg==
+-----END EC PRIVATE KEY-----
+
+cert.pem:
+openssl req -x509 -new \
+ -key key.pem \
+ -out cert.pem \
+ -sha256 \
+ -days 36500 \
+ -nodes \
+ -subj "/C=CA/ST=British Columbia/L=Vancouver/O=eCash Palace/OU=Software/CN=example.e.cash/emailAddress=example@e.cash"
+*/
+const CERT_PEM = `
+-----BEGIN CERTIFICATE-----
+MIICkTCCAjigAwIBAgIUTWHooPqrdMM/kO1CNMYN7m3P6CswCgYIKoZIzj0EAwIw
+gZ4xCzAJBgNVBAYTAkNBMRkwFwYDVQQIDBBCcml0aXNoIENvbHVtYmlhMRIwEAYD
+VQQHDAlWYW5jb3V2ZXIxFTATBgNVBAoMDGVDYXNoIFBhbGFjZTERMA8GA1UECwwI
+U29mdHdhcmUxFzAVBgNVBAMMDmV4YW1wbGUuZS5jYXNoMR0wGwYJKoZIhvcNAQkB
+Fg5leGFtcGxlQGUuY2FzaDAgFw0yNTAzMDIyMDMyNDNaGA8yMTI1MDIwNjIwMzI0
+M1owgZ4xCzAJBgNVBAYTAkNBMRkwFwYDVQQIDBBCcml0aXNoIENvbHVtYmlhMRIw
+EAYDVQQHDAlWYW5jb3V2ZXIxFTATBgNVBAoMDGVDYXNoIFBhbGFjZTERMA8GA1UE
+CwwIU29mdHdhcmUxFzAVBgNVBAMMDmV4YW1wbGUuZS5jYXNoMR0wGwYJKoZIhvcN
+AQkBFg5leGFtcGxlQGUuY2FzaDBWMBAGByqGSM49AgEGBSuBBAAKA0IABNt4xWlW
+KO67pYF22eVmgAxWigs5/C5Ombmzqmm7F66nzNyWika6z8Mq5Gl876BbuQ9vJBez
+lw7KJrU1h/YeUpajUzBRMB0GA1UdDgQWBBQKzjEN/xneEG0oc/OgW/w7/imk4TAf
+BgNVHSMEGDAWgBQKzjEN/xneEG0oc/OgW/w7/imk4TAPBgNVHRMBAf8EBTADAQH/
+MAoGCCqGSM49BAMCA0cAMEQCIDOPA89LS1r0/3nm8i4hT/VhHHEYPh3Mf42COhRP
+bpoDAiA2Mf29zeOXOxZ7B021KAJUpsSFtMEvBnllMQjQqsoaog==
+-----END CERTIFICATE-----
+`;
+
+describe('ASN1', () => {
+ const ecc = new Ecc();
+ it('asn1.parseCertPem self-signed secp256k1', () => {
+ const cert = asn1.parseCertPem(CERT_PEM);
+
+ expect(cert.sigAlg).to.deep.equal({
+ oid: x509.OID_ECDSA_WITH_SHA256,
+ params: undefined,
+ });
+ const subject = [
+ {
+ oid: x509.OID_COUNTRY_NAME,
+ value: 'CA',
+ },
+ {
+ oid: x509.OID_STATE_OR_PROVINCE_NAME,
+ value: 'British Columbia',
+ },
+ {
+ oid: x509.OID_LOCALITY_NAME,
+ value: 'Vancouver',
+ },
+ {
+ oid: x509.OID_ORGANIZATION_NAME,
+ value: 'eCash Palace',
+ },
+ {
+ oid: x509.OID_ORGANIZATIONAL_UNIT_NAME,
+ value: 'Software',
+ },
+ {
+ oid: x509.OID_COMMON_NAME,
+ value: 'example.e.cash',
+ },
+ {
+ oid: x509.OID_EMAIL_ADDRESS,
+ value: 'example@e.cash',
+ },
+ ];
+
+ expect(cert.tbs.version).to.equal(2);
+ expect(cert.tbs.sigAlg).to.deep.equal({
+ oid: x509.OID_ECDSA_WITH_SHA256,
+ params: undefined,
+ });
+ expect(toHex(cert.tbs.serial)).to.equal(
+ '4d61e8a0faab74c33f90ed4234c60dee6dcfe82b',
+ );
+ expect(cert.tbs.issuer).to.deep.equal(subject);
+ expect(cert.tbs.validity).to.deep.equal({
+ notBefore: 1740947563,
+ notAfter: 4894547563,
+ });
+ expect(cert.tbs.subject).to.deep.equal(subject);
+ expect(cert.tbs.pubkey.alg).to.deep.equal({
+ oid: x509.OID_EC_PUBLIC_KEY,
+ params: x509.OID_ANSIP256K1,
+ });
+ expect(toHex(cert.tbs.pubkey.data)).to.equal(
+ '04db78c5695628eebba58176d9e566800c568a0b39fc2e4e99b9b3aa69bb17aea7ccdc968a46bacfc32ae4697cefa05bb90f6f2417b3970eca26b53587f61e5296',
+ );
+
+ // Self-signed certificate is valid
+ ecc.ecdsaVerify(
+ cert.sig,
+ sha256(cert.tbs.raw),
+ ecc.compressPk(cert.tbs.pubkey.data),
+ );
+ });
+
+ it('asn1.parseCertPem built-in root certs', () => {
+ // Try parsing all built in root certificates
+ for (const certPem of tls.rootCertificates) {
+ // Throws an error if one cert failed
+ const cert = asn1.parseCertPem(certPem);
+ // Cert has at least one entry in subject
+ expect(cert.tbs.subject.length).to.be.greaterThan(0);
+ }
+ });
+});
diff --git a/modules/ecash-lib/src/payment/asn1.ts b/modules/ecash-lib/src/payment/asn1.ts
new file mode 100644
index 000000000..865fb88b8
--- /dev/null
+++ b/modules/ecash-lib/src/payment/asn1.ts
@@ -0,0 +1,440 @@
+// Copyright (c) 2025 The Bitcoin developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+import { bytesToStr } from '../io/str.js';
+import { Bytes } from '../io/bytes.js';
+
+interface Tag {
+ type: number;
+ isPrimitive: boolean;
+ size: number;
+}
+
+/** Identifier of a signature algorithm */
+export interface AlgIdent {
+ /** OID of the algorithm */
+ oid: string;
+ /** OID of the params */
+ params: string | undefined;
+}
+
+/** Entry of an issuer/subject */
+export interface Entry {
+ /** OID of the entry */
+ oid: string;
+ /** Value of the entry */
+ value: string;
+}
+
+/** Time range of the validity of the certificate */
+export interface Validity {
+ /** Certificate is not valid before this UNIX timestamp */
+ notBefore: number;
+ /** Certificate is not valid after this UNIX timestamp */
+ notAfter: number;
+}
+
+/** Public key of a certificate */
+export interface PubKey {
+ /** Signature algorithm of the public key */
+ alg: AlgIdent;
+ /** Bytes of the public key */
+ data: Uint8Array;
+}
+
+/** To-be-signed data of the certificate */
+export interface TBS {
+ /** Version of the certificate data */
+ version: number | undefined;
+ /** Serial number of the certificate */
+ serial: Uint8Array;
+ /** Signature algorithm used by the certificate */
+ sigAlg: AlgIdent;
+ /** Fields of the issuer of the certificate */
+ issuer: Entry[];
+ /** Time range the certificate is valid */
+ validity: Validity;
+ /** Fields of the subject of the certificate */
+ subject: Entry[];
+ /** Public key of the certificate */
+ pubkey: PubKey;
+ /** Raw bytes of the data to be signed */
+ raw: Uint8Array;
+}
+
+export interface Cert {
+ /** Data to be signed by the signature */
+ tbs: TBS;
+ /** Algorithm used for the signature */
+ sigAlg: AlgIdent;
+ /** Signature signing `tbs` */
+ sig: Uint8Array;
+ /** Raw bytes of the certificate */
+ raw: Uint8Array;
+}
+
+const TAG_VERSION = 0x00;
+const TAG_INT = 0x02;
+const TAG_BITSTR = 0x03;
+const TAG_OCTSTR = 0x04;
+const TAG_NULL = 0x05;
+const TAG_OID = 0x06;
+const TAG_UTF8STR = 0x0c;
+const TAG_SEQ = 0x10;
+const TAG_SET = 0x11;
+const TAG_NUMSTR = 0x12;
+const TAG_PRINSTR = 0x13;
+const TAG_T61STR = 0x14;
+const TAG_VIDEOSTR = 0x15;
+const TAG_IA5STR = 0x16;
+const TAG_UTCTIME = 0x17;
+const TAG_GENTIME = 0x18;
+const TAG_GRAPHSTR = 0x19;
+const TAG_ISO646STR = 0x1a;
+const TAG_GENSTR = 0x1b;
+const TAG_UNISTR = 0x1c;
+const TAG_CHARSTR = 0x1d;
+const TAG_BMPSTR = 0x1e;
+
+const REGEX_PEM_CERT =
+ /-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----/s;
+const REGEX_DIGITS = /\d+/;
+
+function readSize(bytes: Bytes, isPrimitive: boolean): number {
+ let size = bytes.readU8();
+
+ // Indefinite form
+ if (!isPrimitive && size === 0x80) {
+ throw new Error('Indefinite size.');
+ }
+
+ // Definite form
+ if ((size & 0x80) === 0) {
+ // Short form
+ return size;
+ }
+
+ // Long form
+ const numBytes = size & 0x7f;
+
+ if (numBytes > 3) {
+ throw new Error('Length octet is too long.');
+ }
+
+ size = 0;
+ for (let i = 0; i < numBytes; i++) {
+ size <<= 8;
+ size |= bytes.readU8();
+ }
+
+ return size;
+}
+
+function readTag(bytes: Bytes): Tag {
+ let type = bytes.readU8();
+ const isPrimitive = (type & 0x20) === 0;
+
+ if ((type & 0x1f) === 0x1f) {
+ let oct = type;
+ type = 0;
+ while ((oct & 0x80) === 0x80) {
+ oct = bytes.readU8();
+ type <<= 7;
+ type |= oct & 0x7f;
+ }
+ } else {
+ type &= 0x1f;
+ }
+
+ return {
+ type,
+ isPrimitive,
+ size: readSize(bytes, isPrimitive),
+ };
+}
+
+function readSeq(bytes: Bytes): Uint8Array {
+ const tag = readTag(bytes);
+ if (tag.type !== TAG_SEQ) {
+ throw new Error(
+ `Expected sequence type ${TAG_SEQ}, but got ${tag.type}`,
+ );
+ }
+ return bytes.readBytes(tag.size);
+}
+
+function alignBitstr(data: Uint8Array): Uint8Array {
+ const padding = data[0];
+ const bits = (data.length - 1) * 8 - padding;
+ const buf = data.slice(1);
+ const shift = 8 - (bits % 8);
+
+ if (shift === 8 || buf.length === 0) {
+ return buf;
+ }
+
+ const out = Buffer.allocUnsafe(buf.length);
+ out[0] = buf[0] >>> shift;
+
+ for (let i = 1; i < buf.length; i++) {
+ out[i] = buf[i - 1] << (8 - shift);
+ out[i] |= buf[i] >>> shift;
+ }
+
+ return out;
+}
+
+function readBitstr(bytes: Bytes): Uint8Array {
+ const tag = readTag(bytes);
+ if (tag.type !== TAG_BITSTR) {
+ throw new Error(
+ `Expected sequence type ${TAG_BITSTR}, but got ${tag.type}`,
+ );
+ }
+ return alignBitstr(bytes.readBytes(tag.size));
+}
+
+function readString(bytes: Bytes): Uint8Array {
+ const tag = readTag(bytes);
+ switch (tag.type) {
+ case TAG_BITSTR: {
+ return alignBitstr(bytes.readBytes(tag.size));
+ }
+ case TAG_OCTSTR:
+ case TAG_NUMSTR:
+ case TAG_PRINSTR:
+ case TAG_T61STR:
+ case TAG_VIDEOSTR:
+ case TAG_IA5STR:
+ case TAG_GRAPHSTR:
+ case TAG_UTF8STR:
+ case TAG_ISO646STR:
+ case TAG_GENSTR:
+ case TAG_UNISTR:
+ case TAG_CHARSTR:
+ case TAG_BMPSTR: {
+ return bytes.readBytes(tag.size);
+ }
+ default: {
+ throw new Error(`Expected string tag, got ${tag.type}`);
+ }
+ }
+}
+
+function readInt(bytes: Bytes): Uint8Array {
+ const tag = readTag(bytes);
+ if (tag.type !== TAG_INT) {
+ throw new Error(
+ `Expected integer type ${TAG_INT}, but got ${tag.type}`,
+ );
+ }
+ return bytes.readBytes(tag.size);
+}
+
+function bytesToBE(bytes: Uint8Array): number {
+ let num = 0;
+ for (const b of bytes) {
+ num <<= 8;
+ num |= b;
+ }
+ return num;
+}
+
+function readVersion(bytes: Bytes): number | undefined {
+ const startIdx = bytes.idx;
+ const tag = readTag(bytes);
+ if (tag.type != TAG_VERSION) {
+ bytes.idx = startIdx;
+ return undefined;
+ }
+ return bytesToBE(readInt(bytes));
+}
+
+function readAlgIdent(bytes: Bytes): AlgIdent {
+ let params: string | undefined = undefined;
+ bytes = new Bytes(readSeq(bytes));
+ const oid = readOID(bytes);
+ if (oid === undefined) {
+ throw new Error('Algorithm cannot be NULL');
+ }
+
+ if (bytes.idx < bytes.data.length) {
+ params = readOID(bytes);
+ }
+
+ return { oid, params };
+}
+
+function readOID(bytes: Bytes): string | undefined {
+ const tag = readTag(bytes);
+ if (tag.type === TAG_NULL) {
+ return undefined;
+ }
+
+ if (tag.type !== TAG_OID) {
+ throw new Error(`Expected OID tag ${TAG_OID}, but got ${tag.type}`);
+ }
+ const data = bytes.readBytes(tag.size);
+ const ids = [];
+ let ident = 0;
+ let subident = 0;
+ for (const byte of data) {
+ subident = byte;
+ ident <<= 7;
+ ident |= subident & 0x7f;
+ if ((subident & 0x80) === 0) {
+ ids.push(ident);
+ ident = 0;
+ }
+ }
+
+ if (subident & 0x80) {
+ ids.push(ident);
+ }
+
+ const first = (ids[0] / 40) | 0;
+ const second = ids[0] % 40;
+ const result = [first, second].concat(ids.slice(1));
+
+ return result.join('.');
+}
+
+function readEntries(bytes: Bytes): Entry[] {
+ const values = [];
+ bytes = new Bytes(readSeq(bytes));
+
+ while (bytes.idx < bytes.data.length) {
+ const tagSet = readTag(bytes);
+ if (tagSet.type !== TAG_SET) {
+ throw new Error(
+ `Expected set tag ${TAG_SET}, but got ${tagSet.type}`,
+ );
+ }
+ const tagSeq = readTag(bytes);
+ if (tagSeq.type !== TAG_SEQ) {
+ throw new Error(
+ `Expected seq tag ${TAG_SEQ}, but got ${tagSeq.type}`,
+ );
+ }
+ const oid = readOID(bytes);
+ if (oid === undefined) {
+ throw new Error('OID for issuer or subject cannot be NULL');
+ }
+ values.push({
+ oid: oid,
+ value: bytesToStr(readString(bytes)),
+ });
+ }
+
+ return values;
+}
+
+function readTime(bytes: Bytes): number {
+ const tag = readTag(bytes);
+ const decoder = new TextDecoder('ascii', { fatal: true });
+ const str = decoder.decode(bytes.readBytes(tag.size));
+ let year: number;
+ let mon: number;
+ let day: number;
+ let hour: number;
+ let min: number;
+ let sec: number;
+
+ let pos = 0;
+ const readDigits = (numDigits: number) => {
+ const digits = str.slice(pos, pos + numDigits);
+ if (!REGEX_DIGITS.test(digits)) {
+ throw new Error(`Expected ${numDigits} decimal digits`);
+ }
+ pos += numDigits;
+ return Number(digits) | 0;
+ };
+
+ switch (tag.type) {
+ case TAG_UTCTIME: {
+ year = readDigits(2);
+ mon = readDigits(2);
+ day = readDigits(2);
+ hour = readDigits(2);
+ min = readDigits(2);
+ sec = readDigits(2);
+ if (year < 70) {
+ year = 2000 + year;
+ } else {
+ year = 1900 + year;
+ }
+ break;
+ }
+ case TAG_GENTIME: {
+ year = readDigits(4);
+ mon = readDigits(2);
+ day = readDigits(2);
+ hour = readDigits(2);
+ min = readDigits(2);
+ sec = readDigits(2);
+ break;
+ }
+ default: {
+ throw new Error(`Unexpected tag: ${tag.type}.`);
+ }
+ }
+
+ return Date.UTC(year, mon - 1, day, hour, min, sec, 0) / 1000;
+}
+
+function readValidity(bytes: Bytes): Validity {
+ bytes = new Bytes(readSeq(bytes));
+ return {
+ notBefore: readTime(bytes),
+ notAfter: readTime(bytes),
+ };
+}
+
+function readPubkey(bytes: Bytes): PubKey {
+ bytes = new Bytes(readSeq(bytes));
+ return {
+ alg: readAlgIdent(bytes),
+ data: readBitstr(bytes),
+ };
+}
+
+function readToBeSigned(bytes: Bytes): TBS {
+ const startIdx = bytes.idx;
+ const tbsBytes = new Bytes(readSeq(bytes));
+ const endIdx = bytes.idx;
+ return {
+ version: readVersion(tbsBytes),
+ serial: readInt(tbsBytes),
+ sigAlg: readAlgIdent(tbsBytes),
+ issuer: readEntries(tbsBytes),
+ validity: readValidity(tbsBytes),
+ subject: readEntries(tbsBytes),
+ pubkey: readPubkey(tbsBytes),
+ raw: bytes.data.slice(startIdx, endIdx),
+ };
+}
+
+/** Parse a ASN1 certificate from the given bytes */
+export function parseCertRaw(rawCert: Uint8Array): Cert {
+ const bytes = new Bytes(rawCert);
+ const certBytes = new Bytes(readSeq(bytes));
+ return {
+ tbs: readToBeSigned(certBytes),
+ sigAlg: readAlgIdent(certBytes),
+ sig: readBitstr(certBytes),
+ raw: rawCert,
+ };
+}
+
+export function parseCertPem(pem: string): Cert {
+ const match = REGEX_PEM_CERT.exec(pem);
+ if (match === null) {
+ throw new Error(
+ 'No PEM encoded certificate found. It should start with ' +
+ '"-----BEGIN CERTIFICATE-----"',
+ );
+ }
+ const certRaw = Uint8Array.from(atob(match[1]), c => c.charCodeAt(0));
+ return parseCertRaw(certRaw);
+}
diff --git a/modules/ecash-lib/src/payment/index.ts b/modules/ecash-lib/src/payment/index.ts
new file mode 100644
index 000000000..1273fac63
--- /dev/null
+++ b/modules/ecash-lib/src/payment/index.ts
@@ -0,0 +1,6 @@
+// Copyright (c) 2025 The Bitcoin developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+export * as asn1 from './asn1.js';
+export * as x509 from './x509.js';
diff --git a/modules/ecash-lib/src/payment/x509.ts b/modules/ecash-lib/src/payment/x509.ts
new file mode 100644
index 000000000..8b4073bee
--- /dev/null
+++ b/modules/ecash-lib/src/payment/x509.ts
@@ -0,0 +1,15 @@
+// Copyright (c) 2025 The Bitcoin developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+export const OID_ECDSA_WITH_SHA256 = '1.2.840.10045.4.3.2';
+export const OID_EC_PUBLIC_KEY = '1.2.840.10045.2.1';
+export const OID_ANSIP256K1 = '1.3.132.0.10';
+
+export const OID_COUNTRY_NAME = '2.5.4.6';
+export const OID_STATE_OR_PROVINCE_NAME = '2.5.4.8';
+export const OID_LOCALITY_NAME = '2.5.4.7';
+export const OID_ORGANIZATION_NAME = '2.5.4.10';
+export const OID_ORGANIZATIONAL_UNIT_NAME = '2.5.4.11';
+export const OID_COMMON_NAME = '2.5.4.3';
+export const OID_EMAIL_ADDRESS = '1.2.840.113549.1.9.1';
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Wed, May 21, 20:47 (1 d, 14 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5865917
Default Alt Text
(19 KB)
Attached To
rABC Bitcoin ABC
Event Timeline
Log In to Comment