diff --git a/cmd/ots/pdf.go b/cmd/ots/pdf.go new file mode 100644 index 0000000..bccfe12 --- /dev/null +++ b/cmd/ots/pdf.go @@ -0,0 +1,374 @@ +package main + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "log/slog" + "os" + "path/filepath" + "strings" + "time" + + "git.intruders.space/public/opentimestamps" + "git.intruders.space/public/opentimestamps/ots" + "git.intruders.space/public/opentimestamps/verifyer" + "github.com/jung-kurt/gofpdf" + "github.com/spf13/cobra" +) + +var ( + // PDF command flags + pdfOutput string + pdfIncludeContent bool + pdfTitle string + pdfComment string + pdfEsploraURL string +) + +// pdfCmd represents the pdf command +var pdfCmd = &cobra.Command{ + Use: "pdf [flags] ", + Short: "Generate a PDF certificate for a timestamp", + Long: `Generate a PDF certificate that explains how to verify the timestamp. +The PDF includes all necessary information such as hashes, operations, +and instructions for manual verification.`, + Args: cobra.ExactArgs(2), + Run: runPdfCmd, +} + +func init() { + // Local flags for the pdf command + pdfCmd.Flags().StringVarP(&pdfOutput, "output", "o", "", "Output PDF filename (default: original filename with .pdf extension)") + pdfCmd.Flags().BoolVar(&pdfIncludeContent, "include-content", false, "Include the original file content in the PDF (for text files only)") + pdfCmd.Flags().StringVar(&pdfTitle, "title", "OpenTimestamps Certificate", "Title for the PDF document") + pdfCmd.Flags().StringVar(&pdfComment, "comment", "", "Additional comment to include in the certificate") + pdfCmd.Flags().StringVar(&pdfEsploraURL, "esplora", "https://blockstream.info/api", "URL of Esplora API for fetching block information") +} + +func runPdfCmd(cmd *cobra.Command, args []string) { + filePath := args[0] + otsPath := args[1] + + // Determine output file path + outputPath := pdfOutput + if outputPath == "" { + outputPath = filePath + ".certificate.pdf" + } + + // Read the original file + fileData, err := os.ReadFile(filePath) + if err != nil { + slog.Error("Failed to read file", "file", filePath, "error", err) + os.Exit(1) + } + + // Compute the file digest + fileDigest := sha256.Sum256(fileData) + slog.Debug("Computed file digest", "digest", hex.EncodeToString(fileDigest[:])) + + // Read and parse the OTS file + otsData, err := os.ReadFile(otsPath) + if err != nil { + slog.Error("Failed to read OTS file", "file", otsPath, "error", err) + os.Exit(1) + } + + timestampFile, err := opentimestamps.ReadFromFile(otsData) + if err != nil { + slog.Error("Failed to parse OTS file", "file", otsPath, "error", err) + os.Exit(1) + } + + // Check if the digests match + if !bytes.Equal(fileDigest[:], timestampFile.Digest) { + slog.Warn("Warning: File digest doesn't match timestamp digest", + "file_digest", hex.EncodeToString(fileDigest[:]), + "timestamp_digest", hex.EncodeToString(timestampFile.Digest)) + } + + // Extract Bitcoin attestations + bitcoinSeqs := timestampFile.GetBitcoinAttestedSequences() + if len(bitcoinSeqs) == 0 { + slog.Warn("No Bitcoin attestations found in timestamp, certificate will be for pending timestamps") + } + + // Create Bitcoin interface for block time lookup + bitcoin := verifyer.NewEsploraClient(pdfEsploraURL, 30*time.Second) + slog.Info("Using Esplora API for block information", "url", pdfEsploraURL) + + // Generate the PDF + err = generateVerificationPDF( + outputPath, + filePath, + fileData, + fileDigest[:], + timestampFile, + bitcoinSeqs, + bitcoin, + ) + if err != nil { + slog.Error("Failed to generate PDF", "error", err) + os.Exit(1) + } + + slog.Info("PDF certificate generated successfully", "file", outputPath) +} + +func generateVerificationPDF(outputPath, filePath string, fileData []byte, fileDigest []byte, + timestamp *ots.File, bitcoinSeqs []ots.Sequence, bitcoin verifyer.Bitcoin) error { + + // Create a new PDF + pdf := gofpdf.New("P", "mm", "A4", "") + pdf.SetTitle(pdfTitle, true) + pdf.SetAuthor("OpenTimestamps", true) + pdf.SetCreator("OpenTimestamps CLI", true) + + // Set margins + pdf.SetMargins(25, 25, 25) + pdf.SetAutoPageBreak(true, 25) + + // Add first page - Certificate + pdf.AddPage() + + // Title + pdf.SetFont("Helvetica", "B", 24) + pdf.CellFormat(0, 12, pdfTitle, "", 1, "C", false, 0, "") + pdf.Ln(10) + + // Timestamp information section + pdf.SetFont("Helvetica", "B", 18) + pdf.CellFormat(0, 10, "Timestamp Information", "", 1, "L", false, 0, "") + pdf.Ln(5) + + pdf.SetFont("Helvetica", "B", 12) + pdf.CellFormat(0, 8, "Filename:", "", 1, "L", false, 0, "") + pdf.SetFont("Helvetica", "", 12) + pdf.CellFormat(0, 8, filepath.Base(filePath), "", 1, "L", false, 0, "") + pdf.Ln(2) + + // Timestamp date + pdf.SetFont("Helvetica", "B", 12) + if len(bitcoinSeqs) > 0 { + // Find the earliest Bitcoin attestation + var earliestHeight uint64 + var earliestBlockTime time.Time + + for _, seq := range bitcoinSeqs { + att := seq.GetAttestation() + if earliestHeight == 0 || att.BitcoinBlockHeight < earliestHeight { + earliestHeight = att.BitcoinBlockHeight + + // Fetch the block time from the blockchain + blockHash, err := bitcoin.GetBlockHash(int64(att.BitcoinBlockHeight)) + if err == nil { + blockHeader, err := bitcoin.GetBlockHeader(blockHash) + if err == nil { + earliestBlockTime = time.Unix(int64(blockHeader.Timestamp.Unix()), 0) + } + } + } + } + + // Show Bitcoin timestamp + pdf.CellFormat(0, 8, "Timestamp Created:", "", 1, "L", false, 0, "") + pdf.SetFont("Helvetica", "", 12) + + if !earliestBlockTime.IsZero() { + // If we successfully retrieved the block time, display it + pdf.CellFormat(0, 8, fmt.Sprintf("Bitcoin Block #%d (%s)", + earliestHeight, earliestBlockTime.Format("Jan 02, 2006 15:04:05 UTC")), + "", 1, "L", false, 0, "") + } else { + // Fallback to just showing the block height + pdf.CellFormat(0, 8, fmt.Sprintf("Bitcoin Block #%d", earliestHeight), "", 1, "L", false, 0, "") + } + } else { + // Pending attestation + pdf.CellFormat(0, 8, "Status:", "", 1, "L", false, 0, "") + pdf.SetFont("Helvetica", "", 12) + pdf.CellFormat(0, 8, "Pending confirmation in the Bitcoin blockchain", "", 1, "L", false, 0, "") + } + pdf.Ln(2) + + // File hash + pdf.SetFont("Helvetica", "B", 12) + pdf.CellFormat(0, 8, "File SHA-256 Digest:", "", 1, "L", false, 0, "") + pdf.SetFont("Courier", "", 10) + pdf.CellFormat(0, 8, hex.EncodeToString(fileDigest), "", 1, "L", false, 0, "") + pdf.Ln(5) + + // Include comment if provided + if pdfComment != "" { + pdf.SetFont("Helvetica", "B", 12) + pdf.CellFormat(0, 8, "Comment:", "", 1, "L", false, 0, "") + pdf.SetFont("Helvetica", "", 12) + pdf.MultiCell(0, 8, pdfComment, "", "L", false) + pdf.Ln(5) + } + + // Include file content if requested and looks like text + if pdfIncludeContent && isTextFile(fileData) && len(fileData) < 5000 { + pdf.SetFont("Helvetica", "B", 12) + pdf.CellFormat(0, 8, "File Content:", "", 1, "L", false, 0, "") + pdf.SetFont("Courier", "", 10) + content := string(fileData) + content = strings.ReplaceAll(content, "\r\n", "\n") + pdf.MultiCell(0, 5, content, "", "L", false) + pdf.Ln(5) + } + + // Add Bitcoin transaction information for each attestation + if len(bitcoinSeqs) > 0 { + pdf.SetFont("Helvetica", "B", 18) + pdf.CellFormat(0, 10, "Bitcoin Attestations", "", 1, "L", false, 0, "") + pdf.Ln(5) + + for i, seq := range bitcoinSeqs { + att := seq.GetAttestation() + + pdf.SetFont("Helvetica", "B", 14) + pdf.CellFormat(0, 8, fmt.Sprintf("Attestation #%d", i+1), "", 1, "L", false, 0, "") + pdf.Ln(2) + + // Get block details + blockHash, err := bitcoin.GetBlockHash(int64(att.BitcoinBlockHeight)) + blockTime := "Unknown" + if err == nil { + blockHeader, err := bitcoin.GetBlockHeader(blockHash) + if err == nil { + blockTime = blockHeader.Timestamp.Format("Jan 02, 2006 15:04:05 UTC") + } + } + + pdf.SetFont("Helvetica", "B", 12) + pdf.CellFormat(0, 8, "Bitcoin Block Height:", "", 1, "L", false, 0, "") + pdf.SetFont("Helvetica", "", 12) + pdf.CellFormat(0, 8, fmt.Sprintf("%d", att.BitcoinBlockHeight), "", 1, "L", false, 0, "") + + pdf.SetFont("Helvetica", "B", 12) + pdf.CellFormat(0, 8, "Block Time:", "", 1, "L", false, 0, "") + pdf.SetFont("Helvetica", "", 12) + pdf.CellFormat(0, 8, blockTime, "", 1, "L", false, 0, "") + + pdf.SetFont("Helvetica", "B", 12) + pdf.CellFormat(0, 8, "Block Hash:", "", 1, "L", false, 0, "") + pdf.SetFont("Courier", "", 10) + if blockHash != nil { + pdf.CellFormat(0, 8, blockHash.String(), "", 1, "L", false, 0, "") + } else { + pdf.CellFormat(0, 8, "Unable to retrieve", "", 1, "L", false, 0, "") + } + + // In a full implementation, we would show the Bitcoin merkle root and transaction ID + // This would require additional API calls to get that information + + // Show operations sequence + pdf.SetFont("Helvetica", "B", 12) + pdf.CellFormat(0, 8, "Verification Operations:", "", 1, "L", false, 0, "") + pdf.SetFont("Courier", "", 10) + + // Format the operations + var operationsText string + merkleRoot, _ := seq.Compute(timestamp.Digest) + + operationsText += fmt.Sprintf("Starting with digest: %s\n", hex.EncodeToString(timestamp.Digest)) + + for j, inst := range seq { + if inst.Operation != nil { + if inst.Operation.Binary { + operationsText += fmt.Sprintf("%d. %s %s\n", j+1, inst.Operation.Name, hex.EncodeToString(inst.Argument)) + } else { + operationsText += fmt.Sprintf("%d. %s\n", j+1, inst.Operation.Name) + } + } + } + + operationsText += fmt.Sprintf("\nResulting merkle root: %s\n", hex.EncodeToString(merkleRoot)) + + pdf.MultiCell(0, 5, operationsText, "", "L", false) + pdf.Ln(5) + } + } + + // Add second page - Verification Instructions + pdf.AddPage() + pdf.SetFont("Helvetica", "B", 18) + pdf.CellFormat(0, 10, "Verification Instructions", "", 1, "L", false, 0, "") + pdf.Ln(5) + + // Instructions + addVerificationInstructions(pdf) + + // Save the PDF + return pdf.OutputFileAndClose(outputPath) +} + +func addVerificationInstructions(pdf *gofpdf.Fpdf) { + pdf.SetFont("Helvetica", "B", 14) + pdf.CellFormat(0, 8, "1. Compute the SHA-256 digest of your file", "", 1, "L", false, 0, "") + pdf.SetFont("Helvetica", "", 12) + pdf.MultiCell(0, 6, `To verify this timestamp, first compute the SHA-256 digest of your original file. You can use various tools for this: + +- On Linux/macOS: Use the command "shasum -a 256 filename" +- On Windows: Use tools like PowerShell's "Get-FileHash" or third-party tools +- Online: Various websites can calculate file hashes, but only use trusted sources for sensitive files + +Verify that the digest matches the one shown in this certificate.`, "", "L", false) + pdf.Ln(5) + + pdf.SetFont("Helvetica", "B", 14) + pdf.CellFormat(0, 8, "2. Verify the operations sequence", "", 1, "L", false, 0, "") + pdf.SetFont("Helvetica", "", 12) + pdf.MultiCell(0, 6, `The operations sequence shown in this certificate transforms the file digest into a value that was stored in the Bitcoin blockchain. To verify manually: + +- Start with the file's SHA-256 digest +- Apply each operation in sequence as shown in the certificate +- For "append" operations, add the shown bytes to the end of the current value +- For "prepend" operations, add the shown bytes to the beginning of the current value +- For "sha256" operations, compute the SHA-256 hash of the current value +- The final result should match the merkle root shown in the certificate`, "", "L", false) + pdf.Ln(5) + + pdf.SetFont("Helvetica", "B", 14) + pdf.CellFormat(0, 8, "3. Verify the Bitcoin attestation", "", 1, "L", false, 0, "") + pdf.SetFont("Helvetica", "", 12) + pdf.MultiCell(0, 6, `For full verification, check that the merkle root was recorded in the Bitcoin blockchain: + +- Find the Bitcoin block at the height shown in the certificate +- The block's merkle root or transaction should contain the computed value +- You can use block explorers like blockstream.info or blockchain.com +- The timestamp is confirmed when the merkle root appears in the blockchain`, "", "L", false) + pdf.Ln(5) + + pdf.SetFont("Helvetica", "B", 14) + pdf.CellFormat(0, 8, "4. Use the OpenTimestamps verifier", "", 1, "L", false, 0, "") + pdf.SetFont("Helvetica", "", 12) + pdf.MultiCell(0, 6, `For easier verification, use the OpenTimestamps verification tools: + +- Command line: Use "ots verify " +- Online: Visit https://opentimestamps.org and upload your file and .ots timestamp +- The tools will perform all verification steps automatically`, "", "L", false) + + // Add footer with generation time + pdf.Ln(10) + pdf.SetFont("Helvetica", "I", 10) + pdf.CellFormat(0, 6, fmt.Sprintf("Certificate generated at: %s", time.Now().Format(time.RFC3339)), "", 1, "C", false, 0, "") + pdf.CellFormat(0, 6, "OpenTimestamps - https://opentimestamps.org", "", 1, "C", false, 0, "") +} + +// isTextFile tries to determine if a file is likely to be text +func isTextFile(data []byte) bool { + // Simple heuristic: if the file doesn't contain too many non-printable characters, + // it's probably text + nonPrintable := 0 + for _, b := range data { + if (b < 32 || b > 126) && b != 9 && b != 10 && b != 13 { + nonPrintable++ + } + } + + // Allow up to 1% non-printable characters + return nonPrintable <= len(data)/100 +} diff --git a/cmd/ots/root.go b/cmd/ots/root.go index f8f0d57..0c4778f 100644 --- a/cmd/ots/root.go +++ b/cmd/ots/root.go @@ -33,6 +33,7 @@ func init() { rootCmd.AddCommand(verifyCmd) rootCmd.AddCommand(upgradeCmd) rootCmd.AddCommand(infoCmd) + rootCmd.AddCommand(pdfCmd) } // setupLogging configures the global logger based on the flags diff --git a/cmd/ots/verify.go b/cmd/ots/verify.go index 49ad737..8e4485f 100644 --- a/cmd/ots/verify.go +++ b/cmd/ots/verify.go @@ -2,8 +2,10 @@ package main import ( "crypto/sha256" + "fmt" "log/slog" "os" + "strings" "time" "git.intruders.space/public/opentimestamps" @@ -137,8 +139,21 @@ func runVerifyCmd(cmd *cobra.Command, args []string) { } att := seq.GetAttestation() + + // Get the block time + blockHash, err := bitcoin.GetBlockHash(int64(att.BitcoinBlockHeight)) + blockTime := "" + if err == nil { + blockHeader, err := bitcoin.GetBlockHeader(blockHash) + if err == nil { + // Format the block time + blockTime = fmt.Sprintf(" (%s)", blockHeader.Timestamp.Format(time.RFC3339)) + } + } + slog.Info("Verification successful", - "block_height", att.BitcoinBlockHeight) + "block_height", att.BitcoinBlockHeight, + "block_time", strings.TrimPrefix(blockTime, " ")) if tx != nil { slog.Info("Bitcoin transaction", "txid", tx.TxHash().String()) diff --git a/fixtures/flatearthers-united.txt.certificate.pdf b/fixtures/flatearthers-united.txt.certificate.pdf new file mode 100644 index 0000000..a576ddd Binary files /dev/null and b/fixtures/flatearthers-united.txt.certificate.pdf differ diff --git a/go.mod b/go.mod index 3a1293c..7890c60 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.2 require ( github.com/btcsuite/btcd v0.24.2 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 + github.com/jung-kurt/gofpdf v1.16.2 github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 golang.org/x/crypto v0.36.0 diff --git a/go.sum b/go.sum index c72b310..87b0a1c 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= @@ -63,6 +64,9 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc= +github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -73,9 +77,12 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= @@ -83,6 +90,7 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -95,6 +103,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=