| // Copyright 2014 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| // DTLS implementation. | |
| // | |
| // NOTE: This is a not even a remotely production-quality DTLS | |
| // implementation. It is the bare minimum necessary to be able to | |
| // achieve coverage on BoringSSL's implementation. Of note is that | |
| // this implementation assumes the underlying net.PacketConn is not | |
| // only reliable but also ordered. BoringSSL will be expected to deal | |
| // with simulated loss, but there is no point in forcing the test | |
| // driver to. | |
| package runner | |
| import ( | |
| "bytes" | |
| "cmp" | |
| "encoding/binary" | |
| "errors" | |
| "fmt" | |
| "math/rand" | |
| "net" | |
| "slices" | |
| "time" | |
| "golang.org/x/crypto/cryptobyte" | |
| ) | |
| // A DTLSMessage is a DTLS handshake message or ChangeCipherSpec, along with the | |
| // epoch that it is to be sent under. | |
| type DTLSMessage struct { | |
| Epoch uint16 | |
| IsChangeCipherSpec bool | |
| // The following fields are only used if IsChangeCipherSpec is false. | |
| Type uint8 | |
| Sequence uint16 | |
| Data []byte | |
| } | |
| // Fragment returns a DTLSFragment for the message with the specified offset and | |
| // length. | |
| func (m *DTLSMessage) Fragment(offset, length int) DTLSFragment { | |
| if m.IsChangeCipherSpec { | |
| // Ignore the offset. ChangeCipherSpec cannot be fragmented. | |
| return DTLSFragment{ | |
| Epoch: m.Epoch, | |
| IsChangeCipherSpec: m.IsChangeCipherSpec, | |
| Data: m.Data, | |
| } | |
| } | |
| return DTLSFragment{ | |
| Epoch: m.Epoch, | |
| Sequence: m.Sequence, | |
| Type: m.Type, | |
| Data: m.Data[offset : offset+length], | |
| Offset: offset, | |
| TotalLength: len(m.Data), | |
| } | |
| } | |
| // Split returns two fragments for the message. | |
| func (m *DTLSMessage) Split(offset int) (DTLSFragment, DTLSFragment) { | |
| if m.IsChangeCipherSpec { | |
| panic("tls: cannot split ChangeCipherSpec") | |
| } | |
| return m.Fragment(0, offset), m.Fragment(offset, len(m.Data)-offset) | |
| } | |
| // A DTLSFragment is a DTLS handshake fragment or ChangeCipherSpec, along with | |
| // the epoch that it is to be sent under. | |
| type DTLSFragment struct { | |
| Epoch uint16 | |
| IsChangeCipherSpec bool | |
| // The following fields are only used if IsChangeCipherSpec is false. | |
| Type uint8 | |
| TotalLength int | |
| Sequence uint16 | |
| Offset int | |
| Data []byte | |
| // ShouldDiscard, if true, indicates the shim is expected to discard this | |
| // fragment. A record with such a fragment must not be ACKed by the shim. | |
| ShouldDiscard bool | |
| } | |
| func (f *DTLSFragment) Bytes() []byte { | |
| if f.IsChangeCipherSpec { | |
| return f.Data | |
| } | |
| bb := cryptobyte.NewBuilder(make([]byte, 0, 12+len(f.Data))) | |
| bb.AddUint8(f.Type) | |
| bb.AddUint24(uint32(f.TotalLength)) | |
| bb.AddUint16(f.Sequence) | |
| bb.AddUint24(uint32(f.Offset)) | |
| addUint24LengthPrefixedBytes(bb, f.Data) | |
| return bb.BytesOrPanic() | |
| } | |
| func comparePair[T1 cmp.Ordered, T2 cmp.Ordered](a1 T1, a2 T2, b1 T1, b2 T2) int { | |
| cmp1 := cmp.Compare(a1, b1) | |
| if cmp1 != 0 { | |
| return cmp1 | |
| } | |
| return cmp.Compare(a2, b2) | |
| } | |
| type DTLSRecordNumber struct { | |
| // Store the Epoch as a uint64, so that tests can send ACKs for epochs that | |
| // the shim would never use. | |
| Epoch uint64 | |
| Sequence uint64 | |
| } | |
| // A DTLSRecordNumberInfo contains information about a record received from the | |
| // shim, which we may attempt to ACK. | |
| type DTLSRecordNumberInfo struct { | |
| DTLSRecordNumber | |
| // The first byte covered by this record, inclusive. We only need to store | |
| // one range because we require that the shim arrange fragments in order. | |
| // Any gaps will have been previously-ACKed data, so there is no harm in | |
| // double-ACKing. | |
| MessageStartSequence uint16 | |
| MessageStartOffset int | |
| // The last byte covered by this record, exclusive. | |
| MessageEndSequence uint16 | |
| MessageEndOffset int | |
| } | |
| func (r *DTLSRecordNumberInfo) HasACKInformation() bool { | |
| return comparePair(r.MessageStartSequence, r.MessageStartOffset, r.MessageEndSequence, r.MessageEndOffset) < 0 | |
| } | |
| func (c *Conn) readDTLS13RecordHeader(epoch *epochState, b []byte) (headerLen int, recordLen int, recTyp recordType, err error) { | |
| // The DTLS 1.3 record header starts with the type byte containing | |
| // 0b001CSLEE, where C, S, L, and EE are bits with the following | |
| // meanings: | |
| // | |
| // C=1: Connection ID is present (C=0: CID is absent) | |
| // S=1: the sequence number is 16 bits (S=0: it is 8 bits) | |
| // L=1: 16-bit length field is present (L=0: record goes to end of packet) | |
| // EE: low two bits of the epoch. | |
| // | |
| // A real DTLS implementation would parse these bits and take | |
| // appropriate action based on them. However, this is a test | |
| // implementation, and the code we are testing only ever sends C=0, S=1, | |
| // L=1. This code expects those bits to be set and will error if | |
| // anything else is set. This means we expect the type byte to look like | |
| // 0b001011EE, or 0x2c-0x2f. | |
| recordHeaderLen := 5 | |
| if len(b) < recordHeaderLen { | |
| return 0, 0, 0, errors.New("dtls: failed to read record header") | |
| } | |
| typ := b[0] | |
| if typ&0xfc != 0x2c { | |
| return 0, 0, 0, errors.New("dtls: DTLS 1.3 record header has bad type byte") | |
| } | |
| // For test purposes, require the epoch received be the same as the | |
| // epoch we expect to receive. | |
| epochBits := typ & 0x03 | |
| if epochBits != byte(epoch.epoch&0x03) { | |
| c.sendAlert(alertIllegalParameter) | |
| return 0, 0, 0, c.in.setErrorLocked(fmt.Errorf("dtls: bad epoch")) | |
| } | |
| wireSeq := b[1:3] | |
| if !c.config.Bugs.NullAllCiphers { | |
| sample := b[recordHeaderLen:] | |
| mask := epoch.recordNumberEncrypter.generateMask(sample) | |
| xorSlice(wireSeq, mask) | |
| } | |
| decWireSeq := binary.BigEndian.Uint16(wireSeq) | |
| // Reconstruct the sequence number from the low 16 bits on the wire. | |
| // A real implementation would compute the full sequence number that is | |
| // closest to the highest successfully decrypted record in the | |
| // identified epoch. Since this test implementation errors on decryption | |
| // failures instead of simply discarding packets, it reconstructs a | |
| // sequence number that is not less than c.in.seq. (This matches the | |
| // behavior of the check of the sequence number in the old record | |
| // header format.) | |
| seqInt := binary.BigEndian.Uint64(epoch.seq[:]) | |
| // epoch.seq has the epoch in the upper two bytes - clear those. | |
| seqInt = seqInt &^ (0xffff << 48) | |
| newSeq := seqInt&^0xffff | uint64(decWireSeq) | |
| if newSeq < seqInt { | |
| newSeq += 0x10000 | |
| } | |
| seq := make([]byte, 8) | |
| binary.BigEndian.PutUint64(seq, newSeq) | |
| copy(epoch.seq[2:], seq[2:]) | |
| recordLen = int(b[3])<<8 | int(b[4]) | |
| return recordHeaderLen, recordLen, 0, nil | |
| } | |
| // readDTLSRecordHeader reads the record header from the input. Based on the | |
| // header it reads, it checks the header's validity and sets appropriate state | |
| // as needed. This function returns the record header and the record type | |
| // indicated in the header (if it contains the type). The connection's internal | |
| // sequence number is updated to the value from the header. | |
| func (c *Conn) readDTLSRecordHeader(epoch *epochState, b []byte) (headerLen int, recordLen int, typ recordType, err error) { | |
| if epoch.cipher != nil && c.in.version.protocolVersion() >= VersionTLS13 { | |
| return c.readDTLS13RecordHeader(epoch, b) | |
| } | |
| recordHeaderLen := 13 | |
| // Read out one record. | |
| // | |
| // A real DTLS implementation should be tolerant of errors, | |
| // but this is test code. We should not be tolerant of our | |
| // peer sending garbage. | |
| if len(b) < recordHeaderLen { | |
| return 0, 0, 0, errors.New("dtls: failed to read record header") | |
| } | |
| typ = recordType(b[0]) | |
| vers := uint16(b[1])<<8 | uint16(b[2]) | |
| // Alerts sent near version negotiation do not have a well-defined | |
| // record-layer version prior to TLS 1.3. (In TLS 1.3, the record-layer | |
| // version is irrelevant.) Additionally, if we're reading a retransmission, | |
| // the peer may not know the version yet. | |
| if typ != recordTypeAlert && !c.skipRecordVersionCheck { | |
| if c.haveVers { | |
| wireVersion := c.vers.wire | |
| if c.vers.protocolVersion() >= VersionTLS13 { | |
| wireVersion = VersionDTLS12 | |
| } | |
| if vers != wireVersion { | |
| c.sendAlert(alertProtocolVersion) | |
| return 0, 0, 0, c.in.setErrorLocked(fmt.Errorf("dtls: received record with version %x when expecting version %x", vers, c.vers.wire)) | |
| } | |
| } else { | |
| if expect := c.config.Bugs.ExpectInitialRecordVersion; expect != 0 && vers != expect { | |
| c.sendAlert(alertProtocolVersion) | |
| return 0, 0, 0, c.in.setErrorLocked(fmt.Errorf("dtls: received record with version %x when expecting version %x", vers, expect)) | |
| } | |
| } | |
| } | |
| epochValue := binary.BigEndian.Uint16(b[3:5]) | |
| seq := b[5:11] | |
| // For test purposes, require the sequence number be monotonically | |
| // increasing, so c.in includes the minimum next sequence number. Gaps | |
| // may occur if packets failed to be sent out. A real implementation | |
| // would maintain a replay window and such. | |
| if epochValue != epoch.epoch { | |
| c.sendAlert(alertIllegalParameter) | |
| return 0, 0, 0, c.in.setErrorLocked(fmt.Errorf("dtls: bad epoch")) | |
| } | |
| if bytes.Compare(seq, epoch.seq[2:]) < 0 { | |
| c.sendAlert(alertIllegalParameter) | |
| return 0, 0, 0, c.in.setErrorLocked(fmt.Errorf("dtls: bad sequence number")) | |
| } | |
| copy(epoch.seq[2:], seq) | |
| recordLen = int(b[11])<<8 | int(b[12]) | |
| return recordHeaderLen, recordLen, typ, nil | |
| } | |
| func (c *Conn) dtlsDoReadRecord(epoch *epochState, want recordType) (recordType, []byte, error) { | |
| // Read a new packet only if the current one is empty. | |
| var newPacket bool | |
| bytesAvailableInLastPacket := c.bytesAvailableInPacket | |
| if c.rawInput.Len() == 0 { | |
| // Pick some absurdly large buffer size. | |
| c.rawInput.Grow(maxCiphertext + dtlsMaxRecordHeaderLen) | |
| buf := c.rawInput.AvailableBuffer() | |
| n, err := c.conn.Read(buf[:cap(buf)]) | |
| if err != nil { | |
| return 0, nil, err | |
| } | |
| if c.maxPacketLen != 0 { | |
| if n > c.maxPacketLen { | |
| return 0, nil, fmt.Errorf("dtls: exceeded maximum packet length: got %d, want <=%d", n, c.maxPacketLen) | |
| } | |
| c.bytesAvailableInPacket = c.maxPacketLen - n | |
| } else { | |
| c.bytesAvailableInPacket = 0 | |
| } | |
| c.rawInput.Write(buf[:n]) | |
| newPacket = true | |
| } | |
| // Consume the next record from the buffer. | |
| recordHeaderLen, n, typ, err := c.readDTLSRecordHeader(epoch, c.rawInput.Bytes()) | |
| if err != nil { | |
| return 0, nil, err | |
| } | |
| if n > maxCiphertext || c.rawInput.Len() < recordHeaderLen+n { | |
| c.sendAlert(alertRecordOverflow) | |
| return 0, nil, c.in.setErrorLocked(fmt.Errorf("dtls: oversized record received with length: got %d, want <=%d", n, maxCiphertext)) | |
| } | |
| b := c.rawInput.Next(recordHeaderLen + n) | |
| // Process message. | |
| ok, encTyp, data, alertValue := c.in.decrypt(epoch, recordHeaderLen, b) | |
| if !ok { | |
| // A real DTLS implementation would silently ignore bad records, | |
| // but we want to notice errors from the implementation under | |
| // test. | |
| return 0, nil, c.in.setErrorLocked(c.sendAlert(alertValue)) | |
| } | |
| if typ == 0 { | |
| // readDTLSRecordHeader sets typ=0 when decoding the DTLS 1.3 | |
| // record header. When the new record header format is used, the | |
| // type is returned by decrypt() in encTyp. | |
| typ = encTyp | |
| } | |
| if typ == recordTypeChangeCipherSpec || typ == recordTypeHandshake { | |
| // If this is not the first record in the flight, check if it was packed | |
| // efficiently. | |
| if c.lastRecordInFlight != nil { | |
| // 12-byte header + 1-byte fragment is the minimum to make progress. | |
| const handshakeBytesNeeded = 13 | |
| if typ == recordTypeHandshake && c.lastRecordInFlight.typ == recordTypeHandshake && epoch.epoch == c.lastRecordInFlight.epoch { | |
| // The previous record was compatible with this one. The shim | |
| // should have fit more in this record before making a new one. | |
| // TODO(crbug.com/374991962): Enforce this for plaintext records | |
| // too. | |
| if c.lastRecordInFlight.bytesAvailable > handshakeBytesNeeded && epoch.epoch > 0 { | |
| return 0, nil, c.in.setErrorLocked(fmt.Errorf("dtls: previous handshake record had %d bytes available, but shim did not fit another fragment in it", c.lastRecordInFlight.bytesAvailable)) | |
| } | |
| } else if newPacket { | |
| // The shim had to make a new record, but it did not need to | |
| // make a new packet if this record fit in the previous. | |
| bytesNeeded := 1 | |
| if typ == recordTypeHandshake { | |
| bytesNeeded = handshakeBytesNeeded | |
| } | |
| bytesNeeded += recordHeaderLen + c.in.maxEncryptOverhead(epoch, bytesNeeded) | |
| if bytesNeeded < bytesAvailableInLastPacket { | |
| return 0, nil, c.in.setErrorLocked(fmt.Errorf("dtls: previous packet had %d bytes available, but shim did not fit record of type %d into it", bytesAvailableInLastPacket, typ)) | |
| } | |
| } | |
| } | |
| // Save information about the current record, including how many more | |
| // bytes the shim could have added. | |
| recordBytesAvailable := c.bytesAvailableInPacket + c.rawInput.Len() | |
| if cbc, ok := epoch.cipher.(*cbcMode); ok { | |
| // It is possible that adding a byte would have added another block. | |
| recordBytesAvailable = max(0, recordBytesAvailable-cbc.BlockSize()) | |
| } | |
| c.lastRecordInFlight = &dtlsRecordInfo{typ: typ, epoch: epoch.epoch, bytesAvailable: recordBytesAvailable} | |
| } else { | |
| c.lastRecordInFlight = nil | |
| } | |
| return typ, data, nil | |
| } | |
| func (c *Conn) dtlsWriteRecord(typ recordType, data []byte) (n int, err error) { | |
| epoch := &c.out.epoch | |
| // Outgoing DTLS records are buffered in several stages, to test the various | |
| // layers that data may be combined in. | |
| // | |
| // First, handshake and ChangeCipherSpec records are buffered in | |
| // c.nextFlight, to be flushed by dtlsWriteFlight. | |
| // | |
| // dtlsWriteFlight, with the aid of a test-supplied callback, will divide | |
| // them into handshake records containing fragments, possibly with some | |
| // rounds of shim retransmit tests. Those records and any other | |
| // non-handshake application data records are encrypted by dtlsPackRecord | |
| // into c.pendingPacket, which may combine multiple records into one packet. | |
| // | |
| // Finally, dtlsFlushPacket writes the packet to the shim. | |
| if typ == recordTypeChangeCipherSpec { | |
| // Don't send ChangeCipherSpec in DTLS 1.3. | |
| // TODO(crbug.com/383078468): Add an option to send them anyway and test | |
| // what our implementation does with unexpected ones. | |
| if c.vers.protocolVersion() >= VersionTLS13 { | |
| return | |
| } | |
| c.nextFlight = append(c.nextFlight, DTLSMessage{ | |
| Epoch: epoch.epoch, | |
| IsChangeCipherSpec: true, | |
| Data: slices.Clone(data), | |
| }) | |
| err = c.out.changeCipherSpec() | |
| if err != nil { | |
| return n, c.sendAlertLocked(alertLevelError, err.(alert)) | |
| } | |
| return len(data), nil | |
| } | |
| if typ == recordTypeHandshake { | |
| // Handshake messages have to be modified to include fragment | |
| // offset and length and with the header replicated. Save the | |
| // TLS header here. | |
| header := data[:4] | |
| body := data[4:] | |
| c.nextFlight = append(c.nextFlight, DTLSMessage{ | |
| Epoch: epoch.epoch, | |
| Sequence: c.sendHandshakeSeq, | |
| Type: header[0], | |
| Data: slices.Clone(body), | |
| }) | |
| c.sendHandshakeSeq++ | |
| return len(data), nil | |
| } | |
| // Flush any packets buffered from the handshake. | |
| err = c.dtlsWriteFlight() | |
| if err != nil { | |
| return | |
| } | |
| if typ == recordTypeApplicationData && len(data) > 1 && c.config.Bugs.SplitAndPackAppData { | |
| _, _, err = c.dtlsPackRecord(epoch, typ, data[:len(data)/2], false) | |
| if err != nil { | |
| return | |
| } | |
| _, _, err = c.dtlsPackRecord(epoch, typ, data[len(data)/2:], true) | |
| if err != nil { | |
| return | |
| } | |
| n = len(data) | |
| } else { | |
| n, _, err = c.dtlsPackRecord(epoch, typ, data, false) | |
| if err != nil { | |
| return | |
| } | |
| } | |
| err = c.dtlsFlushPacket() | |
| return | |
| } | |
| // dtlsWriteFlight packs the pending handshake flight into the pending record. | |
| // Callers should follow up with dtlsFlushPacket to write the packets. | |
| func (c *Conn) dtlsWriteFlight() error { | |
| if len(c.nextFlight) == 0 { | |
| return nil | |
| } | |
| // Avoid re-entrancy issues by updating the state immediately. The callback | |
| // may try to write records. | |
| prev, received, next, records := c.previousFlight, c.receivedFlight, c.nextFlight, c.receivedFlightRecords | |
| c.previousFlight, c.receivedFlight, c.nextFlight, c.receivedFlightRecords = next, nil, nil, nil | |
| controller := newDTLSController(c, received) | |
| if c.config.Bugs.WriteFlightDTLS != nil { | |
| c.config.Bugs.WriteFlightDTLS(&controller, prev, received, next, records) | |
| } else { | |
| controller.WriteFlight(next) | |
| } | |
| if err := controller.Err(); err != nil { | |
| return err | |
| } | |
| if c.receivedFlight != nil || c.receivedFlightRecords != nil || c.nextFlight != nil { | |
| panic("tls: flight state changed while writing flight") | |
| } | |
| if controller.mergeIntoNextFlight { | |
| c.previousFlight, c.receivedFlight, c.nextFlight, c.receivedFlightRecords = prev, received, next, records | |
| } | |
| // Flush any ACKs, etc., we may have written. | |
| return c.dtlsFlushPacket() | |
| } | |
| func (c *Conn) dtlsFlushHandshake() error { | |
| if err := c.dtlsWriteFlight(); err != nil { | |
| return err | |
| } | |
| if err := c.dtlsFlushPacket(); err != nil { | |
| return err | |
| } | |
| return nil | |
| } | |
| func (c *Conn) dtlsACKHandshake() error { | |
| if len(c.receivedFlight) == 0 { | |
| return nil | |
| } | |
| if len(c.nextFlight) != 0 { | |
| panic("tls: not a final flight; more messages were queued up") | |
| } | |
| // Avoid re-entrancy issues by updating the state immediately. The callback | |
| // may try to write records. | |
| prev, received, records := c.previousFlight, c.receivedFlight, c.receivedFlightRecords | |
| c.previousFlight, c.receivedFlight, c.receivedFlightRecords = nil, nil, nil | |
| controller := newDTLSController(c, received) | |
| if c.config.Bugs.ACKFlightDTLS != nil { | |
| c.config.Bugs.ACKFlightDTLS(&controller, prev, received, records) | |
| } else { | |
| if c.vers.protocolVersion() >= VersionTLS13 { | |
| controller.WriteACK(controller.OutEpoch(), records) | |
| } | |
| } | |
| if err := controller.Err(); err != nil { | |
| return err | |
| } | |
| if c.previousFlight != nil || c.receivedFlight != nil || c.receivedFlightRecords != nil { | |
| panic("tls: flight state changed while ACKing flight") | |
| } | |
| if controller.mergeIntoNextFlight { | |
| c.previousFlight, c.receivedFlight, c.receivedFlightRecords = prev, received, records | |
| } | |
| // Flush any ACKs, etc., we may have written. | |
| return c.dtlsFlushPacket() | |
| } | |
| // appendDTLS13RecordHeader appends to b the record header for a record of length | |
| // recordLen. | |
| func (c *Conn) appendDTLS13RecordHeader(b, seq []byte, recordLen int) []byte { | |
| // Set the top 3 bits on the type byte to indicate the DTLS 1.3 record | |
| // header format. | |
| typ := byte(0x20) | |
| // Set the Connection ID bit | |
| if c.config.Bugs.DTLS13RecordHeaderSetCIDBit && c.handshakeComplete { | |
| typ |= 0x10 | |
| } | |
| // Set the sequence number length bit | |
| if !c.config.DTLSUseShortSeqNums { | |
| typ |= 0x08 | |
| } | |
| // Set the length presence bit | |
| if !c.config.DTLSRecordHeaderOmitLength { | |
| typ |= 0x04 | |
| } | |
| // Set the epoch bits | |
| typ |= seq[1] & 0x3 | |
| b = append(b, typ) | |
| if c.config.DTLSUseShortSeqNums { | |
| b = append(b, seq[7]) | |
| } else { | |
| b = append(b, seq[6], seq[7]) | |
| } | |
| if !c.config.DTLSRecordHeaderOmitLength { | |
| b = append(b, byte(recordLen>>8), byte(recordLen)) | |
| } | |
| return b | |
| } | |
| // dtlsPackRecord packs a single record to the pending packet, flushing it | |
| // if necessary. The caller should call dtlsFlushPacket to flush the current | |
| // pending packet afterwards. | |
| func (c *Conn) dtlsPackRecord(epoch *epochState, typ recordType, data []byte, mustPack bool) (n int, num DTLSRecordNumber, err error) { | |
| maxLen := c.config.Bugs.MaxHandshakeRecordLength | |
| if maxLen <= 0 { | |
| maxLen = 1024 | |
| } | |
| vers := c.vers.wire | |
| if vers == 0 { | |
| // Some TLS servers fail if the record version is greater than | |
| // TLS 1.0 for the initial ClientHello. | |
| vers = VersionDTLS10 | |
| } | |
| if c.vers.protocolVersion() >= VersionTLS13 || c.out.version.protocolVersion() >= VersionTLS13 { | |
| vers = VersionDTLS12 | |
| } | |
| useDTLS13RecordHeader := c.out.version.protocolVersion() >= VersionTLS13 && epoch.cipher != nil && !c.useDTLSPlaintextHeader() | |
| headerHasLength := true | |
| record := make([]byte, 0, dtlsMaxRecordHeaderLen+len(data)+c.out.maxEncryptOverhead(epoch, len(data))) | |
| seq := c.out.sequenceNumberForOutput(epoch) | |
| if useDTLS13RecordHeader { | |
| record = c.appendDTLS13RecordHeader(record, seq, len(data)) | |
| headerHasLength = !c.config.DTLSRecordHeaderOmitLength | |
| } else { | |
| record = append(record, byte(typ)) | |
| record = append(record, byte(vers>>8)) | |
| record = append(record, byte(vers)) | |
| // DTLS records include an explicit sequence number. | |
| record = append(record, seq...) | |
| record = append(record, byte(len(data)>>8)) | |
| record = append(record, byte(len(data))) | |
| } | |
| recordHeaderLen := len(record) | |
| record, err = c.out.encrypt(epoch, record, data, typ, recordHeaderLen, headerHasLength) | |
| if err != nil { | |
| return | |
| } | |
| num = c.out.lastRecordNumber(epoch, true /* isOut */) | |
| // Encrypt the sequence number. | |
| if useDTLS13RecordHeader && !c.config.Bugs.NullAllCiphers { | |
| sample := record[recordHeaderLen:] | |
| mask := epoch.recordNumberEncrypter.generateMask(sample) | |
| seqLen := 2 | |
| if c.config.DTLSUseShortSeqNums { | |
| seqLen = 1 | |
| } | |
| // The sequence number starts at index 1 in the record header. | |
| xorSlice(record[1:1+seqLen], mask) | |
| } | |
| // Flush the current pending packet if necessary. | |
| if !mustPack && len(record)+len(c.pendingPacket) > c.config.Bugs.PackHandshakeRecords { | |
| err = c.dtlsFlushPacket() | |
| if err != nil { | |
| return | |
| } | |
| } | |
| // Add the record to the pending packet. | |
| c.pendingPacket = append(c.pendingPacket, record...) | |
| if c.config.DTLSRecordHeaderOmitLength { | |
| if c.config.Bugs.SplitAndPackAppData { | |
| panic("incompatible config") | |
| } | |
| err = c.dtlsFlushPacket() | |
| if err != nil { | |
| return | |
| } | |
| } | |
| n = len(data) | |
| return | |
| } | |
| func (c *Conn) dtlsFlushPacket() error { | |
| if c.hand.Len() == 0 { | |
| c.lastRecordInFlight = nil | |
| } | |
| if len(c.pendingPacket) == 0 { | |
| return nil | |
| } | |
| _, err := c.conn.Write(c.pendingPacket) | |
| c.pendingPacket = nil | |
| return err | |
| } | |
| func readDTLSFragment(s *cryptobyte.String) (DTLSFragment, error) { | |
| var f DTLSFragment | |
| var totLen, fragOffset uint32 | |
| if !s.ReadUint8(&f.Type) || | |
| !s.ReadUint24(&totLen) || | |
| !s.ReadUint16(&f.Sequence) || | |
| !s.ReadUint24(&fragOffset) || | |
| !readUint24LengthPrefixedBytes(s, &f.Data) { | |
| return DTLSFragment{}, errors.New("dtls: bad handshake record") | |
| } | |
| f.TotalLength = int(totLen) | |
| f.Offset = int(fragOffset) | |
| if f.Offset > f.TotalLength || len(f.Data) > f.TotalLength-f.Offset { | |
| return DTLSFragment{}, errors.New("dtls: bad fragment offset") | |
| } | |
| // Although syntactically valid, the shim should never send empty fragments | |
| // of non-empty messages. | |
| if len(f.Data) == 0 && f.TotalLength != 0 { | |
| return DTLSFragment{}, errors.New("dtls: fragment makes no progress") | |
| } | |
| return f, nil | |
| } | |
| func (c *Conn) makeDTLSRecordNumberInfo(epoch *epochState, data []byte) (DTLSRecordNumberInfo, error) { | |
| info := DTLSRecordNumberInfo{DTLSRecordNumber: c.in.lastRecordNumber(epoch, false /* isOut */)} | |
| s := cryptobyte.String(data) | |
| first := true | |
| for !s.Empty() { | |
| f, err := readDTLSFragment(&s) | |
| if err != nil { | |
| return DTLSRecordNumberInfo{}, err | |
| } | |
| // This assumes the shim sent fragments in order. This isn't checked | |
| // here, but the caller will check when processing the fragments. | |
| if first { | |
| info.MessageStartSequence = f.Sequence | |
| info.MessageStartOffset = f.Offset | |
| first = false | |
| } | |
| info.MessageEndSequence = f.Sequence | |
| info.MessageEndOffset = f.Offset + len(f.Data) | |
| } | |
| return info, nil | |
| } | |
| func (c *Conn) dtlsDoReadHandshake() ([]byte, error) { | |
| // Assemble a full handshake message. For test purposes, this | |
| // implementation assumes fragments arrive in order. It may | |
| // need to be cleverer if we ever test BoringSSL's retransmit | |
| // behavior. | |
| for len(c.handMsg) < 4+c.handMsgLen { | |
| // Get a new handshake record if the previous has been | |
| // exhausted. | |
| if c.hand.Len() == 0 { | |
| if err := c.in.err; err != nil { | |
| return nil, err | |
| } | |
| if err := c.readRecord(recordTypeHandshake); err != nil { | |
| return nil, err | |
| } | |
| } | |
| // Read the next fragment. It must fit entirely within | |
| // the record. | |
| s := cryptobyte.String(c.hand.Bytes()) | |
| f, err := readDTLSFragment(&s) | |
| if err != nil { | |
| return nil, err | |
| } | |
| c.hand.Next(c.hand.Len() - len(s)) | |
| // Check it's a fragment for the right message. | |
| if f.Sequence != c.recvHandshakeSeq { | |
| return nil, errors.New("dtls: bad handshake sequence number") | |
| } | |
| // Check that the length is consistent. | |
| if c.handMsg == nil { | |
| c.handMsgLen = f.TotalLength | |
| if c.handMsgLen > maxHandshake { | |
| return nil, c.in.setErrorLocked(c.sendAlert(alertInternalError)) | |
| } | |
| // Start with the TLS handshake header, | |
| // without the DTLS bits. | |
| c.handMsg = []byte{f.Type, byte(f.TotalLength >> 16), byte(f.TotalLength >> 8), byte(f.TotalLength)} | |
| } else if f.TotalLength != c.handMsgLen { | |
| return nil, errors.New("dtls: bad handshake length") | |
| } | |
| // Add the fragment to the pending message. | |
| if 4+f.Offset != len(c.handMsg) { | |
| return nil, errors.New("dtls: bad fragment offset") | |
| } | |
| // If the message isn't complete, check that the peer could not have | |
| // fit more into the record. | |
| c.handMsg = append(c.handMsg, f.Data...) | |
| if len(c.handMsg) < 4+c.handMsgLen { | |
| if c.hand.Len() != 0 { | |
| return nil, errors.New("dtls: truncated handshake fragment was not last in the record") | |
| } | |
| if c.lastRecordInFlight.bytesAvailable > 0 { | |
| return nil, fmt.Errorf("dtls: handshake fragment was truncated, but record could have fit %d more bytes", c.lastRecordInFlight.bytesAvailable) | |
| } | |
| } | |
| // Sending part of the next flight implicitly ACKs the previous flight. | |
| // Having triggered this, the shim is expected to clear its ACK buffer. | |
| c.expectedACK = nil | |
| } | |
| c.recvHandshakeSeq++ | |
| ret := c.handMsg | |
| c.handMsg, c.handMsgLen = nil, 0 | |
| c.receivedFlight = append(c.receivedFlight, DTLSMessage{ | |
| Epoch: c.in.epoch.epoch, | |
| Type: ret[0], | |
| Sequence: c.recvHandshakeSeq - 1, | |
| Data: ret[4:], | |
| }) | |
| return ret, nil | |
| } | |
| func (c *Conn) checkACK(data []byte) error { | |
| s := cryptobyte.String(data) | |
| var child cryptobyte.String | |
| if !s.ReadUint16LengthPrefixed(&child) || !s.Empty() { | |
| return fmt.Errorf("tls: could not parse ACK record") | |
| } | |
| var acks []DTLSRecordNumber | |
| for !child.Empty() { | |
| var num DTLSRecordNumber | |
| if !child.ReadUint64(&num.Epoch) || !child.ReadUint64(&num.Sequence) { | |
| return fmt.Errorf("tls: could not parse ACK record") | |
| } | |
| acks = append(acks, num) | |
| } | |
| // Determine the expected ACKs, if any. | |
| expected := c.expectedACK | |
| if len(expected) > shimConfig.MaxACKBuffer { | |
| expected = expected[len(expected)-shimConfig.MaxACKBuffer:] | |
| } | |
| // If we've configured a tighter MTU, the shim might have needed to truncate | |
| // the list. Tolerate this as long as the shim sent the more recent records | |
| // and still sent a plausible minimum number of ACKs. | |
| if c.maxPacketLen != 0 && len(acks) > 10 && len(acks) < len(expected) { | |
| expected = expected[len(expected)-len(acks):] | |
| } | |
| // The shim is expected to sort the record numbers in the ACK. | |
| expected = slices.Clone(expected) | |
| slices.SortFunc(expected, func(a, b DTLSRecordNumber) int { | |
| cmp1 := cmp.Compare(a.Epoch, b.Epoch) | |
| if cmp1 != 0 { | |
| return cmp1 | |
| } | |
| return cmp.Compare(a.Sequence, b.Sequence) | |
| }) | |
| if !slices.Equal(acks, expected) { | |
| return fmt.Errorf("tls: got ACKs %+v, but expected %+v", acks, expected) | |
| } | |
| return nil | |
| } | |
| // DTLSServer returns a new DTLS server side connection | |
| // using conn as the underlying transport. | |
| // The configuration config must be non-nil and must have | |
| // at least one certificate. | |
| func DTLSServer(conn net.Conn, config *Config) *Conn { | |
| c := &Conn{config: config, isDTLS: true, conn: conn} | |
| c.init() | |
| return c | |
| } | |
| // DTLSClient returns a new DTLS client side connection | |
| // using conn as the underlying transport. | |
| // The config cannot be nil: users must set either ServerHostname or | |
| // InsecureSkipVerify in the config. | |
| func DTLSClient(conn net.Conn, config *Config) *Conn { | |
| c := &Conn{config: config, isClient: true, isDTLS: true, conn: conn} | |
| c.init() | |
| return c | |
| } | |
| type WriteFlightFunc = func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) | |
| type ACKFlightFunc = func(c *DTLSController, prev, received []DTLSMessage, records []DTLSRecordNumberInfo) | |
| // A DTLSController is passed to a test callback and allows the callback to | |
| // customize how an individual flight is sent. This is used to test DTLS's | |
| // retransmission logic. | |
| // | |
| // Although DTLS runs over a lossy, reordered channel, runner assumes a | |
| // reliable, ordered channel. When simulating packet loss, runner processes the | |
| // shim's "lost" flight as usual. But, instead of responding, it calls a | |
| // test-provided function type WriteFlightFunc. | |
| // | |
| // WriteFlight will be called next as the flight for the runner to send. prev is | |
| // the previous flight sent by the runner, and received is the most recent | |
| // flight received by the shim. prev and received may be nil if those flights do | |
| // not exist. | |
| // | |
| // WriteFlight should send next to the shim, by calling methods on the | |
| // DTLSController, and then return. The shim will then be expected to progress | |
| // the connection. However, WriteFlight, may send fragments arbitrarily | |
| // reordered or duplicated. It may also simulate packet loss with timeouts or | |
| // retransmitted past fragments, and then test that the shim retransmits. | |
| // | |
| // WriteFlight must return as soon as the shim is expected to progress the | |
| // connection. If WriteFlight expects the shim to send an alert, it must also | |
| // return, at which point the logic to progress the connection will consume the | |
| // alert and report it as a connection failure, to be captured in the test | |
| // expectation. | |
| // | |
| // If unspecified, the default implementation of WriteFlight is: | |
| // | |
| // func WriteFlight(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) { | |
| // c.WriteFlight(next) | |
| // } | |
| // | |
| // When the shim speaks last in a handshake or post-handshake transaction, there | |
| // is no reply to implicitly acknowledge the flight. The runner will instead | |
| // call a second callback type ACKFlightFunc. | |
| // | |
| // Like WriteFlight, ACKFlight may simulate packet loss with the DTLSController. | |
| // It returns when it is ready to proceed. If not specified, it does nothing in | |
| // DTLS 1.2 and ACKs the final flight in DTLS 1.3. | |
| // | |
| // This test design implicitly assumes the shim will never start a | |
| // post-handshake transaction before the previous one is complete. Otherwise the | |
| // retransmissions will get mixed up with the second transaction. | |
| // | |
| // For convenience, the DTLSController internally tracks whether it has | |
| // encountered an error (e.g. an I/O error with the shim) and, if so, silently | |
| // makes all methods do nothing. The Err method may be used to query if it is in | |
| // this state, if it would otherwise cause an infinite loop. | |
| type DTLSController struct { | |
| conn *Conn | |
| err error | |
| // retransmitNeeded contains the list of fragments which the shim must | |
| // retransmit. | |
| retransmitNeeded []DTLSFragment | |
| mergeIntoNextFlight bool | |
| } | |
| func newDTLSController(conn *Conn, received []DTLSMessage) DTLSController { | |
| var retransmitNeeded []DTLSFragment | |
| for i := range received { | |
| msg := &received[i] | |
| retransmitNeeded = append(retransmitNeeded, msg.Fragment(0, len(msg.Data))) | |
| } | |
| return DTLSController{conn: conn, retransmitNeeded: retransmitNeeded} | |
| } | |
| func (c *DTLSController) getOutEpochOrPanic(epochValue uint16) *epochState { | |
| epoch, ok := c.conn.out.getEpoch(epochValue) | |
| if !ok { | |
| panic(fmt.Sprintf("tls: could not find epoch %d", epochValue)) | |
| } | |
| return epoch | |
| } | |
| func (c *DTLSController) getInEpochOrPanic(epochValue uint16) *epochState { | |
| epoch, ok := c.conn.in.getEpoch(epochValue) | |
| if !ok { | |
| panic(fmt.Sprintf("tls: could not find epoch %d", epochValue)) | |
| } | |
| return epoch | |
| } | |
| // Err returns whether the controller has stopped due to an error, or nil | |
| // otherwise. If it returns non-nil, other methods will silently do nothing. | |
| func (c *DTLSController) Err() error { return c.err } | |
| // OutEpoch returns the current outgoing epoch. | |
| func (c *DTLSController) OutEpoch() uint16 { | |
| return c.conn.out.epoch.epoch | |
| } | |
| // InEpoch returns the current incoming epoch. | |
| func (c *DTLSController) InEpoch() uint16 { | |
| return c.conn.in.epoch.epoch | |
| } | |
| // flush flushes the current outgoing packet. Packets are buffered so tests can | |
| // exercise cases when one flight is packed with the next flight, but any | |
| // non-write DTLSController methods must implicitly flush so they are testing | |
| // against a shim in the expected state. | |
| func (c *DTLSController) flush() error { | |
| if c.err != nil { | |
| return c.err | |
| } | |
| if err := c.conn.dtlsFlushPacket(); err != nil { | |
| c.err = err | |
| return err | |
| } | |
| return nil | |
| } | |
| // AdvanceClock advances the shim's clock by duration. It is a test failure if | |
| // the shim sends anything before picking up the command. | |
| func (c *DTLSController) AdvanceClock(duration time.Duration) { | |
| if c.flush() != nil { | |
| return | |
| } | |
| adaptor := c.conn.config.Bugs.PacketAdaptor | |
| if adaptor == nil { | |
| panic("tls: no PacketAdapter set") | |
| } | |
| received, err := adaptor.SendReadTimeout(duration) | |
| if err != nil { | |
| c.err = err | |
| } else if len(received) != 0 { | |
| c.err = fmt.Errorf("tls: received %d unexpected packets while simulating a timeout", len(received)) | |
| } | |
| } | |
| // SetMTU updates the shim's MTU to mtu. | |
| func (c *DTLSController) SetMTU(mtu int) { | |
| if c.flush() != nil { | |
| return | |
| } | |
| adaptor := c.conn.config.Bugs.PacketAdaptor | |
| if adaptor == nil { | |
| panic("tls: no PacketAdapter set") | |
| } | |
| c.conn.maxPacketLen = mtu | |
| c.err = adaptor.SetPeerMTU(mtu) | |
| } | |
| // SetTimeout updates the shim's timeout to mtu. | |
| func (c *DTLSController) SetTimeout(d time.Duration) { | |
| if c.flush() != nil { | |
| return | |
| } | |
| adaptor := c.conn.config.Bugs.PacketAdaptor | |
| if adaptor == nil { | |
| panic("tls: no PacketAdapter set") | |
| } | |
| c.err = adaptor.SetPeerTimeout(d) | |
| } | |
| // WriteFlight writes msgs to the shim, using the default fragmenting logic. | |
| // This may be used when the test is not concerned with fragmentation. | |
| func (c *DTLSController) WriteFlight(msgs []DTLSMessage) { | |
| config := c.conn.config | |
| if c.err != nil { | |
| return | |
| } | |
| // Buffer up fragments to reorder them. | |
| var fragments []DTLSFragment | |
| // TODO(davidben): All this could also have been implemented in the custom | |
| // fallbacks. These options date to before we had the callback. Should some | |
| // of them be moved out? | |
| for _, msg := range msgs { | |
| if msg.IsChangeCipherSpec { | |
| fragments = append(fragments, msg.Fragment(0, len(msg.Data))) | |
| continue | |
| } | |
| maxLen := config.Bugs.MaxHandshakeRecordLength | |
| if maxLen <= 0 { | |
| maxLen = 1024 | |
| } | |
| if config.Bugs.SendEmptyFragments { | |
| fragments = append(fragments, msg.Fragment(0, 0)) | |
| fragments = append(fragments, msg.Fragment(len(msg.Data), 0)) | |
| } | |
| firstRun := true | |
| fragOffset := 0 | |
| for firstRun || fragOffset < len(msg.Data) { | |
| firstRun = false | |
| fragLen := min(len(msg.Data)-fragOffset, maxLen) | |
| fragment := msg.Fragment(fragOffset, fragLen) | |
| fragments = append(fragments, fragment) | |
| if config.Bugs.ReorderHandshakeFragments { | |
| // Don't duplicate Finished to avoid the peer | |
| // interpreting it as a retransmit request. | |
| if msg.Type != typeFinished { | |
| fragments = append(fragments, fragment) | |
| } | |
| if fragLen > (maxLen+1)/2 { | |
| // Overlap each fragment by half. | |
| fragLen = (maxLen + 1) / 2 | |
| } | |
| } | |
| fragOffset += fragLen | |
| } | |
| if config.Bugs.MixCompleteMessageWithFragments { | |
| fragments = append(fragments, msg.Fragment(0, len(msg.Data))) | |
| } | |
| } | |
| // Reorder the fragments, but only within an epoch. | |
| for start := 0; start < len(fragments); { | |
| end := start + 1 | |
| for end < len(fragments) && fragments[start].Epoch == fragments[end].Epoch { | |
| end++ | |
| } | |
| chunk := fragments[start:end] | |
| if config.Bugs.ReorderHandshakeFragments { | |
| rand.Shuffle(len(chunk), func(i, j int) { chunk[i], chunk[j] = chunk[j], chunk[i] }) | |
| } | |
| start = end | |
| } | |
| c.WriteFragments(fragments) | |
| } | |
| // WriteFragments writes the specified handshake fragments to the shim. | |
| func (c *DTLSController) WriteFragments(fragments []DTLSFragment) { | |
| config := c.conn.config | |
| if c.err != nil { | |
| return | |
| } | |
| maxRecordLen := config.Bugs.PackHandshakeFragments | |
| packRecord := func(epoch *epochState, typ recordType, data []byte, anyDiscard bool) error { | |
| _, num, err := c.conn.dtlsPackRecord(epoch, typ, data, false) | |
| if err != nil { | |
| return err | |
| } | |
| if !anyDiscard && typ == recordTypeHandshake { | |
| c.conn.expectedACK = append(c.conn.expectedACK, num) | |
| } | |
| return nil | |
| } | |
| // Pack handshake fragments into records. | |
| var record []byte | |
| var epoch *epochState | |
| var anyDiscard bool | |
| flush := func() error { | |
| if len(record) > 0 { | |
| if err := packRecord(epoch, recordTypeHandshake, record, anyDiscard); err != nil { | |
| return err | |
| } | |
| } | |
| record = nil | |
| anyDiscard = false | |
| return nil | |
| } | |
| for i := range fragments { | |
| f := &fragments[i] | |
| if epoch != nil && (f.Epoch != epoch.epoch || f.IsChangeCipherSpec) { | |
| c.err = flush() | |
| if c.err != nil { | |
| return | |
| } | |
| epoch = nil | |
| } | |
| if epoch == nil { | |
| epoch = c.getOutEpochOrPanic(f.Epoch) | |
| } | |
| if f.IsChangeCipherSpec { | |
| c.err = packRecord(epoch, recordTypeChangeCipherSpec, f.Bytes(), false) | |
| if c.err != nil { | |
| return | |
| } | |
| continue | |
| } | |
| fBytes := f.Bytes() | |
| if n := config.Bugs.SplitFragments; n > 0 { | |
| if len(fBytes) > n { | |
| c.err = packRecord(epoch, recordTypeHandshake, fBytes[:n], f.ShouldDiscard) | |
| if c.err != nil { | |
| return | |
| } | |
| c.err = packRecord(epoch, recordTypeHandshake, fBytes[n:], f.ShouldDiscard) | |
| if c.err != nil { | |
| return | |
| } | |
| } else { | |
| c.err = packRecord(epoch, recordTypeHandshake, fBytes, f.ShouldDiscard) | |
| if c.err != nil { | |
| return | |
| } | |
| } | |
| } else { | |
| if len(record)+len(fBytes) > maxRecordLen { | |
| c.err = flush() | |
| if c.err != nil { | |
| return | |
| } | |
| } | |
| if f.ShouldDiscard { | |
| anyDiscard = true | |
| } | |
| record = append(record, fBytes...) | |
| } | |
| } | |
| c.err = flush() | |
| } | |
| // WriteACK writes the specified record numbers in an ACK record to the shim, | |
| // and updates shim expectations according to the specified byte ranges. To send | |
| // an ACK which the shim is expected to ignore (e.g. because it should have | |
| // forgotten a packet number), use a DTLSRecordNumberInfo with the | |
| // MessageStartSequence, etc., fields all set to zero. | |
| func (c *DTLSController) WriteACK(epoch uint16, records []DTLSRecordNumberInfo) { | |
| if c.err != nil { | |
| return | |
| } | |
| // Send the ACK. | |
| ack := cryptobyte.NewBuilder(make([]byte, 0, 2+8*len(records))) | |
| ack.AddUint16LengthPrefixed(func(recordNumbers *cryptobyte.Builder) { | |
| for _, r := range records { | |
| recordNumbers.AddUint64(r.Epoch) | |
| recordNumbers.AddUint64(r.Sequence) | |
| } | |
| }) | |
| _, _, c.err = c.conn.dtlsPackRecord(c.getOutEpochOrPanic(epoch), recordTypeACK, ack.BytesOrPanic(), false) | |
| if c.err != nil { | |
| return | |
| } | |
| // Update the list of expectations. This is inefficient, but is fine for | |
| // test code. | |
| for _, r := range records { | |
| if !r.HasACKInformation() { | |
| continue | |
| } | |
| var update []DTLSFragment | |
| for _, f := range c.retransmitNeeded { | |
| endOffset := f.Offset + len(f.Data) | |
| // Compute two, possibly empty, intersections: the fragment with | |
| // [0, ackStart) and the fragment with [ackStart, infinity). | |
| // First, the portion of the fragment that is before the ACK: | |
| if comparePair(f.Sequence, f.Offset, r.MessageStartSequence, r.MessageStartOffset) < 0 { | |
| // The fragment begins before the ACK. | |
| if comparePair(f.Sequence, endOffset, r.MessageStartSequence, r.MessageStartOffset) <= 0 { | |
| // The fragment ends before the ACK. | |
| update = append(update, f) | |
| } else { | |
| // The ACK starts in the middle of the fragment. Retain a | |
| // prefix of the fragment. | |
| prefix := f | |
| prefix.Data = f.Data[:r.MessageStartOffset-f.Offset] | |
| update = append(update, prefix) | |
| } | |
| } | |
| // Next, the portion of the fragment that is after the ACK: | |
| if comparePair(r.MessageEndSequence, r.MessageEndOffset, f.Sequence, endOffset) < 0 { | |
| // The fragment ends after the ACK. | |
| if comparePair(r.MessageEndSequence, r.MessageEndOffset, f.Sequence, f.Offset) <= 0 { | |
| // The fragment begins after the ACK. | |
| update = append(update, f) | |
| } else { | |
| // The ACK ends in the middle of the fragment. Retain a | |
| // suffix of the fragment. | |
| suffix := f | |
| suffix.Offset = r.MessageEndOffset | |
| suffix.Data = f.Data[r.MessageEndOffset-f.Offset:] | |
| update = append(update, suffix) | |
| } | |
| } | |
| } | |
| c.retransmitNeeded = update | |
| } | |
| } | |
| // ReadRetransmit indicates the shim is expected to retransmit its current | |
| // flight and consumes the retransmission. It returns the record numbers of the | |
| // retransmission, for the test to ACK if it chooses. | |
| func (c *DTLSController) ReadRetransmit() []DTLSRecordNumberInfo { | |
| if c.err != nil { | |
| return nil | |
| } | |
| var ret []DTLSRecordNumberInfo | |
| ret, c.err = c.doReadRetransmit() | |
| return ret | |
| } | |
| func (c *DTLSController) doReadRetransmit() ([]DTLSRecordNumberInfo, error) { | |
| if err := c.conn.dtlsFlushPacket(); err != nil { | |
| return nil, err | |
| } | |
| var records []DTLSRecordNumberInfo | |
| expected := slices.Clone(c.retransmitNeeded) | |
| for len(expected) > 0 { | |
| // Read a record from the expected epoch. The peer should retransmit in | |
| // order. | |
| wantTyp := recordTypeHandshake | |
| if expected[0].IsChangeCipherSpec { | |
| wantTyp = recordTypeChangeCipherSpec | |
| } | |
| epoch := c.getInEpochOrPanic(expected[0].Epoch) | |
| // Retransmitted ClientHellos predate the shim learning the version. | |
| // Ideally we would enforce the initial record-layer version, but | |
| // post-HelloVerifyRequest ClientHellos and post-HelloRetryRequest | |
| // ClientHellos look the same, but have different expectations. | |
| c.conn.skipRecordVersionCheck = !expected[0].IsChangeCipherSpec && expected[0].Type == typeClientHello | |
| typ, data, err := c.conn.dtlsDoReadRecord(epoch, wantTyp) | |
| c.conn.skipRecordVersionCheck = false | |
| if err != nil { | |
| return nil, err | |
| } | |
| if typ != wantTyp { | |
| return nil, fmt.Errorf("tls: got record of type %d in retransmit, but expected %d", typ, wantTyp) | |
| } | |
| if typ == recordTypeChangeCipherSpec { | |
| if len(data) != 1 || data[0] != 1 { | |
| return nil, errors.New("tls: got invalid ChangeCipherSpec") | |
| } | |
| expected = expected[1:] | |
| continue | |
| } | |
| // Consume all the handshake fragments and match them to what we expect. | |
| s := cryptobyte.String(data) | |
| if s.Empty() { | |
| return nil, fmt.Errorf("tls: got empty record in retransmit") | |
| } | |
| for !s.Empty() { | |
| if len(expected) == 0 || expected[0].Epoch != epoch.epoch || expected[0].IsChangeCipherSpec { | |
| return nil, fmt.Errorf("tls: got excess data at epoch %d in retransmit", epoch.epoch) | |
| } | |
| exp := &expected[0] | |
| var f DTLSFragment | |
| f, err = readDTLSFragment(&s) | |
| if f.Type != exp.Type || f.TotalLength != exp.TotalLength || f.Sequence != exp.Sequence || f.Offset != exp.Offset { | |
| return nil, fmt.Errorf("tls: got offset %d of message %d (type %d, length %d), expected offset %d of message %d (type %d, length %d)", f.Offset, f.Sequence, f.Type, f.TotalLength, exp.Offset, exp.Sequence, exp.Type, exp.TotalLength) | |
| } | |
| if len(f.Data) > len(exp.Data) { | |
| return nil, fmt.Errorf("tls: got %d bytes at offset %d of message %d but only %d bytes were missing", len(f.Data), f.Offset, f.Sequence, len(exp.Data)) | |
| } | |
| if !bytes.Equal(f.Data, exp.Data[:len(f.Data)]) { | |
| return nil, fmt.Errorf("tls: got %d bytes at offset %d of message %d but did not match original", len(f.Data), f.Offset, f.Sequence) | |
| } | |
| if len(f.Data) == len(exp.Data) { | |
| expected = expected[1:] | |
| } else { | |
| // We only got part of the fragment we wanted. | |
| exp.Offset += len(f.Data) | |
| exp.Data = exp.Data[len(f.Data):] | |
| // Check that the peer could not have fit more into the record. | |
| if !s.Empty() { | |
| return nil, errors.New("dtls: truncated handshake fragment was not last in the record") | |
| } | |
| if c.conn.lastRecordInFlight.bytesAvailable > 0 { | |
| return nil, fmt.Errorf("dtls: handshake fragment was truncated, but record could have fit %d more bytes", c.conn.lastRecordInFlight.bytesAvailable) | |
| } | |
| } | |
| } | |
| record, err := c.conn.makeDTLSRecordNumberInfo(epoch, data) | |
| if err != nil { | |
| return nil, err | |
| } | |
| records = append(records, record) | |
| } | |
| return records, nil | |
| } | |
| // ReadACK indicates the shim is expected to send an ACK at the specified epoch. | |
| // The contents of the ACK are checked against the connection's internal | |
| // simulation of the shim's expected behavior. | |
| func (c *DTLSController) ReadACK(epoch uint16) { | |
| if c.err != nil { | |
| return | |
| } | |
| c.err = c.conn.dtlsFlushPacket() | |
| if c.err != nil { | |
| return | |
| } | |
| typ, data, err := c.conn.dtlsDoReadRecord(c.getInEpochOrPanic(epoch), recordTypeACK) | |
| if err != nil { | |
| c.err = err | |
| return | |
| } | |
| if typ != recordTypeACK { | |
| c.err = fmt.Errorf("tls: got record of type %d, but expected ACK", typ) | |
| return | |
| } | |
| c.err = c.conn.checkACK(data) | |
| } | |
| // WriteAppData writes an application data record to the shim. This may be used | |
| // to test that post-handshake retransmits may interleave with application data. | |
| func (c *DTLSController) WriteAppData(epoch uint16, data []byte) { | |
| if c.err != nil { | |
| return | |
| } | |
| _, _, c.err = c.conn.dtlsPackRecord(c.getOutEpochOrPanic(epoch), recordTypeApplicationData, data, false) | |
| } | |
| // ReadAppData indicates the shim is expected to send the specified application | |
| // data record. This may be used to test that post-handshake retransmits may | |
| // interleave with application data. | |
| func (c *DTLSController) ReadAppData(epoch uint16, expected []byte) { | |
| if c.flush() != nil { | |
| return | |
| } | |
| typ, data, err := c.conn.dtlsDoReadRecord(c.getInEpochOrPanic(epoch), recordTypeApplicationData) | |
| if err != nil { | |
| c.err = err | |
| return | |
| } | |
| if typ != recordTypeApplicationData { | |
| c.err = fmt.Errorf("tls: got record of type %d, but expected application data", typ) | |
| return | |
| } | |
| if !bytes.Equal(data, expected) { | |
| c.err = fmt.Errorf("tls: got app data record containing %x, but expected %x", data, expected) | |
| return | |
| } | |
| } | |
| // ExpectNextTimeout indicates the shim's next timeout should be d from now. | |
| func (c *DTLSController) ExpectNextTimeout(d time.Duration) { | |
| if c.flush() != nil { | |
| return | |
| } | |
| c.err = c.conn.config.Bugs.PacketAdaptor.ExpectNextTimeout(d) | |
| } | |
| // ExpectNoNext indicates the shim should not have a next timeout. | |
| func (c *DTLSController) ExpectNoNextTimeout() { | |
| if c.flush() != nil { | |
| return | |
| } | |
| c.err = c.conn.config.Bugs.PacketAdaptor.ExpectNoNextTimeout() | |
| } | |
| // MergeIntoNextFlight indicates the state from this flight should be merged | |
| // into the next WriteFlight or ACKFlight call. This allows the test to control | |
| // two independent post-handshake messages as a single unit. | |
| func (c *DTLSController) MergeIntoNextFlight() { | |
| c.mergeIntoNextFlight = true | |
| } |