diff --git a/beacon_chain/filepath.nim b/beacon_chain/filepath.nim index f9eca55ec..6a3f49547 100644 --- a/beacon_chain/filepath.nim +++ b/beacon_chain/filepath.nim @@ -13,11 +13,59 @@ else: import chronicles import stew/io2 -export io2 +import spec/keystore when defined(windows): import stew/[windows/acl] +type + ByteChar = byte | char + +const + INCOMPLETE_ERROR = + when defined(windows): + IoErrorCode(996) # ERROR_IO_INCOMPLETE + else: + IoErrorCode(28) # ENOSPC + +proc openLockedFile*(keystorePath: string): IoResult[FileLockHandle] = + let + flags = {OpenFlags.Read, OpenFlags.Write, OpenFlags.Exclusive} + handle = ? openFile(keystorePath, flags) + + var success = false + defer: + if not(success): + discard closeFile(handle) + + let lock = ? lockFile(handle, LockType.Exclusive) + success = true + ok(FileLockHandle(ioHandle: lock, opened: true)) + +proc getData*(lockHandle: FileLockHandle, + maxBufferSize: int): IoResult[string] = + let filesize = ? getFileSize(lockHandle.ioHandle.handle) + let length = min(filesize, maxBufferSize) + var buffer = newString(length) + let bytesRead = ? readFile(lockHandle.ioHandle.handle, buffer) + if uint64(bytesRead) != uint64(len(buffer)): + err(INCOMPLETE_ERROR) + else: + ok(buffer) + +proc closeLockedFile*(lockHandle: FileLockHandle): IoResult[void] = + if lockHandle.opened: + var success = false + defer: + lockHandle.opened = false + if not(success): + discard lockHandle.ioHandle.handle.closeFile() + + ? lockHandle.ioHandle.unlockFile() + success = true + ? lockHandle.ioHandle.handle.closeFile() + ok() + proc secureCreatePath*(path: string): IoResult[void] = when defined(windows): let sres = createFoldersUserOnlySecurityDescriptor() @@ -31,16 +79,55 @@ proc secureCreatePath*(path: string): IoResult[void] = else: createPath(path, 0o700) -proc secureWriteFile*[T: byte|char](path: string, - data: openArray[T]): IoResult[void] = +proc secureWriteFile*[T: ByteChar](path: string, + data: openArray[T]): IoResult[void] = when defined(windows): let sres = createFilesUserOnlySecurityDescriptor() if sres.isErr(): error "Could not allocate security descriptor", path = path, errorMsg = ioErrorMsg(sres.error), errorCode = $sres.error - err(sres.error) + err(sres.error()) else: var sd = sres.get() - writeFile(path, data, 0o600, secDescriptor = sd.getDescriptor()) + let res = writeFile(path, data, 0o600, sd.getDescriptor()) + if res.isErr(): + # writeFile() will not attempt to remove file on failure + discard removeFile(path) + err(res.error()) + else: + ok() else: - writeFile(path, data, 0o600) + let res = writeFile(path, data, 0o600) + if res.isErr(): + # writeFile() will not attempt to remove file on failure + discard removeFile(path) + err(res.error()) + else: + ok() + +proc secureWriteLockedFile*[T: ByteChar](path: string, + data: openArray[T] + ): IoResult[FileLockHandle] = + let handle = + block: + let flags = {OpenFlags.Write, OpenFlags.Truncate, OpenFlags.Create, + OpenFlags.Exclusive} + when defined(windows): + var sd = ? createFilesUserOnlySecurityDescriptor() + ? openFile(path, flags, 0o600, sd.getDescriptor()) + else: + ? openFile(path, flags, 0o600) + var success = false + defer: + if not(success): + discard closeFile(handle) + # We will try to remove file, if something goes wrong. + discard removeFile(path) + let bytesWrote = ? writeFile(handle, data) + if uint64(bytesWrote) != uint64(len(data)): + # Data was partially written, and `write` did not return any errors, so + # lets return INCOMPLETE_ERROR. + return err(INCOMPLETE_ERROR) + let res = ? lockFile(handle, LockType.Exclusive) + success = true + ok(FileLockHandle(ioHandle: res, opened: true)) diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index 2cdb5f615..5c98e9b37 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -1562,6 +1562,7 @@ proc stop(node: BeaconNode) = warn "Couldn't stop network", msg = exc.msg node.attachedValidators.slashingProtection.close() + node.attachedValidators[].close() node.db.close() notice "Databases closed" diff --git a/beacon_chain/spec/keystore.nim b/beacon_chain/spec/keystore.nim index 5ebad1bd4..0155a221b 100644 --- a/beacon_chain/spec/keystore.nim +++ b/beacon_chain/spec/keystore.nim @@ -17,14 +17,14 @@ import # Third-party libraries normalize, # Status libraries - stew/[results, bitops2, base10], stew/shims/macros, + stew/[results, bitops2, base10, io2], stew/shims/macros, eth/keyfile/uuid, blscurve, json_serialization, nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, scrypt], # Local modules libp2p/crypto/crypto as lcrypto, ./datatypes/base, ./signatures -export base, uri +export base, uri, io2 # We use `ncrutils` for constant-time hexadecimal encoding/decoding procedures. import nimcrypto/utils as ncrutils @@ -140,10 +140,15 @@ type id*: uint32 pubkey*: ValidatorPubKey + FileLockHandle* = ref object + ioHandle*: IoLockHandle + opened*: bool + KeystoreData* = object version*: uint64 pubkey*: ValidatorPubKey description*: Option[string] + handle*: FileLockHandle case kind*: KeystoreKind of KeystoreKind.Local: privateKey*: ValidatorPrivKey diff --git a/beacon_chain/validators/keystore_management.nim b/beacon_chain/validators/keystore_management.nim index 712205338..b32add575 100644 --- a/beacon_chain/validators/keystore_management.nim +++ b/beacon_chain/validators/keystore_management.nim @@ -38,6 +38,7 @@ const NetKeystoreFileName* = "network_keystore.json" FeeRecipientFilename* = "suggested_fee_recipient.hex" KeyNameSize* = 98 # 0x + hexadecimal key representation 96 characters. + MaxKeystoreFileSize* = 65536 type WalletPathPair* = object @@ -52,6 +53,8 @@ type KmResult*[T] = Result[T, cstring] + AnyKeystore* = RemoteKeystore | Keystore + RemoveValidatorStatus* {.pure.} = enum deleted = "Deleted" notFound = "Not found" @@ -82,7 +85,7 @@ proc echoP*(msg: string) = func init*(T: type KeystoreData, privateKey: ValidatorPrivKey, - keystore: Keystore): T = + keystore: Keystore, handle: FileLockHandle): T {.raises: [Defect].} = KeystoreData( kind: KeystoreKind.Local, privateKey: privateKey, @@ -90,35 +93,39 @@ func init*(T: type KeystoreData, else: some(keystore.description[]), path: keystore.path, uuid: keystore.uuid, + handle: handle, version: uint64(keystore.version), pubkey: privateKey.toPubKey().toPubKey() ) -func init*(T: type KeystoreData, - keystore: RemoteKeystore): Result[T, cstring] = +func init*(T: type KeystoreData, keystore: RemoteKeystore, + handle: FileLockHandle): Result[T, cstring] {.raises: [Defect].} = let cookedKey = block: let res = keystore.pubkey.load() if res.isNone(): return err("Invalid validator's public key") res.get() - ok KeystoreData( + ok(KeystoreData( kind: KeystoreKind.Remote, + handle: handle, pubkey: cookedKey.toPubKey, description: keystore.description, version: keystore.version, remotes: keystore.remotes, threshold: keystore.threshold - ) + )) func init*(T: type KeystoreData, cookedKey: CookedPubKey, - remotes: seq[RemoteSignerInfo], threshold: uint32): T = + remotes: seq[RemoteSignerInfo], threshold: uint32, + handle: FileLockHandle): T = KeystoreData( kind: KeystoreKind.Remote, + handle: handle, pubkey: cookedKey.toPubKey(), version: 2'u64, remotes: remotes, - threshold: threshold + threshold: threshold, ) func init(T: type AddValidatorFailure, status: AddValidatorStatus, @@ -339,38 +346,12 @@ proc keyboardGetPassword[T](prompt: string, attempts: int, dec(remainingAttempts) err("Failed to decrypt keystore") -proc loadKeystoreFile*(path: string): KsResult[Keystore] {. - raises: [Defect].} = - try: - ok(Json.loadFile(path, Keystore)) - except IOError as err: - return err("Could not read keystore file") - except SerializationError as err: - return err("Could not decode keystore file: " & err.formatMsg(path)) - proc loadSecretFile*(path: string): KsResult[KeystorePass] {. raises: [Defect].} = - try: - ok(KeystorePass.init(readFile(path))) - except IOError: - return err("Could not read password file") - -proc loadKeystoreUnsafe*(validatorsDir, secretsDir, - keyName: string): KsResult[KeystoreData] = - ## Load keystore without any checks on keystore/secret permissions. - let - keystorePath = validatorsDir / keyName / KeystoreFileName - keystore = ? loadKeystoreFile(keystorePath) - - let - passphrasePath = secretsDir / keyName - passphrase = ? loadSecretFile(passphrasePath) - - let res = decryptKeystore(keystore, passphrase) - if res.isOk(): - ok(KeystoreData.init(res.get(), keystore)) - else: - err("Failed to decrypt keystore") + let res = readAllChars(path) + if res.isErr(): + return err(ioErrorMsg(res.error())) + ok(KeystorePass.init(res.get())) proc loadRemoteKeystoreImpl(validatorsDir, keyName: string): Option[KeystoreData] = @@ -381,42 +362,84 @@ proc loadRemoteKeystoreImpl(validatorsDir, key_path = keystorePath return - let keyStore = + let handle = block: - let remoteKeystore = - try: - Json.loadFile(keystorePath, RemoteKeystore) - except IOError as err: - error "Failed to read remote keystore file", err = err.msg, - path = keystorePath - return - except SerializationError as e: - error "Invalid remote keystore file", - path = keystorePath, - err_msg = e.formatMsg(keystorePath) - return - let res = init(KeystoreData, remoteKeystore) + let res = openLockedFile(keystorePath) if res.isErr(): - error "Invalid remote keystore file", - path = keystorePath + error "Unable to lock keystore file", key_path = keystorePath, + error_msg = ioErrorMsg(res.error()) return res.get() - some(keyStore) -proc loadKeystoreImpl(validatorsDir, secretsDir, keyName: string, - nonInteractive: bool): Option[KeystoreData] = + var success = false + defer: + if not(success): + discard handle.closeLockedFile() + + let keystore = + block: + let gres = handle.getData(MaxKeystoreFileSize) + if gres.isErr(): + error "Could not read remote keystore file", key_path = keystorePath, + error_msg = ioErrorMsg(gres.error()) + return + let buffer = gres.get() + let data = + try: + Json.decode(buffer, RemoteKeystore, requireAllFields = true, + allowUnknownFields = true) + except SerializationError as e: + error "Invalid remote keystore file", key_path = keystorePath, + error_msg = e.formatMsg(keystorePath) + return + let kres = KeystoreData.init(data, handle) + if kres.isErr(): + error "Invalid remote keystore file", key_path = keystorePath, + error_msg = kres.error() + return + kres.get() + + success = true + some(keystore) + +proc loadLocalKeystoreImpl(validatorsDir, secretsDir, keyName: string, + nonInteractive: bool): Option[KeystoreData] = let keystorePath = validatorsDir / keyName / KeystoreFileName - keystore = + passphrasePath = secretsDir / keyName + handle = block: - let res = loadKeystoreFile(keystorePath) + let res = openLockedFile(keystorePath) if res.isErr(): - error "Failed to read keystore file", error = res.error(), - path = keystorePath + error "Unable to lock keystore file", key_path = keystorePath, + error_msg = ioErrorMsg(res.error()) return res.get() - let passphrasePath = secretsDir / keyName + var success = false + defer: + if not(success): + discard handle.closeLockedFile() + + let + keystore = + block: + let gres = handle.getData(MaxKeystoreFileSize) + if gres.isErr(): + error "Could not read local keystore file", key_path = keystorePath, + error_msg = ioErrorMsg(gres.error()) + return + let buffer = gres.get() + let data = + try: + Json.decode(buffer, Keystore, requireAllFields = true, + allowUnknownFields = true) + except SerializationError as e: + error "Invalid local keystore file", key_path = keystorePath, + error_msg = e.formatMsg(keystorePath) + return + data + if fileExists(passphrasePath): if not(checkSensitiveFilePermissions(passphrasePath)): error "Password file has insecure permissions", key_path = keystorePath @@ -426,21 +449,24 @@ proc loadKeystoreImpl(validatorsDir, secretsDir, keyName: string, block: let res = loadSecretFile(passphrasePath) if res.isErr(): - error "Failed to read passphrase file", err = res.error(), + error "Failed to read passphrase file", error_msg = res.error(), path = passphrasePath return res.get() let res = decryptKeystore(keystore, passphrase) if res.isOk(): - return some(KeystoreData.init(res.get(), keystore)) + success = true + return some(KeystoreData.init(res.get(), keystore, handle)) else: - error "Failed to decrypt keystore", keystorePath, passphrasePath + error "Failed to decrypt keystore", key_path = keystorePath, + secure_path = passphrasePath return if nonInteractive: - error "Unable to load validator key store. Please ensure matching passphrase exists in the secrets dir", - keyName, validatorsDir, secretsDir = secretsDir + error "Unable to load validator key store. Please ensure matching " & + "passphrase exists in the secrets dir", key_path = keystorePath, + key_name = keyName, validatorsDir, secretsDir = secretsDir return let prompt = "Please enter passphrase for key \"" & @@ -449,15 +475,17 @@ proc loadKeystoreImpl(validatorsDir, secretsDir, keyName: string, proc (password: string): KsResult[ValidatorPrivKey] = let decrypted = decryptKeystore(keystore, KeystorePass.init password) if decrypted.isErr(): - error "Keystore decryption failed. Please try again", keystorePath + error "Keystore decryption failed. Please try again", + keystore_path = keystorePath decrypted ) - if res.isOk(): - some(KeystoreData.init(res.get(), keystore)) - else: + if res.isErr(): return + success = true + some(KeystoreData.init(res.get(), keystore, handle)) + proc loadKeystore*(validatorsDir, secretsDir, keyName: string, nonInteractive: bool): Option[KeystoreData] = let @@ -466,7 +494,7 @@ proc loadKeystore*(validatorsDir, secretsDir, keyName: string, remoteKeystorePath = keystorePath / RemoteKeystoreFileName if fileExists(localKeystorePath): - loadKeystoreImpl(validatorsDir, secretsDir, keyName, nonInteractive) + loadLocalKeystoreImpl(validatorsDir, secretsDir, keyName, nonInteractive) elif fileExists(remoteKeystorePath): loadRemoteKeystoreImpl(validatorsDir, keyName) else: @@ -524,20 +552,24 @@ proc removeValidatorFiles*(validatorsDir, secretsDir, keyName: string, func fsName(pubkey: ValidatorPubKey|CookedPubKey): string = "0x" & pubkey.toHex() -proc removeValidatorFiles*(conf: AnyConf, keyName: string, - kind: KeystoreKind): KmResult[RemoveValidatorStatus] - {.raises: [Defect].} = +proc removeValidatorFiles*( + conf: AnyConf, keyName: string, + kind: KeystoreKind + ): KmResult[RemoveValidatorStatus] {.raises: [Defect].} = removeValidatorFiles(conf.validatorsDir(), conf.secretsDir(), keyName, kind) proc removeValidator*(pool: var ValidatorPool, conf: AnyConf, publicKey: ValidatorPubKey, - kind: KeystoreKind): KmResult[RemoveValidatorStatus] - {.raises: [Defect].} = + kind: KeystoreKind): KmResult[RemoveValidatorStatus] {. + raises: [Defect].} = let validator = pool.getValidator(publicKey) if isNil(validator): return ok(RemoveValidatorStatus.notFound) if validator.kind.toKeystoreKind() != kind: return ok(RemoveValidatorStatus.notFound) + let cres = validator.data.handle.closeLockedFile() + if cres.isErr(): + return err("Could not unlock validator keystore file") let res = removeValidatorFiles(conf, publicKey.fsName, kind) if res.isErr(): return err(res.error()) @@ -573,6 +605,42 @@ proc existsKeystore*(keystoreDir: string, return true false +iterator listLoadableKeys*(validatorsDir, secretsDir: string, + keysMask: set[KeystoreKind]): CookedPubKey = + try: + for kind, file in walkDir(validatorsDir): + if kind == pcDir: + let + keyName = splitFile(file).name + keystoreDir = validatorsDir / keyName + + if not(checkKeyName(keyName)): + # Skip folders which name do not satisfy "0x[a-fA-F0-9]{96, 96}". + continue + + if not(existsKeystore(keystoreDir, keysMask)): + # Skip folder which do not satisfy `keysMask`. + continue + + let kres = ValidatorPubKey.fromHex(keyName) + if kres.isErr(): + # Skip folders which could not be decoded to ValidatorPubKey. + continue + let publicKey = kres.get() + + let cres = publicKey.load() + if cres.isNone(): + # Skip folders which has invalid ValidatorPubKey + # (point is not on curve). + continue + + yield cres.get() + + except OSError as err: + error "Validator keystores directory not accessible", + path = validatorsDir, err = err.msg + quit 1 + iterator listLoadableKeystores*(validatorsDir, secretsDir: string, nonInteractive: bool, keysMask: set[KeystoreKind]): KeystoreData = @@ -715,10 +783,11 @@ proc saveNetKeystore*(rng: var HmacDrbgContext, keystorePath: string, key_path = keystorePath res.mapErrTo(FailedToCreateKeystoreFile) -proc createValidatorFiles*(secretsDir, validatorsDir, keystoreDir, secretFile, - passwordAsString, keystoreFile, - encodedStorage: string - ): Result[void, KeystoreGenerationError] = +proc createLocalValidatorFiles*( + secretsDir, validatorsDir, keystoreDir, + secretFile, passwordAsString, keystoreFile, + encodedStorage: string + ): Result[void, KeystoreGenerationError] {.raises: [Defect].} = var success = false # becomes true when everything is created successfully @@ -730,7 +799,7 @@ proc createValidatorFiles*(secretsDir, validatorsDir, keystoreDir, secretFile, if not(secretsDirExisted): ? secureCreatePath(secretsDir).mapErrTo(FailedToCreateSecretsDir) defer: - if not (success or secretsDirExisted): + if not (success or secretsDirExisted): discard io2.removeDir(secretsDir) # validatorsDir: @@ -738,7 +807,7 @@ proc createValidatorFiles*(secretsDir, validatorsDir, keystoreDir, secretFile, if not(validatorsDirExisted): ? secureCreatePath(validatorsDir).mapErrTo(FailedToCreateValidatorsDir) defer: - if not (success or validatorsDirExisted): + if not (success or validatorsDirExisted): discard io2.removeDir(validatorsDir) # keystoreDir: @@ -761,9 +830,57 @@ proc createValidatorFiles*(secretsDir, validatorsDir, keystoreDir, secretFile, success = true ok() -proc createValidatorFiles*(validatorsDir, keystoreDir, keystoreFile, - encodedStorage: string - ): Result[void, KeystoreGenerationError] = +proc createLockedLocalValidatorFiles*( + secretsDir, validatorsDir, keystoreDir, + secretFile, passwordAsString, keystoreFile, + encodedStorage: string + ): Result[FileLockHandle, KeystoreGenerationError] {.raises: [Defect].} = + + var + success = false # becomes true when everything is created successfully + cleanupSecretsDir = true # becomes false if secretsDir already existed + cleanupValidatorsDir = true # becomes false if validatorsDir already existed + + # secretsDir: + let secretsDirExisted: bool = dirExists(secretsDir) + if not(secretsDirExisted): + ? secureCreatePath(secretsDir).mapErrTo(FailedToCreateSecretsDir) + defer: + if not (success or secretsDirExisted): + discard io2.removeDir(secretsDir) + + # validatorsDir: + let validatorsDirExisted: bool = dirExists(validatorsDir) + if not(validatorsDirExisted): + ? secureCreatePath(validatorsDir).mapErrTo(FailedToCreateValidatorsDir) + defer: + if not (success or validatorsDirExisted): + discard io2.removeDir(validatorsDir) + + # keystoreDir: + ? secureCreatePath(keystoreDir).mapErrTo(FailedToCreateKeystoreDir) + defer: + if not success: + discard io2.removeDir(keystoreDir) + + # secretFile: + ? secureWriteFile(secretFile, + passwordAsString).mapErrTo(FailedToCreateSecretFile) + defer: + if not success: + discard io2.removeFile(secretFile) + + # keystoreFile: + let lock = + ? secureWriteLockedFile(keystoreFile, + encodedStorage).mapErrTo(FailedToCreateKeystoreFile) + + success = true + ok(lock) + +proc createRemoteValidatorFiles*( + validatorsDir, keystoreDir, keystoreFile, encodedStorage: string + ): Result[void, KeystoreGenerationError] {.raises: [Defect].} = var success = false # becomes true when everything is created successfully @@ -787,13 +904,41 @@ proc createValidatorFiles*(validatorsDir, keystoreDir, keystoreFile, success = true ok() -proc saveKeystore*(rng: var HmacDrbgContext, - validatorsDir, secretsDir: string, - signingKey: ValidatorPrivKey, - signingPubKey: CookedPubKey, - signingKeyPath: KeyPath, - password: string, - mode = Secure): Result[void, KeystoreGenerationError] = +proc createLockedRemoteValidatorFiles*( + validatorsDir, keystoreDir, keystoreFile, encodedStorage: string + ): Result[FileLockHandle, KeystoreGenerationError] {.raises: [Defect].} = + var + success = false # becomes true when everything is created successfully + + # validatorsDir: + let validatorsDirExisted: bool = dirExists(validatorsDir) + if not(validatorsDirExisted): + ? secureCreatePath(validatorsDir).mapErrTo(FailedToCreateValidatorsDir) + defer: + if not (success or validatorsDirExisted): + discard io2.removeDir(validatorsDir) + + # keystoreDir: + ? secureCreatePath(keystoreDir).mapErrTo(FailedToCreateKeystoreDir) + defer: + if not success: + discard io2.removeDir(keystoreDir) + + # keystoreFile: + let lock = ? secureWriteLockedFile( + keystoreFile, encodedStorage).mapErrTo(FailedToCreateKeystoreFile) + success = true + ok(lock) + +proc saveKeystore*( + rng: var HmacDrbgContext, + validatorsDir, secretsDir: string, + signingKey: ValidatorPrivKey, + signingPubKey: CookedPubKey, + signingKeyPath: KeyPath, + password: string, + mode = Secure + ): Result[void, KeystoreGenerationError] {.raises: [Defect].} = let keypass = KeystorePass.init(password) keyName = signingPubKey.fsName @@ -811,29 +956,70 @@ proc saveKeystore*(rng: var HmacDrbgContext, keypass, signingKeyPath, mode = mode) - var encodedStorage: string - try: - encodedStorage = Json.encode(keyStore) - except SerializationError as e: - error "Could not serialize keystorage", key_path = keystoreFile - return err(KeystoreGenerationError( - kind: FailedToCreateKeystoreFile, error: e.msg)) - - ? createValidatorFiles(secretsDir, validatorsDir, - keystoreDir, - secretsDir / keyName, keypass.str, - keystoreFile, encodedStorage) + let encodedStorage = + try: + Json.encode(keyStore) + except SerializationError as e: + error "Could not serialize keystorage", key_path = keystoreFile + return err(KeystoreGenerationError( + kind: FailedToCreateKeystoreFile, error: e.msg)) + ? createLocalValidatorFiles(secretsDir, validatorsDir, + keystoreDir, + secretsDir / keyName, keypass.str, + keystoreFile, encodedStorage) ok() -proc saveKeystore*(validatorsDir: string, - publicKey: ValidatorPubKey, - urls: seq[RemoteSignerInfo], - threshold: uint32, - flags: set[RemoteKeystoreFlag] = {}, - remoteType = RemoteSignerType.Web3Signer, - desc = ""): Result[void, KeystoreGenerationError] - {.raises: [Defect].} = +proc saveLockedKeystore*( + rng: var HmacDrbgContext, + validatorsDir, secretsDir: string, + signingKey: ValidatorPrivKey, + signingPubKey: CookedPubKey, + signingKeyPath: KeyPath, + password: string, + mode = Secure + ): Result[FileLockHandle, KeystoreGenerationError] {.raises: [Defect].} = + let + keypass = KeystorePass.init(password) + keyName = signingPubKey.fsName + keystoreDir = validatorsDir / keyName + keystoreFile = keystoreDir / KeystoreFileName + + if dirExists(keystoreDir): + return err(KeystoreGenerationError(kind: DuplicateKeystoreDir, + error: "Keystore directory already exists")) + if fileExists(keystoreFile): + return err(KeystoreGenerationError(kind: DuplicateKeystoreFile, + error: "Keystore file already exists")) + + let keyStore = createKeystore(kdfPbkdf2, rng, signingKey, + keypass, signingKeyPath, + mode = mode) + + let encodedStorage = + try: + Json.encode(keyStore) + except SerializationError as e: + error "Could not serialize keystorage", key_path = keystoreFile + return err(KeystoreGenerationError( + kind: FailedToCreateKeystoreFile, error: e.msg)) + + let lock = ? createLockedLocalValidatorFiles(secretsDir, validatorsDir, + keystoreDir, + secretsDir / keyName, + keypass.str, + keystoreFile, encodedStorage) + ok(lock) + +proc saveKeystore*( + validatorsDir: string, + publicKey: ValidatorPubKey, + urls: seq[RemoteSignerInfo], + threshold: uint32, + flags: set[RemoteKeystoreFlag] = {}, + remoteType = RemoteSignerType.Web3Signer, + desc = "" + ): Result[void, KeystoreGenerationError] {.raises: [Defect].} = let keyName = publicKey.fsName keystoreDir = validatorsDir / keyName @@ -863,27 +1049,90 @@ proc saveKeystore*(validatorsDir: string, return err(KeystoreGenerationError( kind: FailedToCreateKeystoreFile, error: exc.msg)) - ? createValidatorFiles(validatorsDir, keystoreDir, keystoreFile, - encodedStorage) + ? createRemoteValidatorFiles(validatorsDir, keystoreDir, keystoreFile, + encodedStorage) ok() -proc saveKeystore*(validatorsDir: string, - publicKey: ValidatorPubKey, - url: HttpHostUri): Result[void, KeystoreGenerationError] - {.raises: [Defect].} = +proc saveLockedKeystore*( + validatorsDir: string, + publicKey: ValidatorPubKey, + urls: seq[RemoteSignerInfo], + threshold: uint32, + flags: set[RemoteKeystoreFlag] = {}, + remoteType = RemoteSignerType.Web3Signer, + desc = "" + ): Result[FileLockHandle, KeystoreGenerationError] {.raises: [Defect].} = + let + keyName = publicKey.fsName + keystoreDir = validatorsDir / keyName + keystoreFile = keystoreDir / RemoteKeystoreFileName + keystoreDesc = if len(desc) == 0: none[string]() else: some(desc) + keyStore = RemoteKeystore( + version: 2'u64, + description: keystoreDesc, + remoteType: remoteType, + pubkey: publicKey, + threshold: threshold, + remotes: urls, + flags: flags) + + if dirExists(keystoreDir): + return err(KeystoreGenerationError(kind: DuplicateKeystoreDir, + error: "Keystore directory already exists")) + if fileExists(keystoreFile): + return err(KeystoreGenerationError(kind: DuplicateKeystoreFile, + error: "Keystore file already exists")) + + let encodedStorage = + try: + Json.encode(keyStore) + except SerializationError as exc: + error "Could not serialize keystorage", key_path = keystoreFile + return err(KeystoreGenerationError( + kind: FailedToCreateKeystoreFile, error: exc.msg)) + + let lock = ? createLockedRemoteValidatorFiles(validatorsDir, keystoreDir, + keystoreFile, encodedStorage) + ok(lock) + +proc saveKeystore*( + validatorsDir: string, + publicKey: ValidatorPubKey, + url: HttpHostUri + ): Result[void, KeystoreGenerationError] {.raises: [Defect].} = let remoteInfo = RemoteSignerInfo(url: url, id: 0) saveKeystore(validatorsDir, publicKey, @[remoteInfo], 1) -proc saveKeystore*(conf: AnyConf, - publicKey: ValidatorPubKey, - remotes: seq[RemoteSignerInfo], - threshold: uint32, - flags: set[RemoteKeystoreFlag] = {}, - remoteType = RemoteSignerType.Web3Signer, - desc = ""): Result[void, KeystoreGenerationError] - {.raises: [Defect].} = - saveKeystore( - conf.validatorsDir(), +proc saveLockedKeystore*( + validatorsDir: string, + publicKey: ValidatorPubKey, + url: HttpHostUri + ): Result[FileLockHandle, KeystoreGenerationError] {.raises: [Defect].} = + let remoteInfo = RemoteSignerInfo(url: url, id: 0) + saveLockedKeystore(validatorsDir, publicKey, @[remoteInfo], 1) + +proc saveKeystore*( + conf: AnyConf, + publicKey: ValidatorPubKey, + remotes: seq[RemoteSignerInfo], + threshold: uint32, + flags: set[RemoteKeystoreFlag] = {}, + remoteType = RemoteSignerType.Web3Signer, + desc = "" + ): Result[FileLockHandle, KeystoreGenerationError] {.raises: [Defect].} = + saveKeystore(conf.validatorsDir(), + publicKey, remotes, threshold, flags, remoteType, desc) + +proc saveLockedKeystore*( + conf: AnyConf, + publicKey: ValidatorPubKey, + remotes: seq[RemoteSignerInfo], + threshold: uint32, + flags: set[RemoteKeystoreFlag] = {}, + remoteType = RemoteSignerType.Web3Signer, + desc = "" + ): Result[FileLockHandle, KeystoreGenerationError] {.raises: [Defect].} = + saveLockedKeystore(conf.validatorsDir(), publicKey, remotes, threshold, flags, remoteType, desc) proc importKeystore*(pool: var ValidatorPool, conf: AnyConf, @@ -914,11 +1163,13 @@ proc importKeystore*(pool: var ValidatorPool, conf: AnyConf, if existsKeystore(keystoreDir, {KeystoreKind.Local, KeystoreKind.Remote}): return err(AddValidatorFailure.init(AddValidatorStatus.existingArtifacts)) - let res = saveKeystore(conf, publicKey, keystore.remotes, keystore.threshold) + let res = saveLockedKeystore(conf, publicKey, keystore.remotes, + keystore.threshold) if res.isErr(): return err(AddValidatorFailure.init(AddValidatorStatus.failed, $res.error())) - ok(KeystoreData.init(cookedKey, keystore.remotes, keystore.threshold)) + ok(KeystoreData.init(cookedKey, keystore.remotes, keystore.threshold, + res.get())) proc importKeystore*(pool: var ValidatorPool, rng: var HmacDrbgContext, @@ -951,14 +1202,14 @@ proc importKeystore*(pool: var ValidatorPool, if existsKeystore(keystoreDir, {KeystoreKind.Local, KeystoreKind.Remote}): return err(AddValidatorFailure.init(AddValidatorStatus.existingArtifacts)) - let res = saveKeystore(rng, validatorsDir, secretsDir, - privateKey, publicKey, keystore.path, password) + let res = saveLockedKeystore(rng, validatorsDir, secretsDir, + privateKey, publicKey, keystore.path, password) if res.isErr(): return err(AddValidatorFailure.init(AddValidatorStatus.failed, $res.error())) - ok(KeystoreData.init(privateKey, keystore)) + ok(KeystoreData.init(privateKey, keystore, res.get())) proc generateDistirbutedStore*(rng: var HmacDrbgContext, shares: seq[SecretShare], diff --git a/beacon_chain/validators/validator_pool.nim b/beacon_chain/validators/validator_pool.nim index 5fa9053d1..530f56fca 100644 --- a/beacon_chain/validators/validator_pool.nim +++ b/beacon_chain/validators/validator_pool.nim @@ -20,6 +20,7 @@ import ../spec/datatypes/[phase0, altair], ../spec/eth2_apis/[rest_types, eth2_rest_serialization, rest_remote_signer_calls], + ../filepath, ./slashing_protection export @@ -146,6 +147,14 @@ proc updateValidator*(pool: var ValidatorPool, pubkey: ValidatorPubKey, v.index = some(index) pool.validators[pubkey] = v +proc close*(pool: var ValidatorPool) = + ## Unlock and close all validator keystore's files managed by ``pool``. + for validator in pool.validators.values(): + let res = validator.data.handle.closeLockedFile() + if res.isErr(): + notice "Could not unlock validator's keystore file", + pubkey = validator.pubkey, validator = shortLog(validator) + iterator publicKeys*(pool: ValidatorPool): ValidatorPubKey = for item in pool.validators.keys(): yield item diff --git a/tests/test_keymanager_api.nim b/tests/test_keymanager_api.nim index 10bccbd40..fa1c18e28 100644 --- a/tests/test_keymanager_api.nim +++ b/tests/test_keymanager_api.nim @@ -199,39 +199,55 @@ const secretNetBytes = hexToSeqByte "08021220fe442379443d6e2d7d75d3a58f96fbb35f0a9c7217796825fc9040e3b89c5736" proc listLocalValidators(validatorsDir, - secretsDir: string): seq[KeystoreInfo] {. + secretsDir: string): seq[ValidatorPubKey] {. raises: [Defect].} = - var validators: seq[KeystoreInfo] - + var validators: seq[ValidatorPubKey] try: - for el in listLoadableKeystores(validatorsDir, secretsDir, true, - {KeystoreKind.Local}): - validators.add KeystoreInfo(validating_pubkey: el.pubkey, - derivation_path: el.path.string, - readonly: false) + for el in listLoadableKeys(validatorsDir, secretsDir, + {KeystoreKind.Local}): + validators.add el.toPubKey() except OSError as err: error "Failure to list the validator directories", validatorsDir, secretsDir, err = err.msg - validators proc listRemoteValidators(validatorsDir, - secretsDir: string): seq[RemoteKeystoreInfo] {. + secretsDir: string): seq[ValidatorPubKey] {. raises: [Defect].} = - var validators: seq[RemoteKeystoreInfo] - + var validators: seq[ValidatorPubKey] try: - for el in listLoadableKeystores(validatorsDir, secretsDir, true, - {KeystoreKind.Remote}): - validators.add RemoteKeystoreInfo(pubkey: el.pubkey, - url: el.remotes[0].url) - + for el in listLoadableKeys(validatorsDir, secretsDir, + {KeystoreKind.Remote}): + validators.add el.toPubKey() except OSError as err: error "Failure to list the validator directories", validatorsDir, secretsDir, err = err.msg - validators +proc `==`(a: seq[ValidatorPubKey], + b: seq[KeystoreInfo | RemoteKeystoreInfo]): bool = + if len(a) != len(b): + return false + var indices: seq[int] + for publicKey in a: + let index = + block: + var res = -1 + for k, v in b.pairs(): + let key = + when b is seq[KeystoreInfo]: + v.validating_pubkey + else: + v.pubkey + if key == publicKey: + res = k + break + res + if (index == -1) or (index in indices): + return false + indices.add(index) + true + proc runTests {.async.} = while bnStatus != BeaconNodeStatus.Running: await sleepAsync(1.seconds) @@ -301,9 +317,9 @@ proc runTests {.async.} = let key = ValidatorPubKey.fromHex(item).tryGet() res.add(RemoteKeystoreInfo(pubkey: key, url: newPublicKeysUrl)) # Adding non-remote keys which are already present in filesystem - res.add(RemoteKeystoreInfo(pubkey: localList[0].validating_pubkey, + res.add(RemoteKeystoreInfo(pubkey: localList[0], url: newPublicKeysUrl)) - res.add(RemoteKeystoreInfo(pubkey: localList[1].validating_pubkey, + res.add(RemoteKeystoreInfo(pubkey: localList[1], url: newPublicKeysUrl)) ImportRemoteKeystoresBody(remote_keys: res) @@ -338,8 +354,8 @@ proc runTests {.async.} = pubkeys: @[ ValidatorPubKey.fromHex(oldPublicKeys[0]).tryGet(), ValidatorPubKey.fromHex(oldPublicKeys[1]).tryGet(), - localList[0].validating_pubkey, - localList[1].validating_pubkey + localList[0], + localList[1] ] ) @@ -546,11 +562,11 @@ proc runTests {.async.} = suite "ListKeys requests" & preset(): asyncTest "Correct token provided" & preset(): let - filesystemKeystores = sorted( + filesystemKeys = sorted( listLocalValidators(validatorsDir, secretsDir)) apiKeystores = sorted((await client.listKeys(correctTokenValue)).data) - check filesystemKeystores == apiKeystores + check filesystemKeys == apiKeystores asyncTest "Missing Authorization header" & preset(): let @@ -599,20 +615,20 @@ proc runTests {.async.} = responseJson1["data"][i]["message"].getStr() == "" let - filesystemKeystores1 = sorted( + filesystemKeys1 = sorted( listLocalValidators(validatorsDir, secretsDir)) apiKeystores1 = sorted((await client.listKeys(correctTokenValue)).data) check: - filesystemKeystores1 == apiKeystores1 - importKeystoresBody1.keystores[0].pubkey in filesystemKeystores1 - importKeystoresBody1.keystores[1].pubkey in filesystemKeystores1 - importKeystoresBody1.keystores[2].pubkey in filesystemKeystores1 - importKeystoresBody1.keystores[3].pubkey in filesystemKeystores1 - importKeystoresBody1.keystores[4].pubkey in filesystemKeystores1 - importKeystoresBody1.keystores[5].pubkey in filesystemKeystores1 - importKeystoresBody1.keystores[6].pubkey in filesystemKeystores1 - importKeystoresBody1.keystores[7].pubkey in filesystemKeystores1 + filesystemKeys1 == apiKeystores1 + importKeystoresBody1.keystores[0].pubkey in filesystemKeys1 + importKeystoresBody1.keystores[1].pubkey in filesystemKeys1 + importKeystoresBody1.keystores[2].pubkey in filesystemKeys1 + importKeystoresBody1.keystores[3].pubkey in filesystemKeys1 + importKeystoresBody1.keystores[4].pubkey in filesystemKeys1 + importKeystoresBody1.keystores[5].pubkey in filesystemKeys1 + importKeystoresBody1.keystores[6].pubkey in filesystemKeys1 + importKeystoresBody1.keystores[7].pubkey in filesystemKeys1 let response2 = await client.importKeystoresPlain( @@ -639,20 +655,20 @@ proc runTests {.async.} = responseJson3["data"][i]["message"].getStr() == "" let - filesystemKeystores2 = sorted( + filesystemKeys2 = sorted( listLocalValidators(validatorsDir, secretsDir)) apiKeystores2 = sorted((await client.listKeys(correctTokenValue)).data) check: - filesystemKeystores2 == apiKeystores2 - deleteKeysBody1.pubkeys[0] notin filesystemKeystores2 - deleteKeysBody1.pubkeys[1] notin filesystemKeystores2 - deleteKeysBody1.pubkeys[2] notin filesystemKeystores2 - deleteKeysBody1.pubkeys[3] notin filesystemKeystores2 - deleteKeysBody1.pubkeys[4] notin filesystemKeystores2 - deleteKeysBody1.pubkeys[5] notin filesystemKeystores2 - deleteKeysBody1.pubkeys[6] notin filesystemKeystores2 - deleteKeysBody1.pubkeys[7] notin filesystemKeystores2 + filesystemKeys2 == apiKeystores2 + deleteKeysBody1.pubkeys[0] notin filesystemKeys2 + deleteKeysBody1.pubkeys[1] notin filesystemKeys2 + deleteKeysBody1.pubkeys[2] notin filesystemKeys2 + deleteKeysBody1.pubkeys[3] notin filesystemKeys2 + deleteKeysBody1.pubkeys[4] notin filesystemKeys2 + deleteKeysBody1.pubkeys[5] notin filesystemKeys2 + deleteKeysBody1.pubkeys[6] notin filesystemKeys2 + deleteKeysBody1.pubkeys[7] notin filesystemKeys2 asyncTest "Missing Authorization header" & preset(): let @@ -732,12 +748,12 @@ proc runTests {.async.} = suite "ListRemoteKeys requests" & preset(): asyncTest "Correct token provided" & preset(): let - filesystemKeystores = sorted( + filesystemKeys = sorted( listRemoteValidators(validatorsDir, secretsDir)) apiKeystores = sorted(( await client.listRemoteKeys(correctTokenValue)).data) - check filesystemKeystores == apiKeystores + check filesystemKeys == apiKeystores asyncTest "Missing Authorization header" & preset(): let @@ -953,21 +969,21 @@ proc runTests {.async.} = responseJson1["data"][i]["message"].getStr() == "" let - filesystemKeystores1 = sorted( + filesystemKeys1 = sorted( listRemoteValidators(validatorsDir, secretsDir)) apiKeystores1 = sorted(( await client.listRemoteKeys(correctTokenValue)).data) check: - filesystemKeystores1 == apiKeystores1 + filesystemKeys1 == apiKeystores1 for item in newPublicKeys: let key = ValidatorPubKey.fromHex(item).tryGet() let found = block: var res = false - for keystore in filesystemKeystores1: - if keystore.pubkey == key: + for keystore in filesystemKeys1: + if keystore == key: res = true break res @@ -991,16 +1007,16 @@ proc runTests {.async.} = responseJson2["data"][7]["status"].getStr() == "deleted" let - filesystemKeystores2 = sorted( + filesystemKeys2 = sorted( listRemoteValidators(validatorsDir, secretsDir)) apiKeystores2 = sorted(( await client.listRemoteKeys(correctTokenValue)).data) check: - filesystemKeystores2 == apiKeystores2 + filesystemKeys2 == apiKeystores2 - for keystore in filesystemKeystores2: - let key = "0x" & keystore.pubkey.toHex() + for keystore in filesystemKeys2: + let key = "0x" & keystore.toHex() check: key notin newPublicKeys diff --git a/tests/test_keystore_management.nim b/tests/test_keystore_management.nim index 317fe4d7f..ecde0be02 100644 --- a/tests/test_keystore_management.nim +++ b/tests/test_keystore_management.nim @@ -432,10 +432,10 @@ suite "createValidatorFiles()": test "Add keystore files [LOCAL]": let - res = createValidatorFiles(testSecretsDir, testValidatorsDir, - keystoreDir, - secretFile, password, - keystoreFile, keystoreJsonContents) + res = createLocalValidatorFiles(testSecretsDir, testValidatorsDir, + keystoreDir, + secretFile, password, + keystoreFile, keystoreJsonContents) validatorsCount = directoryItemsCount(testValidatorsDir) secretsCount = directoryItemsCount(testSecretsDir) @@ -458,15 +458,15 @@ suite "createValidatorFiles()": test "Add keystore files twice [LOCAL]": let - res1 = createValidatorFiles(testSecretsDir, testValidatorsDir, - keystoreDir, - secretFile, password, - keystoreFile, keystoreJsonContents) + res1 = createLocalValidatorFiles(testSecretsDir, testValidatorsDir, + keystoreDir, + secretFile, password, + keystoreFile, keystoreJsonContents) - res2 = createValidatorFiles(testSecretsDir, testValidatorsDir, - keystoreDir, - secretFile, password, - keystoreFile, keystoreJsonContents) + res2 = createLocalValidatorFiles(testSecretsDir, testValidatorsDir, + keystoreDir, + secretFile, password, + keystoreFile, keystoreJsonContents) validatorsCount = directoryItemsCount(testValidatorsDir) secretsCount = directoryItemsCount(testSecretsDir) @@ -493,9 +493,9 @@ suite "createValidatorFiles()": remoteKeystoreFile = curKeystoreDir / RemoteKeystoreFileName localKeystoreFile = curKeystoreDir / KeystoreFileName - res = createValidatorFiles(testValidatorsDir, curKeystoreDir, - remoteKeystoreFile, - MultipleRemoteKeystoreJsons[0]) + res = createRemoteValidatorFiles(testValidatorsDir, curKeystoreDir, + remoteKeystoreFile, + MultipleRemoteKeystoreJsons[0]) validatorsCount = directoryItemsCount(testValidatorsDir) secretsCount = directoryItemsCount(testSecretsDir) @@ -525,13 +525,13 @@ suite "createValidatorFiles()": remoteKeystoreFile = curKeystoreDir / RemoteKeystoreFileName localKeystoreFile = curKeystoreDir / KeystoreFileName - res1 = createValidatorFiles(testValidatorsDir, curKeystoreDir, - remoteKeystoreFile, - MultipleRemoteKeystoreJsons[0]) + res1 = createRemoteValidatorFiles(testValidatorsDir, curKeystoreDir, + remoteKeystoreFile, + MultipleRemoteKeystoreJsons[0]) - res2 = createValidatorFiles(testValidatorsDir, curKeystoreDir, - remoteKeystoreFile, - MultipleRemoteKeystoreJsons[0]) + res2 = createRemoteValidatorFiles(testValidatorsDir, curKeystoreDir, + remoteKeystoreFile, + MultipleRemoteKeystoreJsons[0]) validatorsCount = directoryItemsCount(testValidatorsDir) secretsCount = directoryItemsCount(testSecretsDir) @@ -557,16 +557,16 @@ suite "createValidatorFiles()": # TODO The following tests are disabled on Windows because the io2 module # doesn't implement the permission/mode parameter at the moment: when not defined(windows): - test "`createValidatorFiles` with `secretsDir` without permissions": + test "`createLocalValidatorFiles` with `secretsDir` without permissions": # Creating `secrets` dir with `UserRead` permissions before # calling `createValidatorFiles` which should result in problem # with creating a secret file inside the dir: let secretsDirNoPermissions = createPath(testSecretsDir, 0o400) - res = createValidatorFiles(testSecretsDir, testValidatorsDir, - keystoreDir, - secretFile, password, - keystoreFile, keystoreJsonContents) + res = createLocalValidatorFiles(testSecretsDir, testValidatorsDir, + keystoreDir, + secretFile, password, + keystoreFile, keystoreJsonContents) check: res.isErr and res.error.kind == FailedToCreateSecretFile @@ -578,16 +578,16 @@ suite "createValidatorFiles()": # The creation of the validators dir should be rolled-back not dirExists(testValidatorsDir) - test "`createValidatorFiles` with `validatorsDir` without permissions": + test "`createLocalValidatorFiles` with `validatorsDir` without permissions": # Creating `validators` dir with `UserRead` permissions before # calling `createValidatorFiles` which should result in problems # creating `keystoreDir` inside the dir. let validatorsDirNoPermissions = createPath(testValidatorsDir, 0o400) - res = createValidatorFiles(testSecretsDir, testValidatorsDir, - keystoreDir, - secretFile, password, - keystoreFile, keystoreJsonContents) + res = createLocalValidatorFiles(testSecretsDir, testValidatorsDir, + keystoreDir, + secretFile, password, + keystoreFile, keystoreJsonContents) check: res.isErr and res.error.kind == FailedToCreateKeystoreDir @@ -599,17 +599,17 @@ suite "createValidatorFiles()": dirExists(testValidatorsDir) testValidatorsDir.isEmptyDir - test "`createValidatorFiles` with `keystoreDir` without permissions": + test "`createLocalValidatorFiles` with `keystoreDir` without permissions": # Creating `keystore` dir with `UserRead` permissions before # calling `createValidatorFiles` which should result in problems # creating keystore file inside this dir: let validatorsDir = createPath(testValidatorsDir, 0o700) keystoreDirNoPermissions = createPath(keystoreDir, 0o400) - res = createValidatorFiles(testSecretsDir, testValidatorsDir, - keystoreDir, - secretFile, password, - keystoreFile, keystoreJsonContents) + res = createLocalValidatorFiles(testSecretsDir, testValidatorsDir, + keystoreDir, + secretFile, password, + keystoreFile, keystoreJsonContents) check: res.isErr and res.error.kind == FailedToCreateKeystoreFile @@ -634,14 +634,14 @@ suite "createValidatorFiles()": validatorsCountBefore = directoryItemsCount(testValidatorsDir) secretsCountBefore = directoryItemsCount(testSecretsDir) - # Creating `keystore` dir with `UserRead` permissions before calling `createValidatorFiles` - # which will result in error + # Creating `keystore` dir with `UserRead` permissions before calling + # `createValidatorFiles` which will result in error keystoreDirNoPermissions = createPath(keystoreDir, 0o400) - res = createValidatorFiles(testSecretsDir, testValidatorsDir, - keystoreDir, - secretFile, password, - keystoreFile, keystoreJsonContents) + res = createLocalValidatorFiles(testSecretsDir, testValidatorsDir, + keystoreDir, + secretFile, password, + keystoreFile, keystoreJsonContents) validatorsCountAfter = directoryItemsCount(testValidatorsDir) secretsCountAfter = directoryItemsCount(testSecretsDir) diff --git a/vendor/nim-stew b/vendor/nim-stew index 9a3130eb5..0476bcad1 160000 --- a/vendor/nim-stew +++ b/vendor/nim-stew @@ -1 +1 @@ -Subproject commit 9a3130eb5b76c6504d6e6f8f86556c34f70300ef +Subproject commit 0476bcad1b580a627af056ae78e872c790029958