Skip to content

Commit

Permalink
feat(askar): improved hardware key support (#1968)
Browse files Browse the repository at this point in the history
* feat(askar): improved hardware key support

Signed-off-by: Berend Sliedrecht <sliedrecht@berend.io>

* chore: resolved feedback and formatting

Signed-off-by: Berend Sliedrecht <sliedrecht@berend.io>

---------

Signed-off-by: Berend Sliedrecht <sliedrecht@berend.io>
  • Loading branch information
berendsliedrecht committed Jul 29, 2024
1 parent b387574 commit 1a941e7
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 339 deletions.
9 changes: 8 additions & 1 deletion packages/askar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"tsyringe": "^4.8.0"
},
"devDependencies": {
"@animo-id/expo-secure-environment": "^0.0.1-alpha.0",
"@hyperledger/aries-askar-nodejs": "^0.2.3",
"@hyperledger/aries-askar-shared": "^0.2.3",
"@types/bn.js": "^5.1.0",
Expand All @@ -44,6 +45,12 @@
"typescript": "~5.5.2"
},
"peerDependencies": {
"@hyperledger/aries-askar-shared": "^0.2.3"
"@hyperledger/aries-askar-shared": "^0.2.3",
"@animo-id/expo-secure-environment": "^0.0.1-alpha.0"
},
"peerDependenciesMeta": {
"@animo-id/expo-secure-environment": {
"optional": true
}
}
}
1 change: 1 addition & 0 deletions packages/askar/src/secureEnvironment/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './secureEnvironment'
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function importSecureEnvironment() {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const secureEnvironment = require('@animo-id/expo-secure-environment')
return secureEnvironment
} catch (error) {
throw new Error('@animo-id/expo-secure-environment must be installed as a peer dependency')
}
}
9 changes: 9 additions & 0 deletions packages/askar/src/secureEnvironment/secureEnvironment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function importSecureEnvironment(): {
sign: (id: string, message: Uint8Array) => Promise<Uint8Array>
getPublicBytesForKeyId: (id: string) => Uint8Array
generateKeypair: (id: string) => void
} {
throw new Error(
'@animo-id/expo-secure-environment cannot be imported in Node.js. Currently, there is no hardware key support for node.js'
)
}
80 changes: 76 additions & 4 deletions packages/askar/src/wallet/AskarBaseWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,20 @@ import {
Key,
TypedArrayEncoder,
KeyBackend,
KeyType,
utils,
} from '@credo-ts/core'
import { CryptoBox, Store, Key as AskarKey, keyAlgFromString } from '@hyperledger/aries-askar-shared'
import BigNumber from 'bn.js'

import { importSecureEnvironment } from '../secureEnvironment'
import {
AskarErrorCode,
AskarKeyTypePurpose,
isAskarError,
isKeyTypeSupportedByAskarForPurpose,
keyTypesSupportedByAskar,
} from '../utils'
import { convertToAskarKeyBackend } from '../utils/askarKeyBackend'

import { didcommV1Pack, didcommV1Unpack } from './didcommV1'

Expand Down Expand Up @@ -131,6 +133,7 @@ export abstract class AskarBaseWallet implements Wallet {
seed,
privateKey,
keyType,
keyId,
keyBackend = KeyBackend.Software,
}: WalletCreateKeyOptions): Promise<Key> {
try {
Expand All @@ -146,7 +149,14 @@ export abstract class AskarBaseWallet implements Wallet {
throw new WalletError('Invalid private key provided')
}

if (isKeyTypeSupportedByAskarForPurpose(keyType, AskarKeyTypePurpose.KeyManagement)) {
if (keyBackend === KeyBackend.SecureElement && keyType !== KeyType.P256) {
throw new WalletError(`Keytype '${keyType}' is not supported for the secure element`)
}

if (
isKeyTypeSupportedByAskarForPurpose(keyType, AskarKeyTypePurpose.KeyManagement) &&
keyBackend === KeyBackend.Software
) {
const algorithm = keyAlgFromString(keyType)

// Create key
Expand All @@ -156,7 +166,7 @@ export abstract class AskarBaseWallet implements Wallet {
? AskarKey.fromSecretBytes({ secretKey: privateKey, algorithm })
: seed
? AskarKey.fromSeed({ seed, algorithm })
: AskarKey.generate(algorithm, convertToAskarKeyBackend(keyBackend))
: AskarKey.generate(algorithm)

// FIXME: we need to create a separate const '_key' so TS definitely knows _key is defined in the session callback.
// This will be fixed once we use the new 'using' syntax
Expand All @@ -166,7 +176,7 @@ export abstract class AskarBaseWallet implements Wallet {

// Store key
await this.withSession((session) =>
session.insertKey({ key: _key, name: TypedArrayEncoder.toBase58(keyPublicBytes) })
session.insertKey({ key: _key, name: keyId ?? TypedArrayEncoder.toBase58(keyPublicBytes) })
)

key.handle.free()
Expand All @@ -181,6 +191,22 @@ export abstract class AskarBaseWallet implements Wallet {
// Otherwise re-throw error
throw error
}
} else if (keyBackend === KeyBackend.SecureElement && keyType === KeyType.P256) {
const secureEnvironment = importSecureEnvironment()
const kid = keyId ?? utils.uuid()

// Generate a hardware-backed P-256 keypair
secureEnvironment.generateKeypair(kid)
const publicKeyBytes = secureEnvironment.getPublicBytesForKeyId(kid)
const publicKeyBase58 = TypedArrayEncoder.toBase58(publicKeyBytes)

await this.storeSecureEnvironmentKeyById({
keyType,
publicKeyBase58,
keyId: kid,
})

return new Key(publicKeyBytes, keyType, kid)
} else {
// Check if there is a signing key provider for the specified key type.
if (this.signingKeyProviderRegistry.hasProviderForKeyType(keyType)) {
Expand Down Expand Up @@ -250,6 +276,16 @@ export abstract class AskarBaseWallet implements Wallet {
// Now we can remove it from the custom record as we have imported it into Askar
await this.deleteKeyPair(key.publicKeyBase58)
keyPair = undefined
} else {
if (!(await this.doesSecureEnvironmentKeyExist(key.keyId))) {
throw new WalletError(`Secure Environment key with id '${key.keyId}' not found`)
}

if (Array.isArray(data[0])) {
throw new WalletError('Multi signature is not supported for the Secure Environment')
}

return Buffer.from(await importSecureEnvironment().sign(key.keyId, new Uint8Array(data as Buffer)))
}
}

Expand Down Expand Up @@ -482,6 +518,18 @@ export abstract class AskarBaseWallet implements Wallet {
}
}

private async doesSecureEnvironmentKeyExist(keyId: string): Promise<boolean> {
try {
const entryObject = await this.withSession((session) =>
session.fetch({ category: 'SecureEnvironmentKeyRecord', name: keyId })
)

return !!entryObject
} catch (error) {
throw new WalletError('Error retrieving Secure Environment record', { cause: error })
}
}

private async deleteKeyPair(publicKeyBase58: string): Promise<void> {
try {
await this.withSession((session) => session.remove({ category: 'KeyPairRecord', name: `key-${publicKeyBase58}` }))
Expand Down Expand Up @@ -509,4 +557,28 @@ export abstract class AskarBaseWallet implements Wallet {
throw new WalletError('Error saving KeyPair record', { cause: error })
}
}

private async storeSecureEnvironmentKeyById(options: {
keyId: string
publicKeyBase58: string
keyType: KeyType
}): Promise<void> {
try {
await this.withSession((session) =>
session.insert({
category: 'SecureEnvironmentKeyRecord',
name: options.keyId,
value: JSON.stringify(options),
tags: {
keyType: options.keyType,
},
})
)
} catch (error) {
if (isAskarError(error, AskarErrorCode.Duplicate)) {
throw new WalletKeyExistsError('Key already exists')
}
throw new WalletError('Error saving SecureEnvironment record', { cause: error })
}
}
}
2 changes: 1 addition & 1 deletion packages/askar/src/wallet/__tests__/AskarWallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ describe('AskarWallet basic operations', () => {
await expect(askarWallet.createKey({ seed, keyType: KeyType.P384 })).rejects.toThrow(WalletError)
})

test('Fail to create a P256 keypair in hardware', async () => {
test('Fail to create a P256 keypair in the secure environment', async () => {
await expect(
askarWallet.createKey({ keyType: KeyType.P256, keyBackend: KeyBackend.SecureElement })
).rejects.toThrow(WalletError)
Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/crypto/Key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,17 @@ export class Key {
public readonly publicKey: Buffer
public readonly keyType: KeyType

public constructor(publicKey: Uint8Array, keyType: KeyType) {
/**
*
* the identifier of the key. If not provided in the constructor the base58 encoded public key will be used as the key identifier by default
*
*/
public keyId: string

public constructor(publicKey: Uint8Array, keyType: KeyType, keyId?: string) {
this.publicKey = Buffer.from(publicKey)
this.keyType = keyType
this.keyId = keyId ?? TypedArrayEncoder.toBase58(this.publicKey)
}

public static fromPublicKey(publicKey: Uint8Array, keyType: KeyType) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/wallet/Wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export interface WalletCreateKeyOptions {
seed?: Buffer
privateKey?: Buffer
keyBackend?: KeyBackend
keyId?: string
}

export interface WalletSignOptions {
Expand Down
Loading

0 comments on commit 1a941e7

Please sign in to comment.