From 7d577f383e88bf9869bb3f7d7fb9ca368a0bd2ea Mon Sep 17 00:00:00 2001 From: lcoffe Date: Fri, 11 Apr 2025 13:57:38 +0200 Subject: [PATCH] add pdf command, show block time in verify command --- cmd/ots/pdf.go | 374 ++++++++++++++++++ cmd/ots/root.go | 1 + cmd/ots/verify.go | 17 +- .../flatearthers-united.txt.certificate.pdf | Bin 0 -> 6831 bytes go.mod | 1 + go.sum | 9 + 6 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 cmd/ots/pdf.go create mode 100644 fixtures/flatearthers-united.txt.certificate.pdf 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 0000000000000000000000000000000000000000..a576ddd53c0023799ae265416d3163cba2a8309c GIT binary patch literal 6831 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 literal 0 HcmV?d00001 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=