Go-安全指南-二-

41 阅读42分钟

Go 安全指南(二)

原文:annas-archive.org/md5/7656fc72aaece258c02033b14e33ea12

译者:飞龙

协议:CC BY-NC-SA 4.0

第四章:法医学

法医学是收集证据以侦测犯罪的过程。数字法医学指的就是寻找数字证据,包括定位可能包含相关信息的异常文件、寻找隐藏数据、确定文件最后修改时间、确定谁发送了邮件、对文件进行哈希处理、收集有关攻击 IP 的信息,或捕获网络通信。

除了法医学,本章还将介绍一个简单的隐写术示例——将档案隐藏在图像中。隐写术是一种将信息隐藏在其他信息中的技巧,使其不易被发现。

哈希处理与法医学相关,详细内容见第六章,密码学,而数据包捕获则在第五章,数据包捕获与注入中进行讲解。你将在本书的各章中找到可能对法医调查员有用的示例。

在这一章中,你将学习以下内容:

  • 文件法医学

  • 获取基本文件信息

  • 查找大文件

  • 查找最近修改的文件

  • 读取磁盘的启动扇区

  • 网络法医学

  • 查找主机名和 IP 地址

  • 查找 MX 邮件记录

  • 查找主机的名称服务器

  • 隐写术

  • 将档案隐藏在图像中

  • 检测隐藏在图像中的档案

  • 生成随机图像

  • 创建一个 ZIP 压缩档案

文件

文件法医学很重要,因为攻击者可能会留下痕迹,需要在做出更多更改或丢失信息之前收集证据。这包括确定文件的所有者、文件最后修改时间、谁有权限访问文件,并检查文件中是否有隐藏的数据。

获取文件信息

让我们从一些简单的内容开始。本程序将打印出关于文件的信息,即文件最后修改时间、文件所有者、文件大小以及文件权限。这也将作为一个良好的测试,确保你的 Go 开发环境已正确设置。

如果调查员发现了异常文件,首先需要检查所有基本的元数据。这将提供关于文件所有者、哪些群组可以访问该文件、文件最后修改时间、是否是可执行文件以及文件的大小等信息。这些信息可能都非常有用。

我们将使用的主要函数是os.Stat()。它返回一个FileInfo结构体,我们将打印出来。为了调用os.Stat(),我们需要在开始时导入os包。os.Stat()会返回两个变量,这与许多只允许返回一个变量的语言不同。如果你想忽略某个返回变量(如错误),可以使用下划线(_)符号代替变量名。

我们导入的fmt(格式化输出的缩写)包包含了典型的打印函数,如fmt.Println()fmt.Printf()log包包含了log.Printf()log.Println()fmtlog的区别在于,log在消息前会打印一个时间戳,并且是线程安全的。

log包有一个fmt包没有的函数,即log.Fatal(),它在打印信息后会立即调用os.Exit(1)退出程序。log.Fatal()函数对于处理某些错误条件很有用,它会打印错误信息并退出。如果你想要干净的输出并完全控制格式,请使用fmt的打印函数。如果你需要在每条消息中附带时间戳,可以使用log包的打印函数。在收集法医线索时,记录下每个操作的时间是非常重要的。

在这个例子中,变量在main函数之前的独立部分中定义。在这个作用域内的变量对于整个包都是可用的。这意味着每个函数都在同一个文件中,其他文件也在相同目录下,并使用相同的包声明。这个定义变量的方法只是为了展示 Go 语言是如何实现的,它是 Pascal 语言对 Go 的影响之一,此外还有:=操作符。将所有变量在顶部明确列出并标明数据类型是很方便的。为了在后面的例子中节省空间,我们将使用声明并赋值操作符或:=符号。这在编写代码时非常方便,因为你不需要先声明变量类型,编译时会自动推断数据类型。然而,在阅读源代码时,明确声明变量类型有助于读者理解代码。我们也可以将整个var声明放入main函数内,以进一步限制作用域:

package main

import (
   "fmt"
   "log"
   "os"
)

var (
   fileInfo os.FileInfo
   err error
)

func main() {
   // Stat returns file info. It will return
   // an error if there is no file.
   fileInfo, err = os.Stat("test.txt")
   if err != nil {
      log.Fatal(err)
   }
   fmt.Println("File name:", fileInfo.Name())
   fmt.Println("Size in bytes:", fileInfo.Size())
   fmt.Println("Permissions:", fileInfo.Mode())
   fmt.Println("Last modified:", fileInfo.ModTime())
   fmt.Println("Is Directory: ", fileInfo.IsDir())
   fmt.Printf("System interface type: %T\n", fileInfo.Sys())
   fmt.Printf("System info: %+v\n\n", fileInfo.Sys())
}

查找最大文件

在调查过程中,大文件通常是嫌疑的首选对象。大型数据库转储、密码转储、彩虹表、信用卡缓存、被盗的知识产权以及其他数据常常被存储在一个大型档案中,如果你有合适的工具,这类文件很容易被发现。此外,寻找异常大的图像或视频文件也很有帮助,因为它们可能隐藏有通过隐写术加密的信息。隐写术将在本章进一步讨论。

这个程序将在一个目录及其所有子目录中搜索所有文件,并按文件大小排序。我们将使用ioutil.ReadDir()来探索初始目录,以获取作为os.FileInfo结构体切片的内容。为了检查文件是否是目录,我们将使用os.IsDir()。然后我们将创建一个名为FileNode的自定义数据结构来存储所需的信息。我们使用链表来存储文件信息。在将元素插入链表之前,我们会遍历它,找到合适的位置,以保持链表的正确排序。请注意,在像/这样的目录上运行程序可能会花费很长时间。尝试使用更具体的目录,比如你的home文件夹:

package main

import (
   "container/list"
   "fmt"
   "io/ioutil"
   "log"
   "os"
   "path/filepath"
)

type FileNode struct {
   FullPath string
   Info os.FileInfo
}

func insertSorted(fileList *list.List, fileNode FileNode) {
   if fileList.Len() == 0 { 
      // If list is empty, just insert and return
      fileList.PushFront(fileNode)
      return
   }

   for element := fileList.Front(); element != nil; element =    
      element.Next() {
      if fileNode.Info.Size() < element.Value.(FileNode).Info.Size()       
      {
         fileList.InsertBefore(fileNode, element)
         return
      }
   }
   fileList.PushBack(fileNode)
}

func getFilesInDirRecursivelyBySize(fileList *list.List, path string) {
   dirFiles, err := ioutil.ReadDir(path)
   if err != nil {
      log.Println("Error reading directory: " + err.Error())
   }

   for _, dirFile := range dirFiles {
      fullpath := filepath.Join(path, dirFile.Name())
      if dirFile.IsDir() {
         getFilesInDirRecursivelyBySize(
            fileList,
            filepath.Join(path, dirFile.Name()),
         )
      } else if dirFile.Mode().IsRegular() {
         insertSorted(
            fileList,
            FileNode{FullPath: fullpath, Info: dirFile},
         )
      }
   }
}

func main() {
   fileList := list.New()
   getFilesInDirRecursivelyBySize(fileList, "/home")

   for element := fileList.Front(); element != nil; element =   
      element.Next() {
      fmt.Printf("%d ", element.Value.(FileNode).Info.Size())
      fmt.Printf("%s\n", element.Value.(FileNode).FullPath)
   }
}

查找最近修改的文件

在法医检查受害者机器时,首先可以做的一件事是查找最近被修改的文件。这可能会为你提供关于攻击者查看了哪些地方、修改了哪些设置,或他们的动机是什么的线索。

然而,如果调查员正在检查攻击者的机器,那么目标会有所不同。最近访问的文件可能会提供线索,告诉你攻击者使用了哪些工具,在哪些地方可能隐藏了数据,或者他们使用了什么软件。

以下示例将搜索一个目录及其子目录,找到所有文件,并按最后修改时间排序。这个示例与前一个非常相似,不同之处在于排序是通过使用time.Time.Before()函数比较时间戳来完成的:

package main

import (
   "container/list"
   "fmt"
   "io/ioutil"
   "log"
   "os"
   "path/filepath"
)

type FileNode struct {
   FullPath string
   Info os.FileInfo
}

func insertSorted(fileList *list.List, fileNode FileNode) {
   if fileList.Len() == 0 { 
      // If list is empty, just insert and return
      fileList.PushFront(fileNode)
      return
   }

   for element := fileList.Front(); element != nil; element = 
      element.Next() {
      if fileNode.Info.ModTime().Before(element.Value.
        (FileNode).Info.ModTime()) {
            fileList.InsertBefore(fileNode, element)
            return
        }
    }

    fileList.PushBack(fileNode)
}

func GetFilesInDirRecursivelyBySize(fileList *list.List, path string) {
    dirFiles, err := ioutil.ReadDir(path)
    if err != nil {
        log.Println("Error reading directory: " + err.Error())
    }

    for _, dirFile := range dirFiles {
        fullpath := filepath.Join(path, dirFile.Name())
        if dirFile.IsDir() {
            GetFilesInDirRecursivelyBySize(
            fileList,
            filepath.Join(path, dirFile.Name()),
            )
        } else if dirFile.Mode().IsRegular() {
           insertSorted(
              fileList,
              FileNode{FullPath: fullpath, Info: dirFile},
           )
        }
    }
}

func main() {
    fileList := list.New()
    GetFilesInDirRecursivelyBySize(fileList, "/")

    for element := fileList.Front(); element != nil; element =    
       element.Next() {
        fmt.Print(element.Value.(FileNode).Info.ModTime())
        fmt.Printf("%s\n", element.Value.(FileNode).FullPath)
    }
}

读取引导扇区

这个程序将读取磁盘的前 512 字节,并将结果以十进制值、十六进制和字符串的形式打印出来。io.ReadFull()函数类似于普通的读取操作,但它确保你提供的数据字节切片被完全填充。如果文件中的字节不足以填充字节切片,它会返回一个错误。

这种方法的实际应用是检查机器的引导扇区,看看它是否被修改。Rootkit 和恶意软件可能通过修改引导扇区劫持引导过程。你可以手动检查其中是否有任何异常,或者将其与已知的良好版本进行比较。也许可以将机器的备份镜像或全新安装的版本与其进行比较,看看是否有所变化。

请注意,技术上你可以传递任何文件名,而不仅仅是磁盘,因为在 Linux 中一切都被视为文件。如果你直接传递设备的名称,例如/dev/sda,它将读取磁盘的前512字节,即引导扇区。主要的磁盘设备通常是/dev/sda,但也可能是/dev/sdb/dev/sdc。使用mountdf工具可以获取更多关于磁盘名称的信息。你需要以sudo身份运行该应用程序,以便有权限直接读取磁盘设备。

有关文件、输入和输出的更多信息,请参考osbufioio包,如以下代码块所示:

package main

// Device is typically /dev/sda but may also be /dev/sdb, /dev/sdc
// Use mount, or df -h to get info on which drives are being used
// You will need sudo to access some disks at this level

import (
   "io"
   "log"
   "os"
)

func main() {
   path := "/dev/sda"
   log.Println("[+] Reading boot sector of " + path)

   file, err := os.Open(path)
   if err != nil {
      log.Fatal("Error: " + err.Error())
   }

   // The file.Read() function will read a tiny file in to a large
   // byte slice, but io.ReadFull() will return an
   // error if the file is smaller than the byte slice.
   byteSlice := make([]byte, 512)
   // ReadFull Will error if 512 bytes not available to read
   numBytesRead, err := io.ReadFull(file, byteSlice)
   if err != nil {
      log.Fatal("Error reading 512 bytes from file. " + err.Error())
   }

   log.Printf("Bytes read: %d\n\n", numBytesRead)
   log.Printf("Data as decimal:\n%d\n\n", byteSlice)
   log.Printf("Data as hex:\n%x\n\n", byteSlice)
   log.Printf("Data as string:\n%s\n\n", byteSlice)
}

隐写术

隐写术是将信息隐藏在非秘密信息中的技术。不要与速记术混淆,速记术是记录口述内容的技术,比如法庭记录员在庭审过程中将口头发言转录下来。隐写术有着悠久的历史,一个古老的例子是将摩尔斯电码信息缝在衣物的缝线上。

在数字世界中,人们可以将任何类型的二进制数据隐藏在图像、音频或视频文件中。这个过程可能会影响原始文件的质量,也可能不会。一些图像可以完全保持其原始完整性,但它们在表面下隐藏了额外的数据,形式是一个.zip.rar压缩包。有些隐写算法比较复杂,将原始二进制数据隐藏在每个字节的最低位,只会略微降低原始质量。其他隐写算法比较简单,仅仅是将图像文件和压缩包合并成一个文件。我们将看看如何将压缩包隐藏在图像中,以及如何检测隐藏的压缩包。

生成带有随机噪声的图像

这个程序将创建一张每个像素都设置为随机颜色的 JPEG 图片。这是一个简单的程序,所以我们只有一个 JPEG 图片可以处理。Go 标准库提供了jpeggifpng包。所有不同图像类型的接口是相同的,因此从jpeg切换到gifpng包非常简单:

package main

import (
   "image"
   "image/jpeg"
   "log"
   "math/rand"
   "os"
)

func main() {
   // 100x200 pixels
   myImage := image.NewRGBA(image.Rect(0, 0, 100, 200))

   for p := 0; p < 100*200; p++ {
      pixelOffset := 4 * p
      myImage.Pix[0+pixelOffset] = uint8(rand.Intn(256)) // Red
      myImage.Pix[1+pixelOffset] = uint8(rand.Intn(256)) // Green
      myImage.Pix[2+pixelOffset] = uint8(rand.Intn(256)) // Blue
      myImage.Pix[3+pixelOffset] = 255 // Alpha
   }

   outputFile, err := os.Create("test.jpg")
   if err != nil {
      log.Fatal(err)
   }

   jpeg.Encode(outputFile, myImage, nil)

   err = outputFile.Close()
   if err != nil {
      log.Fatal(err)
   }
}

创建 ZIP 压缩包

这个程序将创建一个 ZIP 压缩包,以便我们进行隐写术实验。Go 标准库提供了一个zip包,但它也支持通过tar包处理 TAR 压缩包。这个示例生成一个包含两个文件的 ZIP 文件:test.txttest2.txt。为了简化起见,每个文件的内容在源代码中都作为硬编码字符串给出:

package main

import (
   "crypto/md5"
   "crypto/sha1"
   "crypto/sha256"
   "crypto/sha512"
   "fmt"
   "io/ioutil"
   "log"
   "os"
)

func printUsage() {
   fmt.Println("Usage: " + os.Args[0] + " <filepath>")
   fmt.Println("Example: " + os.Args[0] + " document.txt")
}

func checkArgs() string {
   if len(os.Args) < 2 {
      printUsage()
      os.Exit(1)
   }
   return os.Args[1]
}

func main() {
   filename := checkArgs()

   // Get bytes from file
   data, err := ioutil.ReadFile(filename)
   if err != nil {
      log.Fatal(err)
   }

   // Hash the file and output results
   fmt.Printf("Md5: %x\n\n", md5.Sum(data))
   fmt.Printf("Sha1: %x\n\n", sha1.Sum(data))
   fmt.Printf("Sha256: %x\n\n", sha256.Sum256(data))
   fmt.Printf("Sha512: %x\n\n", sha512.Sum512(data))
}

创建隐写图像压缩包

现在我们有了一个图像和一个 ZIP 压缩包,我们可以将它们结合起来,将压缩包“隐藏”在图像内。这可能是最原始的隐写术形式。更高级的方法是将文件逐字节拆分,将信息存储在图像的低位中,使用特定程序从图像中提取数据,然后重建原始数据。这个示例非常好,因为我们可以轻松地测试和验证它是否仍然作为图片加载并且仍然像 ZIP 压缩包一样工作。

以下示例将使用一张 JPEG 图片和一个 ZIP 压缩包,并将它们结合起来创建一个隐藏的压缩包。文件将保留.jpg扩展名,仍然会像普通图片一样显示和运作。但是,该文件仍然可以作为 ZIP 压缩包使用。你可以解压.jpg文件,压缩包内的文件将被提取出来:

package main

import (
   "io"
   "log"
   "os"
)

func main() {
   // Open original file
   firstFile, err := os.Open("test.jpg")
   if err != nil {
      log.Fatal(err)
   }
   defer firstFile.Close()

   // Second file
   secondFile, err := os.Open("test.zip")
   if err != nil {
      log.Fatal(err)
   }
   defer secondFile.Close()

   // New file for output
   newFile, err := os.Create("stego_image.jpg")
   if err != nil {
      log.Fatal(err)
   }
   defer newFile.Close()

   // Copy the bytes to destination from source
   _, err = io.Copy(newFile, firstFile)
   if err != nil {
      log.Fatal(err)
   }
   _, err = io.Copy(newFile, secondFile)
   if err != nil {
      log.Fatal(err)
   }
}

在 JPEG 图像中检测 ZIP 压缩包

如果使用前面示例中的技术隐藏了数据,可以通过在图像中搜索 ZIP 文件签名来检测。一个文件可能有.jpg扩展名,仍然能够在照片查看器中正确加载,但它仍可能包含一个 ZIP 存档。以下程序会遍历文件并查找 ZIP 文件签名。我们可以使用它检查前一个示例中创建的文件:

package main

import (
   "bufio"
   "bytes"
   "log"
   "os"
)

func main() {
   // Zip signature is "\x50\x4b\x03\x04"
   filename := "stego_image.jpg"
   file, err := os.Open(filename)
   if err != nil {
      log.Fatal(err)
   }
   bufferedReader := bufio.NewReader(file)

   fileStat, _ := file.Stat()
   // 0 is being cast to an int64 to force i to be initialized as
   // int64 because filestat.Size() returns an int64 and must be
   // compared against the same type
   for i := int64(0); i < fileStat.Size(); i++ {
      myByte, err := bufferedReader.ReadByte()
      if err != nil {
         log.Fatal(err)
      }

      if myByte == '\x50' { 
         // First byte match. Check the next 3 bytes
         byteSlice := make([]byte, 3)
         // Get bytes without advancing pointer with Peek
         byteSlice, err = bufferedReader.Peek(3)
         if err != nil {
            log.Fatal(err)
         }

         if bytes.Equal(byteSlice, []byte{'\x4b', '\x03', '\x04'}) {
            log.Printf("Found zip signature at byte %d.", i)
         }
      }
   }
}

网络

有时,日志中会出现一个奇怪的 IP 地址,您需要找出更多信息,或者可能有一个域名,您需要根据 IP 地址来进行地理定位。这些示例展示了如何收集主机信息。数据包捕获也是网络取证调查的一个重要部分,但关于数据包捕获有很多可以讨论的内容,因此,第五章,数据包捕获与注入专门讲解数据包捕获和注入。

从 IP 地址查找主机名

这个程序将接受一个 IP 地址,并找出对应的主机名。net.parseIP()函数用于验证提供的 IP 地址,而net.LookupAddr()执行实际的工作,找出主机名是什么。

默认情况下,使用的是纯 Go 解析器。可以通过设置GODEBUG环境变量中的netdns值来覆盖解析器。将GODEBUG的值设置为gocgo。在 Linux 中,您可以使用以下 Shell 命令进行设置:

export GODEBUG=netdns=go # force pure Go resolver (Default)
export GODEBUG=netdns=cgo # force cgo resolver

这是程序的代码:

package main

import (
   "fmt"
   "log"
   "net"
   "os"
)

func main() {
   if len(os.Args) != 2 {
      log.Fatal("No IP address argument provided.")
   }
   arg := os.Args[1]

   // Parse the IP for validation
   ip := net.ParseIP(arg)
   if ip == nil {
      log.Fatal("Valid IP not detected. Value provided: " + arg)
   }

   fmt.Println("Looking up hostnames for IP address: " + arg)
   hostnames, err := net.LookupAddr(ip.String())
   if err != nil {
      log.Fatal(err)
   }
   for _, hostnames := range hostnames {
      fmt.Println(hostnames)
   }
}

从主机名查找 IP 地址

以下示例接受一个主机名并返回 IP 地址。它与之前的示例非常相似,但顺序相反。net.LookupHost()函数承担了主要工作:

package main

import (
   "fmt"
   "log"
   "net"
   "os"
)

func main() {
   if len(os.Args) != 2 {
      log.Fatal("No hostname argument provided.")
   }
   arg := os.Args[1]

   fmt.Println("Looking up IP addresses for hostname: " + arg)

   ips, err := net.LookupHost(arg)
   if err != nil {
      log.Fatal(err)
   }
   for _, ip := range ips {
      fmt.Println(ip)
   }
}

查找 MX 记录

该程序将接受一个域名并返回 MX 记录。MX 记录(邮件交换记录)是指向邮件服务器的 DNS 记录。例如,www.devdungeon.com/的 MX 服务器是mail.devdungeon.comnet.LookupMX()函数执行此查找并返回一个net.MX结构体切片:

package main

import (
   "fmt"
   "log"
   "net"
   "os"
)

func main() {
   if len(os.Args) != 2 {
      log.Fatal("No domain name argument provided")
   }
   arg := os.Args[1]

   fmt.Println("Looking up MX records for " + arg)

   mxRecords, err := net.LookupMX(arg)
   if err != nil {
      log.Fatal(err)
   }
   for _, mxRecord := range mxRecords {
      fmt.Printf("Host: %s\tPreference: %d\n", mxRecord.Host,   
         mxRecord.Pref)
   }
}

查找主机名的 DNS 服务器

该程序将查找与给定主机名相关联的 DNS 服务器。这里的主要功能是net.LookupNS()

package main

import (
   "fmt"
   "log"
   "net"
   "os"
)

func main() {
   if len(os.Args) != 2 {
      log.Fatal("No domain name argument provided")
   }
   arg := os.Args[1]

   fmt.Println("Looking up nameservers for " + arg)

   nameservers, err := net.LookupNS(arg)
   if err != nil {
      log.Fatal(err)
   }
   for _, nameserver := range nameservers {
      fmt.Println(nameserver.Host)
   }
}

总结

阅读完本章后,您应该对数字取证调查的目标有了基本了解。每个主题都可以深入讨论,取证是一个专业领域,值得拥有自己的书籍,更不用说是一个章节了。

使用您阅读过的示例作为起点,思考一下如果您面对一台被攻破的机器,并且您的目标是找出攻击者如何入侵、发生的时间、他们访问了什么、修改了什么、动机是什么、泄露了多少数据以及您能够找到的其他信息,以便识别攻击者身份或其在系统上采取的行动。

一个熟练的对手会尽力掩盖自己的踪迹并避免被取证检测。因此,保持对最新工具和趋势的了解非常重要,这样在调查时你才能知道应该寻找哪些技巧和线索。

这些示例可以扩展、自动化,并集成到其他执行大规模取证搜索的应用程序中。借助 Go 语言的可扩展性,可以轻松创建一个工具,以高效的方式搜索整个文件系统或网络。

在下一章,我们将讨论如何使用 Go 进行数据包捕获。我们将从基本的内容开始,比如获取网络设备列表并将网络流量转储到文件中。接着,我们将讨论如何使用过滤器来查找特定的网络流量。此外,我们还将探讨使用 Go 接口解码和检查数据包的更高级技巧。我们还将介绍如何创建自定义数据包层以及从网络卡伪造和发送数据包,从而允许你发送任意数据包。

第五章:数据包捕获与注入

数据包捕获是监控通过网络传输的原始流量的过程。这适用于有线以太网和无线网络设备。tcpdumplibpcap 包是数据包捕获的标准。它们是在 1980 年代编写的,至今仍在使用。gopacket 包不仅封装了 C 库,还增加了 Go 的抽象层,使其更加符合 Go 语言的习惯并更便于使用。

pcap 库允许你收集关于网络设备的信息,读取从网络传输的数据包,从链路上 存储流量到 .pcap 文件,根据多个标准过滤流量,或者伪造自定义数据包并通过网络设备发送。对于 pcap 库,过滤是通过 伯克利数据包过滤器 (BPF) 完成的。

数据包捕获有无数的用途。它可以用来设置蜜罐并监控接收到的流量类型。它可以帮助法医调查,以确定哪些主机执行了恶意操作,哪些主机被利用。它可以协助识别网络中的瓶颈。它也可以被恶意使用,用于从无线网络窃取信息、执行数据包扫描、模糊测试、ARP 欺骗等攻击。

这些示例需要非 Go 依赖项和 libpcap 包,因此,运行起来可能会更具挑战性。如果你没有将 Linux 作为主要桌面操作系统,我强烈建议你使用 Ubuntu 或其他 Linux 发行版,并在虚拟机中运行这些示例,以获得最佳效果。

Tcpdump 是由 libpcap 的作者编写的应用程序。Tcpdump 提供了一个命令行工具来捕获数据包。这些示例将让你复制 tcpdump 包的功能,并将其嵌入到其他应用程序中。有些示例与 tcpdump 的现有功能非常相似,并且在适用的情况下,将提供 tcpdump 的示例用法。由于 gopackettcpdump 都依赖于相同的底层 libpcap 包,它们的文件格式是兼容的。你可以使用 tcpdump 捕获文件并使用 gopacket 读取,也可以使用 gopacket 捕获数据包并用任何支持 libpcap 的应用程序读取,比如 Wireshark。

gopacket 包的官方文档可以在 godoc.org/github.com/google/gopacket 查阅。

前提条件

在运行这些示例之前,你需要安装 libpcap。此外,我们还需要使用一个第三方 Go 包。幸运的是,这个包是由 Google 提供的,一个值得信赖的来源。Go 的 get 功能会下载并安装这个远程包。Git 也需要正确安装,才能使 go get 正常工作。

安装 libpcap 和 Git

libpcap 包依赖项并不是大多数系统默认安装的,每个操作系统的安装过程有所不同。这里将涵盖在 Ubuntu、Windows 和 macOS 上安装 libpcapgit 的步骤。我强烈建议你使用 Ubuntu 或其他 Linux 发行版,以获得最佳效果。没有 libpcapgopacket 将无法工作,而 git 则是获取 gopacket 依赖项所必需的。

在 Ubuntu 上安装 libpcap

在 Ubuntu 中,libpcap-0.8 已经默认安装。但为了安装 gopacket 库,你还需要开发包中的头文件。你可以通过 libpcap-dev 包来安装头文件。我们还将安装 git,因为在稍后安装 gopacket 时需要运行 go get 命令:

sudo apt-get install git libpcap-dev

在 Windows 上安装 libpcap

Windows 是最棘手的,且会出现最多问题。Windows 的实现支持不太好,效果可能因人而异。WinPcap 与 libpcap 兼容,示例中使用的源代码无需修改即可正常工作。在 Windows 中运行时,唯一明显的区别是网络设备的命名方式。

可以从 www.winpcap.org/ 获取 WinPcap 安装程序,这是一个必需组件。如果需要开发者包,可以从 www.winpcap.org/devel.htm 获取,其中包含 C 语言编写的头文件和示例程序。在大多数情况下,你不需要开发者包。Git 可以从 git-scm.com/download/win 下载。你还需要从 www.mingw.org 获取用于编译器的 MinGW。你需要确保 32 位和 64 位设置一致。你可以设置 GOARCH=386GOARCH=amd64 环境变量来切换 32 位和 64 位模式。

在 macOS 上安装 libpcap

在 macOS 中,libpcap 已经默认安装。你还需要 Git,可以通过 Homebrew 从 brew.sh 安装,或者通过 Git 包管理器安装,后者可以从 git-scm.com/downloads 获取。

安装 gopacket

在安装了 libpcapgit 包后,你可以从 GitHub 获取 gopacket 包:

go get github.com/google/gopacket  

权限问题

在 Linux 和 macOS 环境中执行程序时,如果尝试访问网络设备,可能会遇到权限问题。你可以使用 sudo 提升权限或将用户切换为 root,但不推荐这样做。

获取网络设备列表

pcap 库的一部分包含一个获取网络设备列表的功能。

该程序将简单地获取网络设备列表并列出其信息。在 Linux 中,常见的默认设备名称是eth0wlan0。在 Mac 上是en0。在 Windows 上,名称较长且不可读,因为它们代表的是唯一的 ID。你将在后续示例中使用设备名称作为字符串来标识要捕获的设备。如果你没有看到确切的设备列表,可能需要使用管理员权限(例如sudo)运行该示例。

用于列出设备的等效tcpdump命令如下:

tcpdump -D

你也可以使用以下命令:

tcpdump --list-interfaces

你还可以使用ifconfigip等工具来获取网络设备的名称:

package main

import (
   "fmt"
   "log"
   "github.com/google/gopacket/pcap"
)

func main() {
   // Find all devices
   devices, err := pcap.FindAllDevs()
   if err != nil {
      log.Fatal(err)
   }

   // Print device information
   fmt.Println("Devices found:")
   for _, device := range devices {
      fmt.Println("\nName: ", device.Name)
      fmt.Println("Description: ", device.Description)
      fmt.Println("Devices addresses: ", device.Description)
      for _, address := range device.Addresses {
         fmt.Println("- IP address: ", address.IP)
         fmt.Println("- Subnet mask: ", address.Netmask)
      }
   }
}

捕获数据包

以下程序演示了捕获数据包的基础知识。设备名称作为字符串传入。如果你不知道设备名称,可以使用之前的示例获取机器上可用设备的列表。如果没有看到准确列出的设备,可能需要提升权限并使用sudo运行该程序。

混杂模式是你可以启用的一种选项,用来监听那些不是为你的设备指定的数据包。混杂模式对于无线设备尤为重要,因为无线网络设备实际上具备接收空中广播的数据包的能力,这些数据包本应发送给其他接收者。

无线流量特别容易受到嗅探攻击,因为所有数据包都是通过空中广播的,而不是通过以太网进行传输,后者需要物理访问才能拦截流量。为顾客提供不加密的免费无线网络在咖啡馆等场所非常常见。这对客人很方便,但也会让你的信息面临风险。如果某个场所提供加密的无线网络,这并不意味着它就一定更安全。如果密码贴在墙上或者随便发放,那么任何拥有密码的人都可以解密无线流量。为了增强客用无线网络的安全性,常用的一种技术是捕获门户。捕获门户要求用户以某种方式进行身份验证,即使是作为访客,然后他们的会话会通过独立的加密进行隔离,这样其他人就无法解密。

提供完全未加密流量的无线接入点必须小心使用。如果你连接到一个传输敏感信息的网站,请确保该网站使用 HTTPS,以便你与访问的网络服务器之间的数据是加密的。VPN 连接也提供通过未加密通道的加密隧道。

有些网站由不知情或疏忽的程序员构建,他们没有在服务器上实现 SSL。有些网站只加密登录页面,以确保你的密码安全,但随后将会话 Cookie 以明文传递。这意味着任何能够捕获无线流量的人都可以看到会话 Cookie,并利用它冒充受害者与 Web 服务器交互。Web 服务器会把攻击者当作受害者已登录的用户。攻击者从未知道密码,但只要会话保持活动状态,就不需要密码。

有些网站没有会话过期时间,用户的会话将保持活动状态,直到显式退出。移动应用特别容易受到这种问题的影响,因为用户很少退出并重新登录应用程序。关闭应用并重新打开并不一定会创建一个新的会话。

这个示例将打开网络设备进行实时捕获,然后打印每个接收到的包的详细信息。程序将持续运行,直到程序通过Ctrl + C被终止:

package main

import (
   "fmt"
   "github.com/google/gopacket"
   "github.com/google/gopacket/pcap"
   "log"
   "time"
)

var (
   device            = "eth0"
   snapshotLen int32 = 1024
   promiscuous       = false
   err         error
   timeout     = 30 * time.Second
   handle      *pcap.Handle
)

func main() {
   // Open device
   handle, err = pcap.OpenLive(device, snapshotLen, promiscuous,  
      timeout)
   if err != nil {
      log.Fatal(err)
   }
   defer handle.Close()

   // Use the handle as a packet source to process all packets
   packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
   for packet := range packetSource.Packets() {
      // Process packet here
      fmt.Println(packet)
   }
}

使用过滤器进行捕获

以下程序演示了如何设置过滤器。过滤器使用 BPF 格式。如果你曾使用过 Wireshark,你可能已经熟悉过滤器。有很多过滤器选项可以进行逻辑组合。过滤器可以非常复杂,并且网上有很多常见过滤器和巧妙技巧的备忘单。以下是一些示例,帮助你了解一些非常基础的过滤器:

  • host 192.168.0.123

  • dst net 192.168.0.0/24

  • port 22

  • not broadcast and not multicast

前面的一些过滤器应该是显而易见的。host过滤器将只显示发送到或来自该主机的包。dst net过滤器将捕获发送到192.168.0.*地址的传入流量。port过滤器只关注端口22的流量。not broadcast and not multicast过滤器演示了如何否定并组合多个过滤器。过滤掉broadcastmulticast非常有用,因为它们往往会干扰捕获。

对于基本捕获,等效的tcpdump命令就是运行它并传递一个接口:

tcpdump -i eth0

如果你想应用过滤器,只需将其作为命令行参数传递,像这样:

tcpdump -i eth0 tcp port 80

这个示例使用了一个过滤器,它只会捕获80端口上的流量,这应该是 HTTP 流量。它并没有指定是本地端口还是远程端口为80,因此它会捕获任何进出端口80的流量。如果你在个人电脑上运行,可能没有运行 Web 服务器,所以它会捕获你通过浏览器产生的 HTTP 流量。如果你在 Web 服务器上运行该捕获,它会捕获传入的 HTTP 请求流量。

在此示例中,使用pcap.OpenLive()创建网络设备的句柄。在从设备读取数据包之前,通过handle.SetBPFFilter()设置过滤器,然后从句柄中读取数据包。关于过滤器的更多信息,请访问en.wikipedia.org/wiki/Berkeley_Packet_Filter

此示例打开网络设备进行实时捕获,然后使用SetBPFFilter()设置过滤器。在此案例中,我们将使用tcp and port 80过滤器来查找 HTTP 流量。所有捕获的数据包将打印到标准输出:

package main

import (
   "fmt"
   "github.com/google/gopacket"
   "github.com/google/gopacket/pcap"
   "log"
   "time"
)

var (
   device            = "eth0"
   snapshotLen int32 = 1024
   promiscuous       = false
   err         error
   timeout     = 30 * time.Second
   handle      *pcap.Handle
)

func main() {
   // Open device
   handle, err = pcap.OpenLive(device, snapshotLen, promiscuous,  
      timeout)
   if err != nil {
      log.Fatal(err)
   }
   defer handle.Close()

   // Set filter
   var filter string = "tcp and port 80" // or os.Args[1]
   err = handle.SetBPFFilter(filter)
   if err != nil {
      log.Fatal(err)
   }
   fmt.Println("Only capturing TCP port 80 packets.")

   packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
   for packet := range packetSource.Packets() {
      // Do something with a packet here.
      fmt.Println(packet)
   }
}

保存到 pcap 文件

该程序将执行数据包捕获并将结果存储到文件中。在此示例中,关键步骤是调用pcapgo包的WriterWriteFileHeader()函数。之后,可以使用WritePacket()函数将所需的数据包写入文件。如果需要,可以捕获所有流量并根据自己的过滤标准选择仅写入特定数据包。也许你只想将奇数或格式错误的数据包写入日志以记录异常。

要使用tcpdump实现相同的功能,只需传递-w标志和文件名,如以下命令所示:

tcpdump -i eth0 -w my_capture.pcap

使用此示例创建的 pcap 文件可以通过 Wireshark 打开,并像使用tcpdump创建的文件一样查看。

此示例创建一个名为test.pcap的输出文件,并打开网络设备进行实时捕获。它将 100 个数据包捕获到文件中,然后退出:

package main

import (
   "fmt"
   "os"
   "time"

   "github.com/google/gopacket"
   "github.com/google/gopacket/layers"
   "github.com/google/gopacket/pcap"
   "github.com/google/gopacket/pcapgo"
)

var (
   deviceName        = "eth0"
   snapshotLen int32 = 1024
   promiscuous       = false
   err         error
   timeout     = -1 * time.Second
   handle      *pcap.Handle
   packetCount = 0
)

func main() {
   // Open output pcap file and write header
   f, _ := os.Create("test.pcap")
   w := pcapgo.NewWriter(f)
   w.WriteFileHeader(uint32(snapshotLen), layers.LinkTypeEthernet)
   defer f.Close()

   // Open the device for capturing
   handle, err = pcap.OpenLive(deviceName, snapshotLen, promiscuous, 
      timeout)
   if err != nil {
      fmt.Printf("Error opening device %s: %v", deviceName, err)
      os.Exit(1)
   }
   defer handle.Close()

   // Start processing packets
   packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
   for packet := range packetSource.Packets() {
      // Process packet here
      fmt.Println(packet)
      w.WritePacket(packet.Metadata().CaptureInfo, packet.Data())
      packetCount++

      // Only capture 100 and then stop
      if packetCount > 100 {
         break
      }
   }
}

从 pcap 文件中读取

除了打开设备进行实时捕获外,你还可以打开一个 pcap 文件进行离线检查。获取句柄后,无论是通过pcap.OpenLive()还是pcap.OpenOffline()获得的,句柄的处理方式是相同的。创建句柄后,实时设备和捕获文件之间没有区别,唯一的区别是实时设备会继续传送数据包,而文件最终会结束。

你可以读取任何通过libpcap客户端(包括 Wireshark、tcpdump或其他gopacket应用程序)捕获的 pcap 文件。此示例使用pcap.OpenOffline()打开名为test.pcap的文件,然后使用range遍历数据包并打印基本的数据包信息。将文件名从test.pcap更改为你想要读取的任何文件:

package main

// Use tcpdump to create a test file
// tcpdump -w test.pcap
// or use the example above for writing pcap files

import (
   "fmt"
   "github.com/google/gopacket"
   "github.com/google/gopacket/pcap"
   "log"
)

var (
   pcapFile = "test.pcap"
   handle   *pcap.Handle
   err      error
)

func main() {
   // Open file instead of device
   handle, err = pcap.OpenOffline(pcapFile)
   if err != nil {
      log.Fatal(err)
   }
   defer handle.Close()

   // Loop through packets in file
   packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
   for packet := range packetSource.Packets() {
      fmt.Println(packet)
   }
}

解码数据包层

数据包可以通过packet.Layer()函数逐层解码。该程序将检查数据包,查找 TCP 流量,然后输出以太网层、IP 层、TCP 层和应用层信息。当到达应用层时,它会查找HTTP关键字,如果发现,则输出一条消息:

package main

import (
   "fmt"
   "github.com/google/gopacket"
   "github.com/google/gopacket/layers"
   "github.com/google/gopacket/pcap"
   "log"
   "strings"
   "time"
)

var (
   device            = "eth0"
   snapshotLen int32 = 1024
   promiscuous       = false
   err         error
   timeout     = 30 * time.Second
   handle      *pcap.Handle
)

func main() {
   // Open device
   handle, err = pcap.OpenLive(device, snapshotLen, promiscuous, 
      timeout)
   if err != nil {
      log.Fatal(err)
   }
   defer handle.Close()

   packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
   for packet := range packetSource.Packets() {
      printPacketInfo(packet)
   }
}

func printPacketInfo(packet gopacket.Packet) {
   // Let's see if the packet is an ethernet packet
   ethernetLayer := packet.Layer(layers.LayerTypeEthernet)
   if ethernetLayer != nil {
      fmt.Println("Ethernet layer detected.")
      ethernetPacket, _ := ethernetLayer.(*layers.Ethernet)
      fmt.Println("Source MAC: ", ethernetPacket.SrcMAC)
      fmt.Println("Destination MAC: ", ethernetPacket.DstMAC)
      // Ethernet type is typically IPv4 but could be ARP or other
      fmt.Println("Ethernet type: ", ethernetPacket.EthernetType)
      fmt.Println()
   }

   // Let's see if the packet is IP (even though the ether type told 
   //us)
   ipLayer := packet.Layer(layers.LayerTypeIPv4)
   if ipLayer != nil {
      fmt.Println("IPv4 layer detected.")
      ip, _ := ipLayer.(*layers.IPv4)

      // IP layer variables:
      // Version (Either 4 or 6)
      // IHL (IP Header Length in 32-bit words)
      // TOS, Length, Id, Flags, FragOffset, TTL, Protocol (TCP?),
      // Checksum, SrcIP, DstIP
      fmt.Printf("From %s to %s\n", ip.SrcIP, ip.DstIP)
      fmt.Println("Protocol: ", ip.Protocol)
      fmt.Println()
   }

   // Let's see if the packet is TCP
   tcpLayer := packet.Layer(layers.LayerTypeTCP)
   if tcpLayer != nil {
      fmt.Println("TCP layer detected.")
      tcp, _ := tcpLayer.(*layers.TCP)

      // TCP layer variables:
      // SrcPort, DstPort, Seq, Ack, DataOffset, Window, Checksum, 
      //Urgent
      // Bool flags: FIN, SYN, RST, PSH, ACK, URG, ECE, CWR, NS
      fmt.Printf("From port %d to %d\n", tcp.SrcPort, tcp.DstPort)
      fmt.Println("Sequence number: ", tcp.Seq)
      fmt.Println()
   }

   // Iterate over all layers, printing out each layer type
   fmt.Println("All packet layers:")
   for _, layer := range packet.Layers() {
      fmt.Println("- ", layer.LayerType())
   }

   // When iterating through packet.Layers() above,
   // if it lists Payload layer then that is the same as
   // this applicationLayer. applicationLayer contains the payload
   applicationLayer := packet.ApplicationLayer()
   if applicationLayer != nil {
      fmt.Println("Application layer/Payload found.")
      fmt.Printf("%s\n", applicationLayer.Payload())

      // Search for a string inside the payload
      if strings.Contains(string(applicationLayer.Payload()), "HTTP")    
      {
         fmt.Println("HTTP found!")
      }
   }

   // Check for errors
   if err := packet.ErrorLayer(); err != nil {
      fmt.Println("Error decoding some part of the packet:", err)
   }
}

创建自定义层

你不局限于最常见的层次,如以太网、IP 和 TCP。你可以创建自己的层次。对于大多数人来说,这种功能的使用范围有限,但在某些极其罕见的情况下,替换 TCP 层为定制的层,以满足特定需求,可能是有意义的。

这个示例演示了如何创建一个自定义层。这对于实现gopacket/layers包中未包含的协议非常有用。gopacket已包含超过 100 种层类型。你可以在任何层次创建自定义层。

这段代码做的第一件事是定义一个自定义数据结构来表示我们的层。该数据结构不仅保存我们的自定义数据(SomeByteAnotherByte),还需要一个字节切片来存储其余的实际有效负载以及任何其他层(restOfData):

package main

import (
   "fmt"
   "github.com/google/gopacket"
)

// Create custom layer structure
type CustomLayer struct {
   // This layer just has two bytes at the front
   SomeByte    byte
   AnotherByte byte
   restOfData  []byte
}

// Register the layer type so we can use it
// The first argument is an ID. Use negative
// or 2000+ for custom layers. It must be unique
var CustomLayerType = gopacket.RegisterLayerType(
   2001,
   gopacket.LayerTypeMetadata{
      "CustomLayerType",
      gopacket.DecodeFunc(decodeCustomLayer),
   },
)

// When we inquire about the type, what type of layer should
// we say it is? We want it to return our custom layer type
func (l CustomLayer) LayerType() gopacket.LayerType {
   return CustomLayerType
}

// LayerContents returns the information that our layer
// provides. In this case it is a header layer so
// we return the header information
func (l CustomLayer) LayerContents() []byte {
   return []byte{l.SomeByte, l.AnotherByte}
}

// LayerPayload returns the subsequent layer built
// on top of our layer or raw payload
func (l CustomLayer) LayerPayload() []byte {
   return l.restOfData
}

// Custom decode function. We can name it whatever we want
// but it should have the same arguments and return value
// When the layer is registered we tell it to use this decode function
func decodeCustomLayer(data []byte, p gopacket.PacketBuilder) error {
   // AddLayer appends to the list of layers that the packet has
   p.AddLayer(&CustomLayer{data[0], data[1], data[2:]})

   // The return value tells the packet what layer to expect
   // with the rest of the data. It could be another header layer,
   // nothing, or a payload layer.

   // nil means this is the last layer. No more decoding
   // return nil
   // Returning another layer type tells it to decode
   // the next layer with that layer's decoder function
   // return p.NextDecoder(layers.LayerTypeEthernet)

   // Returning payload type means the rest of the data
   // is raw payload. It will set the application layer
   // contents with the payload
   return p.NextDecoder(gopacket.LayerTypePayload)
}

func main() {
   // If you create your own encoding and decoding you can essentially
   // create your own protocol or implement a protocol that is not
   // already defined in the layers package. In our example we are    
   // just wrapping a normal ethernet packet with our own layer.
   // Creating your own protocol is good if you want to create
   // some obfuscated binary data type that was difficult for others
   // to decode. Finally, decode your packets:
   rawBytes := []byte{0xF0, 0x0F, 65, 65, 66, 67, 68}
   packet := gopacket.NewPacket(
      rawBytes,
      CustomLayerType,
      gopacket.Default,
   )
   fmt.Println("Created packet out of raw bytes.")
   fmt.Println(packet)

   // Decode the packet as our custom layer
   customLayer := packet.Layer(CustomLayerType)
   if customLayer != nil {
      fmt.Println("Packet was successfully decoded.")
      customLayerContent, _ := customLayer.(*CustomLayer)
      // Now we can access the elements of the custom struct
      fmt.Println("Payload: ", customLayerContent.LayerPayload())
      fmt.Println("SomeByte element:", customLayerContent.SomeByte)
      fmt.Println("AnotherByte element:",  
         customLayerContent.AnotherByte)
   }
}

字节与数据包之间的转换

在某些情况下,可能有原始字节,你想将其转换为数据包,或者反之亦然。这个示例创建了一个简单的数据包,然后获取组成该数据包的原始字节。原始字节随后被转换回数据包,以演示这个过程。

在这个示例中,我们将使用gopacket.SerializeLayers()创建并序列化一个数据包。该数据包由几个层次组成:以太网、IP、TCP 和有效负载。在序列化过程中,如果任何数据包返回 nil,这意味着它无法解码为正确的层(格式错误或不正确的数据包类型)。在将数据包序列化到缓冲区后,我们将通过buffer.Bytes()获取组成数据包的原始字节的副本。借助这些原始字节,我们可以使用gopacket.NewPacket()逐层解码数据。通过利用SerializeLayers(),你可以将数据包结构体转换为原始字节,使用gopacket.NewPacket(),你可以将原始字节转换回结构化数据。

NewPacket()将原始字节作为第一个参数。第二个参数是你想解码的最低层次,它会解码该层及其之上的所有层。NewPacket()的第三个参数是解码类型,必须是以下之一:

  • gopacket.Default:这是一次性解码所有内容,最安全的方法。

  • gopacket.Lazy:这是按需解码,但它不是并发安全的。

  • gopacket.NoCopy:这将不会创建缓冲区的副本。仅当你可以保证内存中的数据包数据不会改变时,才使用它。

下面是将数据包结构体转换为字节并再次转换回数据包的完整代码:

package main

import (
   "fmt"
   "github.com/google/gopacket"
   "github.com/google/gopacket/layers"
)

func main() {
   payload := []byte{2, 4, 6}
   options := gopacket.SerializeOptions{}
   buffer := gopacket.NewSerializeBuffer()
   gopacket.SerializeLayers(buffer, options,
      &layers.Ethernet{},
      &layers.IPv4{},
      &layers.TCP{},
      gopacket.Payload(payload),
   )
   rawBytes := buffer.Bytes()

   // Decode an ethernet packet
   ethPacket :=
      gopacket.NewPacket(
         rawBytes,
         layers.LayerTypeEthernet,
         gopacket.Default,
      )

   // with Lazy decoding it will only decode what it needs when it 
   //needs it
   // This is not concurrency safe. If using concurrency, use default
   ipPacket :=
      gopacket.NewPacket(
         rawBytes,
         layers.LayerTypeIPv4,
         gopacket.Lazy,
      )

   // With the NoCopy option, the underlying slices are referenced
   // directly and not copied. If the underlying bytes change so will
   // the packet
   tcpPacket :=
      gopacket.NewPacket(
         rawBytes,
         layers.LayerTypeTCP,
         gopacket.NoCopy,
      )

   fmt.Println(ethPacket)
   fmt.Println(ipPacket)
   fmt.Println(tcpPacket)
}

创建和发送数据包

这个示例做了几件事。首先,它会展示如何使用网络设备发送原始字节,因此你几乎可以像串行连接一样使用它来发送数据。这对于低级别的数据传输非常有用,但如果你想与应用程序交互,你可能想构建一个其他硬件和软件可以识别的数据包。

接下来它会展示如何创建一个包含以太网、IP 和 TCP 层的数据包。不过,这些层都是默认的且为空的,所以它实际上并没有做什么。

最后,我们将创建另一个数据包,但这次我们会为以太网层填入一些 MAC 地址,为 IPv4 填入一些 IP 地址,为 TCP 层填入端口号。你应该能看到如何伪造数据包并模拟设备。

TCP 层结构有布尔字段,用于SYNFINACK标志,这些标志可以读取或设置。这对于操作和模糊化 TCP 握手、会话以及端口扫描非常有用。

pcap库提供了一个简单的发送字节的方式,而gopacket中的layers包帮助我们为各层创建字节结构。

以下是此示例的代码实现:

package main

import (
   "github.com/google/gopacket"
   "github.com/google/gopacket/layers"
   "github.com/google/gopacket/pcap"
   "log"
   "net"
   "time"
)

var (
   device            = "eth0"
   snapshotLen int32 = 1024
   promiscuous       = false
   err         error
   timeout     = 30 * time.Second
   handle      *pcap.Handle
   buffer      gopacket.SerializeBuffer
   options     gopacket.SerializeOptions
)

func main() {
   // Open device
   handle, err = pcap.OpenLive(device, snapshotLen, promiscuous, 
      timeout)
   if err != nil {
      log.Fatal("Error opening device. ", err)
   }
   defer handle.Close()

   // Send raw bytes over wire
   rawBytes := []byte{10, 20, 30}
   err = handle.WritePacketData(rawBytes)
   if err != nil {
      log.Fatal("Error writing bytes to network device. ", err)
   }

   // Create a properly formed packet, just with
   // empty details. Should fill out MAC addresses,
   // IP addresses, etc.
   buffer = gopacket.NewSerializeBuffer()
   gopacket.SerializeLayers(buffer, options,
      &layers.Ethernet{},
      &layers.IPv4{},
      &layers.TCP{},
      gopacket.Payload(rawBytes),
   )
   outgoingPacket := buffer.Bytes()
   // Send our packet
   err = handle.WritePacketData(outgoingPacket)
   if err != nil {
      log.Fatal("Error sending packet to network device. ", err)
   }

   // This time lets fill out some information
   ipLayer := &layers.IPv4{
      SrcIP: net.IP{127, 0, 0, 1},
      DstIP: net.IP{8, 8, 8, 8},
   }
   ethernetLayer := &layers.Ethernet{
      SrcMAC: net.HardwareAddr{0xFF, 0xAA, 0xFA, 0xAA, 0xFF, 0xAA},
      DstMAC: net.HardwareAddr{0xBD, 0xBD, 0xBD, 0xBD, 0xBD, 0xBD},
   }
   tcpLayer := &layers.TCP{
      SrcPort: layers.TCPPort(4321),
      DstPort: layers.TCPPort(80),
   }
   // And create the packet with the layers
   buffer = gopacket.NewSerializeBuffer()
   gopacket.SerializeLayers(buffer, options,
      ethernetLayer,
      ipLayer,
      tcpLayer,
      gopacket.Payload(rawBytes),
   )
   outgoingPacket = buffer.Bytes()
}

更快速地解码数据包

如果我们知道预期的层级,我们可以使用现有结构来存储数据包信息,而不是为每个数据包创建新的结构,这样既能节省时间,也能节省内存。使用DecodingLayerParser会更快,这就像是数据的编组和解编组。

本示例演示了如何在程序开始时创建层变量,并反复使用相同的变量,而不是为每个数据包创建新的变量。通过gopacket.NewDecodingLayerParser()创建一个解析器,并提供我们想要使用的层变量。这里的一个注意事项是,它仅解码你最初创建的层类型。

以下是此示例的代码实现:

package main

import (
   "fmt"
   "github.com/google/gopacket"
   "github.com/google/gopacket/layers"
   "github.com/google/gopacket/pcap"
   "log"
   "time"
)

var (
   device            = "eth0"
   snapshotLen int32 = 1024
   promiscuous       = false
   err         error
   timeout     = 30 * time.Second
   handle      *pcap.Handle
   // Reuse these for each packet
   ethLayer layers.Ethernet
   ipLayer  layers.IPv4
   tcpLayer layers.TCP
)

func main() {
   // Open device
   handle, err = pcap.OpenLive(device, snapshotLen, promiscuous, 
   timeout)
   if err != nil {
      log.Fatal(err)
   }
   defer handle.Close()

   packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
   for packet := range packetSource.Packets() {
      parser := gopacket.NewDecodingLayerParser(
         layers.LayerTypeEthernet,
         &ethLayer,
         &ipLayer,
         &tcpLayer,
      )
      foundLayerTypes := []gopacket.LayerType{}

      err := parser.DecodeLayers(packet.Data(), &foundLayerTypes)
      if err != nil {
         fmt.Println("Trouble decoding layers: ", err)
      }

      for _, layerType := range foundLayerTypes {
         if layerType == layers.LayerTypeIPv4 {
            fmt.Println("IPv4: ", ipLayer.SrcIP, "->", ipLayer.DstIP)
         }
         if layerType == layers.LayerTypeTCP {
            fmt.Println("TCP Port: ", tcpLayer.SrcPort,               
               "->", tcpLayer.DstPort)
            fmt.Println("TCP SYN:", tcpLayer.SYN, " | ACK:", 
               tcpLayer.ACK)
         }
      }
   }
}

总结

阅读完这一章后,你应该对gopacket包有了很好的理解。你应该能够使用本章中的示例编写一个简单的数据包捕获应用程序。再次强调,这并不是要记住所有的函数或关于各层的细节,重要的是要从高层次理解整体框架,并在开发和实现应用程序时能够回忆起可用的工具。

尝试根据这些示例编写你自己的程序,以捕获来自你计算机的有趣网络流量。尝试捕获并检查特定端口或应用程序,看看它在网络上传输的方式。观察使用加密的应用程序与通过明文传输数据的应用程序之间的差异。你可能还想捕获后台所有的流量,看看即使你在计算机空闲时,哪些应用程序在网络上很活跃。

使用gopacket库可以构建各种有用的工具。除了基本的数据包捕获以供后续查看外,你还可以实现一个监控系统,当检测到流量突然激增时进行警报,或者用于识别异常流量。

因为gopacket库也可以用来发送数据包,所以可以创建一个高度定制化的端口扫描器。你可以构造原始数据包来执行仅进行 TCP SYN 扫描的操作,这种扫描中连接从未完全建立;XMAS 扫描,所有标志位都会被打开;NULL 扫描,所有字段都设置为 null;以及其他各种扫描,需要对发送的数据包进行完全控制,包括故意发送格式错误的数据包。你还可以构建模糊测试工具,向网络服务发送不良数据包,以查看它的反应。所以,看看你能想到哪些创意吧。

在下一章,我们将探讨 Go 中的密码学。我们将从哈希算法、校验和、以及安全存储密码开始。然后我们将讨论对称加密和非对称加密,它们是什么,有什么不同,为什么它们有用,以及如何在 Go 中使用它们。我们还将研究如何创建带有证书的加密服务器,以及如何使用加密客户端进行连接。理解密码学的应用对于现代安全至关重要,因此我们将重点讨论最常见和最实际的使用案例。

第六章:密码学

密码学是保障通信安全的实践,即使第三方可以查看这些通信。它包括双向对称和非对称加密方法,以及单向哈希算法。

加密是现代互联网的关键部分。借助像 LetsEncrypt.com 这样的服务,所有人都能获得受信任的 SSL 证书。我们的整个基础设施依赖并信任加密来确保所有机密数据的安全。正确地加密和哈希数据非常重要,且容易配置错误,导致服务漏洞或暴露。

本章涵盖以下内容的示例和用例:

  • 对称和非对称加密

  • 签名和验证消息

  • 哈希

  • 安全存储密码

  • 生成安全的随机数

  • 创建和使用 TLS/SSL 证书

哈希

哈希是将一个可变长度的消息转换为一个唯一的固定长度的字母数字字符串。有多种哈希算法可供选择,例如 MD5 和 SHA1。哈希是单向且不可逆的,不像对称加密函数(如 AES),后者如果有密钥就可以恢复原始消息。因为哈希无法反转,大多数哈希会被暴力破解。攻击者会利用多个 GPU 构建高功耗的计算设备,通过暴力穷举每一种可能的字符组合,直到找到匹配的哈希值。他们还会生成彩虹表或包含所有哈希输出的文件,以便快速查找。

为哈希添加盐值是很重要的原因。加盐是将一个随机字符串附加到用户提供的密码后面的过程,以增加更多的随机性或熵值。考虑一个存储用户登录信息和哈希密码以进行身份验证的应用程序。如果两个用户使用相同的密码,他们的哈希结果将是相同的。如果没有加盐,攻击者可能会发现多个使用相同密码的用户,并且只需要破解一次哈希值。通过为每个用户的密码添加唯一的盐值,你可以确保每个用户的哈希值都是唯一的。加盐减少了彩虹表的有效性,因为即使攻击者知道与每个哈希对应的盐值,他们也必须为每个盐值生成一个彩虹表,而这需要大量的时间。

哈希常用于验证密码。另一个常见的用途是文件完整性。大型下载通常会附带文件的 MD5 或 SHA1 哈希。下载后,你可以对文件进行哈希检查,确保它与预期值匹配。如果不匹配,那么下载的文件可能已被篡改。哈希还常用于记录妥协指标或 IOC(Indicators of Compromise)。已知的恶意或危险文件会被哈希,并将该哈希值存储在目录中。这些通常会公开分享,以便人们将可疑文件与已知风险进行对比。存储并比较哈希值比存储整个文件更高效。

哈希小文件

如果文件足够小,可以容纳在内存中,那么 ReadFile() 方法会很快工作。它将整个文件加载到内存中,然后进行数据摘要。为了演示,使用多种不同的哈希算法计算摘要值:

package main

import (
   "crypto/md5"
   "crypto/sha1"
   "crypto/sha256"
   "crypto/sha512"
   "fmt"
   "io/ioutil"
   "log"
   "os"
)

func printUsage() {
   fmt.Println("Usage: " + os.Args[0] + " <filepath>")
   fmt.Println("Example: " + os.Args[0] + " document.txt")
}

func checkArgs() string {
   if len(os.Args) < 2 {
      printUsage()
      os.Exit(1)
   }
   return os.Args[1]
}

func main() {
   filename := checkArgs()

   // Get bytes from file
   data, err := ioutil.ReadFile(filename)
   if err != nil {
      log.Fatal(err)
   }

   // Hash the file and output results
   fmt.Printf("Md5: %x\n\n", md5.Sum(data))
   fmt.Printf("Sha1: %x\n\n", sha1.Sum(data))
   fmt.Printf("Sha256: %x\n\n", sha256.Sum256(data))
   fmt.Printf("Sha512: %x\n\n", sha512.Sum512(data))
}

哈希大文件

在之前的哈希示例中,整个待哈希的文件在哈希处理前被加载到内存中。当文件达到一定大小时,这种做法既不实际也不可能。物理内存的限制将发挥作用。因为哈希是作为块加密实现的,它将逐块处理,而不需要一次性加载整个文件到内存中:

package main

import (
   "crypto/md5"
   "fmt"
   "io"
   "log"
   "os"
)

func printUsage() {
   fmt.Println("Usage: " + os.Args[0] + " <filename>")
   fmt.Println("Example: " + os.Args[0] + " diskimage.iso")
}

func checkArgs() string {
   if len(os.Args) < 2 {
      printUsage()
      os.Exit(1)
   }
   return os.Args[1]
}

func main() {
   filename := checkArgs()

   // Open file for reading
   file, err := os.Open(filename)
   if err != nil {
      log.Fatal(err)
   }
   defer file.Close()

   // Create new hasher, which is a writer interface
   hasher := md5.New()

   // Default buffer size for copying is 32*1024 or 32kb per copy
   // Use io.CopyBuffer() if you want to specify the buffer to use
   // It will write 32kb at a time to the digest/hash until EOF
   // The hasher implements a Write() function making it satisfy
   // the writer interface. The Write() function performs the digest
   // at the time the data is copied/written to it. It digests
   // and processes the hash one chunk at a time as it is received.
   _, err = io.Copy(hasher, file)
   if err != nil {
      log.Fatal(err)
   }

   // Now get the final sum or checksum.
   // We pass nil to the Sum() function because
   // we already copied the bytes via the Copy to the
   // writer interface and don't need to pass any new bytes
   checksum := hasher.Sum(nil)

   fmt.Printf("Md5 checksum: %x\n", checksum)
}

安全存储密码

现在我们知道如何进行哈希处理后,可以讨论如何安全地存储密码。哈希处理在保护密码时非常重要。其他重要因素包括加盐、使用加密强度高的哈希函数,以及可选使用 基于哈希的消息认证码HMAC),它们都会将额外的秘密密钥加入到哈希算法中。

HMAC 是一个额外的层,它使用一个秘密密钥;因此,即使攻击者获得了包含盐值的哈希密码数据库,没有秘密密钥他们也会很难破解这些密码。秘密密钥应存储在单独的位置,例如环境变量,而不是与哈希密码和盐值一起存储在数据库中。

这个示例应用本身用途有限。可以作为你自己应用的参考。

package main

import (
   "crypto/hmac"
   "crypto/rand"
   "crypto/sha256"
   "encoding/base64"
   "encoding/hex"
   "fmt"
   "io"
   "os"
)

func printUsage() {
   fmt.Println("Usage: " + os.Args[0] + " <password>")
   fmt.Println("Example: " + os.Args[0] + " Password1!")
}

func checkArgs() string {
   if len(os.Args) < 2 {
      printUsage()
      os.Exit(1)
   }
   return os.Args[1]
}

// secretKey should be unique, protected, private,
// and not hard-coded like this. Store in environment var
// or in a secure configuration file.
// This is an arbitrary key that should only be used 
// for example purposes.
var secretKey = "neictr98y85klfgneghre"

// Create a salt string with 32 bytes of crypto/rand data
func generateSalt() string {
   randomBytes := make([]byte, 32)
   _, err := rand.Read(randomBytes)
   if err != nil {
      return ""
   }
   return base64.URLEncoding.EncodeToString(randomBytes)
}

// Hash a password with the salt
func hashPassword(plainText string, salt string) string {
   hash := hmac.New(sha256.New, []byte(secretKey))
   io.WriteString(hash, plainText+salt)
   hashedValue := hash.Sum(nil)
   return hex.EncodeToString(hashedValue)
}

func main() {
   // Get the password from command line argument
   password := checkArgs()
   salt := generateSalt()
   hashedPassword := hashPassword(password, salt)
   fmt.Println("Password: " + password)
   fmt.Println("Salt: " + salt)
   fmt.Println("Hashed password: " + hashedPassword)
}

加密

加密与哈希不同,因为加密是可逆的,原始消息可以被恢复。有些对称加密方法使用密码或共享密钥进行加密和解密。还有一些非对称加密算法使用公钥和私钥对来操作。AES 是对称加密的一个例子,它用于加密 ZIP 文件、PDF 文件或整个文件系统。RSA 是非对称加密的一个例子,它用于 SSL、SSH 密钥和 PGP。

加密安全伪随机数生成器(CSPRNG)

mathrand 包提供的随机性不如 crypto/rand 包。不要在加密应用中使用 math/rand

了解更多关于 Go 的 crypto/rand 包的信息,请访问 golang.org/pkg/crypto/rand/

以下示例将演示如何生成随机字节、随机整数或任何其他带符号或无符号类型的整数:

package main

import (
   "crypto/rand"
   "encoding/binary"
   "fmt"
   "log"
   "math"
   "math/big"
)

func main() {
   // Generate a random int
   limit := int64(math.MaxInt64) // Highest random number allowed
   randInt, err := rand.Int(rand.Reader, big.NewInt(limit))
   if err != nil {
      log.Fatal(err)
   }
   fmt.Println("Random int value: ", randInt)

   // Alternatively, you could generate the random bytes
   // and turn them into the specific data type needed.
   // binary.Read() will only read enough bytes to fill the data type
   var number uint32
   err = binary.Read(rand.Reader, binary.BigEndian, &number)
   if err != nil {
      log.Fatal(err)
   }
   fmt.Println("Random uint32 value: ", number)

   // Or just generate a random byte slice
   numBytes := 4
   randomBytes := make([]byte, numBytes)
   rand.Read(randomBytes)
   fmt.Println("Random byte values: ", randomBytes)
}

对称加密

对称加密是指使用相同的密钥或密码来加密和解密数据。高级加密标准(AES),也称为 Rijndael,是由 NIST 于 2001 年标准化的对称加密算法。

数据加密标准(DES)是另一种对称加密算法,比 AES 更老且不那么安全。除非有特定的要求或规范,否则不应使用 DES 来替代 AES。Go 标准库包含 AES 和 DES 包。

AES

这个程序将使用一个密钥对文件进行加密和解密,该密钥本质上是一个 32 字节(256 位)的密码。

在生成密钥、加密或解密时,输出通常会被发送到STDOUT或终端。你可以使用>运算符轻松地将输出重定向到文件或其他程序。参考使用模式以获取示例。如果你需要将密钥或加密后的数据存储为 ASCII 编码的字符串,可以使用 base64 编码。

在这个示例中,你将看到信息被分成两个部分:初始化向量(IV)和密文。初始化向量(IV)是一个随机值,会被添加到实际的加密信息前面。每次使用 AES 加密信息时,都会生成一个随机值并作为加密的一部分。这个随机值被称为 nonce,意味着它只是一个仅使用一次的数字。

为什么这些一次性值会被创建?特别是,如果它们不是保密的,并且直接放在加密信息前面,这样做有什么意义?随机的初始化向量(IV)类似于盐值(salt)。它的主要作用是确保当相同的信息被反复加密时,每次生成的密文都不同。

要使用Galois/计数器模式GCM)代替 CFB,请更改加密和解密方法。GCM 具有更好的性能和效率,因为它支持并行处理。可以在en.wikipedia.org/wiki/Galois/Counter_Mode上了解更多关于 GCM 的信息。

从 AES 密码开始,调用cipher.NewCFBEncrypter(block, iv)。然后,根据你是需要加密还是解密,你将调用.Seal()并传入你生成的 nonce,或者调用.Open()并传入分离的 nonce 和密文:

package main

import (
   "crypto/aes"
   "crypto/cipher"
   "crypto/rand"
   "fmt"
   "io"
   "io/ioutil"
   "os"
   "log"
)

func printUsage() {
   fmt.Printf(os.Args[0] + `

Encrypt or decrypt a file using AES with a 256-bit key file.
This program can also generate 256-bit keys.

Usage:
  ` + os.Args[0] + ` [-h|--help]
  ` + os.Args[0] + ` [-g|--genkey]
  ` + os.Args[0] + ` <keyFile> <file> [-d|--decrypt]

Examples:
  # Generate a 32-byte (256-bit) key
  ` + os.Args[0] + ` --genkey

  # Encrypt with secret key. Output to STDOUT
  ` + os.Args[0] + ` --genkey > secret.key

  # Encrypt message using secret key. Output to ciphertext.dat
  ` + os.Args[0] + ` secret.key message.txt > ciphertext.dat

  # Decrypt message using secret key. Output to STDOUT
  ` + os.Args[0] + ` secret.key ciphertext.dat -d

  # Decrypt message using secret key. Output to message.txt
  ` + os.Args[0] + ` secret.key ciphertext.dat -d > cleartext.txt
`)
}

// Check command-line arguments.
// If the help or generate key functions are chosen
// they are run and then the program exits
// otherwise it returns keyFile, file, decryptFlag.
func checkArgs() (string, string, bool) {
   if len(os.Args) < 2  || len(os.Args) > 4 {
      printUsage()
      os.Exit(1)
   }

   // One arg provided
   if len(os.Args) == 2 {
      // Only -h, --help and --genkey are valid one-argument uses
      if os.Args[1] == "-h" || os.Args[1] == "--help" {
         printUsage() // Print help text
         os.Exit(0) // Exit gracefully no error
      }
      if os.Args[1] == "-g" || os.Args[1] == "--genkey" {
         // Generate a key and print to STDOUT
         // User should redirect output to a file if needed
         key := generateKey()
         fmt.Printf(string(key[:])) // No newline
         os.Exit(0) // Exit gracefully
      }
   }

   // The only use options left is
   // encrypt <keyFile> <file> [-d|--decrypt]
   // If there are only 2 args provided, they must be the
   // keyFile and file without a decrypt flag.
   if len(os.Args) == 3 {
      // keyFile, file, decryptFlag
      return os.Args[1], os.Args[2], false 
   }
   // If 3 args are provided,
   // check that the last one is -d or --decrypt
   if len(os.Args) == 4 {
      if os.Args[3] != "-d" && os.Args[3] != "--decrypt" {
         fmt.Println("Error: Unknown usage.")
         printUsage()
         os.Exit(1) // Exit with error code
      }
      return os.Args[1], os.Args[2], true
   }
    return "", "", false // Default blank return
}

func generateKey() []byte {
   randomBytes := make([]byte, 32) // 32 bytes, 256 bit
   numBytesRead, err := rand.Read(randomBytes)
   if err != nil {
      log.Fatal("Error generating random key.", err)
   }
   if numBytesRead != 32 {
      log.Fatal("Error generating 32 random bytes for key.")
   }
   return randomBytes
}

// AES encryption
func encrypt(key, message []byte) ([]byte, error) {
   // Initialize block cipher
   block, err := aes.NewCipher(key)
   if err != nil {
      return nil, err
   }

   // Create the byte slice that will hold encrypted message
   cipherText := make([]byte, aes.BlockSize+len(message))

   // Generate the Initialization Vector (IV) nonce
   // which is stored at the beginning of the byte slice
   // The IV is the same length as the AES blocksize
   iv := cipherText[:aes.BlockSize]
   _, err = io.ReadFull(rand.Reader, iv)
   if err != nil {
      return nil, err
   }

   // Choose the block cipher mode of operation
   // Using the cipher feedback (CFB) mode here.
   // CBCEncrypter also available.
   cfb := cipher.NewCFBEncrypter(block, iv)
   // Generate the encrypted message and store it
   // in the remaining bytes after the IV nonce
   cfb.XORKeyStream(cipherText[aes.BlockSize:], message)

   return cipherText, nil
}

// AES decryption
func decrypt(key, cipherText []byte) ([]byte, error) {
   // Initialize block cipher
   block, err := aes.NewCipher(key)
   if err != nil {
      return nil, err
   }

   // Separate the IV nonce from the encrypted message bytes
   iv := cipherText[:aes.BlockSize]
   cipherText = cipherText[aes.BlockSize:]

   // Decrypt the message using the CFB block mode
   cfb := cipher.NewCFBDecrypter(block, iv)
   cfb.XORKeyStream(cipherText, cipherText)

   return cipherText, nil
}

func main() {
   // if generate key flag, just output a key to stdout and exit
   keyFile, file, decryptFlag := checkArgs()

   // Load key from file
   keyFileData, err := ioutil.ReadFile(keyFile)
   if err != nil {
      log.Fatal("Unable to read key file contents.", err)
   }

   // Load file to be encrypted or decrypted
   fileData, err := ioutil.ReadFile(file)
   if err != nil {
      log.Fatal("Unable to read key file contents.", err)
   }

   // Perform encryption unless the decryptFlag was provided
   // Outputs to STDOUT. User can redirect output to file.
   if decryptFlag {
      message, err := decrypt(keyFileData, fileData)
      if err != nil {
         log.Fatal("Error decrypting. ", err)
      }
      fmt.Printf("%s", message)
   } else {
      cipherText, err := encrypt(keyFileData, fileData)
      if err != nil {
         log.Fatal("Error encrypting. ", err)
      }
      fmt.Printf("%s", cipherText)
   }
}

非对称加密

非对称加密是指每一方都有两个密钥。每一方都需要一对公钥和私钥。非对称加密算法包括 RSA、DSA 和 ECDSA。Go 标准库提供了 RSA、DSA 和 ECDSA 的包。使用非对称加密的应用程序包括安全外壳协议SSH)、安全套接层SSL)和非常好的隐私PGP)。

SSL 是 安全套接字层,最初由 Netscape 开发,版本 2 于 1995 年公开发布。它用于加密服务器与客户端之间的通信,提供机密性、完整性和认证功能。TLS(传输层安全性)是 SSL 的新版本,1.2 版本于 2008 年作为 RFC 5246 定义。Go 的 TLS 包并没有完全实现该规范,但它实现了主要部分。阅读更多关于 Go 的 crypto/tls 包的信息,请访问 golang.org/pkg/crypto/tls/

你只能加密小于密钥大小的内容,通常为 2048 位。因此,由于这个大小限制,非对称 RSA 加密不适合加密整个文档,因为文档容易超过 2048 位或 256 字节。另一方面,对称加密(如 AES)可以加密大文档,但它需要双方共享一个密钥。TLS/SSL 使用非对称加密和对称加密的结合。初始连接和握手使用非对称加密,涉及双方的公钥和私钥。一旦连接建立,就会生成并共享一个共享密钥。共享密钥一旦被双方知晓,非对称加密就会被弃用,接下来的通信将使用对称加密(如 AES),并使用共享密钥进行加密。

这里的示例将使用 RSA 密钥。我们将介绍如何生成自己的公钥和私钥并将它们保存为 PEM 编码文件,数字签名消息并验证签名。在接下来的部分中,我们将使用这些密钥创建自签名证书并建立安全的 TLS 连接。

生成公钥和私钥对

在使用非对称加密之前,你需要一个公钥和私钥对。私钥必须保密,不能与任何人共享。公钥应该与他人共享。

RSARivest-Shamir-Adleman)和 ECDSA椭圆曲线数字签名算法)算法在 Go 标准库中可用。ECDSA 被认为更安全,但 RSA 是 SSL 证书中最常用的算法。

你可以选择为你的私钥设置密码保护。虽然不是必需的,但它提供了一层额外的安全保障。由于私钥非常敏感,建议使用密码保护。

如果你希望使用对称加密算法(如 AES)来为你的私钥文件设置密码保护,可以使用一些标准库函数。你需要使用的主要函数是 x509.EncryptPEMBlock()x509.DecryptPEMBlock()x509.IsEncryptedPEMBlock()

要执行相当于使用 OpenSSL 生成私钥和公钥文件的操作,请使用以下命令:

# Generate the private key  
openssl genrsa -out priv.pem 2048 
# Extract the public key from the private key 
openssl rsa -in priv.pem -pubout -out public.pem 

你可以通过 golang.org/pkg/encoding/pem/ 了解更多关于 Go 中 PEM 编码的内容。参考以下代码:

package main

import (
   "crypto/rand"
   "crypto/rsa"
   "crypto/x509"
   "encoding/pem"
   "fmt"
   "log"
   "os"
   "strconv"
)

func printUsage() {
   fmt.Printf(os.Args[0] + `

Generate a private and public RSA keypair and save as PEM files.
If no key size is provided, a default of 2048 is used.

Usage:
  ` + os.Args[0] + ` <private_key_filename> <public_key_filename>       [keysize]

Examples:
  # Store generated private and public key in privkey.pem and   pubkey.pem
  ` + os.Args[0] + ` priv.pem pub.pem
  ` + os.Args[0] + ` priv.pem pub.pem 4096`)
}

func checkArgs() (string, string, int) {
   // Too many or too few arguments
   if len(os.Args) < 3 || len(os.Args) > 4 {
      printUsage()
      os.Exit(1)
   }

   defaultKeySize := 2048

   // If there are 2 args provided, privkey and pubkey filenames
   if len(os.Args) == 3 {
      return os.Args[1], os.Args[2], defaultKeySize
   }

   // If 3 args provided, privkey, pubkey, keysize
   if len(os.Args) == 4 {
      keySize, err := strconv.Atoi(os.Args[3])
      if err != nil {
         printUsage()
         fmt.Println("Invalid keysize. Try 1024 or 2048.")
         os.Exit(1)
      }
      return os.Args[1], os.Args[2], keySize
   }

   return "", "", 0 // Default blank return catch-all
}

// Encode the private key as a PEM file
// PEM is a base-64 encoding of the key
func getPrivatePemFromKey(privateKey *rsa.PrivateKey) *pem.Block {
   encodedPrivateKey := x509.MarshalPKCS1PrivateKey(privateKey)
   var privatePem = &pem.Block {
      Type: "RSA PRIVATE KEY",
      Bytes: encodedPrivateKey,
   }
   return privatePem
}

// Encode the public key as a PEM file
func generatePublicPemFromKey(publicKey rsa.PublicKey) *pem.Block {
   encodedPubKey, err := x509.MarshalPKIXPublicKey(&publicKey)
   if err != nil {
      log.Fatal("Error marshaling PKIX pubkey. ", err)
   }

   // Create a public PEM structure with the data
   var publicPem = &pem.Block{
      Type:  "PUBLIC KEY",
      Bytes: encodedPubKey,
   }
   return publicPem
}

func savePemToFile(pemBlock *pem.Block, filename string) {
   // Save public pem to file
   publicPemOutputFile, err := os.Create(filename)
   if err != nil {
      log.Fatal("Error opening pubkey output file. ", err)
   }
   defer publicPemOutputFile.Close()

   err = pem.Encode(publicPemOutputFile, pemBlock)
   if err != nil {
      log.Fatal("Error encoding public PEM. ", err)
   }
}

// Generate a public and private RSA key in PEM format
func main() {
   privatePemFilename, publicPemFilename, keySize := checkArgs()

   // Generate private key
   privateKey, err := rsa.GenerateKey(rand.Reader, keySize)
   if err != nil {
      log.Fatal("Error generating private key. ", err)
   }

   // Encode keys to PEM format
   privatePem := getPrivatePemFromKey(privateKey)
   publicPem := generatePublicPemFromKey(privateKey.PublicKey)

   // Save the PEM output to files
   savePemToFile(privatePem, privatePemFilename)
   savePemToFile(publicPem, publicPemFilename)

   // Print the public key to STDOUT for convenience
   fmt.Printf("%s", pem.EncodeToMemory(publicPem))
}

数字签名消息

签名消息的目的是让收件人知道消息来自正确的人。要签名一条消息,首先生成消息的哈希值,然后使用你的私钥对哈希值进行加密。加密后的哈希值就是你的签名。

收件人会解密你的签名,得到你提供的原始哈希值,然后他们会对消息进行哈希处理,查看自己生成的哈希值是否与解密后的签名值匹配。如果匹配,收件人就知道签名是有效的,并且来自正确的发送者。

请注意,签名一条消息并不会真正加密该消息。如果需要,你仍然需要在发送消息之前对其进行加密。如果你希望公开发布消息,可能不需要加密消息本身。其他人仍然可以使用签名来验证消息的发布者。

只有小于 RSA 密钥大小的消息才能被签名。由于 SHA-256 哈希始终具有相同的输出长度,我们可以确保它在可接受的大小限制内。在此示例中,我们使用的是 RSA PKCS#1 v1.5 标准签名和 SHA-256 哈希方法。

Go 编程语言自带了用于处理签名和验证的核心包函数。主要的函数是rsa.VerifyPKCS1v5。该函数负责对消息进行哈希处理,然后使用私钥进行加密。

以下程序将接收一条消息和一个私钥,并将签名输出到STDOUT

package main

import (
   "crypto"
   "crypto/rand"
   "crypto/rsa"
   "crypto/sha256"
   "crypto/x509"
   "encoding/pem"
   "fmt"
   "io/ioutil"
   "log"
   "os"
)

func printUsage() {
   fmt.Println(os.Args[0] + `

Cryptographically sign a message using a private key.
Private key should be a PEM encoded RSA key.
Signature is generated using SHA256 hash.
Output signature is stored in filename provided.

Usage:
  ` + os.Args[0] + ` <privateKeyFilename> <messageFilename>   <signatureFilename>

Example:
  # Use priv.pem to encrypt msg.txt and output to sig.txt.256
  ` + os.Args[0] + ` priv.pem msg.txt sig.txt.256
`)
}

// Get arguments from command line
func checkArgs() (string, string, string) {
   // Need exactly 3 arguments provided
   if len(os.Args) != 4 {
      printUsage()
      os.Exit(1)
   }

   // Private key file name and message file name
   return os.Args[1], os.Args[2], os.Args[3]
}

// Cryptographically sign a message= creating a digital signature
// of the original message. Uses SHA-256 hashing.
func signMessage(privateKey *rsa.PrivateKey, message []byte) []byte {
   hashed := sha256.Sum256(message)

   signature, err := rsa.SignPKCS1v15(
      rand.Reader,
      privateKey,
      crypto.SHA256,
      hashed[:],
   )
   if err != nil {
      log.Fatal("Error signing message. ", err)
   }

   return signature
}

// Load the message that will be signed from file
func loadMessageFromFile(messageFilename string) []byte {
   fileData, err := ioutil.ReadFile(messageFilename)
   if err != nil {
      log.Fatal(err)
   }
   return fileData
}

// Load the RSA private key from a PEM encoded file
func loadPrivateKeyFromPemFile(privateKeyFilename string) *rsa.PrivateKey {
   // Quick load file to memory
   fileData, err := ioutil.ReadFile(privateKeyFilename)
   if err != nil {
      log.Fatal(err)
   }

   // Get the block data from the PEM encoded file
   block, _ := pem.Decode(fileData)
   if block == nil || block.Type != "RSA PRIVATE KEY" {
      log.Fatal("Unable to load a valid private key.")
   }

   // Parse the bytes and put it in to a proper privateKey struct
   privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
   if err != nil {
      log.Fatal("Error loading private key.", err)
   }

   return privateKey
}

// Save data to file
func writeToFile(filename string, data []byte) error {
   // Open a new file for writing only
   file, err := os.OpenFile(
      filename,
      os.O_WRONLY|os.O_TRUNC|os.O_CREATE,
      0666,
   )
   if err != nil {
      return err
   }
   defer file.Close()

   // Write bytes to file
   _, err = file.Write(data)
   if err != nil {
      return err
   }

   return nil
}

// Sign a message using a private RSA key
func main() {
   // Get arguments from command line
   privateKeyFilename, messageFilename, sigFilename := checkArgs()

   // Load message and private key files from disk
   message := loadMessageFromFile(messageFilename)
   privateKey := loadPrivateKeyFromPemFile(privateKeyFilename)

   // Cryptographically sign the message
   signature := signMessage(privateKey, message)

   // Output to file
   writeToFile(sigFilename, signature)
}

验证签名

在上一个示例中,我们学习了如何为收件人创建一条消息的签名以供验证。现在让我们看看验证签名的过程。

如果你收到一条消息和一个签名,你必须先使用发送方的公钥解密签名。然后对原始消息进行哈希,检查你的哈希值是否与解密后的签名匹配。如果你的哈希值与解密后的签名匹配,那么你可以确定发送方是拥有与你用来验证的公钥配对的私钥的那个人。

为了验证签名,我们使用与创建签名时相同的算法(RSA PKCS#1 v1.5 和 SHA-256)。

这个示例需要两个命令行参数。第一个参数是创建签名的人的公钥,第二个参数是包含签名的文件。要创建签名文件,可以使用之前示例中的 sign 程序并将输出重定向到一个文件。

与上一节类似,Go 的标准库中有一个用于验证签名的函数。我们可以使用rsa.VerifyPKCS1v5()来比较消息的哈希值与解密后的签名值,看看它们是否匹配:

package main

import (
   "crypto"
   "crypto/rsa"
   "crypto/sha256"
   "crypto/x509"
   "encoding/pem"
   "fmt"
   "io/ioutil"
   "log"
   "os"
)

func printUsage() {
    fmt.Println(os.Args[0] + `

Verify an RSA signature of a message using SHA-256 hashing.
Public key is expected to be a PEM file.

Usage:
  ` + os.Args[0] + ` <publicKeyFilename> <signatureFilename> <messageFilename>

Example:
  ` + os.Args[0] + ` pubkey.pem signature.txt message.txt
`)
}

// Get arguments from command line
func checkArgs() (string, string, string) {
   // Expect 3 arguments: pubkey, signature, message file names
   if len(os.Args) != 4 {
      printUsage()
      os.Exit(1)
   }

   return os.Args[1], os.Args[2], os.Args[3]
}

// Returns bool whether signature was verified
func verifySignature(
   signature []byte,
   message []byte,
   publicKey *rsa.PublicKey) bool {

   hashedMessage := sha256.Sum256(message)

   err := rsa.VerifyPKCS1v15(
      publicKey,
      crypto.SHA256,
      hashedMessage[:],
      signature,
   )

   if err != nil {
      log.Println(err)
      return false
   }
   return true // If no error, match.
}

// Load file to memory
func loadFile(filename string) []byte {
   fileData, err := ioutil.ReadFile(filename)
   if err != nil {
      log.Fatal(err)
   }
   return fileData
}

// Load a public RSA key from a PEM encoded file
func loadPublicKeyFromPemFile(publicKeyFilename string) *rsa.PublicKey {
   // Quick load file to memory
   fileData, err := ioutil.ReadFile(publicKeyFilename)
   if err != nil {
      log.Fatal(err)
   }

   // Get the block data from the PEM encoded file
   block, _ := pem.Decode(fileData)
   if block == nil || block.Type != "PUBLIC KEY" {
      log.Fatal("Unable to load valid public key. ")
   }

   // Parse the bytes and store in a public key format
   publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
   if err != nil {
      log.Fatal("Error loading public key. ", err)
   }

   return publicKey.(*rsa.PublicKey) // Cast interface to PublicKey
}

// Verify a cryptographic signature using RSA PKCS#1 v1.5 with SHA-256
// and a PEM encoded PKIX public key.
func main() {
   // Parse command line arguments
   publicKeyFilename, signatureFilename, messageFilename :=   
      checkArgs()

   // Load all the files from disk
   publicKey := loadPublicKeyFromPemFile(publicKeyFilename)
   signature := loadFile(signatureFilename)
   message := loadFile(messageFilename)

   // Verify signature
   valid := verifySignature(signature, message, publicKey)

   if valid {
      fmt.Println("Signature verified.")
   } else {
      fmt.Println("Signature could not be verified.")
   }
}

TLS

我们通常不使用 RSA 加密整个消息,因为它只能加密小于密钥大小的消息。解决方案通常是在通信开始时使用小消息,通过 RSA 密钥加密。当建立了安全通道后,它们可以安全地交换共享密钥,然后使用该密钥对其余消息进行对称加密,避免大小限制。这就是 SSL 和 TLS 建立安全通信时所采取的方法。握手过程负责协商在生成和共享对称密钥时使用的加密算法。

生成自签名证书

要使用 Go 创建自签名证书,你需要一对公私密钥。x509 包提供了一个用于创建证书的函数。它需要公钥和私钥,以及一个包含所有信息的证书模板。由于我们是自签名,因此证书模板也将作为根证书进行签名。

每个应用程序可能对自签名证书有不同的处理方式。有些应用程序会在证书是自签名时给出警告,有些会拒绝接受,而另一些则会在不警告的情况下愉快地使用它。当你编写自己的应用程序时,你需要决定是否要验证证书或接受自签名证书。

重要的功能是x509.CreateCertificate(),可以参考 golang.org/pkg/crypto/x509/#CreateCertificate。下面是函数签名:

func CreateCertificate (rand io.Reader, template, parent *Certificate, pub, 
   priv interface{}) (cert []byte, err error)

本示例将使用私钥生成一个由该私钥签名的证书,并将其以 PEM 格式保存到文件中。一旦创建了自签名证书,你可以将该证书与私钥一起使用来运行安全的 TLS 套接字监听器和 Web 服务器。

为了简便起见,本示例将证书所有者信息和主机名 IP 硬编码为 localhost。这对于在本地机器上测试已经足够。

根据需要修改这些内容,定制值,通过命令行参数输入,或使用标准输入动态获取用户的值,如以下代码块所示:

package main

import (
   "crypto/rand"
   "crypto/rsa"
   "crypto/x509/pkix"
   "crypto/x509"
   "encoding/pem"
   "fmt"
   "io/ioutil"
   "log"
   "math/big"
   "net"
   "os"
   "time"
)

func printUsage() {
   fmt.Println(os.Args[0] + ` - Generate a self signed TLS certificate

Usage:
  ` + os.Args[0] + ` <privateKeyFilename> <certOutputFilename> [-ca|--cert-authority]

Example:
  ` + os.Args[0] + ` priv.pem cert.pem
  ` + os.Args[0] + ` priv.pem cacert.pem -ca
`)
}

func checkArgs() (string, string, bool) {
   if len(os.Args) < 3 || len(os.Args) > 4 {
      printUsage()
      os.Exit(1)
   }

   // See if the last cert authority option was passed
   isCA := false // Default
   if len(os.Args) == 4 {
      if os.Args[3] == "-ca" || os.Args[3] == "--cert-authority" {
         isCA = true
      }
   }

   // Private key filename, cert output filename, is cert authority
   return os.Args[1], os.Args[2], isCA
}

func setupCertificateTemplate(isCA bool) x509.Certificate {
   // Set valid time frame to start now and end one year from now
   notBefore := time.Now()
   notAfter := notBefore.Add(time.Hour * 24 * 365) // 1 year/365 days

   // Generate secure random serial number
   serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
   randomNumber, err := rand.Int(rand.Reader, serialNumberLimit)
   if err != nil {
      log.Fatal("Error generating random serial number. ", err)
   }

   nameInfo := pkix.Name{
      Organization: []string{"My Organization"},
      CommonName: "localhost",
      OrganizationalUnit: []string{"My Business Unit"},
      Country:        []string{"US"}, // 2-character ISO code
      Province:       []string{"Texas"}, // State
      Locality:       []string{"Houston"}, // City
   }

   // Create the certificate template
   certTemplate := x509.Certificate{
      SerialNumber: randomNumber,
      Subject: nameInfo,
      EmailAddresses: []string{"test@localhost"},
      NotBefore: notBefore,
      NotAfter: notAfter,
      KeyUsage: x509.KeyUsageKeyEncipherment |   
         x509.KeyUsageDigitalSignature,
      // For ExtKeyUsage, default to any, but can specify to use
      // only as server or client authentication, code signing, etc
      ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
      BasicConstraintsValid: true,
      IsCA: false,
   }

   // To create a certificate authority that can sign cert signing   
   // requests, set these
   if isCA {
      certTemplate.IsCA = true
      certTemplate.KeyUsage = certTemplate.KeyUsage |  
         x509.KeyUsageCertSign
   }

   // Add any IP addresses and hostnames covered by this cert
   // This example only covers localhost
   certTemplate.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")}
   certTemplate.DNSNames = []string{"localhost", "localhost.local"}

   return certTemplate
}

// Load the RSA private key from a PEM encoded file
func loadPrivateKeyFromPemFile(privateKeyFilename string) *rsa.PrivateKey {
   // Quick load file to memory
   fileData, err := ioutil.ReadFile(privateKeyFilename)
   if err != nil {
      log.Fatal("Error loading private key file. ", err)
   }

   // Get the block data from the PEM encoded file
   block, _ := pem.Decode(fileData)
   if block == nil || block.Type != "RSA PRIVATE KEY" {
      log.Fatal("Unable to load a valid private key.")
   }

   // Parse the bytes and put it in to a proper privateKey struct
   privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
   if err != nil {
      log.Fatal("Error loading private key. ", err)
   }

   return privateKey
}

// Save the certificate as a PEM encoded file
func writeCertToPemFile(outputFilename string, derBytes []byte ) {
   // Create a PEM from the certificate
   certPem := &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}

   // Open file for writing
   certOutfile, err := os.Create(outputFilename)
   if err != nil {
      log.Fatal("Unable to open certificate output file. ", err)
   }
   pem.Encode(certOutfile, certPem)
   certOutfile.Close()
}

// Create a self-signed TLS/SSL certificate for localhost 
// with an RSA private key
func main() {
   privPemFilename, certOutputFilename, isCA := checkArgs()

   // Private key of signer - self signed means signer==signee
   privKey := loadPrivateKeyFromPemFile(privPemFilename)

   // Public key of signee. Self signing means we are the signer and    
   // the signee so we can just pull our public key from our private key
   pubKey := privKey.PublicKey

   // Set up all the certificate info
   certTemplate := setupCertificateTemplate(isCA)

   // Create (and sign with the priv key) the certificate
   certificate, err := x509.CreateCertificate(
      rand.Reader,
      &certTemplate,
      &certTemplate,
      &pubKey,
      privKey,
   )
   if err != nil {
      log.Fatal("Failed to create certificate. ", err)
   }

   // Format the certificate as a PEM and write to file
   writeCertToPemFile(certOutputFilename, certificate)
}

创建证书签名请求

如果你不想创建自签名证书,你必须创建证书签名请求,并让受信任的证书颁发机构对其进行签名。你可以通过调用 x509.CreateCertificateRequest() 并传递一个包含私钥的 x509.CertificateRequest 对象来创建证书请求。

使用 OpenSSL 执行的等效操作如下:

# Create CSR 
openssl req -new -key priv.pem -out csr.pem 
# View details to verify request was created properly 
openssl req -verify -in csr.pem -text -noout 

本示例演示如何创建证书签名请求:

package main

import (
   "crypto/rand"
   "crypto/rsa"
   "crypto/x509"
   "crypto/x509/pkix"
   "encoding/pem"
   "fmt"
   "io/ioutil"
   "log"
   "net"
   "os"
)

func printUsage() {
   fmt.Println(os.Args[0] + ` - Create a certificate signing request  
   with a private key.

Private key is expected in PEM format. Certificate valid for localhost only.
Certificate signing request is created using the SHA-256 hash.

Usage:
  ` + os.Args[0] + ` <privateKeyFilename> <csrOutputFilename>

Example:
  ` + os.Args[0] + ` priv.pem csr.pem
`)
}

func checkArgs() (string, string) {
   if len(os.Args) != 3 {
      printUsage()
      os.Exit(1)
   }

   // Private key filename, cert signing request output filename
   return os.Args[1], os.Args[2]
}

// Load the RSA private key from a PEM encoded file
func loadPrivateKeyFromPemFile(privateKeyFilename string) *rsa.PrivateKey {
   // Quick load file to memory
   fileData, err := ioutil.ReadFile(privateKeyFilename)
   if err != nil {
      log.Fatal("Error loading private key file. ", err)
   }

   // Get the block data from the PEM encoded file
   block, _ := pem.Decode(fileData)
   if block == nil || block.Type != "RSA PRIVATE KEY" {
      log.Fatal("Unable to load a valid private key.")
   }

   // Parse the bytes and put it in to a proper privateKey struct
   privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
   if err != nil {
      log.Fatal("Error loading private key.", err)
   }

   return privateKey
}

// Create a CSR PEM and save to file
func saveCSRToPemFile(csr []byte, filename string) {
   csrPem := &pem.Block{
      Type:  "CERTIFICATE REQUEST",
      Bytes: csr,
   }
   csrOutfile, err := os.Create(filename)
   if err != nil {
      log.Fatal("Error opening "+filename+" for saving. ", err)
   }
   pem.Encode(csrOutfile, csrPem)
}

// Create a certificate signing request with a private key 
// valid for localhost
func main() {
   // Load parameters
   privKeyFilename, csrOutFilename := checkArgs()
   privKey := loadPrivateKeyFromPemFile(privKeyFilename)

   // Prepare information about organization the cert will belong to
   nameInfo := pkix.Name{
      Organization:       []string{"My Organization Name"},
      CommonName:         "localhost",
      OrganizationalUnit: []string{"Business Unit Name"},
      Country:            []string{"US"}, // 2-character ISO code
      Province:           []string{"Texas"},
      Locality:           []string{"Houston"}, // City
   }

   // Prepare CSR template
   csrTemplate := x509.CertificateRequest{
      Version:            2, // Version 3, zero-indexed values
      SignatureAlgorithm: x509.SHA256WithRSA,
      PublicKeyAlgorithm: x509.RSA,
      PublicKey:          privKey.PublicKey,
      Subject:            nameInfo,

      // Subject Alternate Name values.
      DNSNames:       []string{"Business Unit Name"},
      EmailAddresses: []string{"test@localhost"},
      IPAddresses:    []net.IP{},
   }

   // Create the CSR based off the template
   csr, err := x509.CreateCertificateRequest(rand.Reader,  
      &csrTemplate, privKey)
   if err != nil {
      log.Fatal("Error creating certificate signing request. ", err)
   }
   saveCSRToPemFile(csr, csrOutFilename)
}

签署证书请求

在前一个示例中,当生成自签名证书时,我们已经展示了创建签名证书的过程。在自签名示例中,我们只是使用了与签署者和被签署者相同的证书模板。因此没有单独的代码示例。唯一的不同是进行签名的父证书或待签署的证书模板应该替换为一个不同的证书。

这是 x509.CreateCertificate() 的函数定义:

func CreateCertificate(rand io.Reader, template, parent *Certificate, pub, 
   priv interface{}) (cert []byte, err error)

在自签名示例中,模板和父证书是相同的对象。要签署证书请求,创建一个新的证书对象,并用签名请求中的信息填充字段。将新证书作为模板传递,并使用签署者的证书作为父证书。pub 参数是被签署者的公钥,priv 参数是签署者的私钥。签署者是证书颁发机构,而被签署者是请求者。你可以在 golang.org/pkg/crypto/x509/#CreateCertificate 阅读更多关于此函数的内容。

X509.CreateCertificate() 的参数如下:

  • rand:这是一个加密安全的伪随机数生成器。

  • template:这是从 CSR 中填充信息的证书模板。

  • parent:这是签署者的证书。

  • pub:这是被签署者的公钥。

  • priv:这是签署者的私钥。

使用 OpenSSL 执行相同操作如下:

# Create signed certificate using
# the CSR, CA certificate, and private key 
openssl x509 -req -in csr.pem -CA cacert.pem \
-CAkey capriv.pem -CAcreateserial \
-out cert.pem -sha256
# Print info about cert 
openssl x509 -in cert.pem -text -noout  

TLS 服务器

你可以像正常的套接字连接一样设置监听器,但带有加密。只需调用 TLS 的 Listen() 函数,并提供证书和私钥。之前示例中生成的证书和密钥将能正常工作。

以下程序将创建一个 TLS 服务器,回显接收到的任何数据,然后关闭连接。该服务器不需要或验证客户端证书,但为了参考,如果你想使用证书进行客户端身份验证,相关代码已被注释掉:

package main

import (
   "bufio"
   "crypto/tls"
   "fmt"
   "log"
   "net"
   "os"
)

func printUsage() {
   fmt.Println(os.Args[0] + ` - Start a TLS echo server

Server will echo one message received back to client.
Provide a certificate and private key file in PEM format.
Host string in the format: hostname:port

Usage:
  ` + os.Args[0] + ` <certFilename> <privateKeyFilename> <hostString>

Example:
  ` + os.Args[0] + ` cert.pem priv.pem localhost:9999
`)
}

func checkArgs() (string, string, string) {
  if len(os.Args) != 4 {
     printUsage()
     os.Exit(1)
  }

  return os.Args[1], os.Args[2], os.Args[3]
}

// Create a TLS listener and echo back data received by clients.
func main() {
   certFilename, privKeyFilename, hostString := checkArgs()

   // Load the certificate and private key
   serverCert, err := tls.LoadX509KeyPair(certFilename, privKeyFilename)
   if err != nil {
      log.Fatal("Error loading certificate and private key. ", err)
   }

   // Set up certificates, host/ip, and port
   config := &tls.Config{
      // Specify server certificate
      Certificates: []tls.Certificate{serverCert},

      // By default no client certificate is required.
      // To require and validate client certificates, specify the
      // ClientAuthType to be one of:
      //    NoClientCert, RequestClientCert, RequireAnyClientCert,
      //    VerifyClientCertIfGiven, RequireAndVerifyClientCert)

      // ClientAuth: tls.RequireAndVerifyClientCert

      // Define the list of certificates you will accept as
      // trusted certificate authorities with ClientCAs.

      // ClientCAs: *x509.CertPool
   }

   // Create the TLS socket listener
   listener, err := tls.Listen("tcp", hostString, config)
   if err != nil {
      log.Fatal("Error starting TLS listener. ", err)
   }
   defer listener.Close()

   // Listen forever for connections
   for {
      clientConnection, err := listener.Accept()
      if err != nil {
         log.Println("Error accepting client connection. ", err)
         continue
      }
      // Launch a goroutine(thread)go-1.6 to handle each connection
      go handleConnection(clientConnection)
   }
}

// Function that gets launched in a goroutine to handle client connection
func handleConnection(clientConnection net.Conn) {
   defer clientConnection.Close()
   socketReader := bufio.NewReader(clientConnection)
   for {
      // Read a message from the client
      message, err := socketReader.ReadString('\n')
      if err != nil {
         log.Println("Error reading from client socket. ", err)
         return
      }
      fmt.Println(message)

      // Echo back the data to the client.
      numBytesWritten, err := clientConnection.Write([]byte(message))
      if err != nil {
         log.Println("Error writing data to client socket. ", err)
         return
      }
      fmt.Printf("Wrote %d bytes back to client.\n", numBytesWritten)
   }
}

TLS 客户端

TCP 套接字是网络通信中一种简单且常见的方式。在标准 TCP 套接字上添加 TLS 层,在 Go 的标准库中非常简单。

客户端像标准套接字一样拨打 TLS 服务器。通常客户端不需要任何密钥或证书,但服务器可以实现客户端身份验证,并只允许特定的用户连接。

这个程序将连接到一个 TLS 服务器,并将 STDIN 的内容发送到远程服务器,并读取响应。我们可以使用这个程序来测试我们在上一节中创建的基本 TLS 回显服务器。

在运行此程序之前,请确保上一节中的 TLS 服务器正在运行,以便您可以连接。

请注意,这是一个原始的套接字级别服务器。它不是一个 HTTP 服务器。在第九章 Web 应用程序 中有运行 HTTPS TLS 网络服务器的示例。

默认情况下,客户端会验证服务器的证书是否由受信任的机构签署。我们需要覆盖这个默认设置,并告诉客户端不要验证证书,因为证书是我们自己签署的。受信任的证书机构列表是从系统加载的,但可以通过在 tls.Config 中填充 RootCAs 变量来覆盖。这个示例将不验证服务器证书,但提供了受信任的 RootCAs 列表代码,并为参考注释掉。

你可以通过查看 golang.org/src/crypto/x509/ 中的 root_*.go 文件来了解 Go 如何为每个系统加载证书池。例如,root_windows.goroot_linux.go 加载系统的默认证书。

如果你想连接到服务器并检查或存储其证书,你可以连接后检查客户端的 net.Conn.ConnectionState().PeerCertificates。它以标准的 x509.Certificate 结构体形式呈现。要做到这一点,请参考以下代码块:

package main

import (
   "crypto/tls"
   "fmt"
   "log"
   "os"
)

func printUsage() {
   fmt.Println(os.Args[0] + ` - Send and receive a message to a TLS server

Usage:
  ` + os.Args[0] + ` <hostString>

Example:
  ` + os.Args[0] + ` localhost:9999
`)
}

func checkArgs() string {
   if len(os.Args) != 2 {
      printUsage()
      os.Exit(1)
   }

   // Host string e.g. localhost:9999
   return os.Args[1]
}

// Simple TLS client that sends a message and receives a message
func main() {
   hostString := checkArgs()
   messageToSend := "Hello?\n"

   // Configure TLS settings
   tlsConfig := &tls.Config{
      // Required to accept self-signed certs
      InsecureSkipVerify: true, 
      // Provide your client certificate if necessary
      // Certificates: []Certificate

      // ServerName is used to verify the hostname (unless you are     
      // skipping verification)
      // It is also included in the handshake in case the server uses   
      // virtual hosts Can also just be an IP address 
      // instead of a hostname.
      // ServerName: string,

      // RootCAs that you are willing to accept
      // If RootCAs is nil, the host's default root CAs are used
      // RootCAs: *x509.CertPool
   }

   // Set up dialer and call the server
   connection, err := tls.Dial("tcp", hostString, tlsConfig)
   if err != nil {
      log.Fatal("Error dialing server. ", err)
   }
   defer connection.Close()

   // Write data to socket
   numBytesWritten, err := connection.Write([]byte(messageToSend))
   if err != nil {
      log.Println("Error writing to socket. ", err)
      os.Exit(1)
   }
   fmt.Printf("Wrote %d bytes to the socket.\n", numBytesWritten)

   // Read data from socket and print to STDOUT
   buffer := make([]byte, 100)
   numBytesRead, err := connection.Read(buffer)
   if err != nil {
      log.Println("Error reading from socket. ", err)
      os.Exit(1)
   }
   fmt.Printf("Read %d bytes to the socket.\n", numBytesRead)
   fmt.Printf("Message received:\n%s\n", buffer)
}

其他加密包

以下部分没有源代码示例,但值得一提。这些由 Go 提供的包是建立在前面示例中展示的原理之上的。

OpenPGP

PGP 代表 Pretty Good Privacy,而 OpenPGP 是标准 RFC 4880。PGP 是一套便捷的加密工具,适用于加密文本、文件、目录和磁盘。所有的原理与前一节讨论的 SSL 和 TLS 密钥/证书相同。加密、签名和验证的方式都是一样的。Go 提供了一个 OpenPGP 包。阅读更多关于它的信息,访问 godoc.org/golang.org/x/crypto/openpgp

离线记录 (OTR) 消息传递

离线记录OTR 消息传递是一种端到端加密的形式,允许用户通过任何消息媒介加密其通信。它很方便,因为你可以在任何协议上实现加密层,即使该协议本身没有加密。例如,OTR 消息传递可以在 XMPP、IRC 和许多其他聊天协议上运行。许多聊天客户端如 Pidgin、Adium 和 Xabber 都支持 OTR,支持方式有原生支持或通过插件。Go 提供了一个实现 OTR 消息传递的包。阅读更多有关 Go 的 OTR 支持信息,访问 godoc.org/golang.org/x/crypto/otr/

概述

阅读完本章后,你应该对 Go 的加密包有一个清晰的了解。通过本章中的示例作为参考,你应该能够熟练进行基本的哈希操作、加密、解密、生成密钥以及使用密钥。

此外,你还应该理解对称加密和非对称加密之间的区别,以及它们与哈希的不同。你应该对运行 TLS 服务器和连接 TLS 客户端的基本操作感到熟悉。

记住,目标不是记住每个细节,而是记住有哪些选项可供选择,以便你能为任务选择最佳工具。

在下一章,我们将讨论如何使用安全外壳(SSH)。首先介绍如何使用公钥和私钥对以及密码进行身份验证,并讲解如何验证远程主机的密钥。我们还将探讨如何在远程服务器上执行命令以及如何创建交互式 shell。安全外壳利用了本章中讨论的加密技术。它是加密技术最常见和最实用的应用之一。继续阅读以了解更多关于在 Go 中使用 SSH 的内容。