Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Registry & its registration example #47

Merged
merged 14 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .yarn/install-state.gz
Binary file not shown.
2 changes: 2 additions & 0 deletions examples/auto-id/.env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
RPC_URL=<rpc_url>
KEYPAIR_URI=<keypair_uri>
17 changes: 17 additions & 0 deletions examples/auto-id/README.md
Original file line number Diff line number Diff line change
@@ -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
```
30 changes: 30 additions & 0 deletions examples/auto-id/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "auto-id-examples",
"version": "0.1.0",
"private": true,
"license": "MIT",
"packageManager": "[email protected]",
"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"
}
}
78 changes: 78 additions & 0 deletions examples/auto-id/src/registerAutoId.ts
Original file line number Diff line number Diff line change
@@ -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)
})
10 changes: 10 additions & 0 deletions examples/auto-id/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src/"
},
"include": [
"src/**/*",
],
}
15 changes: 9 additions & 6 deletions packages/auto-id/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
}
1 change: 1 addition & 0 deletions packages/auto-id/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './certificateManager'
export * from './keyManagement'
export * from './registry'
export * from './utils'
151 changes: 151 additions & 0 deletions packages/auto-id/src/registry.ts
Original file line number Diff line number Diff line change
@@ -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<RegistrationResult> {
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 }
}
}
35 changes: 19 additions & 16 deletions packages/auto-id/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
3 changes: 0 additions & 3 deletions packages/auto-id/tests/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -18,8 +17,6 @@ describe('Verify crypto functions', () => {

// DER encode the OID
const derEncodedOID = derEncodeSignatureAlgorithmOID(signatureAlgorithmOID)
// <Buffer 30 0d 06 09 2a 86 48 86 f7 0d 01 01 0b 05 00>
// console.log(Buffer.from(derEncodedOID))

// Convert derEncodedOID to hex string for comparison
const derEncodedOIDHex = Buffer.from(derEncodedOID).toString('hex')
Expand Down
Loading
Loading