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 7 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
2 changes: 2 additions & 0 deletions packages/auto-id/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
RPC_URL=<rpc_url>
KEYPAIR_URI=<keypair_uri>
78 changes: 78 additions & 0 deletions packages/auto-id/examples/registerAutoId.ts
abhi3700 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Example of how to register an auto-id
*/

import * as x509 from '@peculiar/x509'
import { Keyring } from '@polkadot/api'
import { cryptoWaitReady } from '@polkadot/util-crypto'
import { config } from 'dotenv'
import { CertificateManager, Registry, generateEd25519KeyPair2 } from '../src/index'

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('test')
const issuerId = await register(selfIssuedCert, registry)

const userKeys = await generateEd25519KeyPair2()
const userCm = new CertificateManager(null, userKeys[0], userKeys[1])
const userCsr = await userCm.createAndSignCSR('user')
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)
})
6 changes: 4 additions & 2 deletions packages/auto-id/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
"build": "tsc",
"clean": "rm -rf dist",
"format": "prettier --write \"src/**/*.ts\"",
"test": "jest"
"test": "jest",
"register": "tsc && ts-node examples/registerAutoId.ts"
},
"dependencies": {
"@autonomys/auto-utils": "workspace:*",
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/asn1-x509": "^2.3.8",
"@peculiar/webcrypto": "^1.5.0",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5"
"asn1js": "^3.0.5",
"dotenv": "^16.4.5"
},
"files": [
"dist",
Expand Down
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: 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 }
}
}
61 changes: 53 additions & 8 deletions packages/auto-id/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,73 @@
import { compactAddLength } from '@polkadot/util'
import { ObjectIdentifier } from 'asn1js'
import { randomBytes } from 'crypto'

/**
* 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,
* and then applies SCALE encoding by prepending the length of the DER-encoded data in compact format.
*
* @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.
* @returns Uint8Array containing the SCALE encoded, DER-encoded OID with appended NULL parameter.
*/
export function derEncodeSignatureAlgorithmOID(oid: string): Uint8Array {
const objectIdentifier = new ObjectIdentifier({ value: oid })
const berArrayBuffer = objectIdentifier.toBER(false)
const oidEncoded = objectIdentifier.toBER(false)

// 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

// Calculate the total length including OID and NULL parameter
const totalLength = berArrayBuffer.byteLength + nullParameter.length
const totalLength = oidEncoded.byteLength + nullParameter.length

const sequenceHeader = [0x30, totalLength] // 0x30 is the DER tag for SEQUENCE
// Construct the sequence header
const sequenceHeader = [0x30] // 0x30 is the DER tag for SEQUENCE
if (totalLength < 128) {
sequenceHeader.push(totalLength) // Short form length
} else {
// Long form length encoding
const lengthBytes = []
let tempLength = totalLength
while (tempLength > 0) {
lengthBytes.push(tempLength & 0xff)
tempLength >>= 8
}
sequenceHeader.push(0x80 | lengthBytes.length, ...lengthBytes.reverse())
}

return new Uint8Array([...sequenceHeader, ...new Uint8Array(berArrayBuffer), ...nullParameter])
// Combine the sequence header, OID, and NULL parameter into one Uint8Array
const derSequence = new Uint8Array(
sequenceHeader.length + oidEncoded.byteLength + nullParameter.length,
)
derSequence.set(sequenceHeader, 0)
derSequence.set(new Uint8Array(oidEncoded), sequenceHeader.length)
derSequence.set(nullParameter, sequenceHeader.length + oidEncoded.byteLength)
abhi3700 marked this conversation as resolved.
Show resolved Hide resolved

// Apply SCALE encoding by prepending the compact length of the entire DER sequence
return compactAddLength(derSequence)
abhi3700 marked this conversation as resolved.
Show resolved Hide resolved
}

// CLEANUP: Remove later when all registry functionalities work.
// /**
// * Encodes a given string representation of an OID into its DER format.
// * This function is specifically used to encode signature algorithm OIDs.
// *
// * @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.
// */
// export function derEncodeSignatureAlgorithmOID(oid: string): Uint8Array {
abhi3700 marked this conversation as resolved.
Show resolved Hide resolved
// const objectIdentifier = new ObjectIdentifier({ value: oid })
// const berArrayBuffer = objectIdentifier.toBER(false)

// // 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

// // 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])
// }

export function addDaysToCurrentDate(days: number): Date {
const currentDate = new Date() // This gives you the current date and time
currentDate.setUTCDate(currentDate.getUTCDate() + days) // Adds the specified number of days
Expand Down
5 changes: 2 additions & 3 deletions packages/auto-id/tests/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ 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))
// <Buffer 3c 30 0d 06 09 2a 86 48 86 f7 0d 01 01 0b 05 00>

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

// Expected DER encoded OID from the result of tests in https://github.com/subspace/subspace/blob/d875a5aac35c1732eec61ce4359782eff58ff6fc/domains/pallets/auto-id/src/tests.rs#L127
expect(derEncodedOIDHex).toEqual('300d06092a864886f70d01010b0500')
expect(derEncodedOIDHex).toEqual('3c300d06092a864886f70d01010b0500')
})
})
5 changes: 2 additions & 3 deletions packages/auto-id/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
},
"include": [
"src/**/*",
],
"exclude": [
"examples/**/*"
]
],
"exclude": []
}
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ __metadata:
"@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"
Expand Down
Loading