x509roots/fallback/bundle: add bundle package to export root certs

Fixes golang/go#69898

Change-Id: Idbb1bbe48016a622414c84a56fe26f48bfe712c8
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/687155
Reviewed-by: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Roland Shoemaker <roland@golang.org>
Reviewed-by: Mateusz Poliwczak <mpoliwczak34@gmail.com>
diff --git a/x509roots/fallback/bundle.der b/x509roots/fallback/bundle/bundle.der
similarity index 100%
rename from x509roots/fallback/bundle.der
rename to x509roots/fallback/bundle/bundle.der
Binary files differ
diff --git a/x509roots/fallback/bundle.go b/x509roots/fallback/bundle/bundle.go
similarity index 99%
rename from x509roots/fallback/bundle.go
rename to x509roots/fallback/bundle/bundle.go
index ee99a40..be9e857 100644
--- a/x509roots/fallback/bundle.go
+++ b/x509roots/fallback/bundle/bundle.go
@@ -1,6 +1,6 @@
 // Code generated by gen_fallback_bundle.go; DO NOT EDIT.
 
-package fallback
+package bundle
 
 var unparsedCertificates = []unparsedCertificate{
 	{
diff --git a/x509roots/fallback/bundle_test.go b/x509roots/fallback/bundle/bundle_test.go
similarity index 97%
rename from x509roots/fallback/bundle_test.go
rename to x509roots/fallback/bundle/bundle_test.go
index a8922cc..3eafe15 100644
--- a/x509roots/fallback/bundle_test.go
+++ b/x509roots/fallback/bundle/bundle_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package fallback
+package bundle
 
 import (
 	"crypto/sha256"
diff --git a/x509roots/fallback/bundle/roots.go b/x509roots/fallback/bundle/roots.go
new file mode 100644
index 0000000..38a1b3d
--- /dev/null
+++ b/x509roots/fallback/bundle/roots.go
@@ -0,0 +1,73 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package bundle contains the bundle of root certificates parsed from the NSS
+// trust store, using x509roots/nss.
+package bundle
+
+import (
+	"crypto/x509"
+	_ "embed"
+	"fmt"
+	"iter"
+	"time"
+)
+
+//go:embed bundle.der
+var rawCerts []byte
+
+// Root represents a root certificate parsed from the NSS trust store.
+type Root struct {
+	// Certificate is the DER-encoded certificate (read-only; do not modify!).
+	Certificate []byte
+
+	// Constraint is nil if the root is unconstrained. If Constraint is non-nil,
+	// the certificate has additional constraints that cannot be encoded in
+	// X.509, and when building a certificate chain anchored with this root the
+	// chain should be passed to this function to check its validity. If using a
+	// [crypto/x509.CertPool] the root should be added using
+	// [crypto/x509.CertPool.AddCertWithConstraint].
+	Constraint func([]*x509.Certificate) error
+}
+
+// Roots returns the bundle of root certificates from the NSS trust store. The
+// [Root.Certificate] slice must be treated as read-only and should not be
+// modified.
+func Roots() iter.Seq[Root] {
+	return func(yield func(Root) bool) {
+		for _, unparsed := range unparsedCertificates {
+			root := Root{
+				Certificate: rawCerts[unparsed.certStartOff : unparsed.certStartOff+unparsed.certLength],
+			}
+			// parse possible constraints, this should check all fields of unparsedCertificate.
+			if unparsed.distrustAfter != "" {
+				distrustAfter, err := time.Parse(time.RFC3339, unparsed.distrustAfter)
+				if err != nil {
+					panic(fmt.Sprintf("failed to parse distrustAfter %q: %s", unparsed.distrustAfter, err))
+				}
+				root.Constraint = func(chain []*x509.Certificate) error {
+					for _, c := range chain {
+						if c.NotBefore.After(distrustAfter) {
+							return fmt.Errorf("certificate issued after distrust-after date %q", distrustAfter)
+						}
+					}
+					return nil
+				}
+			}
+			if !yield(root) {
+				return
+			}
+		}
+	}
+}
+
+type unparsedCertificate struct {
+	cn           string
+	sha256Hash   string
+	certStartOff int
+	certLength   int
+
+	// possible constraints
+	distrustAfter string
+}
diff --git a/x509roots/fallback/bundle/roots_test.go b/x509roots/fallback/bundle/roots_test.go
new file mode 100644
index 0000000..04ba9db
--- /dev/null
+++ b/x509roots/fallback/bundle/roots_test.go
@@ -0,0 +1,18 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package bundle
+
+import (
+	"crypto/x509"
+	"testing"
+)
+
+func TestRootsCanBeParsed(t *testing.T) {
+	for root := range Roots() {
+		if _, err := x509.ParseCertificate(root.Certificate); err != nil {
+			t.Fatalf("Could not parse root certificate: %v", err)
+		}
+	}
+}
diff --git a/x509roots/fallback/fallback.go b/x509roots/fallback/fallback.go
index a0dad33..79e1870 100644
--- a/x509roots/fallback/fallback.go
+++ b/x509roots/fallback/fallback.go
@@ -20,13 +20,9 @@
 
 import (
 	"crypto/x509"
-	_ "embed"
-	"fmt"
-	"time"
-)
 
-//go:embed bundle.der
-var rawCerts []byte
+	"golang.org/x/crypto/x509roots/fallback/bundle"
+)
 
 func init() {
 	x509.SetFallbackRoots(newFallbackCertPool())
@@ -34,62 +30,16 @@
 
 func newFallbackCertPool() *x509.CertPool {
 	p := x509.NewCertPool()
-	for _, c := range mustParse(unparsedCertificates) {
-		if len(c.constraints) == 0 {
-			p.AddCert(c.cert)
-		} else {
-			p.AddCertWithConstraint(c.cert, func(chain []*x509.Certificate) error {
-				for _, constraint := range c.constraints {
-					if err := constraint(chain); err != nil {
-						return err
-					}
-				}
-				return nil
-			})
-		}
-	}
-	return p
-}
-
-type unparsedCertificate struct {
-	cn           string
-	sha256Hash   string
-	certStartOff int
-	certLength   int
-
-	// possible constraints
-	distrustAfter string
-}
-
-type parsedCertificate struct {
-	cert        *x509.Certificate
-	constraints []func([]*x509.Certificate) error
-}
-
-func mustParse(unparsedCerts []unparsedCertificate) []parsedCertificate {
-	b := make([]parsedCertificate, 0, len(unparsedCerts))
-	for _, unparsed := range unparsedCerts {
-		cert, err := x509.ParseCertificate(rawCerts[unparsed.certStartOff : unparsed.certStartOff+unparsed.certLength])
+	for c := range bundle.Roots() {
+		cert, err := x509.ParseCertificate(c.Certificate)
 		if err != nil {
 			panic(err)
 		}
-		parsed := parsedCertificate{cert: cert}
-		// parse possible constraints, this should check all fields of unparsedCertificate.
-		if unparsed.distrustAfter != "" {
-			distrustAfter, err := time.Parse(time.RFC3339, unparsed.distrustAfter)
-			if err != nil {
-				panic(fmt.Sprintf("failed to parse distrustAfter %q: %s", unparsed.distrustAfter, err))
-			}
-			parsed.constraints = append(parsed.constraints, func(chain []*x509.Certificate) error {
-				for _, c := range chain {
-					if c.NotBefore.After(distrustAfter) {
-						return fmt.Errorf("certificate issued after distrust-after date %q", distrustAfter)
-					}
-				}
-				return nil
-			})
+		if c.Constraint == nil {
+			p.AddCert(cert)
+		} else {
+			p.AddCertWithConstraint(cert, c.Constraint)
 		}
-		b = append(b, parsed)
 	}
-	return b
+	return p
 }
diff --git a/x509roots/gen_fallback_bundle.go b/x509roots/gen_fallback_bundle.go
index ed2f9f8..810996c 100644
--- a/x509roots/gen_fallback_bundle.go
+++ b/x509roots/gen_fallback_bundle.go
@@ -27,7 +27,7 @@
 
 const tmpl = `// Code generated by gen_fallback_bundle.go; DO NOT EDIT.
 
-package fallback
+package bundle
 
 var unparsedCertificates = []unparsedCertificate{
 `
@@ -35,8 +35,8 @@
 var (
 	certDataURL  = flag.String("certdata-url", "https://hg.mozilla.org/mozilla-central/raw-file/tip/security/nss/lib/ckfw/builtins/certdata.txt", "URL to the raw certdata.txt file to parse (certdata-path overrides this, if provided)")
 	certDataPath = flag.String("certdata-path", "", "Path to the NSS certdata.txt file to parse (this overrides certdata-url, if provided)")
-	output       = flag.String("output", "fallback/bundle.go", "Path to file to write output to")
-	derOutput    = flag.String("deroutput", "fallback/bundle.der", "Path to file to write output to (DER certificate bundle)")
+	output       = flag.String("output", "fallback/bundle/bundle.go", "Path to file to write output to")
+	derOutput    = flag.String("deroutput", "fallback/bundle/bundle.der", "Path to file to write output to (DER certificate bundle)")
 )
 
 func main() {