Skip to content

Commit

Permalink
feat: default to uncompressed keys (#2165)
Browse files Browse the repository at this point in the history
Signed-off-by: Berend Sliedrecht <[email protected]>
  • Loading branch information
berendsliedrecht authored Feb 11, 2025
1 parent bea846b commit 297d209
Show file tree
Hide file tree
Showing 42 changed files with 461 additions and 416 deletions.
9 changes: 9 additions & 0 deletions .changeset/cool-pets-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@credo-ts/anoncreds': minor
'@credo-ts/askar': minor
'@credo-ts/core': minor
---

- Rely on Uint8Array instead of Buffer for internal key bytes representation
- Remove dependency on external Big Number libraries
- Default to use of uncompressed keys for Secp256k1, Secp256r1, Secp384r1 and Secp521r1
9 changes: 9 additions & 0 deletions .changeset/cyan-parents-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@credo-ts/anoncreds': patch
'@credo-ts/askar': patch
'@credo-ts/core': patch
---

- Remove usage of Big Number libraries and rely on native implementations
- By default rely on uncompressed keys instead of compressed (for P256, P384, P521 and K256)
- Utilze Uint8Array more instead of Buffer (i.e. for internally representing a key)
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"@changesets/cli": "^2.27.5",
"@hyperledger/aries-askar-nodejs": "^0.2.3",
"@jest/types": "^29.6.3",
"@types/bn.js": "^5.1.5",
"@types/cors": "^2.8.10",
"@types/eslint": "^8.21.2",
"@types/express": "^4.17.13",
Expand All @@ -49,7 +48,6 @@
"@types/ws": "^8.5.4",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.14.1",
"bn.js": "^5.2.1",
"cors": "^2.8.5",
"eslint": "^8.36.0",
"eslint-config-prettier": "^8.3.0",
Expand Down
2 changes: 0 additions & 2 deletions packages/anoncreds/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@
"@credo-ts/core": "workspace:*",
"@credo-ts/didcomm": "workspace:*",
"@sphereon/pex-models": "^2.3.1",
"big-integer": "^1.6.51",
"bn.js": "^5.2.1",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"reflect-metadata": "^0.1.13"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ import {
injectable,
ClaimFormat,
} from '@credo-ts/core'
import BigNumber from 'bn.js'

import { AnonCredsHolderServiceSymbol, AnonCredsVerifierServiceSymbol } from '../services'
import { fetchCredentialDefinitions, fetchSchemas } from '../utils/anonCredsObjects'
import { bytesToBigint } from '../utils/bytesToBigint'
import { assertLinkSecretsMatch } from '../utils/linkSecret'
import { getAnonCredsTagsFromRecord } from '../utils/w3cAnonCredsUtils'

Expand Down Expand Up @@ -163,7 +163,7 @@ export class AnonCredsDataIntegrityService implements IAnonCredsDataIntegritySer
const credentialsWithMetadata: CredentialWithRevocationMetadata[] = []

const hash = Hasher.hash(TypedArrayEncoder.fromString(challenge), 'sha-256')
const nonce = new BigNumber(hash).toString().slice(0, 20)
const nonce = bytesToBigint(hash).toString().slice(0, 20)

const anonCredsProofRequest: AnonCredsProofRequest = {
version: '1.0',
Expand Down
12 changes: 12 additions & 0 deletions packages/anoncreds/src/utils/bytesToBigint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function bytesToBigint(b: Uint8Array): bigint {
if (b.length === 0) {
throw new Error('Empty byte array is not supported')
}

let value = 0n
for (let i = 0; i < b.length; i++) {
value = (value << 8n) | BigInt(b[i])
}

return value
}
8 changes: 4 additions & 4 deletions packages/anoncreds/src/utils/credential.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { AnonCredsSchema, AnonCredsCredentialValues } from '../models'
import type { CredentialPreviewAttributeOptions, LinkedAttachment } from '@credo-ts/didcomm'

import { Buffer, CredoError, Hasher, TypedArrayEncoder } from '@credo-ts/core'
import { CredoError, Hasher, TypedArrayEncoder } from '@credo-ts/core'
import { encodeAttachment } from '@credo-ts/didcomm'
import bigInt from 'big-integer'

import { bytesToBigint } from './bytesToBigint'

export type AnonCredsClaimRecord = Record<string, string | number>

Expand Down Expand Up @@ -66,9 +67,8 @@ export function encodeCredentialValue(value: unknown) {

const buffer = TypedArrayEncoder.fromString(String(value))
const hash = Hasher.hash(buffer, 'sha-256')
const hex = Buffer.from(hash).toString('hex')

return bigInt(hex, 16).toString()
return bytesToBigint(hash).toString()
}

export const mapAttributeRawValuesToAnonCredsCredentialValues = (
Expand Down
7 changes: 3 additions & 4 deletions packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ import type {
import type { AgentContext } from '@credo-ts/core'

import { Hasher, utils } from '@credo-ts/core'
import BigNumber from 'bn.js'

import {
getDidIndyCredentialDefinitionId,
getDidIndyRevocationRegistryDefinitionId,
getDidIndySchemaId,
} from '../../indy-vdr/src/anoncreds/utils/identifiers'
import { bytesToBigint } from '../src/utils/bytesToBigint'
import {
getUnQualifiedDidIndyDid,
getUnqualifiedRevocationRegistryDefinitionId,
Expand Down Expand Up @@ -377,7 +377,6 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry {
* Does this by hashing the schema id, transforming the hash to a number and taking the first 6 digits.
*/
function getSeqNoFromSchemaId(schemaId: string) {
const seqNo = Number(new BigNumber(Hasher.hash(schemaId, 'sha-256')).toString().slice(0, 5))

return seqNo
const hash = Hasher.hash(schemaId, 'sha-256')
return bytesToBigint(hash).toString().slice(0, 5)
}
4 changes: 1 addition & 3 deletions packages/askar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,15 @@
},
"dependencies": {
"@credo-ts/core": "workspace:*",
"bn.js": "^5.2.1",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"rxjs": "^7.8.0",
"tsyringe": "^4.8.0"
},
"devDependencies": {
"@animo-id/expo-secure-environment": "^0.1.0-alpha.11",
"@animo-id/expo-secure-environment": "^0.1.0-alpha.12",
"@hyperledger/aries-askar-nodejs": "^0.2.3",
"@hyperledger/aries-askar-shared": "^0.2.3",
"@types/bn.js": "^5.1.0",
"@types/ref-array-di": "^1.2.6",
"@types/ref-struct-di": "^1.1.10",
"reflect-metadata": "^0.1.13",
Expand Down
4 changes: 2 additions & 2 deletions packages/askar/src/secureEnvironment/secureEnvironment.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export function importSecureEnvironment(): {
sign: (id: string, message: Uint8Array) => Promise<Uint8Array>
getPublicBytesForKeyId: (id: string) => Uint8Array
generateKeypair: (id: string) => void
getPublicBytesForKeyId: (id: string) => Uint8Array | Promise<Uint8Array>
generateKeypair: (id: string) => void | Promise<Uint8Array>
} {
throw new Error(
'@animo-id/expo-secure-environment cannot be imported in Node.js. Currently, there is no hardware key support for node.js'
Expand Down
16 changes: 11 additions & 5 deletions packages/askar/src/wallet/AskarBaseWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import {
KeyAlgs,
Jwk,
} from '@hyperledger/aries-askar-shared'
import BigNumber from 'bn.js'

import { importSecureEnvironment } from '../secureEnvironment'
import {
Expand Down Expand Up @@ -181,7 +180,7 @@ export abstract class AskarBaseWallet implements Wallet {
// This will be fixed once we use the new 'using' syntax
key = _key

const keyPublicBytes = key.publicBytes
const keyPublicBytes = new Key(key.publicBytes, keyType).publicKey

// Store key
await this.withSession((session) =>
Expand All @@ -206,7 +205,9 @@ export abstract class AskarBaseWallet implements Wallet {

// Generate a hardware-backed P-256 keypair
await secureEnvironment.generateKeypair(kid)
const publicKeyBytes = await secureEnvironment.getPublicBytesForKeyId(kid)
const compressedPublicKeyBytes = await secureEnvironment.getPublicBytesForKeyId(kid)

const publicKeyBytes = new Key(compressedPublicKeyBytes, keyType).publicKey
const publicKeyBase58 = TypedArrayEncoder.toBase58(publicKeyBytes)

await this.storeSecureEnvironmentKeyById({
Expand Down Expand Up @@ -349,7 +350,12 @@ export abstract class AskarBaseWallet implements Wallet {
if (!isError(error)) {
throw new CredoError('Attempted to throw error, but it was not of type Error', { cause: error })
}
throw new WalletError(`Error signing data with verkey ${key.publicKeyBase58}. ${error.message}`, { cause: error })
throw new WalletError(
`Error signing data with key associated with publicKeyBase58 ${key.publicKeyBase58}. ${error.message}`,
{
cause: error,
}
)
} finally {
askarKey?.handle.free()
}
Expand Down Expand Up @@ -592,7 +598,7 @@ export abstract class AskarBaseWallet implements Wallet {
try {
// generate an 80-bit nonce suitable for AnonCreds proofs
const nonce = CryptoBox.randomNonce().slice(0, 10)
return new BigNumber(nonce).toString()
return nonce.reduce((acc, byte) => (acc << 8n) | BigInt(byte), 0n).toString()
} catch (error) {
if (!isError(error)) {
throw new CredoError('Attempted to throw error, but it was not of type Error', { cause: error })
Expand Down
2 changes: 1 addition & 1 deletion packages/bbs-signatures/tests/bbs-signing-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describeSkipNode18('BBS Signing Provider', () => {
key,
})
).rejects.toThrow(
'Error signing data with verkey AeAihfn5UFf7y9oesemKE1oLmTwKMRv7fafTepespr3qceF4RUMggAbogkoC8n6rXgtJytq4oGy59DsVHxmNj9WGWwkiRnP3Sz2r924RLVbc2NdP4T7yEPsSFZPsWmLjgnP1vXHpj4bVXNcTmkUmF6mSXinF3HehnQVip14vRFuMzYVxMUh28ofTJzbtUqxMWZQRu. Unsupported keyType: bls12381g1g2'
'Error signing data with key associated with publicKeyBase58 AeAihfn5UFf7y9oesemKE1oLmTwKMRv7fafTepespr3qceF4RUMggAbogkoC8n6rXgtJytq4oGy59DsVHxmNj9WGWwkiRnP3Sz2r924RLVbc2NdP4T7yEPsSFZPsWmLjgnP1vXHpj4bVXNcTmkUmF6mSXinF3HehnQVip14vRFuMzYVxMUh28ofTJzbtUqxMWZQRu. Unsupported keyType: bls12381g1g2'
)
})

Expand Down
12 changes: 6 additions & 6 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
"prepublishOnly": "pnpm run build"
},
"dependencies": {
"@animo-id/mdoc": "0.3.0",
"@animo-id/pex": "4.1.1-alpha.0",
"@astronautlabs/jsonpath": "^1.1.2",
"@digitalcredentials/jsonld": "^6.0.0",
"@digitalcredentials/jsonld-signatures": "^9.4.0",
"@digitalcredentials/vc": "^6.0.1",
Expand All @@ -35,25 +38,22 @@
"@peculiar/asn1-schema": "^2.3.13",
"@peculiar/asn1-x509": "^2.3.13",
"@peculiar/x509": "^1.12.1",
"@animo-id/mdoc": "0.3.0",
"@sd-jwt/core": "^0.7.2",
"@sd-jwt/decode": "^0.7.2",
"@sd-jwt/jwt-status-list": "^0.7.2",
"@sd-jwt/sd-jwt-vc": "^0.7.2",
"@sd-jwt/types": "^0.7.2",
"@sd-jwt/utils": "^0.7.2",
"@animo-id/pex": "4.1.1-alpha.0",
"@sphereon/pex-models": "^2.3.1",
"@sphereon/ssi-types": "0.30.2-next.135",
"@stablelib/ed25519": "^1.0.2",
"@types/ws": "^8.5.4",
"big-integer": "^1.6.51",
"borc": "^3.0.0",
"buffer": "^6.0.3",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"did-resolver": "^4.1.0",
"@astronautlabs/jsonpath": "^1.1.2",
"ec-compression": "0.0.1-alpha.9",
"lru_map": "^0.4.1",
"make-error": "^1.3.6",
"object-inspect": "^1.10.3",
Expand All @@ -70,9 +70,9 @@
"@types/object-inspect": "^1.8.0",
"@types/uuid": "^9.0.1",
"@types/varint": "^6.0.0",
"nock": "^14.0.0-beta.19",
"rimraf": "^4.4.0",
"tslog": "^4.8.2",
"typescript": "~5.5.2",
"nock": "^14.0.0-beta.19"
"typescript": "~5.5.2"
}
}
11 changes: 7 additions & 4 deletions packages/core/src/crypto/JwsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@ import type { Key } from './Key'
import type { Jwk } from './jose/jwk'
import type { JwkJson } from './jose/jwk/Jwk'
import type { AgentContext } from '../agent'
import type { Buffer } from '../utils'

import { CredoError } from '../error'
import { EncodedX509Certificate, X509ModuleConfig } from '../modules/x509'
import { injectable } from '../plugins'
import { isJsonObject, JsonEncoder, TypedArrayEncoder } from '../utils'
import { Buffer, isJsonObject, JsonEncoder, TypedArrayEncoder } from '../utils'
import { WalletError } from '../wallet/error'

import { X509Service } from './../modules/x509/X509Service'
Expand All @@ -33,14 +32,18 @@ export class JwsService {
const certificate = X509Service.getLeafCertificate(agentContext, { certificateChain: x5c })
if (
certificate.publicKey.keyType !== options.key.keyType ||
!certificate.publicKey.publicKey.equals(options.key.publicKey)
!Buffer.from(certificate.publicKey.publicKey).equals(Buffer.from(options.key.publicKey))
) {
throw new CredoError(`Protected header x5c does not match key for signing.`)
}
}

// Make sure the options.key and jwk from protectedHeader are the same.
if (jwk && (jwk.key.keyType !== options.key.keyType || !jwk.key.publicKey.equals(options.key.publicKey))) {
if (
jwk &&
(jwk.key.keyType !== options.key.keyType ||
!Buffer.from(jwk.key.publicKey).equals(Buffer.from(options.key.publicKey)))
) {
throw new CredoError(`Protected header JWK does not match key for signing.`)
}

Expand Down
21 changes: 14 additions & 7 deletions packages/core/src/crypto/Key.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
import type { KeyType } from './KeyType'

import { Buffer, MultiBaseEncoder, TypedArrayEncoder, VarintEncoder } from '../utils'
import { compressPublicKeyIfPossible, decompressPublicKeyIfPossible } from 'ec-compression'

import { MultiBaseEncoder, TypedArrayEncoder, VarintEncoder } from '../utils'

import { isEncryptionSupportedForKeyType, isSigningSupportedForKeyType } from './keyUtils'
import { getKeyTypeByMultiCodecPrefix, getMultiCodecPrefixByKeyType } from './multiCodecKey'

export class Key {
public readonly publicKey: Buffer
public readonly publicKey: Uint8Array
public readonly keyType: KeyType

public constructor(publicKey: Uint8Array, keyType: KeyType) {
this.publicKey = Buffer.from(publicKey)
this.publicKey = decompressPublicKeyIfPossible(publicKey, keyType)
this.keyType = keyType
}

public get compressedPublicKey() {
return compressPublicKeyIfPossible(this.publicKey, this.keyType)
}

public static fromPublicKey(publicKey: Uint8Array, keyType: KeyType) {
return new Key(Buffer.from(publicKey), keyType)
return new Key(publicKey, keyType)
}

public static fromPublicKeyBase58(publicKey: string, keyType: KeyType) {
const publicKeyBytes = TypedArrayEncoder.fromBase58(publicKey)
const publicKeyBytes = Uint8Array.from(TypedArrayEncoder.fromBase58(publicKey))

return Key.fromPublicKey(publicKeyBytes, keyType)
}
Expand All @@ -28,7 +34,7 @@ export class Key {
const { data } = MultiBaseEncoder.decode(fingerprint)
const [code, byteLength] = VarintEncoder.decode(data)

const publicKey = Buffer.from(data.slice(byteLength))
const publicKey = data.slice(byteLength)
const keyType = getKeyTypeByMultiCodecPrefix(code)

return new Key(publicKey, keyType)
Expand All @@ -41,7 +47,8 @@ export class Key {
const prefixBytes = VarintEncoder.encode(multiCodecPrefix)

// Combine prefix with public key
return Buffer.concat([prefixBytes, this.publicKey])
// Multicodec requires compressable keys to be compressed
return new Uint8Array([...prefixBytes, ...this.compressedPublicKey])
}

public get fingerprint() {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/crypto/__tests__/JwsService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ describe('JwsService', () => {
jwsService.verifyJws(agentContext, {
jws: { signatures: [], payload: '' },
})
).rejects.toThrowError('Unable to verify JWS, no signatures present in JWS.')
).rejects.toThrow('Unable to verify JWS, no signatures present in JWS.')
})
})
})
Loading

0 comments on commit 297d209

Please sign in to comment.