gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/proto/negotiate.go (about) 1 package proto 2 3 import ( 4 "crypto/cipher" 5 "net" 6 "time" 7 8 "gitlab.com/NebulousLabs/errors" 9 "golang.org/x/crypto/chacha20poly1305" 10 11 "gitlab.com/NebulousLabs/encoding" 12 "gitlab.com/SkynetLabs/skyd/build" 13 "gitlab.com/SkynetLabs/skyd/skymodules" 14 "go.sia.tech/siad/crypto" 15 "go.sia.tech/siad/modules" 16 "go.sia.tech/siad/types" 17 ) 18 19 // extendDeadline is a helper function for extending the connection timeout. 20 func extendDeadline(conn net.Conn, d time.Duration) { _ = conn.SetDeadline(time.Now().Add(d)) } 21 22 // startRevision is run at the beginning of each revision iteration. It reads 23 // the host's settings confirms that the values are acceptable, and writes an acceptance. 24 func startRevision(conn net.Conn, host skymodules.HostDBEntry) error { 25 // verify the host's settings and confirm its identity 26 _, err := verifySettings(conn, host) 27 if err != nil { 28 return err 29 } 30 return modules.WriteNegotiationAcceptance(conn) 31 } 32 33 // startDownload is run at the beginning of each download iteration. It reads 34 // the host's settings confirms that the values are acceptable, and writes an acceptance. 35 func startDownload(conn net.Conn, host skymodules.HostDBEntry) error { 36 // verify the host's settings and confirm its identity 37 _, err := verifySettings(conn, host) 38 if err != nil { 39 return err 40 } 41 return modules.WriteNegotiationAcceptance(conn) 42 } 43 44 // verifySettings reads a signed HostSettings object from conn, validates the 45 // signature, and checks for discrepancies between the known settings and the 46 // received settings. If there is a discrepancy, the hostDB is notified. The 47 // received settings are returned. 48 func verifySettings(conn net.Conn, host skymodules.HostDBEntry) (skymodules.HostDBEntry, error) { 49 // convert host key (types.SiaPublicKey) to a crypto.PublicKey 50 if host.PublicKey.Algorithm != types.SignatureEd25519 || len(host.PublicKey.Key) != crypto.PublicKeySize { 51 build.Critical("hostdb did not filter out host with wrong signature algorithm:", host.PublicKey.Algorithm) 52 return skymodules.HostDBEntry{}, errors.New("host used unsupported signature algorithm") 53 } 54 var pk crypto.PublicKey 55 copy(pk[:], host.PublicKey.Key) 56 57 // read signed host settings 58 var recvSettings modules.HostOldExternalSettings 59 if err := crypto.ReadSignedObject(conn, &recvSettings, modules.NegotiateMaxHostExternalSettingsLen, pk); err != nil { 60 return skymodules.HostDBEntry{}, errors.New("couldn't read host's settings: " + err.Error()) 61 } 62 // TODO: check recvSettings against host.HostExternalSettings. If there is 63 // a discrepancy, write the error to conn. 64 if recvSettings.NetAddress != host.NetAddress { 65 // for now, just overwrite the NetAddress, since we know that 66 // host.NetAddress works (it was the one we dialed to get conn) 67 recvSettings.NetAddress = host.NetAddress 68 } 69 host.HostExternalSettings = modules.HostExternalSettings{ 70 AcceptingContracts: recvSettings.AcceptingContracts, 71 MaxDownloadBatchSize: recvSettings.MaxDownloadBatchSize, 72 MaxDuration: recvSettings.MaxDuration, 73 MaxReviseBatchSize: recvSettings.MaxReviseBatchSize, 74 NetAddress: recvSettings.NetAddress, 75 RemainingStorage: recvSettings.RemainingStorage, 76 SectorSize: recvSettings.SectorSize, 77 TotalStorage: recvSettings.TotalStorage, 78 UnlockHash: recvSettings.UnlockHash, 79 WindowSize: recvSettings.WindowSize, 80 Collateral: recvSettings.Collateral, 81 MaxCollateral: recvSettings.MaxCollateral, 82 ContractPrice: recvSettings.ContractPrice, 83 DownloadBandwidthPrice: recvSettings.DownloadBandwidthPrice, 84 StoragePrice: recvSettings.StoragePrice, 85 UploadBandwidthPrice: recvSettings.UploadBandwidthPrice, 86 RevisionNumber: recvSettings.RevisionNumber, 87 Version: recvSettings.Version, 88 } 89 return host, nil 90 } 91 92 // verifyRecentRevision confirms that the host and contractor agree upon the current 93 // state of the contract being revised. 94 func verifyRecentRevision(conn net.Conn, contract *SafeContract, hostVersion string) error { 95 // send contract ID 96 if err := encoding.WriteObject(conn, contract.header.ID()); err != nil { 97 return errors.New("couldn't send contract ID: " + err.Error()) 98 } 99 // read challenge 100 var challenge crypto.Hash 101 if err := encoding.ReadObject(conn, &challenge, 32); err != nil { 102 return errors.New("couldn't read challenge: " + err.Error()) 103 } 104 if build.VersionCmp(hostVersion, "1.3.0") >= 0 { 105 crypto.SecureWipe(challenge[:16]) 106 } 107 // sign and return 108 sig := crypto.SignHash(challenge, contract.header.SecretKey) 109 if err := encoding.WriteObject(conn, sig); err != nil { 110 return errors.New("couldn't send challenge response: " + err.Error()) 111 } 112 // read acceptance 113 if err := modules.ReadNegotiationAcceptance(conn); err != nil { 114 return errors.New("host did not accept revision request: " + err.Error()) 115 } 116 // read last revision and signatures 117 var lastRevision types.FileContractRevision 118 var hostSignatures []types.TransactionSignature 119 if err := encoding.ReadObject(conn, &lastRevision, 2048); err != nil { 120 return errors.New("couldn't read last revision: " + err.Error()) 121 } 122 if err := encoding.ReadObject(conn, &hostSignatures, 2048); err != nil { 123 return errors.New("couldn't read host signatures: " + err.Error()) 124 } 125 // Check that the unlock hashes match; if they do not, something is 126 // seriously wrong. Otherwise, check that the revision numbers match. 127 ourRev := contract.header.LastRevision() 128 if lastRevision.UnlockConditions.UnlockHash() != ourRev.UnlockConditions.UnlockHash() { 129 return errors.New("unlock conditions do not match") 130 } else if lastRevision.NewRevisionNumber != ourRev.NewRevisionNumber { 131 // If the revision number doesn't match try to commit potential 132 // unapplied transactions and check again. 133 if err := contract.managedCommitTxns(); err != nil { 134 return errors.AddContext(err, "failed to commit transactions") 135 } 136 ourRev = contract.header.LastRevision() 137 if lastRevision.NewRevisionNumber != ourRev.NewRevisionNumber { 138 return &revisionNumberMismatchError{ourRev.NewRevisionNumber, lastRevision.NewRevisionNumber} 139 } 140 } 141 // NOTE: we can fake the blockheight here because it doesn't affect 142 // verification; it just needs to be above the fork height and below the 143 // contract expiration (which was checked earlier). 144 return modules.VerifyFileContractRevisionTransactionSignatures(lastRevision, hostSignatures, contract.header.EndHeight()-1) 145 } 146 147 // negotiateRevision sends a revision and actions to the host for approval, 148 // completing one iteration of the revision loop. 149 func negotiateRevision(conn net.Conn, rev types.FileContractRevision, secretKey crypto.SecretKey, height types.BlockHeight) (types.Transaction, error) { 150 // create transaction containing the revision 151 signedTxn := types.Transaction{ 152 FileContractRevisions: []types.FileContractRevision{rev}, 153 TransactionSignatures: []types.TransactionSignature{{ 154 ParentID: crypto.Hash(rev.ParentID), 155 CoveredFields: types.CoveredFields{FileContractRevisions: []uint64{0}}, 156 PublicKeyIndex: 0, // renter key is always first -- see formContract 157 }}, 158 } 159 // sign the transaction 160 encodedSig := crypto.SignHash(signedTxn.SigHash(0, height), secretKey) 161 signedTxn.TransactionSignatures[0].Signature = encodedSig[:] 162 163 // send the revision 164 if err := encoding.WriteObject(conn, rev); err != nil { 165 return types.Transaction{}, errors.New("couldn't send revision: " + err.Error()) 166 } 167 // read acceptance 168 if err := modules.ReadNegotiationAcceptance(conn); err != nil { 169 return types.Transaction{}, errors.New("host did not accept revision: " + err.Error()) 170 } 171 172 // send the new transaction signature 173 if err := encoding.WriteObject(conn, signedTxn.TransactionSignatures[0]); err != nil { 174 return types.Transaction{}, errors.New("couldn't send transaction signature: " + err.Error()) 175 } 176 // read the host's acceptance and transaction signature 177 // NOTE: if the host sends ErrStopResponse, we should continue processing 178 // the revision, but return the error anyway. 179 responseErr := modules.ReadNegotiationAcceptance(conn) 180 if responseErr != nil && !errors.Contains(responseErr, modules.ErrStopResponse) { 181 return types.Transaction{}, errors.New("host did not accept transaction signature: " + responseErr.Error()) 182 } 183 var hostSig types.TransactionSignature 184 if err := encoding.ReadObject(conn, &hostSig, 16e3); err != nil { 185 return types.Transaction{}, errors.New("couldn't read host's signature: " + err.Error()) 186 } 187 188 // add the signature to the transaction and verify it 189 // NOTE: we can fake the blockheight here because it doesn't affect 190 // verification; it just needs to be above the fork height and below the 191 // contract expiration (which was checked earlier). 192 verificationHeight := rev.NewWindowStart - 1 193 signedTxn.TransactionSignatures = append(signedTxn.TransactionSignatures, hostSig) 194 if err := signedTxn.StandaloneValid(verificationHeight); err != nil { 195 return types.Transaction{}, err 196 } 197 198 // if the host sent ErrStopResponse, return it 199 return signedTxn, responseErr 200 } 201 202 // newDownloadRevision revises the current revision to cover the cost of 203 // downloading data. 204 func newDownloadRevision(current types.FileContractRevision, downloadCost types.Currency) (types.FileContractRevision, error) { 205 return current.PaymentRevision(downloadCost) 206 } 207 208 // newUploadRevision revises the current revision to cover the cost of 209 // uploading a sector. 210 func newUploadRevision(current types.FileContractRevision, merkleRoot crypto.Hash, price, collateral types.Currency) (types.FileContractRevision, error) { 211 rev, err := current.PaymentRevision(price) 212 if err != nil { 213 return types.FileContractRevision{}, err 214 } 215 216 // Check that there is enough collateral to cover the cost. 217 if rev.MissedHostOutput().Value.Cmp(collateral) < 0 { 218 return types.FileContractRevision{}, types.ErrRevisionCollateralTooLow 219 } 220 221 // move collateral from host to void 222 rev.SetMissedHostPayout(rev.MissedHostOutput().Value.Sub(collateral)) 223 voidOutput, err := rev.MissedVoidOutput() 224 if err != nil { 225 return types.FileContractRevision{}, errors.AddContext(err, "failed to get void output") 226 } 227 err = rev.SetMissedVoidPayout(voidOutput.Value.Add(collateral)) 228 if err != nil { 229 return types.FileContractRevision{}, errors.AddContext(err, "failed to set void output") 230 } 231 232 // set new filesize and Merkle root 233 rev.NewFileSize += modules.SectorSize 234 rev.NewFileMerkleRoot = merkleRoot 235 return rev, nil 236 } 237 238 // performSessionHandshake conducts the initial handshake exchange of the 239 // renter-host protocol. During the handshake, a shared secret is established, 240 // which is used to initialize an AEAD cipher. This cipher must be used to 241 // encrypt subsequent RPCs. 242 func performSessionHandshake(conn net.Conn, hostPublicKey types.SiaPublicKey) (cipher.AEAD, modules.LoopChallengeRequest, error) { 243 // generate a session key 244 xsk, xpk := crypto.GenerateX25519KeyPair() 245 246 // send our half of the key exchange 247 req := modules.LoopKeyExchangeRequest{ 248 PublicKey: xpk, 249 Ciphers: []types.Specifier{modules.CipherChaCha20Poly1305}, 250 } 251 extendDeadline(conn, modules.NegotiateSettingsTime) 252 if err := encoding.NewEncoder(conn).EncodeAll(modules.RPCLoopEnter, req); err != nil { 253 return nil, modules.LoopChallengeRequest{}, err 254 } 255 // read host's half of the key exchange 256 var resp modules.LoopKeyExchangeResponse 257 if err := encoding.NewDecoder(conn, encoding.DefaultAllocLimit).Decode(&resp); err != nil { 258 return nil, modules.LoopChallengeRequest{}, err 259 } 260 // validate the signature before doing anything else; don't want to punish 261 // the "host" if we're talking to an imposter 262 var hpk crypto.PublicKey 263 copy(hpk[:], hostPublicKey.Key) 264 var sig crypto.Signature 265 copy(sig[:], resp.Signature) 266 if err := crypto.VerifyHash(crypto.HashAll(req.PublicKey, resp.PublicKey), hpk, sig); err != nil { 267 return nil, modules.LoopChallengeRequest{}, err 268 } 269 // check for compatible cipher 270 if resp.Cipher != modules.CipherChaCha20Poly1305 { 271 return nil, modules.LoopChallengeRequest{}, errors.New("host selected unsupported cipher") 272 } 273 // derive shared secret, which we'll use as an encryption key 274 cipherKey := crypto.DeriveSharedSecret(xsk, resp.PublicKey) 275 276 // use cipherKey to initialize an AEAD cipher 277 aead, err := chacha20poly1305.New(cipherKey[:]) 278 if err != nil { 279 build.Critical("could not create cipher") 280 return nil, modules.LoopChallengeRequest{}, err 281 } 282 283 // read host's challenge 284 var challengeReq modules.LoopChallengeRequest 285 if err := modules.ReadRPCMessage(conn, aead, &challengeReq, modules.RPCMinLen); err != nil { 286 return nil, modules.LoopChallengeRequest{}, err 287 } 288 return aead, challengeReq, nil 289 }