UNPKG

@sphereon/ssi-sdk-ext.kms-musap-rn

Version:

Sphereon SSI-SDK react-native plugin for management of keys with musap.

300 lines (261 loc) 10.5 kB
import { PEMToBinary } from '@sphereon/ssi-sdk-ext.x509-utils' import { IKey, ManagedKeyInfo, MinimalImportableKey, TKeyType } from '@veramo/core' import { ExternalSscdSettings, IMusapClient, isSignatureAlgorithmType, JWSAlgorithm, KeyAlgorithm, KeyAlgorithmType, KeyAttribute, KeyGenReq, MusapClient, MusapKey, signatureAlgorithmFromKeyAlgorithm, SignatureAlgorithmType, SignatureAttribute, SignatureFormat, SignatureReq, SscdType, } from '@sphereon/musap-react-native' import { AbstractKeyManagementSystem } from '@veramo/key-manager' import { TextDecoder } from 'text-encoding' import { Loggers } from '@sphereon/ssi-types' import { KeyMetadata } from './index' import { asn1DerToRawPublicKey, calculateJwkThumbprintForKey, hexStringFromUint8Array, isAsn1Der, isRawCompressedPublicKey, toRawCompressedHexPublicKey, } from '@sphereon/ssi-sdk-ext.key-utils' import * as u8a from 'uint8arrays' export const logger = Loggers.DEFAULT.get('sphereon:musap-rn-kms') export class MusapKeyManagementSystem extends AbstractKeyManagementSystem { private musapClient: IMusapClient private readonly sscdType: SscdType private readonly sscdId: string private readonly defaultKeyAttributes: Record<string, string> | undefined private readonly defaultSignAttributes: Record<string, string> | undefined constructor(sscdType?: SscdType, sscdId?: string, opts?: { externalSscdSettings?: ExternalSscdSettings, defaultKeyAttributes?: Record<string, string>, defaultSignAttributes?: Record<string, string> }) { super() try { this.musapClient = MusapClient this.sscdType = sscdType ? sscdType : 'TEE' this.sscdId = sscdId ?? this.sscdType this.defaultKeyAttributes = opts?.defaultKeyAttributes this.defaultSignAttributes = opts?.defaultSignAttributes const enabledSscds = this.musapClient.listEnabledSscds() if (!enabledSscds.some(value => value.sscdId == sscdId)) { this.musapClient.enableSscd(this.sscdType, this.sscdId, opts?.externalSscdSettings) } } catch (e) { console.error('enableSscd', e) throw Error('enableSscd failed') } } async listKeys(): Promise<ManagedKeyInfo[]> { const keysJson: MusapKey[] = (this.musapClient.listKeys()) as MusapKey[] return keysJson.map((key) => this.asMusapKeyInfo(key)) } async createKey(args: { type: TKeyType; meta?: KeyMetadata }): Promise<ManagedKeyInfo> { const { type, meta } = args if (meta === undefined || !('keyAlias' in meta)) { return Promise.reject(Error('a unique keyAlias field is required for MUSAP')) } if (this.sscdType == 'EXTERNAL') { const existingKeys: MusapKey[] = (this.musapClient.listKeys()) as MusapKey[] const extKey = existingKeys.find(musapKey => musapKey.sscdType as string === 'External Signature') // FIXME returning does not match SscdType enum if (extKey) { extKey.algorithm = 'eccp256r1' // FIXME MUSAP announces key as rsa2k, but it's actually EC return this.asMusapKeyInfo(extKey) } return Promise.reject(Error(`No external key was bound yet for sscd ${this.sscdId}`)) } const keyGenReq = { keyAlgorithm: this.mapKeyTypeToAlgorithmType(type), keyUsage: 'keyUsage' in meta ? (meta.keyUsage as string) : 'sign', keyAlias: meta.keyAlias as string, attributes: this.recordToKeyAttributes({ ...this.defaultKeyAttributes, ...('attributes' in meta ? meta.attributes : {}) }), role: 'role' in meta ? (meta.role as string) : 'administrator', } satisfies KeyGenReq try { const generatedKeyUri = await this.musapClient.generateKey(this.sscdType, keyGenReq) if (generatedKeyUri) { logger.debug('Generated key:', generatedKeyUri) const key = this.musapClient.getKeyByUri(generatedKeyUri) return this.asMusapKeyInfo(key) } else { return Promise.reject(new Error('Failed to generate key. No key URI')) } } catch (error) { logger.error('An error occurred:', error) throw error } } private mapKeyTypeToAlgorithmType = (type: TKeyType): KeyAlgorithmType => { switch (type) { case 'Secp256k1': return 'ECCP256K1' case 'Secp256r1': return 'ECCP256R1' case 'RSA': return 'RSA2K' default: throw new Error(`Key type ${type} is not supported by MUSAP`) } } private mapAlgorithmTypeToKeyType = (type: KeyAlgorithm): TKeyType => { switch (type) { case 'eccp256k1': return 'Secp256k1' case 'eccp256r1': return 'Secp256r1' case 'ecc_ed25519': return 'Ed25519' case 'rsa2k': case 'rsa4k': return 'RSA' default: throw new Error(`Key type ${type} is not supported.`) } } async deleteKey({ kid }: { kid: string }): Promise<boolean> { try { const key: MusapKey = this.musapClient.getKeyById(kid) as MusapKey if (key.sscdType as string === 'External Signature') { return true // FIXME we can't remove a eSim key for now because this would mean onboarding again } void this.musapClient.removeKey(kid) return true } catch (error) { console.warn('Failed to delete key:', error) return false } } private determineAlgorithm(providedAlgorithm: string | undefined, keyAlgorithm: KeyAlgorithm): SignatureAlgorithmType { if (providedAlgorithm === undefined) { return signatureAlgorithmFromKeyAlgorithm(keyAlgorithm) } if (isSignatureAlgorithmType(providedAlgorithm)) { return providedAlgorithm } // Veramo translates TKeyType to JWSAlgorithm return signatureAlgorithmFromKeyAlgorithm(providedAlgorithm as JWSAlgorithm) } async sign(args: { keyRef: Pick<IKey, 'kid'>; algorithm?: string; data: Uint8Array; [x: string]: any }): Promise<string> { if (!args.keyRef) { throw new Error('key_not_found: No key ref provided') } const data = new TextDecoder().decode(args.data as Uint8Array) const key: MusapKey = this.musapClient.getKeyById(args.keyRef.kid) as MusapKey if (key.sscdType as string === 'External Signature') { key.algorithm = 'eccp256r1' // FIXME MUSAP announces key as rsa2k, but it's actually EC } const signatureReq: SignatureReq = { keyUri: key.keyUri, data, algorithm: this.determineAlgorithm(args.algorithm, key.algorithm), displayText: args.displayText, transId: args.transId, format: (args.format as SignatureFormat) ?? 'RAW', attributes: this.recordToSignatureAttributes({ ...this.defaultSignAttributes, ...args.attributes }), } return this.musapClient.sign(signatureReq) } async importKey(args: Omit<MinimalImportableKey, 'kms'> & { privateKeyPEM?: string }): Promise<ManagedKeyInfo> { throw new Error('importKey is not implemented for MusapKeyManagementSystem.') } private decodeMusapPublicKey = (args: { publicKey: { pem: string }, keyType: TKeyType }): string => { const { publicKey, keyType } = args // First try the normal PEM decoding path const pemBinary = PEMToBinary(publicKey.pem) // Check if we got a string that looks like base64 (might be double encoded) // Convert Uint8Array to string safely const pemString = u8a.toString(pemBinary, 'utf8') const isDoubleEncoded = pemBinary.length > 0 && typeof pemString === 'string' && pemString.startsWith('MF') if (isDoubleEncoded) { // Handle double-encoded case const actualDerBytes = u8a.fromString(pemString, 'base64') // For double-encoded case, we know the key data starts after the header const keyDataStart = 24 const keyData = actualDerBytes.slice(keyDataStart) // Convert to public key hex let publicKeyHex = u8a.toString(keyData, 'hex') // If it's not compressed yet and doesn't start with 0x04 (uncompressed point marker), add it if (publicKeyHex.length <= 128 && !publicKeyHex.startsWith('04')) { publicKeyHex = '04' + publicKeyHex } // Ensure we have full 65 bytes for uncompressed keys while (publicKeyHex.startsWith('04') && publicKeyHex.length < 130) { publicKeyHex = publicKeyHex + '0' } // Now convert to compressed format if needed if (publicKeyHex.startsWith('04') && publicKeyHex.length === 130) { const xCoord = u8a.fromString(publicKeyHex.slice(2, 66), 'hex') const yCoord = u8a.fromString(publicKeyHex.slice(66, 130), 'hex') const prefix = new Uint8Array([yCoord[31] % 2 === 0 ? 0x02 : 0x03]) const compressedKey = new Uint8Array(33) // 1 byte prefix + 32 bytes x coordinate compressedKey.set(prefix, 0) compressedKey.set(xCoord, 1) return u8a.toString(compressedKey, 'hex') } return publicKeyHex } // Not double encoded, proceed with normal path const publicKeyBinary = isAsn1Der(pemBinary) ? asn1DerToRawPublicKey(pemBinary, keyType) : pemBinary return isRawCompressedPublicKey(publicKeyBinary) ? hexStringFromUint8Array(publicKeyBinary) : toRawCompressedHexPublicKey(publicKeyBinary, keyType) } private asMusapKeyInfo(args: MusapKey): ManagedKeyInfo { const { keyId, publicKey, ...metadata }: KeyMetadata = { ...args } const keyType = this.mapAlgorithmTypeToKeyType(args.algorithm) const publicKeyHex = this.decodeMusapPublicKey({ publicKey: publicKey, keyType: keyType }) const keyInfo: Partial<ManagedKeyInfo> = { kid: keyId, type: keyType, publicKeyHex, meta: metadata, } const jwkThumbprint = calculateJwkThumbprintForKey({ key: keyInfo as ManagedKeyInfo }) keyInfo.meta = { ...keyInfo.meta, jwkThumbprint } return keyInfo as ManagedKeyInfo } sharedSecret(args: { myKeyRef: Pick<IKey, 'kid'>; theirKey: Pick<IKey, 'publicKeyHex' | 'type'> }): Promise<string> { throw new Error('Not supported.') } private recordToKeyAttributes(record?: Record<string, string>): KeyAttribute[] { if (!record) { return [] } return Object.entries(record).map(([key, value]) => ({ name: key, value, })) } private recordToSignatureAttributes(record?: Record<string, string>): SignatureAttribute[] { if (!record) { return [] } return Object.entries(record).map(([key, value]) => ({ name: key, value, })) } }