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

feat: add Certificates store and validation #2072

Merged
merged 27 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9c123bc
chore: start moving certificates to its own store
siepra Nov 15, 2023
51e4b9c
chore: postpone validation work
siepra Nov 15, 2023
5d6eb4f
chore: move certificates to its own store
siepra Nov 15, 2023
10bae11
chore: Merge branch 'chore/validate-csrs' into chore/1899
siepra Nov 16, 2023
c7a1fa1
fix: checking username availability
siepra Nov 16, 2023
00180bf
test: fix
siepra Nov 16, 2023
877a038
fix: lint
siepra Nov 16, 2023
fe15b53
test: temporarily remove empty spec files
siepra Nov 16, 2023
68e9754
fix: resolving promises on certificates loading
siepra Nov 16, 2023
14793ff
fix: updating peer list
siepra Nov 16, 2023
ccb11a8
chore: remove leftover
siepra Nov 17, 2023
a4ad686
test: add certificates store unit tests
siepra Nov 24, 2023
714235b
feat: validate certificates against the authority and format #1899
siepra Nov 28, 2023
73c4f8d
chore: Merge chore/validate-scrs into chore/1899
siepra Nov 28, 2023
f7ff976
chore: Revert "chore: Merge chore/validate-scrs into chore/1899"
siepra Nov 28, 2023
869c389
chore: Merge branch 'develop' into chore/1899
siepra Nov 28, 2023
9b222fe
chore: update CHANGELOG.md
siepra Nov 28, 2023
a725fa4
chore: Merge branch 'chore/validate-csrs' into chore/1899
siepra Nov 28, 2023
45b5547
test: verify certificate against the authority (negative case)
siepra Nov 28, 2023
b704261
fix: lint
siepra Nov 28, 2023
fae74a7
bug: updatePeersList returning wrong data type
vinkabuki Nov 28, 2023
64d64c0
fix: update localdb peers list
vinkabuki Nov 28, 2023
561d8d5
chore: Revert "fix: update localdb peers list"
siepra Nov 28, 2023
c67793e
chore: Merge branch 'develop' into chore/1899
siepra Nov 28, 2023
a3f28a0
test: unskip write event test
siepra Nov 28, 2023
e28124a
test: remove deprecated and broken cases
siepra Nov 29, 2023
3134e20
fix: disable broken test
siepra Nov 29, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { registerDecorator, ValidationArguments, ValidationOptions } from 'class
import Logger from '../common/logger'

const logger = Logger('registration.validators')

export function IsCsr(validationOptions?: ValidationOptions) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I get it right, that we want to introduce the same method for certificate? Also, what are we particularly interested in validating inside CertificateContainsFields?

return function (object: object, propertyName: string) {
registerDecorator({
Expand Down
138 changes: 138 additions & 0 deletions packages/backend/src/nest/storage/certificates/certificates.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { getCrypto } from 'pkijs'

import { EventEmitter } from 'events'
import { StorageEvents } from '../storage.types'

import EventStore from 'orbit-db-eventstore'
import OrbitDB from 'orbit-db'

import { loadCertificate, keyFromCertificate } from '@quiet/identity'

import { ConnectionProcessInfo, NoCryptoEngineError, SocketActionTypes } from '@quiet/types'

import { IsNotEmpty, IsBase64, validate } from 'class-validator'
import { ValidationError } from '@nestjs/common'

import createLogger from '../../common/logger'

const logger = createLogger('CertificatesStore')

class UserCertificateData {
@IsNotEmpty()
@IsBase64()
certificate: string
}

export class CertificatesStore {
public orbitDb: OrbitDB
public store: EventStore<string>

constructor(orbitDb: OrbitDB) {
this.orbitDb = orbitDb
}

public async init(emitter: EventEmitter) {
logger('Initializing certificates log store')

this.store = await this.orbitDb.log<string>('certificates', {
replicate: false,
accessController: {
write: ['*'],
},
})

this.store.events.on('write', async (_address, entry) => {
logger('Saved certificate locally')

emitter.emit(StorageEvents.LOAD_CERTIFICATES, {
certificates: await this.getCertificates(),
})

// await this.updatePeersList()
})

this.store.events.on('ready', async () => {
siepra marked this conversation as resolved.
Show resolved Hide resolved
logger('Loaded certificates to memory')

emitter.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.LOADED_CERTIFICATES)

emitter.emit(StorageEvents.LOAD_CERTIFICATES, {
certificates: await this.getCertificates(),
})
})

this.store.events.on('replicated', async () => {
logger('REPLICATED: Certificates')

emitter.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.CERTIFICATES_REPLICATED)

emitter.emit(StorageEvents.LOAD_CERTIFICATES, {
certificates: await this.getCertificates(),
})

// await this.updatePeersList()
})

}

private async validateCertificate(certificate: string): Promise<boolean> {
logger('Validating certificate')
try {
const crypto = getCrypto()

if (!crypto) {
throw new NoCryptoEngineError()
}

const parsedCertificate = loadCertificate(certificate)
await parsedCertificate.verify()

await this.validateCertificateFormat(certificate)

// Validate

} catch (err) {
logger.error('Failed to validate user certificate:', certificate, err?.message)
return false
}

return true
}

private async validateCertificateFormat(certificate: string): Promise<ValidationError[]> {
const data = new UserCertificateData()
data.certificate = certificate

const validationErrors = await validate(data)

return validationErrors
}

protected async getCertificates() {
const filteredCertificatesMap: Map<string, string> = new Map()

const allCertificates = this.store
.iterator({ limit: -1 })
.collect()
.map(e => e.payload.value)

await Promise.all(
allCertificates
.filter(async certificate => {
const validation = await this.validateCertificate(certificate)
return Boolean(validation)
}).map(async certificate => {
const parsedCertificate = loadCertificate(certificate)
const pubKey = keyFromCertificate(parsedCertificate)

if (filteredCertificatesMap.has(pubKey)) {
filteredCertificatesMap.delete(pubKey)
}

filteredCertificatesMap.set(pubKey, certificate)
})
)
return [...filteredCertificatesMap.values()]
}

}
185 changes: 80 additions & 105 deletions packages/backend/src/nest/storage/storage.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ interface CsrReplicatedPromiseValues {
@Injectable()
export class StorageService extends EventEmitter {
public channels: KeyValueStore<PublicChannel>
private certificates: EventStore<string>
private certificatesRequests: EventStore<string>
public publicChannelsRepos: Map<string, PublicChannelsRepo> = new Map()
public directMessagesRepos: Map<string, DirectMessagesRepo> = new Map()
Expand Down Expand Up @@ -167,9 +166,6 @@ export class StorageService extends EventEmitter {
if (this.channels?.address) {
dbs.push(this.channels.address)
}
if (this.certificates?.address) {
dbs.push(this.certificates.address)
}
if (this.certificatesRequests?.address) {
dbs.push(this.certificatesRequests.address)
}
Expand Down Expand Up @@ -232,7 +228,7 @@ export class StorageService extends EventEmitter {
this.logger('1/5')
await this.createDbForChannels()
this.logger('2/5')
await this.createDbForCertificates()
// await this.attachCertificatesStoreListeners()
this.logger('3/5')
await this.createDbForCertificatesRequests()
this.logger('4/5')
Expand Down Expand Up @@ -313,94 +309,88 @@ export class StorageService extends EventEmitter {
this.logger.error('Error closing channels db', e)
}

try {
await this.certificates?.close()
} catch (e) {
this.logger.error('Error closing certificates db', e)
}
// try {
// await this.certificates?.close()
// } catch (e) {
// this.logger.error('Error closing certificates db', e)
// }

try {
await this.certificatesRequests?.close()
} catch (e) {
this.logger.error('Error closing certificates db', e)
}

try {
await this.communityMetadata?.close()
} catch (e) {
this.logger.error('Error closing community metadata db', e)
}

await this.__stopOrbitDb()
await this.__stopIPFS()
}

public async updatePeersList() {
const allUsers = this.getAllUsers()
const registeredUsers = this.getAllRegisteredUsers()
const peers = [...new Set(await getUsersAddresses(allUsers.concat(registeredUsers)))]
console.log('updatePeersList, peers count:', peers.length)
const community = await this.localDbService.get(LocalDBKeys.COMMUNITY)
this.emit(StorageEvents.UPDATE_PEERS_LIST, { communityId: community.id, peerList: peers })
}

public async loadAllCertificates() {
this.logger('Getting all certificates')
this.emit(StorageEvents.LOAD_CERTIFICATES, {
certificates: this.getAllEventLogEntries(this.certificates),
})
}

public async createDbForCertificates() {
this.logger('createDbForCertificates init')
this.certificates = await this.orbitDb.log<string>('certificates', {
replicate: false,
accessController: {
write: ['*'],
},
})
this.certificates.events.on('replicate.progress', async (_address, _hash, entry, _progress, _total) => {
siepra marked this conversation as resolved.
Show resolved Hide resolved
const certificate = entry.payload.value

const parsedCertificate = parseCertificate(certificate)
const key = keyFromCertificate(parsedCertificate)

const username = getCertFieldValue(parsedCertificate, CertFieldsTypes.nickName)
siepra marked this conversation as resolved.
Show resolved Hide resolved
if (!username) {
this.logger.error(
`Certificates replicate.progress: could not parse certificate for field type ${CertFieldsTypes.nickName}`
)
return
}

this.userNamesMap.set(key, username)
siepra marked this conversation as resolved.
Show resolved Hide resolved
})
this.certificates.events.on('replicated', async () => {
this.logger('REPLICATED: Certificates')
this.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.CERTIFICATES_REPLICATED)
this.emit(StorageEvents.LOAD_CERTIFICATES, {
certificates: this.getAllEventLogEntries(this.certificates),
})
await this.updatePeersList()
})
this.certificates.events.on('write', async (_address, entry) => {
this.logger('Saved certificate locally')
this.emit(StorageEvents.LOAD_CERTIFICATES, {
certificates: this.getAllEventLogEntries(this.certificates),
})
await this.updatePeersList()
})
this.certificates.events.on('ready', () => {
this.logger('Loaded certificates to memory')
this.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.LOADED_CERTIFICATES)
this.emit(StorageEvents.LOAD_CERTIFICATES, {
certificates: this.getAllEventLogEntries(this.certificates),
})
})

// @ts-expect-error - OrbitDB's type declaration of `load` lacks 'options'
await this.certificates.load({ fetchEntryTimeout: 15000 })
const allCertificates = this.getAllEventLogEntries(this.certificates)
this.logger('ALL Certificates COUNT:', allCertificates.length)
this.logger('STORAGE: Finished createDbForCertificates')
}
// public async loadAllCertificates() {
// this.logger('Getting all certificates')
// this.emit(StorageEvents.LOAD_CERTIFICATES, {
// certificates: this.getAllEventLogEntries(this.certificates),
// })
// }

// public async createDbForCertificates() {
// this.logger('createDbForCertificates init')
// this.certificates = await this.orbitDb.log<string>('certificates', {
// replicate: false,
// accessController: {
// write: ['*'],
// },
// })
// this.certificates.events.on('replicate.progress', async (_address, _hash, entry, _progress, _total) => {
// const certificate = entry.payload.value

// const parsedCertificate = parseCertificate(certificate)
// const key = keyFromCertificate(parsedCertificate)

// const username = getCertFieldValue(parsedCertificate, CertFieldsTypes.nickName)
// if (!username) {
// this.logger.error(
// `Certificates replicate.progress: could not parse certificate for field type ${CertFieldsTypes.nickName}`
// )
// return
// }

// this.userNamesMap.set(key, username)
// })
// this.certificates.events.on('replicated', async () => {
// this.logger('REPLICATED: Certificates')
// this.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.CERTIFICATES_REPLICATED)
// this.emit(StorageEvents.LOAD_CERTIFICATES, {
// certificates: this.getAllEventLogEntries(this.certificates),
// })
// await this.updatePeersList()
// })
// this.certificates.events.on('write', async (_address, entry) => {
// this.logger('Saved certificate locally')
// this.emit(StorageEvents.LOAD_CERTIFICATES, {
// certificates: this.getAllEventLogEntries(this.certificates),
// })
// await this.updatePeersList()
// })
// this.certificates.events.on('ready', () => {
// this.logger('Loaded certificates to memory')
// this.emit(SocketActionTypes.CONNECTION_PROCESS_INFO, ConnectionProcessInfo.LOADED_CERTIFICATES)
// this.emit(StorageEvents.LOAD_CERTIFICATES, {
// certificates: this.getAllEventLogEntries(this.certificates),
// })
// })

// // @ts-expect-error - OrbitDB's type declaration of `load` lacks 'options'
// await this.certificates.load({ fetchEntryTimeout: 15000 })
// const allCertificates = this.getAllEventLogEntries(this.certificates)
// this.logger('ALL Certificates COUNT:', allCertificates.length)
// this.logger('STORAGE: Finished createDbForCertificates')
// }

private createCsrReplicatedPromise(id: number) {
let resolveFunction
Expand Down Expand Up @@ -459,6 +449,7 @@ export class StorageService extends EventEmitter {

await this.updatePeersList()
})

this.certificatesRequests.events.on('write', async (_address, entry) => {
const csr: string = entry.payload.value
this.logger('Saved CSR locally')
Expand Down Expand Up @@ -864,16 +855,16 @@ export class StorageService extends EventEmitter {
this.filesManager.emit(IpfsFilesManagerEvents.CANCEL_DOWNLOAD, mid)
}

public async saveCertificate(payload: SaveCertificatePayload): Promise<boolean> {
this.logger('About to save certificate...')
if (!payload.certificate) {
this.logger('Certificate is either null or undefined, not saving to db')
return false
}
this.logger('Saving certificate...')
await this.certificates.add(payload.certificate)
return true
}
// public async saveCertificate(payload: SaveCertificatePayload): Promise<boolean> {
// this.logger('About to save certificate...')
// if (!payload.certificate) {
// this.logger('Certificate is either null or undefined, not saving to db')
// return false
// }
// this.logger('Saving certificate...')
// await this.certificates.add(payload.certificate)
// return true
// }

public async saveCSR(payload: SaveCSRPayload): Promise<boolean> {
this.logger('About to save csr...')
Expand Down Expand Up @@ -915,22 +906,6 @@ export class StorageService extends EventEmitter {
return allUsers
}

public getAllUsers(): UserData[] {
const csrs = this.getAllEventLogEntries(this.certificatesRequests)
this.logger('CSRs count:', csrs.length)
const allUsers: UserData[] = []
for (const csr of csrs) {
const parsedCert = parseCertificationRequest(csr)
const onionAddress = getReqFieldValue(parsedCert, CertFieldsTypes.commonName)
const peerId = getReqFieldValue(parsedCert, CertFieldsTypes.peerId)
const username = getReqFieldValue(parsedCert, CertFieldsTypes.nickName)
const dmPublicKey = getReqFieldValue(parsedCert, CertFieldsTypes.dmPublicKey)
if (!onionAddress || !peerId || !username || !dmPublicKey) continue
allUsers.push({ onionAddress, peerId, username, dmPublicKey })
}
return allUsers
}

public usernameCert(username: string): string | null {
/**
* Check if given username is already in use
Expand Down
Loading