diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz new file mode 100644 index 00000000..df84eec3 Binary files /dev/null and b/.yarn/install-state.gz differ diff --git a/examples/auto-id/.env.local b/examples/auto-id/.env.local new file mode 100644 index 00000000..c53c8bb8 --- /dev/null +++ b/examples/auto-id/.env.local @@ -0,0 +1,2 @@ +RPC_URL= +KEYPAIR_URI= \ No newline at end of file diff --git a/examples/auto-id/README.md b/examples/auto-id/README.md new file mode 100644 index 00000000..8f3ebb87 --- /dev/null +++ b/examples/auto-id/README.md @@ -0,0 +1,17 @@ +# Autonomys SDK - Auto ID Example + +- Register Certificate with Auto ID on auto-id domain. + +## Install + +```bash +yarn +``` + +## Register + +Both issuer & user. + +```bash +yarn register +``` diff --git a/examples/auto-id/package.json b/examples/auto-id/package.json new file mode 100644 index 00000000..7d408b96 --- /dev/null +++ b/examples/auto-id/package.json @@ -0,0 +1,30 @@ +{ + "name": "auto-id-examples", + "version": "0.1.0", + "private": true, + "license": "MIT", + "packageManager": "yarn@4.2.2", + "repository": { + "type": "git", + "url": "https://github.com/subspace/auto-sdk" + }, + "author": { + "name": "Autonomys", + "url": "https://www.autonomys.net" + }, + "scripts": { + "build": "tsc", + "clean": "rm -rf dist", + "format": "prettier --write \"src/**/*.ts\"", + "register": "tsc && ts-node src/registerAutoId.ts" + }, + "dependencies": { + "@autonomys/auto-id": "workspace:*", + "dotenv": "^16.4.5" + }, + "devDependencies": { + "@types/node": "^20.12.12", + "ts-node": "^10.9.2", + "typescript": "^5.4.5" + } +} \ No newline at end of file diff --git a/examples/auto-id/src/registerAutoId.ts b/examples/auto-id/src/registerAutoId.ts new file mode 100644 index 00000000..98322b2b --- /dev/null +++ b/examples/auto-id/src/registerAutoId.ts @@ -0,0 +1,78 @@ +/** + * Example of how to register an auto-id + */ + +import { CertificateManager, Registry, generateEd25519KeyPair2 } from '@autonomys/auto-id' +import * as x509 from '@peculiar/x509' +import { Keyring } from '@polkadot/api' +import { cryptoWaitReady } from '@polkadot/util-crypto' +import { config } from 'dotenv' + +function loadEnv(): { RPC_URL: string; KEYPAIR_URI: string } { + const myEnv = config() + if (myEnv.error) { + throw new Error('Failed to load the .env file.') + } + + const RPC_URL = process.env.RPC_URL + if (!RPC_URL) { + throw new Error('Please set your rpc url in a .env file') + } + + const KEYPAIR_URI = process.env.KEYPAIR_URI + if (!KEYPAIR_URI) { + throw new Error('Please set your keypair uri in a .env file') + } + + return { RPC_URL, KEYPAIR_URI } +} + +async function register( + certificate: x509.X509Certificate, + registry: Registry, + issuerId?: string | null | undefined, +) { + // Attempt to register the certificate + const { receipt, identifier } = await registry.registerAutoId(certificate, issuerId) + if (receipt && receipt.isInBlock) { + console.log( + `Registration successful with Auto ID identifier: ${identifier} in block #${receipt.blockNumber?.toString()}`, + ) + return identifier + } else { + console.log('Registration failed.') + } +} + +async function main() { + await cryptoWaitReady() + + const { RPC_URL, KEYPAIR_URI } = loadEnv() + + // Initialize the signer keypair + const keyring = new Keyring({ type: 'sr25519' }) + const issuer = keyring.addFromUri(KEYPAIR_URI) + + // Initialize the Registry instance + const registry = new Registry(RPC_URL!, issuer) + + const keys = await generateEd25519KeyPair2() + const selfIssuedCm = new CertificateManager(null, keys[0], keys[1]) + const selfIssuedCert = await selfIssuedCm.selfIssueCertificate('test1') + const issuerId = await register(selfIssuedCert, registry) + + const userKeys = await generateEd25519KeyPair2() + const userCm = new CertificateManager(null, userKeys[0], userKeys[1]) + const userCsr = await userCm.createAndSignCSR('user1') + const userCert = await selfIssuedCm.issueCertificate(userCsr) + CertificateManager.prettyPrintCertificate(userCert) + const registerUser = await register(userCert, registry, issuerId!) + + console.log(`auto id from cert: ${CertificateManager.getCertificateAutoId(userCert)}`) +} +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/examples/auto-id/tsconfig.json b/examples/auto-id/tsconfig.json new file mode 100644 index 00000000..560f342c --- /dev/null +++ b/examples/auto-id/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src/" + }, + "include": [ + "src/**/*", + ], +} \ No newline at end of file diff --git a/packages/auto-id/package.json b/packages/auto-id/package.json index 1cd7b0e3..0eab59a8 100644 --- a/packages/auto-id/package.json +++ b/packages/auto-id/package.json @@ -14,12 +14,8 @@ "@peculiar/asn1-x509": "^2.3.8", "@peculiar/webcrypto": "^1.5.0", "@peculiar/x509": "^1.11.0", - "asn1js": "^3.0.5" + "dotenv": "^16.4.5" }, - "files": [ - "dist", - "README.md" - ], "devDependencies": { "@types/jest": "^29.5.12", "@types/node": "^20.12.12", @@ -28,5 +24,12 @@ "ts-node": "^10.9.2", "typescript": "^5.4.5" }, - "gitHead": "bf1aacdebad697135f8cbcf4a359e7c2a9188e24" + "repository": { + "type": "git", + "url": "https://github.com/subspace/auto-sdk" + }, + "author": { + "name": "Autonomys", + "url": "https://www.autonomys.net" + } } diff --git a/packages/auto-id/src/index.ts b/packages/auto-id/src/index.ts index 953fd9ca..634b4cc5 100644 --- a/packages/auto-id/src/index.ts +++ b/packages/auto-id/src/index.ts @@ -1,3 +1,4 @@ export * from './certificateManager' export * from './keyManagement' +export * from './registry' export * from './utils' diff --git a/packages/auto-id/src/registry.ts b/packages/auto-id/src/registry.ts new file mode 100644 index 00000000..2d7a5b9f --- /dev/null +++ b/packages/auto-id/src/registry.ts @@ -0,0 +1,151 @@ +import { AsnParser, AsnSerializer } from '@peculiar/asn1-schema' +import { Certificate } from '@peculiar/asn1-x509' +import { X509Certificate } from '@peculiar/x509' +import { ApiPromise, SubmittableResult, WsProvider } from '@polkadot/api' +import { KeyringPair } from '@polkadot/keyring/types' +import { compactAddLength } from '@polkadot/util' +import { derEncodeSignatureAlgorithmOID } from './utils' + +export enum AutoIdError { + UnknownIssuer = 'UnknownIssuer', + UnknownAutoId = 'UnknownAutoId', + InvalidCertificate = 'InvalidCertificate', + InvalidSignature = 'InvalidSignature', + CertificateSerialAlreadyIssued = 'CertificateSerialAlreadyIssued', + ExpiredCertificate = 'ExpiredCertificate', + CertificateRevoked = 'CertificateRevoked', + CertificateAlreadyRevoked = 'CertificateAlreadyRevoked', + NonceOverflow = 'NonceOverflow', + AutoIdIdentifierAlreadyExists = 'AutoIdIdentifierAlreadyExists', + AutoIdIdentifierMismatch = 'AutoIdIdentifierMismatch', + PublicKeyMismatch = 'PublicKeyMismatch', +} +/** + * Maps an error code to the corresponding enum variant. + * @param errorCode The error code as a hexadecimal string (e.g., "0x09000000"). + * @returns The corresponding enum variant or null if not found. + */ +function mapErrorCodeToEnum(errorCode: string): AutoIdError | null { + // Remove the '0x' prefix and extract the relevant part of the error code + const relevantPart = errorCode.slice(2, 4) // Gets the byte corresponding to the specific error + + switch (relevantPart) { + case '00': + return AutoIdError.UnknownIssuer + case '01': + return AutoIdError.UnknownAutoId + case '02': + return AutoIdError.InvalidCertificate + case '03': + return AutoIdError.InvalidSignature + case '04': + return AutoIdError.CertificateSerialAlreadyIssued + case '05': + return AutoIdError.ExpiredCertificate + case '06': + return AutoIdError.CertificateRevoked + case '07': + return AutoIdError.CertificateAlreadyRevoked + case '08': + return AutoIdError.NonceOverflow + case '09': + return AutoIdError.AutoIdIdentifierAlreadyExists + case '0A': + return AutoIdError.AutoIdIdentifierMismatch + case '0B': + return AutoIdError.PublicKeyMismatch + default: + return null // Or handle unknown error types differently + } +} + +interface RegistrationResult { + receipt: SubmittableResult | null + identifier: string | null +} + +export class Registry { + private api: ApiPromise + private signer: KeyringPair | null + + constructor(rpcUrl: string = 'ws://127.0.0.1:9944', signer: KeyringPair | null = null) { + this.api = new ApiPromise({ provider: new WsProvider(rpcUrl) }) + this.signer = signer + } + + public async registerAutoId( + certificate: X509Certificate, + issuerId?: string | null | undefined, + ): Promise { + await this.api.isReady + + if (!this.signer) { + throw new Error('No signer provided') + } + + const certificateBuffer = Buffer.from(certificate.rawData) + + // Load and parse the certificate + const cert = AsnParser.parse(certificateBuffer, Certificate) + // Extract the OID of the signature algorithm + const signatureAlgorithmOID = cert.signatureAlgorithm.algorithm + + const derEncodedOID = derEncodeSignatureAlgorithmOID(signatureAlgorithmOID) + // CLEANUP: Remove later. Kept for debugging for other modules. + // console.debug(Buffer.from(derEncodedOID)) + // console.debug(`DER encoded OID: ${derEncodedOID}`) + // console.debug(`Bytes length: ${derEncodedOID.length}`) + + // The TBS Certificate is accessible directly via the `tbsCertificate` property + const tbsCertificate = cert.tbsCertificate + + // Serialize the TBS Certificate back to DER format + const tbsCertificateDerVec = AsnSerializer.serialize(tbsCertificate) + + const baseCertificate = { + certificate: compactAddLength(new Uint8Array(tbsCertificateDerVec)), + signature_algorithm: compactAddLength(derEncodedOID), + signature: compactAddLength(new Uint8Array(certificate.signature)), + } + + const certificateParam = issuerId + ? { Leaf: { issuer_id: issuerId, ...baseCertificate } } + : { Root: baseCertificate } + + const req = { X509: certificateParam } + + let identifier: string | null = null + + const receipt: SubmittableResult = await new Promise((resolve, reject) => { + this.api.tx.autoId.registerAutoId(req).signAndSend(this.signer!, (result) => { + const { events = [], status } = result + + if (status.isInBlock || status.isFinalized) { + events.forEach(({ event: { section, method, data } }) => { + if (section === 'system' && method === 'ExtrinsicFailed') { + const [dispatchError] = data + const dispatchErrorJson = JSON.parse(dispatchError.toString()) + + reject( + new Error( + `Extrinsic failed: ${mapErrorCodeToEnum(dispatchErrorJson.module.error)}`, + ), + ) + } + if (section === 'system' && method === 'ExtrinsicSuccess') { + console.debug('Extrinsic succeeded') + } + if (section === 'autoId' && method === 'NewAutoIdRegistered') { + identifier = data[0].toString() + } + }) + resolve(result) + } else if (status.isDropped || status.isInvalid) { + reject(new Error('Transaction dropped or invalid')) + } + }) + }) + + return { receipt, identifier } + } +} diff --git a/packages/auto-id/src/utils.ts b/packages/auto-id/src/utils.ts index e7ac9a94..cd4bb228 100644 --- a/packages/auto-id/src/utils.ts +++ b/packages/auto-id/src/utils.ts @@ -1,26 +1,29 @@ -import { ObjectIdentifier } from 'asn1js' -import { randomBytes } from 'crypto' +import { AsnConvert, OctetString } from '@peculiar/asn1-schema' +import { AlgorithmIdentifier as AsnAlgorithmIdentifier } from '@peculiar/asn1-x509' /** - * Encodes a given string representation of an OID into its DER format. - * This function is specifically used to encode signature algorithm OIDs. + * Encodes a given string representation of an OID into its DER format, + * appropriately handling the parameters. * * @param oid The string representation of the ObjectIdentifier to be encoded. - * @returns Uint8Array containing the DER encoded OID along with NULL params of X.509 signature algorithm. + * @param parameters Optional parameters, null if no parameters. + * @returns Uint8Array containing the DER-encoded OID with appended parameters. */ -export function derEncodeSignatureAlgorithmOID(oid: string): Uint8Array { - const objectIdentifier = new ObjectIdentifier({ value: oid }) - const berArrayBuffer = objectIdentifier.toBER(false) +export function derEncodeSignatureAlgorithmOID( + oid: string, + parameters: ArrayBuffer | null = null, +): Uint8Array { + // Create an instance of AlgorithmIdentifier with proper handling of parameters + const algorithmIdentifier = new AsnAlgorithmIdentifier({ + algorithm: oid, + parameters: parameters ? AsnConvert.serialize(new OctetString(parameters)) : null, + }) - // Typically, in X.509, the algorithm identifier is followed by parameters; for many algorithms, this is just NULL. - const nullParameter = [0x05, 0x00] // DER encoding for NULL + // Convert the entire AlgorithmIdentifier to DER + const derEncoded = AsnConvert.serialize(algorithmIdentifier) - // Calculate the total length including OID and NULL parameter - const totalLength = berArrayBuffer.byteLength + nullParameter.length - - const sequenceHeader = [0x30, totalLength] // 0x30 is the DER tag for SEQUENCE - - return new Uint8Array([...sequenceHeader, ...new Uint8Array(berArrayBuffer), ...nullParameter]) + // Return the resulting DER-encoded data + return new Uint8Array(derEncoded) } export function addDaysToCurrentDate(days: number): Date { diff --git a/packages/auto-id/tests/utils.test.ts b/packages/auto-id/tests/utils.test.ts index 1b58b8dc..245f0ab7 100644 --- a/packages/auto-id/tests/utils.test.ts +++ b/packages/auto-id/tests/utils.test.ts @@ -1,5 +1,4 @@ import { AsnParser } from '@peculiar/asn1-schema' // A library to parse ASN.1 -// TODO: See why X509Certificate (from crypto) is not compatible argument. import { Certificate } from '@peculiar/asn1-x509' // Assuming X.509 certificate handling import fs from 'fs' import { derEncodeSignatureAlgorithmOID } from '../src/utils' @@ -18,8 +17,6 @@ describe('Verify crypto functions', () => { // DER encode the OID const derEncodedOID = derEncodeSignatureAlgorithmOID(signatureAlgorithmOID) - // - // console.log(Buffer.from(derEncodedOID)) // Convert derEncodedOID to hex string for comparison const derEncodedOIDHex = Buffer.from(derEncodedOID).toString('hex') diff --git a/packages/auto-id/tsconfig.json b/packages/auto-id/tsconfig.json index aa702d83..af48926d 100644 --- a/packages/auto-id/tsconfig.json +++ b/packages/auto-id/tsconfig.json @@ -2,12 +2,10 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist", - "rootDir": "./" + "rootDir": "./src/" }, "include": [ - "src/**/*", - ], - "exclude": [ - "examples/**/*" + "src/*", + "**/*.test.ts" ] } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 90501270..d57f6219 100644 --- a/yarn.lock +++ b/yarn.lock @@ -37,7 +37,7 @@ __metadata: languageName: unknown linkType: soft -"@autonomys/auto-id@workspace:packages/auto-id": +"@autonomys/auto-id@workspace:*, @autonomys/auto-id@workspace:packages/auto-id": version: 0.0.0-use.local resolution: "@autonomys/auto-id@workspace:packages/auto-id" dependencies: @@ -48,7 +48,7 @@ __metadata: "@peculiar/x509": "npm:^1.11.0" "@types/jest": "npm:^29.5.12" "@types/node": "npm:^20.12.12" - asn1js: "npm:^3.0.5" + dotenv: "npm:^16.4.5" jest: "npm:^29.7.0" ts-jest: "npm:^29.1.4" ts-node: "npm:^10.9.2" @@ -3114,6 +3114,18 @@ __metadata: languageName: node linkType: hard +"auto-id-examples@workspace:examples/auto-id": + version: 0.0.0-use.local + resolution: "auto-id-examples@workspace:examples/auto-id" + dependencies: + "@autonomys/auto-id": "workspace:*" + "@types/node": "npm:^20.12.12" + dotenv: "npm:^16.4.5" + ts-node: "npm:^10.9.2" + typescript: "npm:^5.4.5" + languageName: unknown + linkType: soft + "auto-sdk-next-example@workspace:examples/next": version: 0.0.0-use.local resolution: "auto-sdk-next-example@workspace:examples/next"