Skip to content

Commit

Permalink
feat: use uncompressed keys by default
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra committed Feb 4, 2025
1 parent 807f0d5 commit ff142b0
Show file tree
Hide file tree
Showing 34 changed files with 503 additions and 247 deletions.
2 changes: 1 addition & 1 deletion packages/askar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"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",
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
11 changes: 8 additions & 3 deletions packages/askar/src/wallet/AskarBaseWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
KeyBackend,
KeyType,
utils,
expandIfPossible,
} from '@credo-ts/core'
import {
CryptoBox,
Expand Down Expand Up @@ -181,7 +182,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 = expandIfPossible(key.publicBytes, keyType)

// Store key
await this.withSession((session) =>
Expand All @@ -206,7 +207,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 = expandIfPossible(compressedPublicKeyBytes, keyType)
const publicKeyBase58 = TypedArrayEncoder.toBase58(publicKeyBytes)

await this.storeSecureEnvironmentKeyById({
Expand Down Expand Up @@ -349,7 +352,9 @@ 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 ${key.publicKeyBase58}. ${error.message}`, {
cause: error,
})
} finally {
askarKey?.handle.free()
}
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 AeAihfn5UFf7y9oesemKE1oLmTwKMRv7fafTepespr3qceF4RUMggAbogkoC8n6rXgtJytq4oGy59DsVHxmNj9WGWwkiRnP3Sz2r924RLVbc2NdP4T7yEPsSFZPsWmLjgnP1vXHpj4bVXNcTmkUmF6mSXinF3HehnQVip14vRFuMzYVxMUh28ofTJzbtUqxMWZQRu. Unsupported keyType: bls12381g1g2'
)
})

Expand Down
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
22 changes: 15 additions & 7 deletions packages/core/src/crypto/Key.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
import type { KeyType } from './KeyType'

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

import { compressIfPossible, expandIfPossible } from './jose/jwk/ecCompression'
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 = expandIfPossible(publicKey, keyType)
this.keyType = keyType
}

public get compressedPublicKey() {
return compressIfPossible(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 +33,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 @@ -40,8 +45,11 @@ export class Key {
// Create Buffer with length of the prefix bytes, then use varint to fill the prefix bytes
const prefixBytes = VarintEncoder.encode(multiCodecPrefix)

// Multicodec requires compressable keys to be compressed
const possiblyCompressedKey = compressIfPossible(this.publicKey, this.keyType)

// Combine prefix with public key
return Buffer.concat([prefixBytes, this.publicKey])
return new Uint8Array([...prefixBytes, ...possiblyCompressedKey])
}

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.')
})
})
})
19 changes: 10 additions & 9 deletions packages/core/src/crypto/jose/jwk/Ed25519Jwk.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { JwkJson } from './Jwk'
import type { Buffer } from '../../../utils'
import type { JwaEncryptionAlgorithm } from '../jwa/alg'

import { TypedArrayEncoder } from '../../../utils'
Expand All @@ -15,12 +14,16 @@ export class Ed25519Jwk extends Jwk {
public static readonly supportedSignatureAlgorithms: JwaSignatureAlgorithm[] = [JwaSignatureAlgorithm.EdDSA]
public static readonly keyType = KeyType.Ed25519

public readonly x: string
private readonly _x: Uint8Array

public constructor({ x }: { x: string }) {
public constructor({ x }: { x: string | Uint8Array }) {
super()

this.x = x
this._x = typeof x === 'string' ? Uint8Array.from(TypedArrayEncoder.fromBase64(x)) : x
}

public get x() {
return TypedArrayEncoder.toBase64URL(this._x)
}

public get kty() {
Expand All @@ -32,7 +35,7 @@ export class Ed25519Jwk extends Jwk {
}

public get publicKey() {
return TypedArrayEncoder.fromBase64(this.x)
return this._x
}

public get keyType() {
Expand Down Expand Up @@ -65,10 +68,8 @@ export class Ed25519Jwk extends Jwk {
})
}

public static fromPublicKey(publicKey: Buffer) {
return new Ed25519Jwk({
x: TypedArrayEncoder.toBase64URL(publicKey),
})
public static fromPublicKey(publicKey: Uint8Array) {
return new Ed25519Jwk({ x: publicKey })
}
}

Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/crypto/jose/jwk/Jwk.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Buffer } from '../../../utils'
import type { KeyType } from '../../KeyType'
import type { JwaKeyType, JwaEncryptionAlgorithm, JwaSignatureAlgorithm } from '../jwa'

Expand All @@ -11,7 +10,7 @@ export interface JwkJson {
}

export abstract class Jwk {
public abstract publicKey: Buffer
public abstract publicKey: Uint8Array
public abstract supportedSignatureAlgorithms: JwaSignatureAlgorithm[]
public abstract supportedEncryptionAlgorithms: JwaEncryptionAlgorithm[]

Expand Down
73 changes: 51 additions & 22 deletions packages/core/src/crypto/jose/jwk/K256Jwk.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
import type { JwkJson } from './Jwk'
import type { JwaEncryptionAlgorithm } from '../jwa/alg'

import { TypedArrayEncoder, Buffer } from '../../../utils'
import { CredoError } from '../../../error'
import { TypedArrayEncoder } from '../../../utils'
import { KeyType } from '../../KeyType'
import { JwaCurve, JwaKeyType } from '../jwa'
import { JwaSignatureAlgorithm } from '../jwa/alg'

import { Jwk } from './Jwk'
import { compress, expand } from './ecCompression'
import {
compressECPoint,
expand,
isValidCompressedPublicKey,
isValidUncompressedPublicKey,
PREFIX_UNCOMPRESSED,
} from './ecCompression'
import { hasKty, hasCrv, hasX, hasY, hasValidUse } from './validate'

export class K256Jwk extends Jwk {
public static readonly supportedEncryptionAlgorithms: JwaEncryptionAlgorithm[] = []
public static readonly supportedSignatureAlgorithms: JwaSignatureAlgorithm[] = [JwaSignatureAlgorithm.ES256K]
public static readonly keyType = KeyType.K256

public readonly x: string
public readonly y: string
private readonly _x: Uint8Array
private readonly _y: Uint8Array

public constructor({ x, y }: { x: string; y: string }) {
public constructor({ x, y }: { x: string | Uint8Array; y: string | Uint8Array }) {
super()

this.x = x
this.y = y
const xAsBytes = typeof x === 'string' ? Uint8Array.from(TypedArrayEncoder.fromBase64(x)) : x
const yAsBytes = typeof y === 'string' ? Uint8Array.from(TypedArrayEncoder.fromBase64(y)) : y

this._x = xAsBytes
this._y = yAsBytes
}

public get kty() {
Expand All @@ -33,17 +43,26 @@ export class K256Jwk extends Jwk {
return JwaCurve.Secp256k1 as const
}

public get x() {
return TypedArrayEncoder.toBase64URL(this._x)
}

public get y() {
return TypedArrayEncoder.toBase64URL(this._y)
}

/**
* Returns the public key of the K-256 JWK.
*
* NOTE: this is the compressed variant. We still need to add support for the
* uncompressed variant.
* Returns the uncompressed public key of the P-256 JWK.
*/
public get publicKey() {
const publicKeyBuffer = Buffer.concat([TypedArrayEncoder.fromBase64(this.x), TypedArrayEncoder.fromBase64(this.y)])
const compressedPublicKey = compress(publicKeyBuffer)
return new Uint8Array([PREFIX_UNCOMPRESSED, ...this._x, ...this._y])
}

return Buffer.from(compressedPublicKey)
/**
* Returns the compressed public key of the K-256 JWK.
*/
public get publicKeyCompressed() {
return compressECPoint(this._x, this._y)
}

public get keyType() {
Expand Down Expand Up @@ -78,15 +97,25 @@ export class K256Jwk extends Jwk {
})
}

public static fromPublicKey(publicKey: Buffer) {
const expanded = expand(publicKey, JwaCurve.Secp256k1)
const x = expanded.slice(0, expanded.length / 2)
const y = expanded.slice(expanded.length / 2)
public static fromPublicKey(publicKey: Uint8Array) {
if (isValidCompressedPublicKey(publicKey, this.keyType)) {
const expanded = expand(publicKey, this.keyType)
const x = expanded.slice(1, expanded.length / 2 + 1)
const y = expanded.slice(expanded.length / 2 + 1)

return new K256Jwk({
x: TypedArrayEncoder.toBase64URL(x),
y: TypedArrayEncoder.toBase64URL(y),
})
return new K256Jwk({ x, y })
}

if (isValidUncompressedPublicKey(publicKey, this.keyType)) {
const x = publicKey.slice(1, publicKey.length / 2 + 1)
const y = publicKey.slice(publicKey.length / 2 + 1)

return new K256Jwk({ x, y })
}

throw new CredoError(
`${this.keyType} public key is neither a valid compressed or uncompressed key. Key prefix '${publicKey[0]}', key length '${publicKey.length}'`
)
}
}

Expand Down
Loading

0 comments on commit ff142b0

Please sign in to comment.