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  }