Skip to content

Commit

Permalink
Merge pull request #2 from animo/create-self-signed-certificate-test
Browse files Browse the repository at this point in the history
feat: create and verify a self signed x.509 certificate in tests
  • Loading branch information
TimoGlastra authored Jun 14, 2024
2 parents f2a1d0b + bfe16a1 commit c5dfd70
Show file tree
Hide file tree
Showing 13 changed files with 11,322 additions and 791 deletions.
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,20 @@
"test": "node --import tsx --test ./tests/*.test.ts"
},
"dependencies": {
"@credo-ts/core": "^0.5.3",
"@hyperledger/aries-askar-shared": "^0.2.1",
"@noble/hashes": "^1.4.0",
"@peculiar/asn1-ecc": "^2.3.8",
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/asn1-x509": "^2.3.8",
"@peculiar/webcrypto": "^1.5.0",
"webcrypto-core": "^1.8.0"
},
"devDependencies": {
"@biomejs/biome": "1.8.1",
"@hyperledger/aries-askar-nodejs": "^0.2.1",
"@peculiar/x509": "^1.11.0",
"@types/node": "^20.14.2",
"tsx": "^4.15.2",
"typescript": "~5.4.5"
}
Expand Down
11,519 changes: 10,779 additions & 740 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

99 changes: 88 additions & 11 deletions src/EcdsaProvider.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,103 @@
import { AsnConvert, AsnParser } from '@peculiar/asn1-schema'
import { SubjectPublicKeyInfo } from '@peculiar/asn1-x509'
import { ecdsaWithSHA256 } from '@peculiar/asn1-ecc'
import * as core from 'webcrypto-core'
import { askarKeyGenerate, askarKeySign, askarKeyVerify } from './askar'
import type { CryptoKeyPair } from './types'
import {
askarExportKeyToJwk,
askarKeyFromSecretBytes,
askarKeyFromPublicBytes,
askarKeyGenerate,
askarKeyGetPublicBytes,
askarKeySign,
askarKeyVerify,
askarKeyFromJwk,
} from './askar'
import type {
CryptoKeyPair,
EcKeyGenParams,
EcKeyImportParams,
EcdsaParams,
JsonWebKey,
KeyFormat,
KeyUsage,
} from './types'

export class Ed25519Provider extends core.Ed25519Provider {
public async onSign(_algorithm: string, key: core.CryptoKey, data: ArrayBuffer): Promise<ArrayBuffer> {
return askarKeySign(key, data)
export class EcdsaProvider extends core.EcdsaProvider {
public async onSign(algorithm: EcdsaParams, key: core.CryptoKey, data: ArrayBuffer): Promise<ArrayBuffer> {
if (algorithm.hash.name !== 'SHA-256') {
throw new Error(`Invalid hashing algorithm. Expected: 'SHA-256', received: ${algorithm.hash.name}`)
}

const signature = askarKeySign({ key, data })

return signature
}

public async onVerify(
_algorithm: string,
algorithm: EcdsaParams,
key: core.CryptoKey,
signature: ArrayBuffer,
data: ArrayBuffer
): Promise<boolean> {
return askarKeyVerify(key, data, signature)
if (algorithm.hash.name !== 'SHA-256') {
throw new Error(`Invalid hashing algorithm. Expected: 'SHA-256', received: ${algorithm.hash.name}`)
}

const isValid = askarKeyVerify({ key, data, signature })

return isValid
}

public async onGenerateKey(
algorithm: { name: string },
_extractable: boolean,
_keyUsages: string[]
algorithm: EcKeyGenParams,
extractable: boolean,
keyUsages: KeyUsage[]
): Promise<CryptoKeyPair> {
return askarKeyGenerate(algorithm.name)
const key = askarKeyGenerate({ algorithm, extractable, keyUsages })

return key
}

public async onExportKey(format: KeyFormat, key: core.CryptoKey): Promise<JsonWebKey | ArrayBuffer> {
switch (format.toLowerCase()) {
case 'spki': {
const publicKeyInfo = new SubjectPublicKeyInfo({
algorithm: ecdsaWithSHA256,
subjectPublicKey: askarKeyGetPublicBytes(key).buffer,
})

const derEncoded = AsnConvert.serialize(publicKeyInfo)
return derEncoded
}
case 'jwk':
return askarExportKeyToJwk(key)
case 'raw':
return askarKeyGetPublicBytes(key).buffer
default:
throw new Error(`Not supported format: ${format}`)
}
}

public async onImportKey(
format: KeyFormat,
keyData: JsonWebKey | ArrayBuffer,
_algorithm: EcKeyImportParams,
extractable: boolean,
keyUsages: KeyUsage[]
): Promise<core.CryptoKey> {
if (format !== 'jwk' && ArrayBuffer.isView(keyData)) {
throw new core.OperationError('non-jwk formats can only be used with an ArrayBuffer')
}

switch (format.toLowerCase()) {
case 'jwk':
return askarKeyFromJwk({
extractable,
keyUsages,
keyData: keyData as JsonWebKey,
})
default:
throw new core.OperationError("Only format 'jwt' is supported for importing keys")
}
}
}
26 changes: 26 additions & 0 deletions src/Ed25519Provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as core from 'webcrypto-core'
import { askarKeyGenerate, askarKeySign, askarKeyVerify } from './askar'
import type { CryptoKeyPair, EcKeyGenParams, KeyUsage } from './types'

export class Ed25519Provider extends core.Ed25519Provider {
public async onSign(_algorithm: string, key: core.CryptoKey, data: ArrayBuffer): Promise<ArrayBuffer> {
return askarKeySign({ key, data })
}

public async onVerify(
_algorithm: string,
key: core.CryptoKey,
signature: ArrayBuffer,
data: ArrayBuffer
): Promise<boolean> {
return askarKeyVerify({ key, data, signature })
}

public async onGenerateKey(
algorithm: EcKeyGenParams,
extractable: boolean,
keyUsages: KeyUsage[]
): Promise<CryptoKeyPair> {
return askarKeyGenerate({ algorithm, keyUsages, extractable })
}
}
19 changes: 19 additions & 0 deletions src/Sha1Provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as core from 'webcrypto-core'
import { sha1 } from '@noble/hashes/sha1'
import type { HashAlgorithm } from './types'

export class Sha1Provider extends core.ProviderCrypto {
public name = 'SHA-1'
public usages = []

public override async onDigest(algorithm: HashAlgorithm, data: ArrayBuffer): Promise<ArrayBuffer> {
switch (algorithm.name.toUpperCase()) {
case 'SHA-1': {
const hash = sha1(new Uint8Array(data))
return hash.buffer
}
default:
throw new Error(`Hashing algorithm: ${JSON.stringify(algorithm)} is not supported`)
}
}
}
163 changes: 156 additions & 7 deletions src/askar.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import type * as core from 'webcrypto-core'
import { getCryptoKey, setCryptoKey } from './storage'
import { Key, type KeyAlgs } from '@hyperledger/aries-askar-shared'
import { Key, type KeyAlgs, CryptoBox, Jwk } from '@hyperledger/aries-askar-shared'
import type { EcKeyGenParams, EcKeyImportParams, JsonWebKey, KeyFormat, KeyUsage } from './types'

export const askarKeySign = (key: core.CryptoKey, data: ArrayBuffer) => {
const CBOX_NONCE_LENGTH = 24

export const askarKeySign = ({
key,
data,
}: {
key: core.CryptoKey
data: ArrayBuffer
}) => {
const internalKey = getCryptoKey(key)
const signature = internalKey.signMessage({
message: new Uint8Array(data),
Expand All @@ -11,7 +20,15 @@ export const askarKeySign = (key: core.CryptoKey, data: ArrayBuffer) => {
return signature
}

export const askarKeyVerify = (key: core.CryptoKey, data: ArrayBuffer, signature: ArrayBuffer) => {
export const askarKeyVerify = ({
key,
data,
signature,
}: {
key: core.CryptoKey
data: ArrayBuffer
signature: ArrayBuffer
}) => {
const internalKey = getCryptoKey(key)

const isVerified = internalKey.verifySignature({
Expand All @@ -22,9 +39,141 @@ export const askarKeyVerify = (key: core.CryptoKey, data: ArrayBuffer, signature
return isVerified
}

export const askarKeyGenerate = (algorithm: string) => {
const key = Key.generate(algorithm as KeyAlgs)
const cKey = setCryptoKey(key)
export const askarKeyGenerate = ({
extractable,
algorithm,
keyUsages,
}: {
algorithm: EcKeyGenParams
extractable: boolean
keyUsages: KeyUsage[]
}) => {
const key = Key.generate(cryptoAlgorithmToAskarAlgorithm(algorithm))

const publicKey = setCryptoKey({
askarKey: key,
extractable,
// Filter out properties that are not possible for the public key
keyUsages: keyUsages.filter((u) => u !== 'sign'),
keyType: 'public',
})

const secretKey = setCryptoKey({
askarKey: key,
keyType: 'private',
keyUsages,
extractable,
})

return { publicKey, privateKey: secretKey }
}

export const askarKeyGetPublicBytes = (key: core.CryptoKey) => {
const cKey = getCryptoKey(key)

return { publicKey: cKey, privateKey: cKey }
return cKey.publicBytes
}

export const askarKeyFromPublicBytes = ({
algorithm,
keyData,
extractable,
keyUsages,
}: {
algorithm: EcKeyImportParams
keyData: Uint8Array
format: KeyFormat
extractable: boolean
keyUsages: KeyUsage[]
}) => {
const publicKey = Key.fromPublicBytes({
algorithm: cryptoAlgorithmToAskarAlgorithm(algorithm),
publicKey: keyData,
})

return setCryptoKey({
askarKey: publicKey,
extractable,
keyUsages,
keyType: 'public',
})
}

export const askarKeyFromSecretBytes = ({
algorithm,
keyData,
extractable,
keyUsages,
}: {
algorithm: EcKeyImportParams
keyData: Uint8Array
format: KeyFormat
extractable: boolean
keyUsages: KeyUsage[]
}) => {
const privateKey = Key.fromSecretBytes({
algorithm: cryptoAlgorithmToAskarAlgorithm(algorithm),
secretKey: keyData,
})

return setCryptoKey({
askarKey: privateKey,
extractable,
keyUsages,
keyType: 'private',
})
}

export const askarExportKeyToJwk = (key: core.CryptoKey) => {
const askarKey = getCryptoKey(key)

if (key.type === 'public') return askarKey.jwkPublic
if (key.type === 'private' || key.type === 'secret') {
return askarKey.jwkSecret
}

throw new Error(`key.type '${key.type}' is not a string of 'public'/'private'/'secret'`)
}

export const askarGetRandomValues = (buffer: Uint8Array): Uint8Array => {
const genCount = Math.ceil(buffer.length / CBOX_NONCE_LENGTH)
const buf = new Uint8Array(genCount * CBOX_NONCE_LENGTH)
for (let i = 0; i < genCount; i++) {
const randomBytes = CryptoBox.randomNonce()
buf.set(randomBytes, CBOX_NONCE_LENGTH * i)
}
buffer.set(buf.subarray(0, buffer.length))
return buffer
}

export const askarKeyFromJwk = ({
keyData,
keyUsages,
extractable,
}: {
keyData: JsonWebKey
keyUsages: KeyUsage[]
extractable: boolean
}) => {
const askarKey = Key.fromJwk({ jwk: new Jwk(keyData) })
try {
askarKey.secretBytes
return setCryptoKey({
askarKey,
keyUsages,
extractable,
keyType: 'private',
})
} catch {
return setCryptoKey({
askarKey,
keyUsages,
extractable,
keyType: 'public',
})
}
}

// TODO: this needs a proper conversion
const cryptoAlgorithmToAskarAlgorithm = (algorithm: EcKeyGenParams) =>
(algorithm.namedCurve ? algorithm.namedCurve : algorithm.name) as KeyAlgs
13 changes: 11 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as core from 'webcrypto-core'
import { Ed25519Provider } from './Ed25519Provider'
import { EcdsaProvider } from './EcdsaProvider'
import { askarGetRandomValues } from './askar'
import { Sha1Provider } from './Sha1Provider'

class Subtle extends core.SubtleCrypto {
public constructor() {
Expand All @@ -9,14 +11,21 @@ class Subtle extends core.SubtleCrypto {
this.providers.set(new EcdsaProvider())

this.providers.set(new Ed25519Provider())

this.providers.set(new Sha1Provider())
}
}

export class Crypto extends core.Crypto {
public subtle = new Subtle()

public getRandomValues<T extends ArrayBufferView | null>(_array: T): T {
throw new Error('getRandomValues is not implemented')
public getRandomValues<T extends ArrayBufferView | null>(array: T): T {
if (!ArrayBuffer.isView(array)) {
throw new TypeError('Input is not an array buffer view')
}
const buffer = new Uint8Array(array.buffer, array.byteOffset, array.byteLength)
askarGetRandomValues(buffer)
return array
}
}

Expand Down
Loading

0 comments on commit c5dfd70

Please sign in to comment.