Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions eng/_util/cmd/updatecryptodocs/packages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft Corporation.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

// CryptoPackage describes a Go crypto package referenced by the generated
// documents.
type CryptoPackage struct {
// ImportPath is the Go import path of the package, e.g. "crypto/aes".
ImportPath string
// InUserGuide indicates whether the package has a dedicated section in the
// FIPS User Guide. When true, the package's section is generated from its
// entry in userGuideContent (userguide_content.go). The order of the User
// Guide sections follows the order of the InUserGuide entries in
// cryptoPackages.
InUserGuide bool
}

// cryptoPackages is the shared source of truth for the set of Go crypto
// packages referenced by the generated documents. Both
// CrossPlatformCryptography.md and fips/UserGuide.md derive their package links
// from this list, and the generator validates that every package they reference
// is registered here so the two documents cannot drift apart.
//
// The InUserGuide entries appear first, in the order they should be rendered in
// the User Guide. Packages that only appear in CrossPlatformCryptography.md
// (typically newer APIs that the manually-maintained User Guide has not
// documented yet) follow.
var cryptoPackages = []CryptoPackage{
// User Guide packages, in User Guide order.
{ImportPath: "crypto/aes", InUserGuide: true},
{ImportPath: "crypto/cipher", InUserGuide: true},
{ImportPath: "crypto/des", InUserGuide: true},
{ImportPath: "crypto/dsa", InUserGuide: true},
{ImportPath: "crypto/ecdh", InUserGuide: true},
{ImportPath: "crypto/ecdsa", InUserGuide: true},
{ImportPath: "crypto/ed25519", InUserGuide: true},
{ImportPath: "crypto/elliptic", InUserGuide: true},
{ImportPath: "crypto/hmac", InUserGuide: true},
{ImportPath: "crypto/md5", InUserGuide: true},
{ImportPath: "crypto/rand", InUserGuide: true},
{ImportPath: "crypto/rc4", InUserGuide: true},
{ImportPath: "crypto/sha1", InUserGuide: true},
{ImportPath: "crypto/sha256", InUserGuide: true},
{ImportPath: "crypto/sha512", InUserGuide: true},
{ImportPath: "crypto/rsa", InUserGuide: true},
{ImportPath: "crypto/subtle", InUserGuide: true},
{ImportPath: "crypto/tls", InUserGuide: true},

// Packages referenced only by CrossPlatformCryptography.md.
{ImportPath: "crypto/sha3"},
{ImportPath: "crypto/mldsa"},
{ImportPath: "crypto/mlkem"},
{ImportPath: "crypto/hpke"},
{ImportPath: "crypto/hkdf"},
{ImportPath: "crypto/pbkdf2"},
}

// packagesByImportPath indexes cryptoPackages by import path.
var packagesByImportPath = func() map[string]CryptoPackage {
m := make(map[string]CryptoPackage, len(cryptoPackages))
for _, p := range cryptoPackages {
m[p.ImportPath] = p
}
return m
}()

// isRegisteredPackage reports whether importPath is a package registered in the
// shared source of truth.
func isRegisteredPackage(importPath string) bool {
_, ok := packagesByImportPath[importPath]
return ok
}

// packageLink returns the Markdown link to the pkg.go.dev documentation for the
// given package.
func packageLink(importPath string) string {
return "https://pkg.go.dev/" + importPath
}

// userGuidePackages returns the registered packages that have a section in the
// User Guide, in the order they should be rendered.
func userGuidePackages() []CryptoPackage {
var out []CryptoPackage
for _, p := range cryptoPackages {
if p.InUserGuide {
out = append(out, p)
}
}
return out
}
66 changes: 55 additions & 11 deletions eng/_util/cmd/updatecryptodocs/updatecryptodocs.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,26 @@ import (

var docPath = flag.String("path", filepath.Join("eng", "doc", "CrossPlatformCryptography.md"), "Path to CrossPlatformCryptography.md")

var userGuidePath = flag.String("userguide-path", filepath.Join("eng", "doc", "fips", "UserGuide.md"), "Path to fips/UserGuide.md")

var whichDoc = flag.String("doc", "all", "Which document to generate: crossplatform, userguide, or all")

var useStdout = flag.Bool("stdout", false, "Write to stdout instead of file")

// document couples a generator function with its output path.
type document struct {
name string
path string
generate func() (string, error)
}

func documents() []document {
return []document{
{name: "crossplatform", path: *docPath, generate: generate},
{name: "userguide", path: *userGuidePath, generate: generateUserGuide},
}
}

func main() {
flag.Parse()

Expand All @@ -29,16 +47,33 @@ func main() {
}

func write() error {
s, err := generate()
if err != nil {
return fmt.Errorf("failed to generate document: %v", err)
var selected []document
for _, d := range documents() {
if *whichDoc == "all" || *whichDoc == d.name {
selected = append(selected, d)
}
}
if len(selected) == 0 {
return fmt.Errorf("unknown document %q: expected crossplatform, userguide, or all", *whichDoc)
}
if *useStdout && len(selected) != 1 {
return fmt.Errorf("-stdout requires selecting a single document with -doc")
}

if *useStdout {
fmt.Print(s)
return nil
for _, d := range selected {
s, err := d.generate()
if err != nil {
return fmt.Errorf("failed to generate %s document: %v", d.name, err)
}
if *useStdout {
fmt.Print(s)
continue
}
if err := os.WriteFile(d.path, []byte(s), 0o644); err != nil {
return err
}
}
return os.WriteFile(*docPath, []byte(s), 0o644)
return nil
}

func generate() (string, error) {
Expand Down Expand Up @@ -74,10 +109,8 @@ func printSection(w io.Writer, section Section, level int) {
fmt.Fprintln(w, "This section includes the following packages:")
fmt.Fprintln(w)
for _, pkg := range validPackages {
if strings.HasPrefix(pkg, "crypto/") {
fmt.Fprintf(w, "- [%s](https://pkg.go.dev/%s)\n", pkg, pkg)
} else if strings.HasPrefix(pkg, "golang.org/") {
fmt.Fprintf(w, "- [%s](https://pkg.go.dev/%s)\n", pkg, pkg)
if isRegisteredPackage(pkg) {
fmt.Fprintf(w, "- [%s](%s)\n", pkg, packageLink(pkg))
} else {
// Fallback
fmt.Fprintf(w, "- %s\n", pkg)
Expand Down Expand Up @@ -314,6 +347,17 @@ func validateSection(section Section) error {
return fmt.Errorf("section missing title")
}

// Every package link must resolve through the shared registry so that the
// generated documents cannot reference packages that are not tracked in the
// single source of truth.
for _, pkg := range section.Packages {
if strings.Contains(pkg, "/") || strings.Contains(pkg, ".") {
if !isRegisteredPackage(pkg) {
return fmt.Errorf("package %q in section %q is not registered in cryptoPackages", pkg, section.Title)
}
}
}

seenNames := make(map[string]bool)

for i := range section.Items {
Expand Down
20 changes: 20 additions & 0 deletions eng/_util/cmd/updatecryptodocs/updatecryptodocs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,23 @@ func TestUpdateCryptoDocsReproducible(t *testing.T) {
t.Errorf("Generated document does not match checked-in document. Run this command to update:\n pwsh eng/run.ps1 updatecryptodocs")
}
}

func TestUpdateUserGuideReproducible(t *testing.T) {
checkedInPath := filepath.Join("..", "..", "..", "..", *userGuidePath)
checkedIn, err := os.ReadFile(checkedInPath)
if err != nil {
t.Fatalf("Failed to read checked-in document at %q: %v", checkedInPath, err)
}

generated, err := generateUserGuide()
if err != nil {
t.Fatalf("Failed to generate document: %v", err)
}

// Normalize Git-checked-out line endings to LF for comparison.
actual := strings.ReplaceAll(string(checkedIn), "\r\n", "\n")

if actual != generated {
t.Errorf("Generated document does not match checked-in document. Run this command to update:\n pwsh eng/run.ps1 updatecryptodocs")
}
}
Loading