package ike import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/rand" "crypto/sha1" "crypto/sha256" "crypto/subtle" "encoding/binary" "errors" "fmt" "io" "math" "net" "sync" ) const ( espHeaderLength = 8 espReplayWindow = 64 ) var ( errESPAuthentication = errors.New("ike: ESP authentication failed") errESPReplay = errors.New("ike: ESP packet is outside the replay window") errESPPolicyDrop = errors.New("ike: ESP packet is not eligible for this CHILD_SA") ) // espTunnel protects complete IPv4 or IPv6 packets using an IKEv2 CHILD_SA. // It deliberately implements only the negotiated suites offered by this // package: AES-CBC with HMAC-SHA1-96 or HMAC-SHA2-256-128 and no ESN. type espTunnel struct { outbound *espDirection inbound *espDirection initiatorSelectors []trafficSelector responderSelectors []trafficSelector } type espDirection struct { spi uint32 block cipher.Block authKey []byte integrity string icvLength int random io.Reader mu sync.Mutex sequence uint32 replay replayWindow } type replayWindow struct { highest uint32 bitmap uint64 } type innerPacketMetadata struct { source net.IP destination net.IP protocol uint8 sourcePort uint16 destinationPort uint16 nextHeader uint8 } func newESPTunnel(config ChildSAConfig, randomSource io.Reader) (*espTunnel, error) { if config.InboundSPI == 0 || config.OutboundSPI == 0 { return nil, errors.New("ike: ESP SPIs must be nonzero") } if randomSource == nil { randomSource = rand.Reader } outbound, err := newESPDirection( config.OutboundSPI, config.OutboundEncKey, config.OutboundAuthKey, config.Encryption, config.Integrity, randomSource, ) if err != nil { return nil, fmt.Errorf("ike: outbound ESP: %w", err) } inbound, err := newESPDirection( config.InboundSPI, config.InboundEncKey, config.InboundAuthKey, config.Encryption, config.Integrity, randomSource, ) if err != nil { return nil, fmt.Errorf("ike: inbound ESP: %w", err) } if len(config.InitiatorSelectors) == 0 || len(config.ResponderSelectors) == 0 { return nil, errors.New("ike: ESP traffic selectors are required") } return &espTunnel{ outbound: outbound, inbound: inbound, initiatorSelectors: copyESPTrafficSelectors(config.InitiatorSelectors), responderSelectors: copyESPTrafficSelectors(config.ResponderSelectors), }, nil } func newESPDirection( spi uint32, encryptionKey []byte, authenticationKey []byte, encryption string, integrity string, randomSource io.Reader, ) (*espDirection, error) { expectedEncryptionLength := 0 switch encryption { case "aes-cbc-128": expectedEncryptionLength = 16 case "aes-cbc-256": expectedEncryptionLength = 32 default: return nil, fmt.Errorf("unsupported encryption suite %q", encryption) } if len(encryptionKey) != expectedEncryptionLength { return nil, fmt.Errorf("AES key has length %d, want %d", len(encryptionKey), expectedEncryptionLength) } expectedAuthenticationLength := 0 icvLength := 0 switch integrity { case "hmac-sha1-96": expectedAuthenticationLength = sha1.Size icvLength = 12 case "hmac-sha2-256-128": expectedAuthenticationLength = sha256.Size icvLength = 16 default: return nil, fmt.Errorf("unsupported integrity suite %q", integrity) } if len(authenticationKey) != expectedAuthenticationLength { return nil, fmt.Errorf( "authentication key has length %d, want %d", len(authenticationKey), expectedAuthenticationLength, ) } block, err := aes.NewCipher(encryptionKey) if err != nil { return nil, err } return &espDirection{ spi: spi, block: block, authKey: append([]byte(nil), authenticationKey...), integrity: integrity, icvLength: icvLength, random: randomSource, }, nil } func (tunnel *espTunnel) seal(innerPacket []byte) ([]byte, error) { if tunnel == nil { return nil, errors.New("ike: nil ESP tunnel") } metadata, err := parseInnerPacket(innerPacket) if err != nil { return nil, fmt.Errorf("%w: %v", errESPPolicyDrop, err) } if !packetAllowed( metadata, tunnel.initiatorSelectors, tunnel.responderSelectors, ) { return nil, fmt.Errorf("%w: outbound packet is outside negotiated traffic selectors", errESPPolicyDrop) } return tunnel.outbound.seal(innerPacket, metadata.nextHeader) } func (tunnel *espTunnel) open(packet []byte) ([]byte, error) { if tunnel == nil { return nil, errors.New("ike: nil ESP tunnel") } return tunnel.inbound.open(packet, func(innerPacket []byte, nextHeader uint8) error { metadata, err := parseInnerPacket(innerPacket) if err != nil { return err } if metadata.nextHeader != nextHeader { return errors.New("ike: ESP trailer does not match the inner IP version") } if !packetAllowed( metadata, tunnel.responderSelectors, tunnel.initiatorSelectors, ) { return errors.New("ike: inbound packet is outside negotiated traffic selectors") } return nil }) } func (direction *espDirection) seal(innerPacket []byte, nextHeader uint8) ([]byte, error) { direction.mu.Lock() defer direction.mu.Unlock() if direction.sequence == math.MaxUint32 { return nil, errors.New("ike: ESP sequence number exhausted; rekey is required") } direction.sequence++ sequence := direction.sequence blockSize := direction.block.BlockSize() paddingLength := (blockSize - ((len(innerPacket) + 2) % blockSize)) % blockSize plaintext := make([]byte, len(innerPacket)+paddingLength+2) copy(plaintext, innerPacket) for index := 0; index < paddingLength; index++ { plaintext[len(innerPacket)+index] = byte(index + 1) } plaintext[len(plaintext)-2] = byte(paddingLength) plaintext[len(plaintext)-1] = nextHeader authenticatedLength := espHeaderLength + blockSize + len(plaintext) packet := make([]byte, authenticatedLength+direction.icvLength) binary.BigEndian.PutUint32(packet[0:4], direction.spi) binary.BigEndian.PutUint32(packet[4:8], sequence) iv := packet[espHeaderLength : espHeaderLength+blockSize] if _, err := io.ReadFull(direction.random, iv); err != nil { return nil, fmt.Errorf("ike: generate ESP IV: %w", err) } cipher.NewCBCEncrypter(direction.block, iv).CryptBlocks( packet[espHeaderLength+blockSize:authenticatedLength], plaintext, ) icv := direction.authenticationCode(packet[:authenticatedLength]) copy(packet[authenticatedLength:], icv) return packet, nil } func (direction *espDirection) open( packet []byte, validate func([]byte, uint8) error, ) ([]byte, error) { direction.mu.Lock() defer direction.mu.Unlock() blockSize := direction.block.BlockSize() minimumLength := espHeaderLength + blockSize + blockSize + direction.icvLength if len(packet) < minimumLength { return nil, errors.New("ike: ESP packet is truncated") } if binary.BigEndian.Uint32(packet[0:4]) != direction.spi { return nil, errors.New("ike: ESP packet has an unexpected SPI") } sequence := binary.BigEndian.Uint32(packet[4:8]) if sequence == 0 || !direction.replay.wouldAccept(sequence) { return nil, errESPReplay } authenticatedLength := len(packet) - direction.icvLength ciphertext := packet[espHeaderLength+blockSize : authenticatedLength] if len(ciphertext) == 0 || len(ciphertext)%blockSize != 0 { return nil, errors.New("ike: ESP ciphertext is not block aligned") } expectedICV := direction.authenticationCode(packet[:authenticatedLength]) if subtle.ConstantTimeCompare(expectedICV, packet[authenticatedLength:]) != 1 { return nil, errESPAuthentication } plaintext := make([]byte, len(ciphertext)) iv := packet[espHeaderLength : espHeaderLength+blockSize] cipher.NewCBCDecrypter(direction.block, iv).CryptBlocks(plaintext, ciphertext) if len(plaintext) < 2 { return nil, errors.New("ike: ESP plaintext is truncated") } paddingLength := int(plaintext[len(plaintext)-2]) if paddingLength > len(plaintext)-2 { return nil, errors.New("ike: ESP padding length is invalid") } paddingStart := len(plaintext) - 2 - paddingLength for index := 0; index < paddingLength; index++ { if plaintext[paddingStart+index] != byte(index+1) { return nil, errors.New("ike: ESP padding bytes are invalid") } } nextHeader := plaintext[len(plaintext)-1] if nextHeader != 4 && nextHeader != 41 { return nil, fmt.Errorf("ike: unsupported ESP next-header value %d", nextHeader) } innerPacket := append([]byte(nil), plaintext[:paddingStart]...) if validate != nil { if err := validate(innerPacket, nextHeader); err != nil { return nil, err } } direction.replay.commit(sequence) return innerPacket, nil } func (direction *espDirection) authenticationCode(packet []byte) []byte { var mac hashWriter switch direction.integrity { case "hmac-sha1-96": mac = hmac.New(sha1.New, direction.authKey) case "hmac-sha2-256-128": mac = hmac.New(sha256.New, direction.authKey) default: panic("unreachable ESP integrity suite") } _, _ = mac.Write(packet) return mac.Sum(nil)[:direction.icvLength] } type hashWriter interface { Write([]byte) (int, error) Sum([]byte) []byte } func (window replayWindow) wouldAccept(sequence uint32) bool { if sequence == 0 { return false } if window.highest == 0 || sequence > window.highest { return true } difference := window.highest - sequence if difference >= espReplayWindow { return false } return window.bitmap&(uint64(1)< window.highest { difference := sequence - window.highest if difference >= espReplayWindow { window.bitmap = 1 } else { window.bitmap = window.bitmap<> 4 { case 4: return parseInnerIPv4(packet) case 6: return parseInnerIPv6(packet) default: return innerPacketMetadata{}, errors.New("ike: inner packet is not IPv4 or IPv6") } } func parseInnerIPv4(packet []byte) (innerPacketMetadata, error) { if len(packet) < 20 { return innerPacketMetadata{}, errors.New("ike: inner IPv4 packet is truncated") } headerLength := int(packet[0]&0x0f) * 4 if headerLength < 20 || headerLength > len(packet) { return innerPacketMetadata{}, errors.New("ike: inner IPv4 header length is invalid") } totalLength := int(binary.BigEndian.Uint16(packet[2:4])) if totalLength != len(packet) || totalLength < headerLength { return innerPacketMetadata{}, errors.New("ike: inner IPv4 total length is invalid") } metadata := innerPacketMetadata{ source: append(net.IP(nil), packet[12:16]...), destination: append(net.IP(nil), packet[16:20]...), protocol: packet[9], nextHeader: 4, } fragmentOffset := binary.BigEndian.Uint16(packet[6:8]) & 0x1fff if fragmentOffset == 0 { parseTransportPorts(packet[headerLength:], &metadata) } return metadata, nil } func parseInnerIPv6(packet []byte) (innerPacketMetadata, error) { if len(packet) < 40 { return innerPacketMetadata{}, errors.New("ike: inner IPv6 packet is truncated") } payloadLength := int(binary.BigEndian.Uint16(packet[4:6])) if payloadLength+40 != len(packet) { return innerPacketMetadata{}, errors.New("ike: inner IPv6 payload length is invalid") } metadata := innerPacketMetadata{ source: append(net.IP(nil), packet[8:24]...), destination: append(net.IP(nil), packet[24:40]...), nextHeader: 41, } protocol := packet[6] offset := 40 firstFragment := true for { switch protocol { case 0, 43, 60: if offset+2 > len(packet) { return innerPacketMetadata{}, errors.New("ike: inner IPv6 extension header is truncated") } length := (int(packet[offset+1]) + 1) * 8 if length < 8 || offset+length > len(packet) { return innerPacketMetadata{}, errors.New("ike: inner IPv6 extension header length is invalid") } protocol = packet[offset] offset += length case 44: if offset+8 > len(packet) { return innerPacketMetadata{}, errors.New("ike: inner IPv6 fragment header is truncated") } firstFragment = binary.BigEndian.Uint16(packet[offset+2:offset+4])&0xfff8 == 0 protocol = packet[offset] offset += 8 case 51: if offset+2 > len(packet) { return innerPacketMetadata{}, errors.New("ike: inner IPv6 AH header is truncated") } length := (int(packet[offset+1]) + 2) * 4 if length < 8 || offset+length > len(packet) { return innerPacketMetadata{}, errors.New("ike: inner IPv6 AH header length is invalid") } protocol = packet[offset] offset += length default: metadata.protocol = protocol if firstFragment { parseTransportPorts(packet[offset:], &metadata) } return metadata, nil } } } func parseTransportPorts(payload []byte, metadata *innerPacketMetadata) { if metadata == nil || (metadata.protocol != 6 && metadata.protocol != 17) || len(payload) < 4 { return } metadata.sourcePort = binary.BigEndian.Uint16(payload[0:2]) metadata.destinationPort = binary.BigEndian.Uint16(payload[2:4]) } func packetAllowed( metadata innerPacketMetadata, sourceSelectors []trafficSelector, destinationSelectors []trafficSelector, ) bool { return endpointAllowed( metadata.source, metadata.protocol, metadata.sourcePort, sourceSelectors, ) && endpointAllowed( metadata.destination, metadata.protocol, metadata.destinationPort, destinationSelectors, ) } func endpointAllowed(ip net.IP, protocol uint8, port uint16, selectors []trafficSelector) bool { for _, selector := range selectors { if selector.IPProtocol != 0 && selector.IPProtocol != protocol { continue } if port < selector.StartPort || port > selector.EndPort { continue } if ipWithinRange(ip, selector.StartIP, selector.EndIP) { return true } } return false } func ipWithinRange(ip net.IP, start net.IP, end net.IP) bool { normalizedIP, normalizedStart, normalizedEnd, ok := normalizeIPRange(ip, start, end) if !ok { return false } return bytes.Compare(normalizedIP, normalizedStart) >= 0 && bytes.Compare(normalizedIP, normalizedEnd) <= 0 } func normalizeIPRange(ip net.IP, start net.IP, end net.IP) ([]byte, []byte, []byte, bool) { if start4 := start.To4(); start4 != nil { ip4 := ip.To4() end4 := end.To4() if ip4 == nil || end4 == nil { return nil, nil, nil, false } return ip4, start4, end4, true } ip16 := ip.To16() start16 := start.To16() end16 := end.To16() if ip16 == nil || start16 == nil || end16 == nil || start.To4() != nil || end.To4() != nil { return nil, nil, nil, false } return ip16, start16, end16, true } func copyESPTrafficSelectors(selectors []trafficSelector) []trafficSelector { cloned := make([]trafficSelector, len(selectors)) for index, selector := range selectors { cloned[index] = selector cloned[index].StartIP = append(net.IP(nil), selector.StartIP...) cloned[index].EndIP = append(net.IP(nil), selector.EndIP...) } return cloned }