go.mod: update osbuild/images to v0.156.0

tag v0.155.0
Tagger: imagebuilder-bot <imagebuilder-bots+imagebuilder-bot@redhat.com>

Changes with 0.155.0

----------------
  * Fedora 43: add shadow-utils when LockRoot is enabled, update cloud-init service name (osbuild/images#1618)
    * Author: Achilleas Koutsou, Reviewers: Gianluca Zuccarelli, Michael Vogt
  * Update osbuild dependency commit ID to latest (osbuild/images#1609)
    * Author: SchutzBot, Reviewers: Achilleas Koutsou, Simon de Vlieger, Tomáš Hozza
  * Update snapshots to 20250626 (osbuild/images#1623)
    * Author: SchutzBot, Reviewers: Achilleas Koutsou, Simon de Vlieger
  * distro/rhel9: xz compress azure-cvm image type [HMS-8587] (osbuild/images#1620)
    * Author: Achilleas Koutsou, Reviewers: Simon de Vlieger, Tomáš Hozza
  * distro/rhel: introduce new image type: Azure SAP Apps [HMS-8738] (osbuild/images#1612)
    * Author: Achilleas Koutsou, Reviewers: Simon de Vlieger, Tomáš Hozza
  * distro/rhel: move ansible-core to sap_extras_pkgset (osbuild/images#1624)
    * Author: Achilleas Koutsou, Reviewers: Brian C. Lane, Tomáš Hozza
  * github/create-tag: allow passing the version when run manually (osbuild/images#1621)
    * Author: Achilleas Koutsou, Reviewers: Lukáš Zapletal, Tomáš Hozza
  * rhel9: move image-config into pure YAML (HMS-8593) (osbuild/images#1616)
    * Author: Michael Vogt, Reviewers: Achilleas Koutsou, Simon de Vlieger
  * test: split manifest checksums into separate files (osbuild/images#1625)
    * Author: Achilleas Koutsou, Reviewers: Simon de Vlieger, Tomáš Hozza

— Somewhere on the Internet, 2025-06-30

---

tag v0.156.0
Tagger: imagebuilder-bot <imagebuilder-bots+imagebuilder-bot@redhat.com>

Changes with 0.156.0

----------------
  * Many: delete repositories for EOL distributions (HMS-7044) (osbuild/images#1607)
    * Author: Tomáš Hozza, Reviewers: Michael Vogt, Simon de Vlieger
  * RHSM/facts: add 'image-builder CLI' API type (osbuild/images#1640)
    * Author: Tomáš Hozza, Reviewers: Brian C. Lane, Simon de Vlieger
  * Update dependencies 2025-06-29 (osbuild/images#1628)
    * Author: SchutzBot, Reviewers: Simon de Vlieger, Tomáš Hozza
  * Update osbuild dependency commit ID to latest (osbuild/images#1627)
    * Author: SchutzBot, Reviewers: Simon de Vlieger, Tomáš Hozza
  * [RFC] image: drop `InstallWeakDeps` from image.DiskImage (osbuild/images#1642)
    * Author: Michael Vogt, Reviewers: Brian C. Lane, Simon de Vlieger, Tomáš Hozza
  * build(deps): bump the go-deps group across 1 directory with 3 updates (osbuild/images#1632)
    * Author: dependabot[bot], Reviewers: SchutzBot, Tomáš Hozza
  * distro/rhel10: xz compress azure-cvm image type (osbuild/images#1638)
    * Author: Achilleas Koutsou, Reviewers: Brian C. Lane, Simon de Vlieger
  * distro: cleanup/refactor distro/{defs,generic} (HMS-8744) (osbuild/images#1570)
    * Author: Michael Vogt, Reviewers: Simon de Vlieger, Tomáš Hozza
  * distro: remove some hardcoded values from generic/images.go (osbuild/images#1636)
    * Author: Michael Vogt, Reviewers: Simon de Vlieger, Tomáš Hozza
  * distro: small tweaks for the YAML based imagetypes (osbuild/images#1622)
    * Author: Michael Vogt, Reviewers: Brian C. Lane, Simon de Vlieger
  * fedora/wsl: packages and locale (osbuild/images#1635)
    * Author: Simon de Vlieger, Reviewers: Michael Vogt, Tomáš Hozza
  * image/many: make compression more generic (osbuild/images#1634)
    * Author: Simon de Vlieger, Reviewers: Brian C. Lane, Michael Vogt
  * manifest: handle content template name with spaces (osbuild/images#1641)
    * Author: Bryttanie, Reviewers: Brian C. Lane, Michael Vogt, Tomáš Hozza
  * many: implement gzip (osbuild/images#1633)
    * Author: Simon de Vlieger, Reviewers: Michael Vogt, Tomáš Hozza
  * rhel/azure: set GRUB_TERMINAL based on architecture [RHEL-91383] (osbuild/images#1626)
    * Author: Achilleas Koutsou, Reviewers: Simon de Vlieger, Tomáš Hozza

— Somewhere on the Internet, 2025-07-07

---
This commit is contained in:
Achilleas Koutsou 2025-07-10 16:14:25 +02:00
parent 60c5f10af8
commit 3fd7092db5
1486 changed files with 124742 additions and 82516 deletions

201
vendor/github.com/spiffe/go-spiffe/v2/LICENSE generated vendored Normal file
View file

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,200 @@
package jwtbundle
import (
"crypto"
"encoding/json"
"errors"
"io"
"os"
"sync"
"github.com/go-jose/go-jose/v4"
"github.com/spiffe/go-spiffe/v2/internal/jwtutil"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/zeebo/errs"
)
var jwtbundleErr = errs.Class("jwtbundle")
// Bundle is a collection of trusted JWT authorities for a trust domain.
type Bundle struct {
trustDomain spiffeid.TrustDomain
mtx sync.RWMutex
jwtAuthorities map[string]crypto.PublicKey
}
// New creates a new bundle.
func New(trustDomain spiffeid.TrustDomain) *Bundle {
return &Bundle{
trustDomain: trustDomain,
jwtAuthorities: make(map[string]crypto.PublicKey),
}
}
// FromJWTAuthorities creates a new bundle from JWT authorities
func FromJWTAuthorities(trustDomain spiffeid.TrustDomain, jwtAuthorities map[string]crypto.PublicKey) *Bundle {
return &Bundle{
trustDomain: trustDomain,
jwtAuthorities: jwtutil.CopyJWTAuthorities(jwtAuthorities),
}
}
// Load loads a bundle from a file on disk. The file must contain a standard RFC 7517 JWKS document.
func Load(trustDomain spiffeid.TrustDomain, path string) (*Bundle, error) {
bundleBytes, err := os.ReadFile(path)
if err != nil {
return nil, jwtbundleErr.New("unable to read JWT bundle: %w", err)
}
return Parse(trustDomain, bundleBytes)
}
// Read decodes a bundle from a reader. The contents must contain a standard RFC 7517 JWKS document.
func Read(trustDomain spiffeid.TrustDomain, r io.Reader) (*Bundle, error) {
b, err := io.ReadAll(r)
if err != nil {
return nil, jwtbundleErr.New("unable to read: %v", err)
}
return Parse(trustDomain, b)
}
// Parse parses a bundle from bytes. The data must be a standard RFC 7517 JWKS document.
func Parse(trustDomain spiffeid.TrustDomain, bundleBytes []byte) (*Bundle, error) {
jwks := new(jose.JSONWebKeySet)
if err := json.Unmarshal(bundleBytes, jwks); err != nil {
return nil, jwtbundleErr.New("unable to parse JWKS: %v", err)
}
bundle := New(trustDomain)
for i, key := range jwks.Keys {
if err := bundle.AddJWTAuthority(key.KeyID, key.Key); err != nil {
return nil, jwtbundleErr.New("error adding authority %d of JWKS: %v", i, errors.Unwrap(err))
}
}
return bundle, nil
}
// TrustDomain returns the trust domain that the bundle belongs to.
func (b *Bundle) TrustDomain() spiffeid.TrustDomain {
return b.trustDomain
}
// JWTAuthorities returns the JWT authorities in the bundle, keyed by key ID.
func (b *Bundle) JWTAuthorities() map[string]crypto.PublicKey {
b.mtx.RLock()
defer b.mtx.RUnlock()
return jwtutil.CopyJWTAuthorities(b.jwtAuthorities)
}
// FindJWTAuthority finds the JWT authority with the given key ID from the bundle. If the authority
// is found, it is returned and the boolean is true. Otherwise, the returned
// value is nil and the boolean is false.
func (b *Bundle) FindJWTAuthority(keyID string) (crypto.PublicKey, bool) {
b.mtx.RLock()
defer b.mtx.RUnlock()
if jwtAuthority, ok := b.jwtAuthorities[keyID]; ok {
return jwtAuthority, true
}
return nil, false
}
// HasJWTAuthority returns true if the bundle has a JWT authority with the given key ID.
func (b *Bundle) HasJWTAuthority(keyID string) bool {
b.mtx.RLock()
defer b.mtx.RUnlock()
_, ok := b.jwtAuthorities[keyID]
return ok
}
// AddJWTAuthority adds a JWT authority to the bundle. If a JWT authority already exists
// under the given key ID, it is replaced. A key ID must be specified.
func (b *Bundle) AddJWTAuthority(keyID string, jwtAuthority crypto.PublicKey) error {
if keyID == "" {
return jwtbundleErr.New("keyID cannot be empty")
}
b.mtx.Lock()
defer b.mtx.Unlock()
b.jwtAuthorities[keyID] = jwtAuthority
return nil
}
// RemoveJWTAuthority removes the JWT authority identified by the key ID from the bundle.
func (b *Bundle) RemoveJWTAuthority(keyID string) {
b.mtx.Lock()
defer b.mtx.Unlock()
delete(b.jwtAuthorities, keyID)
}
// SetJWTAuthorities sets the JWT authorities in the bundle.
func (b *Bundle) SetJWTAuthorities(jwtAuthorities map[string]crypto.PublicKey) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.jwtAuthorities = jwtutil.CopyJWTAuthorities(jwtAuthorities)
}
// Empty returns true if the bundle has no JWT authorities.
func (b *Bundle) Empty() bool {
b.mtx.RLock()
defer b.mtx.RUnlock()
return len(b.jwtAuthorities) == 0
}
// Marshal marshals the JWT bundle into a standard RFC 7517 JWKS document. The
// JWKS does not contain any SPIFFE-specific parameters.
func (b *Bundle) Marshal() ([]byte, error) {
b.mtx.RLock()
defer b.mtx.RUnlock()
jwks := jose.JSONWebKeySet{}
for keyID, jwtAuthority := range b.jwtAuthorities {
jwks.Keys = append(jwks.Keys, jose.JSONWebKey{
Key: jwtAuthority,
KeyID: keyID,
})
}
return json.Marshal(jwks)
}
// Clone clones the bundle.
func (b *Bundle) Clone() *Bundle {
b.mtx.RLock()
defer b.mtx.RUnlock()
return FromJWTAuthorities(b.trustDomain, b.jwtAuthorities)
}
// Equal compares the bundle for equality against the given bundle.
func (b *Bundle) Equal(other *Bundle) bool {
if b == nil || other == nil {
return b == other
}
return b.trustDomain == other.trustDomain &&
jwtutil.JWTAuthoritiesEqual(b.jwtAuthorities, other.jwtAuthorities)
}
// GetJWTBundleForTrustDomain returns the JWT bundle for the given trust
// domain. It implements the Source interface. An error will be returned if
// the trust domain does not match that of the bundle.
func (b *Bundle) GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) {
b.mtx.RLock()
defer b.mtx.RUnlock()
if b.trustDomain != trustDomain {
return nil, jwtbundleErr.New("no JWT bundle for trust domain %q", trustDomain)
}
return b, nil
}

View file

@ -0,0 +1,43 @@
// Package jwtbundle provides JWT bundle related functionality.
//
// A bundle represents a collection of JWT authorities, i.e., those that
// are used to authenticate SPIFFE JWT-SVIDs.
//
// You can create a new bundle for a specific trust domain:
//
// td := spiffeid.RequireTrustDomainFromString("example.org")
// bundle := jwtbundle.New(td)
//
// Or you can load it from disk:
//
// td := spiffeid.RequireTrustDomainFromString("example.org")
// bundle := jwtbundle.Load(td, "bundle.jwks")
//
// The bundle can be initialized with JWT authorities:
//
// td := spiffeid.RequireTrustDomainFromString("example.org")
// var jwtAuthorities map[string]crypto.PublicKey = ...
// bundle := jwtbundle.FromJWTAuthorities(td, jwtAuthorities)
//
// In addition, you can add JWT authorities to the bundle:
//
// var keyID string = ...
// var publicKey crypto.PublicKey = ...
// bundle.AddJWTAuthority(keyID, publicKey)
//
// Bundles can be organized into a set, keyed by trust domain:
//
// set := jwtbundle.NewSet()
// set.Add(bundle)
//
// A Source is source of JWT bundles for a trust domain. Both the Bundle
// and Set types implement Source:
//
// // Initialize the source from a bundle or set
// var source jwtbundle.Source = bundle
// // ... or ...
// var source jwtbundle.Source = set
//
// // Use the source to query for bundles by trust domain
// bundle, err := source.GetJWTBundleForTrustDomain(td)
package jwtbundle

View file

@ -0,0 +1,105 @@
package jwtbundle
import (
"sort"
"sync"
"github.com/spiffe/go-spiffe/v2/spiffeid"
)
// Set is a set of bundles, keyed by trust domain.
type Set struct {
mtx sync.RWMutex
bundles map[spiffeid.TrustDomain]*Bundle
}
// NewSet creates a new set initialized with the given bundles.
func NewSet(bundles ...*Bundle) *Set {
bundlesMap := make(map[spiffeid.TrustDomain]*Bundle)
for _, b := range bundles {
if b != nil {
bundlesMap[b.trustDomain] = b
}
}
return &Set{
bundles: bundlesMap,
}
}
// Add adds a new bundle into the set. If a bundle already exists for the
// trust domain, the existing bundle is replaced.
func (s *Set) Add(bundle *Bundle) {
s.mtx.Lock()
defer s.mtx.Unlock()
if bundle != nil {
s.bundles[bundle.trustDomain] = bundle
}
}
// Remove removes the bundle for the given trust domain.
func (s *Set) Remove(trustDomain spiffeid.TrustDomain) {
s.mtx.Lock()
defer s.mtx.Unlock()
delete(s.bundles, trustDomain)
}
// Has returns true if there is a bundle for the given trust domain.
func (s *Set) Has(trustDomain spiffeid.TrustDomain) bool {
s.mtx.RLock()
defer s.mtx.RUnlock()
_, ok := s.bundles[trustDomain]
return ok
}
// Get returns a bundle for the given trust domain. If the bundle is in the set
// it is returned and the boolean is true. Otherwise, the returned value is
// nil and the boolean is false.
func (s *Set) Get(trustDomain spiffeid.TrustDomain) (*Bundle, bool) {
s.mtx.RLock()
defer s.mtx.RUnlock()
bundle, ok := s.bundles[trustDomain]
return bundle, ok
}
// Bundles returns the bundles in the set sorted by trust domain.
func (s *Set) Bundles() []*Bundle {
s.mtx.RLock()
defer s.mtx.RUnlock()
out := make([]*Bundle, 0, len(s.bundles))
for _, bundle := range s.bundles {
out = append(out, bundle)
}
sort.Slice(out, func(a, b int) bool {
return out[a].TrustDomain().Compare(out[b].TrustDomain()) < 0
})
return out
}
// Len returns the number of bundles in the set.
func (s *Set) Len() int {
s.mtx.RLock()
defer s.mtx.RUnlock()
return len(s.bundles)
}
// GetJWTBundleForTrustDomain returns the JWT bundle for the given trust
// domain. It implements the Source interface.
func (s *Set) GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) {
s.mtx.RLock()
defer s.mtx.RUnlock()
bundle, ok := s.bundles[trustDomain]
if !ok {
return nil, jwtbundleErr.New("no JWT bundle for trust domain %q", trustDomain)
}
return bundle, nil
}

View file

@ -0,0 +1,12 @@
package jwtbundle
import (
"github.com/spiffe/go-spiffe/v2/spiffeid"
)
// Source represents a source of JWT bundles keyed by trust domain.
type Source interface {
// GetJWTBundleForTrustDomain returns the JWT bundle for the given trust
// domain.
GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error)
}

View file

@ -0,0 +1,485 @@
package spiffebundle
import (
"crypto"
"crypto/x509"
"encoding/json"
"errors"
"io"
"os"
"sync"
"time"
"github.com/go-jose/go-jose/v4"
"github.com/spiffe/go-spiffe/v2/bundle/jwtbundle"
"github.com/spiffe/go-spiffe/v2/bundle/x509bundle"
"github.com/spiffe/go-spiffe/v2/internal/jwtutil"
"github.com/spiffe/go-spiffe/v2/internal/x509util"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/zeebo/errs"
)
const (
x509SVIDUse = "x509-svid"
jwtSVIDUse = "jwt-svid"
)
var spiffebundleErr = errs.Class("spiffebundle")
type bundleDoc struct {
jose.JSONWebKeySet
SequenceNumber *uint64 `json:"spiffe_sequence,omitempty"`
RefreshHint *int64 `json:"spiffe_refresh_hint,omitempty"`
}
// Bundle is a collection of trusted public key material for a trust domain,
// conforming to the SPIFFE Bundle Format as part of the SPIFFE Trust Domain
// and Bundle specification:
// https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md
type Bundle struct {
trustDomain spiffeid.TrustDomain
mtx sync.RWMutex
refreshHint *time.Duration
sequenceNumber *uint64
jwtAuthorities map[string]crypto.PublicKey
x509Authorities []*x509.Certificate
}
// New creates a new bundle.
func New(trustDomain spiffeid.TrustDomain) *Bundle {
return &Bundle{
trustDomain: trustDomain,
jwtAuthorities: make(map[string]crypto.PublicKey),
}
}
// Load loads a bundle from a file on disk. The file must contain a JWKS
// document following the SPIFFE Trust Domain and Bundle specification.
func Load(trustDomain spiffeid.TrustDomain, path string) (*Bundle, error) {
bundleBytes, err := os.ReadFile(path)
if err != nil {
return nil, spiffebundleErr.New("unable to read SPIFFE bundle: %w", err)
}
return Parse(trustDomain, bundleBytes)
}
// Read decodes a bundle from a reader. The contents must contain a JWKS
// document following the SPIFFE Trust Domain and Bundle specification.
func Read(trustDomain spiffeid.TrustDomain, r io.Reader) (*Bundle, error) {
b, err := io.ReadAll(r)
if err != nil {
return nil, spiffebundleErr.New("unable to read: %v", err)
}
return Parse(trustDomain, b)
}
// Parse parses a bundle from bytes. The data must be a JWKS document following
// the SPIFFE Trust Domain and Bundle specification.
func Parse(trustDomain spiffeid.TrustDomain, bundleBytes []byte) (*Bundle, error) {
jwks := &bundleDoc{}
if err := json.Unmarshal(bundleBytes, jwks); err != nil {
return nil, spiffebundleErr.New("unable to parse JWKS: %v", err)
}
bundle := New(trustDomain)
if jwks.RefreshHint != nil {
bundle.SetRefreshHint(time.Second * time.Duration(*jwks.RefreshHint))
}
if jwks.SequenceNumber != nil {
bundle.SetSequenceNumber(*jwks.SequenceNumber)
}
if jwks.Keys == nil {
// The parameter keys MUST be present.
// https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#413-keys
return nil, spiffebundleErr.New("no authorities found")
}
for i, key := range jwks.Keys {
switch key.Use {
// Two SVID types are supported: x509-svid and jwt-svid.
case x509SVIDUse:
if len(key.Certificates) != 1 {
return nil, spiffebundleErr.New("expected a single certificate in %s entry %d; got %d", x509SVIDUse, i, len(key.Certificates))
}
bundle.AddX509Authority(key.Certificates[0])
case jwtSVIDUse:
if err := bundle.AddJWTAuthority(key.KeyID, key.Key); err != nil {
return nil, spiffebundleErr.New("error adding authority %d of JWKS: %v", i, errors.Unwrap(err))
}
}
}
return bundle, nil
}
// FromX509Bundle creates a bundle from an X.509 bundle.
// The function panics in case of a nil X.509 bundle.
func FromX509Bundle(x509Bundle *x509bundle.Bundle) *Bundle {
bundle := New(x509Bundle.TrustDomain())
bundle.x509Authorities = x509Bundle.X509Authorities()
return bundle
}
// FromJWTBundle creates a bundle from a JWT bundle.
// The function panics in case of a nil JWT bundle.
func FromJWTBundle(jwtBundle *jwtbundle.Bundle) *Bundle {
bundle := New(jwtBundle.TrustDomain())
bundle.jwtAuthorities = jwtBundle.JWTAuthorities()
return bundle
}
// FromX509Authorities creates a bundle from X.509 certificates.
func FromX509Authorities(trustDomain spiffeid.TrustDomain, x509Authorities []*x509.Certificate) *Bundle {
bundle := New(trustDomain)
bundle.x509Authorities = x509util.CopyX509Authorities(x509Authorities)
return bundle
}
// FromJWTAuthorities creates a new bundle from JWT authorities.
func FromJWTAuthorities(trustDomain spiffeid.TrustDomain, jwtAuthorities map[string]crypto.PublicKey) *Bundle {
bundle := New(trustDomain)
bundle.jwtAuthorities = jwtutil.CopyJWTAuthorities(jwtAuthorities)
return bundle
}
// TrustDomain returns the trust domain that the bundle belongs to.
func (b *Bundle) TrustDomain() spiffeid.TrustDomain {
return b.trustDomain
}
// X509Authorities returns the X.509 authorities in the bundle.
func (b *Bundle) X509Authorities() []*x509.Certificate {
b.mtx.RLock()
defer b.mtx.RUnlock()
return x509util.CopyX509Authorities(b.x509Authorities)
}
// AddX509Authority adds an X.509 authority to the bundle. If the authority already
// exists in the bundle, the contents of the bundle will remain unchanged.
func (b *Bundle) AddX509Authority(x509Authority *x509.Certificate) {
b.mtx.Lock()
defer b.mtx.Unlock()
for _, r := range b.x509Authorities {
if r.Equal(x509Authority) {
return
}
}
b.x509Authorities = append(b.x509Authorities, x509Authority)
}
// RemoveX509Authority removes an X.509 authority from the bundle.
func (b *Bundle) RemoveX509Authority(x509Authority *x509.Certificate) {
b.mtx.Lock()
defer b.mtx.Unlock()
for i, r := range b.x509Authorities {
if r.Equal(x509Authority) {
b.x509Authorities = append(b.x509Authorities[:i], b.x509Authorities[i+1:]...)
return
}
}
}
// HasX509Authority checks if the given X.509 authority exists in the bundle.
func (b *Bundle) HasX509Authority(x509Authority *x509.Certificate) bool {
b.mtx.RLock()
defer b.mtx.RUnlock()
for _, r := range b.x509Authorities {
if r.Equal(x509Authority) {
return true
}
}
return false
}
// SetX509Authorities sets the X.509 authorities in the bundle.
func (b *Bundle) SetX509Authorities(authorities []*x509.Certificate) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.x509Authorities = x509util.CopyX509Authorities(authorities)
}
// JWTAuthorities returns the JWT authorities in the bundle, keyed by key ID.
func (b *Bundle) JWTAuthorities() map[string]crypto.PublicKey {
b.mtx.RLock()
defer b.mtx.RUnlock()
return jwtutil.CopyJWTAuthorities(b.jwtAuthorities)
}
// FindJWTAuthority finds the JWT authority with the given key ID from the bundle. If the authority
// is found, it is returned and the boolean is true. Otherwise, the returned
// value is nil and the boolean is false.
func (b *Bundle) FindJWTAuthority(keyID string) (crypto.PublicKey, bool) {
b.mtx.RLock()
defer b.mtx.RUnlock()
jwtAuthority, ok := b.jwtAuthorities[keyID]
return jwtAuthority, ok
}
// HasJWTAuthority returns true if the bundle has a JWT authority with the given key ID.
func (b *Bundle) HasJWTAuthority(keyID string) bool {
b.mtx.RLock()
defer b.mtx.RUnlock()
_, ok := b.jwtAuthorities[keyID]
return ok
}
// AddJWTAuthority adds a JWT authority to the bundle. If a JWT authority already exists
// under the given key ID, it is replaced. A key ID must be specified.
func (b *Bundle) AddJWTAuthority(keyID string, jwtAuthority crypto.PublicKey) error {
if keyID == "" {
return spiffebundleErr.New("keyID cannot be empty")
}
b.mtx.Lock()
defer b.mtx.Unlock()
b.jwtAuthorities[keyID] = jwtAuthority
return nil
}
// RemoveJWTAuthority removes the JWT authority identified by the key ID from the bundle.
func (b *Bundle) RemoveJWTAuthority(keyID string) {
b.mtx.Lock()
defer b.mtx.Unlock()
delete(b.jwtAuthorities, keyID)
}
// SetJWTAuthorities sets the JWT authorities in the bundle.
func (b *Bundle) SetJWTAuthorities(jwtAuthorities map[string]crypto.PublicKey) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.jwtAuthorities = jwtutil.CopyJWTAuthorities(jwtAuthorities)
}
// Empty returns true if the bundle has no X.509 and JWT authorities.
func (b *Bundle) Empty() bool {
b.mtx.RLock()
defer b.mtx.RUnlock()
return len(b.x509Authorities) == 0 && len(b.jwtAuthorities) == 0
}
// RefreshHint returns the refresh hint. If the refresh hint is set in
// the bundle, it is returned and the boolean is true. Otherwise, the returned
// value is zero and the boolean is false.
func (b *Bundle) RefreshHint() (refreshHint time.Duration, ok bool) {
b.mtx.RLock()
defer b.mtx.RUnlock()
if b.refreshHint != nil {
return *b.refreshHint, true
}
return 0, false
}
// SetRefreshHint sets the refresh hint. The refresh hint value will be
// truncated to time.Second.
func (b *Bundle) SetRefreshHint(refreshHint time.Duration) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.refreshHint = &refreshHint
}
// ClearRefreshHint clears the refresh hint.
func (b *Bundle) ClearRefreshHint() {
b.mtx.Lock()
defer b.mtx.Unlock()
b.refreshHint = nil
}
// SequenceNumber returns the sequence number. If the sequence number is set in
// the bundle, it is returned and the boolean is true. Otherwise, the returned
// value is zero and the boolean is false.
func (b *Bundle) SequenceNumber() (uint64, bool) {
b.mtx.RLock()
defer b.mtx.RUnlock()
if b.sequenceNumber != nil {
return *b.sequenceNumber, true
}
return 0, false
}
// SetSequenceNumber sets the sequence number.
func (b *Bundle) SetSequenceNumber(sequenceNumber uint64) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.sequenceNumber = &sequenceNumber
}
// ClearSequenceNumber clears the sequence number.
func (b *Bundle) ClearSequenceNumber() {
b.mtx.Lock()
defer b.mtx.Unlock()
b.sequenceNumber = nil
}
// Marshal marshals the bundle according to the SPIFFE Trust Domain and Bundle
// specification. The trust domain is not marshaled as part of the bundle and
// must be conveyed separately. See the specification for details.
func (b *Bundle) Marshal() ([]byte, error) {
b.mtx.RLock()
defer b.mtx.RUnlock()
jwks := bundleDoc{}
if b.refreshHint != nil {
tr := int64((*b.refreshHint + (time.Second - 1)) / time.Second)
jwks.RefreshHint = &tr
}
jwks.SequenceNumber = b.sequenceNumber
for _, x509Authority := range b.x509Authorities {
jwks.Keys = append(jwks.Keys, jose.JSONWebKey{
Key: x509Authority.PublicKey,
Certificates: []*x509.Certificate{x509Authority},
Use: x509SVIDUse,
})
}
for keyID, jwtAuthority := range b.jwtAuthorities {
jwks.Keys = append(jwks.Keys, jose.JSONWebKey{
Key: jwtAuthority,
KeyID: keyID,
Use: jwtSVIDUse,
})
}
return json.Marshal(jwks)
}
// Clone clones the bundle.
func (b *Bundle) Clone() *Bundle {
b.mtx.RLock()
defer b.mtx.RUnlock()
return &Bundle{
trustDomain: b.trustDomain,
refreshHint: copyRefreshHint(b.refreshHint),
sequenceNumber: copySequenceNumber(b.sequenceNumber),
x509Authorities: x509util.CopyX509Authorities(b.x509Authorities),
jwtAuthorities: jwtutil.CopyJWTAuthorities(b.jwtAuthorities),
}
}
// X509Bundle returns an X.509 bundle containing the X.509 authorities in the SPIFFE
// bundle.
func (b *Bundle) X509Bundle() *x509bundle.Bundle {
b.mtx.RLock()
defer b.mtx.RUnlock()
// FromX509Authorities makes a copy, so we can pass our internal slice directly.
return x509bundle.FromX509Authorities(b.trustDomain, b.x509Authorities)
}
// JWTBundle returns a JWT bundle containing the JWT authorities in the SPIFFE bundle.
func (b *Bundle) JWTBundle() *jwtbundle.Bundle {
b.mtx.RLock()
defer b.mtx.RUnlock()
// FromJWTBundle makes a copy, so we can pass our internal slice directly.
return jwtbundle.FromJWTAuthorities(b.trustDomain, b.jwtAuthorities)
}
// GetBundleForTrustDomain returns the SPIFFE bundle for the given trust
// domain. It implements the Source interface. An error will be returned if the
// trust domain does not match that of the bundle.
func (b *Bundle) GetBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) {
b.mtx.RLock()
defer b.mtx.RUnlock()
if b.trustDomain != trustDomain {
return nil, spiffebundleErr.New("no SPIFFE bundle for trust domain %q", trustDomain)
}
return b, nil
}
// GetX509BundleForTrustDomain returns the X.509 bundle for the given trust
// domain. It implements the x509bundle.Source interface. An error will be
// returned if the trust domain does not match that of the bundle.
func (b *Bundle) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*x509bundle.Bundle, error) {
b.mtx.RLock()
defer b.mtx.RUnlock()
if b.trustDomain != trustDomain {
return nil, spiffebundleErr.New("no X.509 bundle for trust domain %q", trustDomain)
}
return b.X509Bundle(), nil
}
// GetJWTBundleForTrustDomain returns the JWT bundle of the given trust domain.
// It implements the jwtbundle.Source interface. An error will be returned if
// the trust domain does not match that of the bundle.
func (b *Bundle) GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*jwtbundle.Bundle, error) {
b.mtx.RLock()
defer b.mtx.RUnlock()
if b.trustDomain != trustDomain {
return nil, spiffebundleErr.New("no JWT bundle for trust domain %q", trustDomain)
}
return b.JWTBundle(), nil
}
// Equal compares the bundle for equality against the given bundle.
func (b *Bundle) Equal(other *Bundle) bool {
if b == nil || other == nil {
return b == other
}
return b.trustDomain == other.trustDomain &&
refreshHintEqual(b.refreshHint, other.refreshHint) &&
sequenceNumberEqual(b.sequenceNumber, other.sequenceNumber) &&
jwtutil.JWTAuthoritiesEqual(b.jwtAuthorities, other.jwtAuthorities) &&
x509util.CertsEqual(b.x509Authorities, other.x509Authorities)
}
func refreshHintEqual(a, b *time.Duration) bool {
if a == nil || b == nil {
return a == b
}
return *a == *b
}
func sequenceNumberEqual(a, b *uint64) bool {
if a == nil || b == nil {
return a == b
}
return *a == *b
}
func copyRefreshHint(refreshHint *time.Duration) *time.Duration {
if refreshHint == nil {
return nil
}
copied := *refreshHint
return &copied
}
func copySequenceNumber(sequenceNumber *uint64) *uint64 {
if sequenceNumber == nil {
return nil
}
copied := *sequenceNumber
return &copied
}

View file

@ -0,0 +1,59 @@
// Package spiffebundle provides SPIFFE bundle related functionality.
//
// A bundle represents a SPIFFE bundle, a collection authorities for
// authenticating SVIDs.
//
// You can create a new bundle for a specific trust domain:
//
// td := spiffeid.RequireTrustDomainFromString("example.org")
// bundle := spiffebundle.New(td)
//
// Or you can load it from disk:
//
// td := spiffeid.RequireTrustDomainFromString("example.org")
// bundle := spiffebundle.Load(td, "bundle.json")
//
// The bundle can be initialized with X.509 or JWT authorities:
//
// td := spiffeid.RequireTrustDomainFromString("example.org")
//
// var x509Authorities []*x509.Certificate = ...
// bundle := spiffebundle.FromX509Authorities(td, x509Authorities)
// // ... or ...
// var jwtAuthorities map[string]crypto.PublicKey = ...
// bundle := spiffebundle.FromJWTAuthorities(td, jwtAuthorities)
//
// In addition, you can add authorities to the bundle:
//
// var x509CA *x509.Certificate = ...
// bundle.AddX509Authority(x509CA)
// var keyID string = ...
// var publicKey crypto.PublicKey = ...
// bundle.AddJWTAuthority(keyID, publicKey)
//
// Bundles can be organized into a set, keyed by trust domain:
//
// set := spiffebundle.NewSet()
// set.Add(bundle)
//
// A Source is source of bundles for a trust domain. Both the
// Bundle and Set types implement Source:
//
// // Initialize the source from a bundle or set
// var source spiffebundle.Source = bundle
// // ... or ...
// var source spiffebundle.Source = set
//
// // Use the source to query for X.509 bundles by trust domain
// bundle, err := source.GetBundleForTrustDomain(td)
//
// Additionally the Bundle and Set types also implement the x509bundle.Source and jwtbundle.Source interfaces:
//
// // As an x509bundle.Source...
// var source x509bundle.Source = bundle // or set
// x509Bundle, err := source.GetX509BundleForTrustDomain(td)
//
// // As a jwtbundle.Source...
// var source jwtbundle.Source = bundle // or set
// jwtBundle, err := source.GetJWTBundleForTrustDomain(td)
package spiffebundle

View file

@ -0,0 +1,135 @@
package spiffebundle
import (
"sort"
"sync"
"github.com/spiffe/go-spiffe/v2/bundle/jwtbundle"
"github.com/spiffe/go-spiffe/v2/bundle/x509bundle"
"github.com/spiffe/go-spiffe/v2/spiffeid"
)
// Set is a set of bundles, keyed by trust domain.
type Set struct {
mtx sync.RWMutex
bundles map[spiffeid.TrustDomain]*Bundle
}
// NewSet creates a new set initialized with the given bundles.
func NewSet(bundles ...*Bundle) *Set {
bundlesMap := make(map[spiffeid.TrustDomain]*Bundle)
for _, b := range bundles {
if b != nil {
bundlesMap[b.trustDomain] = b
}
}
return &Set{
bundles: bundlesMap,
}
}
// Add adds a new bundle into the set. If a bundle already exists for the
// trust domain, the existing bundle is replaced.
func (s *Set) Add(bundle *Bundle) {
s.mtx.Lock()
defer s.mtx.Unlock()
if bundle != nil {
s.bundles[bundle.trustDomain] = bundle
}
}
// Remove removes the bundle for the given trust domain.
func (s *Set) Remove(trustDomain spiffeid.TrustDomain) {
s.mtx.Lock()
defer s.mtx.Unlock()
delete(s.bundles, trustDomain)
}
// Has returns true if there is a bundle for the given trust domain.
func (s *Set) Has(trustDomain spiffeid.TrustDomain) bool {
s.mtx.RLock()
defer s.mtx.RUnlock()
_, ok := s.bundles[trustDomain]
return ok
}
// Get returns a bundle for the given trust domain. If the bundle is in the set
// it is returned and the boolean is true. Otherwise, the returned value is
// nil and the boolean is false.
func (s *Set) Get(trustDomain spiffeid.TrustDomain) (*Bundle, bool) {
s.mtx.RLock()
defer s.mtx.RUnlock()
bundle, ok := s.bundles[trustDomain]
return bundle, ok
}
// Bundles returns the bundles in the set sorted by trust domain.
func (s *Set) Bundles() []*Bundle {
s.mtx.RLock()
defer s.mtx.RUnlock()
out := make([]*Bundle, 0, len(s.bundles))
for _, bundle := range s.bundles {
out = append(out, bundle)
}
sort.Slice(out, func(a, b int) bool {
return out[a].TrustDomain().Compare(out[b].TrustDomain()) < 0
})
return out
}
// Len returns the number of bundles in the set.
func (s *Set) Len() int {
s.mtx.RLock()
defer s.mtx.RUnlock()
return len(s.bundles)
}
// GetBundleForTrustDomain returns the SPIFFE bundle for the given trust
// domain. It implements the Source interface.
func (s *Set) GetBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) {
s.mtx.RLock()
defer s.mtx.RUnlock()
bundle, ok := s.bundles[trustDomain]
if !ok {
return nil, spiffebundleErr.New("no SPIFFE bundle for trust domain %q", trustDomain)
}
return bundle, nil
}
// GetX509BundleForTrustDomain returns the X.509 bundle for the given trust
// domain. It implements the x509bundle.Source interface.
func (s *Set) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*x509bundle.Bundle, error) {
s.mtx.RLock()
defer s.mtx.RUnlock()
bundle, ok := s.bundles[trustDomain]
if !ok {
return nil, spiffebundleErr.New("no X.509 bundle for trust domain %q", trustDomain)
}
return bundle.X509Bundle(), nil
}
// GetJWTBundleForTrustDomain returns the JWT bundle for the given trust
// domain. It implements the jwtbundle.Source interface.
func (s *Set) GetJWTBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*jwtbundle.Bundle, error) {
s.mtx.RLock()
defer s.mtx.RUnlock()
bundle, ok := s.bundles[trustDomain]
if !ok {
return nil, spiffebundleErr.New("no JWT bundle for trust domain %q", trustDomain)
}
return bundle.JWTBundle(), nil
}

View file

@ -0,0 +1,10 @@
package spiffebundle
import "github.com/spiffe/go-spiffe/v2/spiffeid"
// Source represents a source of SPIFFE bundles keyed by trust domain.
type Source interface {
// GetBundleForTrustDomain returns the SPIFFE bundle for the given trust
// domain.
GetBundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error)
}

View file

@ -0,0 +1,202 @@
package x509bundle
import (
"crypto/x509"
"io"
"os"
"sync"
"github.com/spiffe/go-spiffe/v2/internal/pemutil"
"github.com/spiffe/go-spiffe/v2/internal/x509util"
"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/zeebo/errs"
)
var x509bundleErr = errs.Class("x509bundle")
// Bundle is a collection of trusted X.509 authorities for a trust domain.
type Bundle struct {
trustDomain spiffeid.TrustDomain
mtx sync.RWMutex
x509Authorities []*x509.Certificate
}
// New creates a new bundle.
func New(trustDomain spiffeid.TrustDomain) *Bundle {
return &Bundle{
trustDomain: trustDomain,
}
}
// FromX509Authorities creates a bundle from X.509 certificates.
func FromX509Authorities(trustDomain spiffeid.TrustDomain, authorities []*x509.Certificate) *Bundle {
return &Bundle{
trustDomain: trustDomain,
x509Authorities: x509util.CopyX509Authorities(authorities),
}
}
// Load loads a bundle from a file on disk. The file must contain PEM-encoded
// certificate blocks.
func Load(trustDomain spiffeid.TrustDomain, path string) (*Bundle, error) {
fileBytes, err := os.ReadFile(path)
if err != nil {
return nil, x509bundleErr.New("unable to load X.509 bundle file: %w", err)
}
return Parse(trustDomain, fileBytes)
}
// Read decodes a bundle from a reader. The contents must be PEM-encoded
// certificate blocks.
func Read(trustDomain spiffeid.TrustDomain, r io.Reader) (*Bundle, error) {
b, err := io.ReadAll(r)
if err != nil {
return nil, x509bundleErr.New("unable to read X.509 bundle: %v", err)
}
return Parse(trustDomain, b)
}
// Parse parses a bundle from bytes. The data must be PEM-encoded certificate
// blocks.
func Parse(trustDomain spiffeid.TrustDomain, b []byte) (*Bundle, error) {
bundle := New(trustDomain)
if len(b) == 0 {
return bundle, nil
}
certs, err := pemutil.ParseCertificates(b)
if err != nil {
return nil, x509bundleErr.New("cannot parse certificate: %v", err)
}
for _, cert := range certs {
bundle.AddX509Authority(cert)
}
return bundle, nil
}
// ParseRaw parses a bundle from bytes. The certificate must be ASN.1 DER (concatenated
// with no intermediate padding if there are more than one certificate)
func ParseRaw(trustDomain spiffeid.TrustDomain, b []byte) (*Bundle, error) {
bundle := New(trustDomain)
if len(b) == 0 {
return bundle, nil
}
certs, err := x509.ParseCertificates(b)
if err != nil {
return nil, x509bundleErr.New("cannot parse certificate: %v", err)
}
for _, cert := range certs {
bundle.AddX509Authority(cert)
}
return bundle, nil
}
// TrustDomain returns the trust domain that the bundle belongs to.
func (b *Bundle) TrustDomain() spiffeid.TrustDomain {
return b.trustDomain
}
// X509Authorities returns the X.509 x509Authorities in the bundle.
func (b *Bundle) X509Authorities() []*x509.Certificate {
b.mtx.RLock()
defer b.mtx.RUnlock()
return x509util.CopyX509Authorities(b.x509Authorities)
}
// AddX509Authority adds an X.509 authority to the bundle. If the authority already
// exists in the bundle, the contents of the bundle will remain unchanged.
func (b *Bundle) AddX509Authority(x509Authority *x509.Certificate) {
b.mtx.Lock()
defer b.mtx.Unlock()
for _, r := range b.x509Authorities {
if r.Equal(x509Authority) {
return
}
}
b.x509Authorities = append(b.x509Authorities, x509Authority)
}
// RemoveX509Authority removes an X.509 authority from the bundle.
func (b *Bundle) RemoveX509Authority(x509Authority *x509.Certificate) {
b.mtx.Lock()
defer b.mtx.Unlock()
for i, r := range b.x509Authorities {
if r.Equal(x509Authority) {
// remove element from slice
b.x509Authorities = append(b.x509Authorities[:i], b.x509Authorities[i+1:]...)
return
}
}
}
// HasX509Authority checks if the given X.509 authority exists in the bundle.
func (b *Bundle) HasX509Authority(x509Authority *x509.Certificate) bool {
b.mtx.RLock()
defer b.mtx.RUnlock()
for _, r := range b.x509Authorities {
if r.Equal(x509Authority) {
return true
}
}
return false
}
// SetX509Authorities sets the X.509 authorities in the bundle.
func (b *Bundle) SetX509Authorities(x509Authorities []*x509.Certificate) {
b.mtx.Lock()
defer b.mtx.Unlock()
b.x509Authorities = x509util.CopyX509Authorities(x509Authorities)
}
// Empty returns true if the bundle has no X.509 x509Authorities.
func (b *Bundle) Empty() bool {
b.mtx.RLock()
defer b.mtx.RUnlock()
return len(b.x509Authorities) == 0
}
// Marshal marshals the X.509 bundle into PEM-encoded certificate blocks.
func (b *Bundle) Marshal() ([]byte, error) {
b.mtx.RLock()
defer b.mtx.RUnlock()
return pemutil.EncodeCertificates(b.x509Authorities), nil
}
// Equal compares the bundle for equality against the given bundle.
func (b *Bundle) Equal(other *Bundle) bool {
if b == nil || other == nil {
return b == other
}
return b.trustDomain == other.trustDomain &&
x509util.CertsEqual(b.x509Authorities, other.x509Authorities)
}
// Clone clones the bundle.
func (b *Bundle) Clone() *Bundle {
b.mtx.RLock()
defer b.mtx.RUnlock()
return FromX509Authorities(b.trustDomain, b.x509Authorities)
}
// GetX509BundleForTrustDomain returns the X.509 bundle for the given trust
// domain. It implements the Source interface. An error will be
// returned if the trust domain does not match that of the bundle.
func (b *Bundle) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) {
if b.trustDomain != trustDomain {
return nil, x509bundleErr.New("no X.509 bundle found for trust domain: %q", trustDomain)
}
return b, nil
}

View file

@ -0,0 +1,42 @@
// Package x509bundle provides X.509 bundle related functionality.
//
// A bundle represents a collection of X.509 authorities, i.e., those that
// are used to authenticate SPIFFE X509-SVIDs.
//
// You can create a new bundle for a specific trust domain:
//
// td := spiffeid.RequireTrustDomainFromString("example.org")
// bundle := x509bundle.New(td)
//
// Or you can load it from disk:
//
// td := spiffeid.RequireTrustDomainFromString("example.org")
// bundle := x509bundle.Load(td, "bundle.pem")
//
// The bundle can be initialized with X.509 authorities:
//
// td := spiffeid.RequireTrustDomainFromString("example.org")
// var x509Authorities []*x509.Certificate = ...
// bundle := x509bundle.FromX509Authorities(td, x509Authorities)
//
// In addition, you can add X.509 authorities to the bundle:
//
// var x509CA *x509.Certificate = ...
// bundle.AddX509Authority(x509CA)
//
// Bundles can be organized into a set, keyed by trust domain:
//
// set := x509bundle.NewSet()
// set.Add(bundle)
//
// A Source is source of X.509 bundles for a trust domain. Both the Bundle
// and Set types implement Source:
//
// // Initialize the source from a bundle or set
// var source x509bundle.Source = bundle
// // ... or ...
// var source x509bundle.Source = set
//
// // Use the source to query for bundles by trust domain
// bundle, err := source.GetX509BundleForTrustDomain(td)
package x509bundle

View file

@ -0,0 +1,105 @@
package x509bundle
import (
"sort"
"sync"
"github.com/spiffe/go-spiffe/v2/spiffeid"
)
// Set is a set of bundles, keyed by trust domain.
type Set struct {
mtx sync.RWMutex
bundles map[spiffeid.TrustDomain]*Bundle
}
// NewSet creates a new set initialized with the given bundles.
func NewSet(bundles ...*Bundle) *Set {
bundlesMap := make(map[spiffeid.TrustDomain]*Bundle)
for _, b := range bundles {
if b != nil {
bundlesMap[b.trustDomain] = b
}
}
return &Set{
bundles: bundlesMap,
}
}
// Add adds a new bundle into the set. If a bundle already exists for the
// trust domain, the existing bundle is replaced.
func (s *Set) Add(bundle *Bundle) {
s.mtx.Lock()
defer s.mtx.Unlock()
if bundle != nil {
s.bundles[bundle.trustDomain] = bundle
}
}
// Remove removes the bundle for the given trust domain.
func (s *Set) Remove(trustDomain spiffeid.TrustDomain) {
s.mtx.Lock()
defer s.mtx.Unlock()
delete(s.bundles, trustDomain)
}
// Has returns true if there is a bundle for the given trust domain.
func (s *Set) Has(trustDomain spiffeid.TrustDomain) bool {
s.mtx.RLock()
defer s.mtx.RUnlock()
_, ok := s.bundles[trustDomain]
return ok
}
// Get returns a bundle for the given trust domain. If the bundle is in the set
// it is returned and the boolean is true. Otherwise, the returned value is
// nil and the boolean is false.
func (s *Set) Get(trustDomain spiffeid.TrustDomain) (*Bundle, bool) {
s.mtx.RLock()
defer s.mtx.RUnlock()
bundle, ok := s.bundles[trustDomain]
return bundle, ok
}
// Bundles returns the bundles in the set sorted by trust domain.
func (s *Set) Bundles() []*Bundle {
s.mtx.RLock()
defer s.mtx.RUnlock()
out := make([]*Bundle, 0, len(s.bundles))
for _, bundle := range s.bundles {
out = append(out, bundle)
}
sort.Slice(out, func(a, b int) bool {
return out[a].TrustDomain().Compare(out[b].TrustDomain()) < 0
})
return out
}
// Len returns the number of bundles in the set.
func (s *Set) Len() int {
s.mtx.RLock()
defer s.mtx.RUnlock()
return len(s.bundles)
}
// GetX509BundleForTrustDomain returns the X.509 bundle for the given trust
// domain. It implements the Source interface.
func (s *Set) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error) {
s.mtx.RLock()
defer s.mtx.RUnlock()
bundle, ok := s.bundles[trustDomain]
if !ok {
return nil, x509bundleErr.New("no X.509 bundle for trust domain %q", trustDomain)
}
return bundle, nil
}

View file

@ -0,0 +1,12 @@
package x509bundle
import (
"github.com/spiffe/go-spiffe/v2/spiffeid"
)
// Source represents a source of X.509 bundles keyed by trust domain.
type Source interface {
// GetX509BundleForTrustDomain returns the X.509 bundle for the given trust
// domain.
GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*Bundle, error)
}

View file

@ -0,0 +1,34 @@
package cryptoutil
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"fmt"
)
func PublicKeyEqual(a, b crypto.PublicKey) (bool, error) {
switch a := a.(type) {
case *rsa.PublicKey:
rsaPublicKey, ok := b.(*rsa.PublicKey)
return ok && RSAPublicKeyEqual(a, rsaPublicKey), nil
case *ecdsa.PublicKey:
ecdsaPublicKey, ok := b.(*ecdsa.PublicKey)
return ok && ECDSAPublicKeyEqual(a, ecdsaPublicKey), nil
case ed25519.PublicKey:
ed25519PublicKey, ok := b.(ed25519.PublicKey)
return ok && bytes.Equal(a, ed25519PublicKey), nil
default:
return false, fmt.Errorf("unsupported public key type %T", a)
}
}
func RSAPublicKeyEqual(a, b *rsa.PublicKey) bool {
return a.E == b.E && a.N.Cmp(b.N) == 0
}
func ECDSAPublicKeyEqual(a, b *ecdsa.PublicKey) bool {
return a.Curve == b.Curve && a.X.Cmp(b.X) == 0 && a.Y.Cmp(b.Y) == 0
}

View file

@ -0,0 +1,34 @@
package jwtutil
import (
"crypto"
"github.com/spiffe/go-spiffe/v2/internal/cryptoutil"
)
// CopyJWTAuthorities copies JWT authorities from a map to a new map.
func CopyJWTAuthorities(jwtAuthorities map[string]crypto.PublicKey) map[string]crypto.PublicKey {
copiedJWTAuthorities := make(map[string]crypto.PublicKey)
for key, jwtAuthority := range jwtAuthorities {
copiedJWTAuthorities[key] = jwtAuthority
}
return copiedJWTAuthorities
}
func JWTAuthoritiesEqual(a, b map[string]crypto.PublicKey) bool {
if len(a) != len(b) {
return false
}
for k, pka := range a {
pkb, ok := b[k]
if !ok {
return false
}
if equal, _ := cryptoutil.PublicKeyEqual(pka, pkb); !equal {
return false
}
}
return true
}

View file

@ -0,0 +1,123 @@
package pemutil
import (
"crypto"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
)
const (
certType string = "CERTIFICATE"
keyType string = "PRIVATE KEY"
)
func ParseCertificates(certsBytes []byte) ([]*x509.Certificate, error) {
objects, err := parseBlocks(certsBytes, certType)
if err != nil {
return nil, err
}
certs := []*x509.Certificate{}
for _, object := range objects {
cert, ok := object.(*x509.Certificate)
if !ok {
return nil, fmt.Errorf("expected *x509.Certificate; got %T", object)
}
certs = append(certs, cert)
}
return certs, nil
}
func ParsePrivateKey(keyBytes []byte) (crypto.PrivateKey, error) {
objects, err := parseBlocks(keyBytes, keyType)
if err != nil {
return nil, err
}
if len(objects) == 0 {
return nil, nil
}
privateKey, ok := objects[0].(crypto.PrivateKey)
if !ok {
return nil, fmt.Errorf("expected crypto.PrivateKey; got %T", objects[0])
}
return privateKey, nil
}
func EncodePKCS8PrivateKey(privateKey interface{}) ([]byte, error) {
keyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
return nil, err
}
return pem.EncodeToMemory(&pem.Block{
Type: keyType,
Bytes: keyBytes,
}), nil
}
func EncodeCertificates(certificates []*x509.Certificate) []byte {
pemBytes := []byte{}
for _, cert := range certificates {
pemBytes = append(pemBytes, pem.EncodeToMemory(&pem.Block{
Type: certType,
Bytes: cert.Raw,
})...)
}
return pemBytes
}
func parseBlocks(blocksBytes []byte, expectedType string) ([]interface{}, error) {
objects := []interface{}{}
var foundBlocks = false
for {
if len(blocksBytes) == 0 {
if len(objects) == 0 && !foundBlocks {
return nil, errors.New("no PEM blocks found")
}
return objects, nil
}
object, rest, foundBlock, err := parseBlock(blocksBytes, expectedType)
blocksBytes = rest
if foundBlock {
foundBlocks = true
}
switch {
case err != nil:
return nil, err
case object != nil:
objects = append(objects, object)
}
}
}
func parseBlock(pemBytes []byte, pemType string) (interface{}, []byte, bool, error) {
pemBlock, rest := pem.Decode(pemBytes)
if pemBlock == nil {
return nil, nil, false, nil
}
if pemBlock.Type != pemType {
return nil, rest, true, nil
}
var object interface{}
var err error
switch pemType {
case certType:
object, err = x509.ParseCertificate(pemBlock.Bytes)
case keyType:
object, err = x509.ParsePKCS8PrivateKey(pemBlock.Bytes)
default:
err = fmt.Errorf("PEM type not supported: %q", pemType)
}
if err != nil {
return nil, nil, false, err
}
return object, rest, true, nil
}

View file

@ -0,0 +1,53 @@
package x509util
import (
"crypto/x509"
)
// NewCertPool returns a new CertPool with the given X.509 certificates
func NewCertPool(certs []*x509.Certificate) *x509.CertPool {
pool := x509.NewCertPool()
for _, cert := range certs {
pool.AddCert(cert)
}
return pool
}
// CopyX509Authorities copies a slice of X.509 certificates to a new slice.
func CopyX509Authorities(x509Authorities []*x509.Certificate) []*x509.Certificate {
copiedX509Authorities := make([]*x509.Certificate, len(x509Authorities))
copy(copiedX509Authorities, x509Authorities)
return copiedX509Authorities
}
// CertsEqual returns true if the slices of X.509 certificates are equal.
func CertsEqual(a, b []*x509.Certificate) bool {
if len(a) != len(b) {
return false
}
for i, cert := range a {
if !cert.Equal(b[i]) {
return false
}
}
return true
}
func RawCertsFromCerts(certs []*x509.Certificate) [][]byte {
rawCerts := make([][]byte, 0, len(certs))
for _, cert := range certs {
rawCerts = append(rawCerts, cert.Raw)
}
return rawCerts
}
func ConcatRawCertsFromCerts(certs []*x509.Certificate) []byte {
var rawCerts []byte
for _, cert := range certs {
rawCerts = append(rawCerts, cert.Raw...)
}
return rawCerts
}

View file

@ -0,0 +1,42 @@
//go:build spiffeid_charset_backcompat
// +build spiffeid_charset_backcompat
package spiffeid
func isBackcompatTrustDomainChar(c uint8) bool {
if isSubDelim(c) {
return true
}
switch c {
// unreserved
case '~':
return true
default:
return false
}
}
func isBackcompatPathChar(c uint8) bool {
if isSubDelim(c) {
return true
}
switch c {
// unreserved
case '~':
return true
// gen-delims
case ':', '[', ']', '@':
return true
default:
return false
}
}
func isSubDelim(c uint8) bool {
switch c {
case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=':
return true
default:
return false
}
}

View file

@ -0,0 +1,12 @@
//go:build !spiffeid_charset_backcompat
// +build !spiffeid_charset_backcompat
package spiffeid
func isBackcompatTrustDomainChar(c uint8) bool {
return false
}
func isBackcompatPathChar(c uint8) bool {
return false
}

View file

@ -0,0 +1,15 @@
package spiffeid
import "errors"
var (
errBadTrustDomainChar = errors.New("trust domain characters are limited to lowercase letters, numbers, dots, dashes, and underscores")
errBadPathSegmentChar = errors.New("path segment characters are limited to letters, numbers, dots, dashes, and underscores")
errDotSegment = errors.New("path cannot contain dot segments")
errNoLeadingSlash = errors.New("path must have a leading slash")
errEmpty = errors.New("cannot be empty")
errEmptySegment = errors.New("path cannot contain empty segments")
errMissingTrustDomain = errors.New("trust domain is missing")
errTrailingSlash = errors.New("path cannot have a trailing slash")
errWrongScheme = errors.New("scheme is missing or invalid")
)

258
vendor/github.com/spiffe/go-spiffe/v2/spiffeid/id.go generated vendored Normal file
View file

@ -0,0 +1,258 @@
package spiffeid
import (
"errors"
"fmt"
"net/url"
"strings"
)
const (
schemePrefix = "spiffe://"
schemePrefixLen = len(schemePrefix)
)
// FromPath returns a new SPIFFE ID in the given trust domain and with the
// given path. The supplied path must be a valid absolute path according to the
// SPIFFE specification.
// See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path
func FromPath(td TrustDomain, path string) (ID, error) {
if err := ValidatePath(path); err != nil {
return ID{}, err
}
return makeID(td, path)
}
// FromPathf returns a new SPIFFE ID from the formatted path in the given trust
// domain. The formatted path must be a valid absolute path according to the
// SPIFFE specification.
// See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path
func FromPathf(td TrustDomain, format string, args ...interface{}) (ID, error) {
path, err := FormatPath(format, args...)
if err != nil {
return ID{}, err
}
return makeID(td, path)
}
// FromSegments returns a new SPIFFE ID in the given trust domain with joined
// path segments. The path segments must be valid according to the SPIFFE
// specification and must not contain path separators.
// See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path
func FromSegments(td TrustDomain, segments ...string) (ID, error) {
path, err := JoinPathSegments(segments...)
if err != nil {
return ID{}, err
}
return makeID(td, path)
}
// FromString parses a SPIFFE ID from a string.
func FromString(id string) (ID, error) {
switch {
case id == "":
return ID{}, errEmpty
case !strings.HasPrefix(id, schemePrefix):
return ID{}, errWrongScheme
}
pathidx := schemePrefixLen
for ; pathidx < len(id); pathidx++ {
c := id[pathidx]
if c == '/' {
break
}
if !isValidTrustDomainChar(c) {
return ID{}, errBadTrustDomainChar
}
}
if pathidx == schemePrefixLen {
return ID{}, errMissingTrustDomain
}
if err := ValidatePath(id[pathidx:]); err != nil {
return ID{}, err
}
return ID{
id: id,
pathidx: pathidx,
}, nil
}
// FromStringf parses a SPIFFE ID from a formatted string.
func FromStringf(format string, args ...interface{}) (ID, error) {
return FromString(fmt.Sprintf(format, args...))
}
// FromURI parses a SPIFFE ID from a URI.
func FromURI(uri *url.URL) (ID, error) {
return FromString(uri.String())
}
// ID is a SPIFFE ID
type ID struct {
id string
// pathidx tracks the index to the beginning of the path inside of id. This
// is used when extracting the trust domain or path portions of the id.
pathidx int
}
// TrustDomain returns the trust domain of the SPIFFE ID.
func (id ID) TrustDomain() TrustDomain {
if id.IsZero() {
return TrustDomain{}
}
return TrustDomain{name: id.id[schemePrefixLen:id.pathidx]}
}
// MemberOf returns true if the SPIFFE ID is a member of the given trust domain.
func (id ID) MemberOf(td TrustDomain) bool {
return id.TrustDomain() == td
}
// Path returns the path of the SPIFFE ID inside the trust domain.
func (id ID) Path() string {
return id.id[id.pathidx:]
}
// String returns the string representation of the SPIFFE ID, e.g.,
// "spiffe://example.org/foo/bar".
func (id ID) String() string {
return id.id
}
// URL returns a URL for SPIFFE ID.
func (id ID) URL() *url.URL {
if id.IsZero() {
return &url.URL{}
}
return &url.URL{
Scheme: "spiffe",
Host: id.TrustDomain().String(),
Path: id.Path(),
}
}
// IsZero returns true if the SPIFFE ID is the zero value.
func (id ID) IsZero() bool {
return id.id == ""
}
// AppendPath returns an ID with the appended path. It will fail if called on a
// zero value. The path to append must be a valid absolute path according to
// the SPIFFE specification.
// See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path
func (id ID) AppendPath(path string) (ID, error) {
if id.IsZero() {
return ID{}, errors.New("cannot append path on a zero ID value")
}
if err := ValidatePath(path); err != nil {
return ID{}, err
}
id.id += path
return id, nil
}
// AppendPathf returns an ID with the appended formatted path. It will fail if
// called on a zero value. The formatted path must be a valid absolute path
// according to the SPIFFE specification.
// See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path
func (id ID) AppendPathf(format string, args ...interface{}) (ID, error) {
if id.IsZero() {
return ID{}, errors.New("cannot append path on a zero ID value")
}
path, err := FormatPath(format, args...)
if err != nil {
return ID{}, err
}
id.id += path
return id, nil
}
// AppendSegments returns an ID with the appended joined path segments. It
// will fail if called on a zero value. The path segments must be valid
// according to the SPIFFE specification and must not contain path separators.
// See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path
func (id ID) AppendSegments(segments ...string) (ID, error) {
if id.IsZero() {
return ID{}, errors.New("cannot append path segments on a zero ID value")
}
path, err := JoinPathSegments(segments...)
if err != nil {
return ID{}, err
}
id.id += path
return id, nil
}
// Replace path returns an ID with the given path in the same trust domain. It
// will fail if called on a zero value. The given path must be a valid absolute
// path according to the SPIFFE specification.
// See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path
func (id ID) ReplacePath(path string) (ID, error) {
if id.IsZero() {
return ID{}, errors.New("cannot replace path on a zero ID value")
}
return FromPath(id.TrustDomain(), path)
}
// ReplacePathf returns an ID with the formatted path in the same trust domain.
// It will fail if called on a zero value. The formatted path must be a valid
// absolute path according to the SPIFFE specification.
// See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path
func (id ID) ReplacePathf(format string, args ...interface{}) (ID, error) {
if id.IsZero() {
return ID{}, errors.New("cannot replace path on a zero ID value")
}
return FromPathf(id.TrustDomain(), format, args...)
}
// ReplaceSegments returns an ID with the joined path segments in the same
// trust domain. It will fail if called on a zero value. The path segments must
// be valid according to the SPIFFE specification and must not contain path
// separators.
// See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path
func (id ID) ReplaceSegments(segments ...string) (ID, error) {
if id.IsZero() {
return ID{}, errors.New("cannot replace path segments on a zero ID value")
}
return FromSegments(id.TrustDomain(), segments...)
}
// MarshalText returns a text representation of the ID. If the ID is the zero
// value, nil is returned.
func (id ID) MarshalText() ([]byte, error) {
if id.IsZero() {
return nil, nil
}
return []byte(id.String()), nil
}
// UnmarshalText decodes a text representation of the ID. If the text is empty,
// the ID is set to the zero value.
func (id *ID) UnmarshalText(text []byte) error {
if len(text) == 0 {
*id = ID{}
return nil
}
unmarshaled, err := FromString(string(text))
if err != nil {
return err
}
*id = unmarshaled
return nil
}
func makeID(td TrustDomain, path string) (ID, error) {
if td.IsZero() {
return ID{}, errors.New("trust domain is empty")
}
return ID{
id: schemePrefix + td.name + path,
pathidx: schemePrefixLen + len(td.name),
}, nil
}

View file

@ -0,0 +1,47 @@
package spiffeid
import "fmt"
// Matcher is used to match a SPIFFE ID.
type Matcher func(ID) error
// MatchAny matches any SPIFFE ID.
func MatchAny() Matcher {
return Matcher(func(actual ID) error {
return nil
})
}
// MatchID matches a specific SPIFFE ID.
func MatchID(expected ID) Matcher {
return Matcher(func(actual ID) error {
if actual != expected {
return fmt.Errorf("unexpected ID %q", actual)
}
return nil
})
}
// MatchOneOf matches any SPIFFE ID in the given list of IDs.
func MatchOneOf(expected ...ID) Matcher {
set := make(map[ID]struct{})
for _, id := range expected {
set[id] = struct{}{}
}
return Matcher(func(actual ID) error {
if _, ok := set[actual]; !ok {
return fmt.Errorf("unexpected ID %q", actual)
}
return nil
})
}
// MatchMemberOf matches any SPIFFE ID in the given trust domain.
func MatchMemberOf(expected TrustDomain) Matcher {
return Matcher(func(actual ID) error {
if !actual.MemberOf(expected) {
return fmt.Errorf("unexpected trust domain %q", actual.TrustDomain())
}
return nil
})
}

107
vendor/github.com/spiffe/go-spiffe/v2/spiffeid/path.go generated vendored Normal file
View file

@ -0,0 +1,107 @@
package spiffeid
import (
"fmt"
"strings"
)
// FormatPath builds a path by formatting the given formatting string with
// the given args (i.e. fmt.Sprintf). The resulting path must be valid or
// an error is returned.
func FormatPath(format string, args ...interface{}) (string, error) {
path := fmt.Sprintf(format, args...)
if err := ValidatePath(path); err != nil {
return "", err
}
return path, nil
}
// JoinPathSegments joins one or more path segments into a slash separated
// path. Segments cannot contain slashes. The resulting path must be valid or
// an error is returned. If no segments are provided, an empty string is
// returned.
func JoinPathSegments(segments ...string) (string, error) {
var builder strings.Builder
for _, segment := range segments {
if err := ValidatePathSegment(segment); err != nil {
return "", err
}
builder.WriteByte('/')
builder.WriteString(segment)
}
return builder.String(), nil
}
// ValidatePath validates that a path string is a conformant path for a SPIFFE
// ID.
// See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path
func ValidatePath(path string) error {
switch {
case path == "":
return nil
case path[0] != '/':
return errNoLeadingSlash
}
segmentStart := 0
segmentEnd := 0
for ; segmentEnd < len(path); segmentEnd++ {
c := path[segmentEnd]
if c == '/' {
switch path[segmentStart:segmentEnd] {
case "/":
return errEmptySegment
case "/.", "/..":
return errDotSegment
}
segmentStart = segmentEnd
continue
}
if !isValidPathSegmentChar(c) {
return errBadPathSegmentChar
}
}
switch path[segmentStart:segmentEnd] {
case "/":
return errTrailingSlash
case "/.", "/..":
return errDotSegment
}
return nil
}
// ValidatePathSegment validates that a string is a conformant segment for
// inclusion in the path for a SPIFFE ID.
// See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#22-path
func ValidatePathSegment(segment string) error {
switch segment {
case "":
return errEmptySegment
case ".", "..":
return errDotSegment
}
for i := 0; i < len(segment); i++ {
if !isValidPathSegmentChar(segment[i]) {
return errBadPathSegmentChar
}
}
return nil
}
func isValidPathSegmentChar(c uint8) bool {
switch {
case c >= 'a' && c <= 'z':
return true
case c >= 'A' && c <= 'Z':
return true
case c >= '0' && c <= '9':
return true
case c == '-', c == '.', c == '_':
return true
case isBackcompatPathChar(c):
return true
default:
return false
}
}

View file

@ -0,0 +1,103 @@
package spiffeid
import (
"net/url"
)
// RequireFromPath is similar to FromPath except that instead of returning an
// error on malformed input, it panics. It should only be used when the input
// is statically verifiable.
func RequireFromPath(td TrustDomain, path string) ID {
id, err := FromPath(td, path)
panicOnErr(err)
return id
}
// RequireFromPathf is similar to FromPathf except that instead of returning an
// error on malformed input, it panics. It should only be used when the input
// is statically verifiable.
func RequireFromPathf(td TrustDomain, format string, args ...interface{}) ID {
id, err := FromPathf(td, format, args...)
panicOnErr(err)
return id
}
// RequireFromSegments is similar to FromSegments except that instead of
// returning an error on malformed input, it panics. It should only be used
// when the input is statically verifiable.
func RequireFromSegments(td TrustDomain, segments ...string) ID {
id, err := FromSegments(td, segments...)
panicOnErr(err)
return id
}
// RequireFromString is similar to FromString except that instead of returning
// an error on malformed input, it panics. It should only be used when the
// input is statically verifiable.
func RequireFromString(s string) ID {
id, err := FromString(s)
panicOnErr(err)
return id
}
// RequireFromStringf is similar to FromStringf except that instead of
// returning an error on malformed input, it panics. It should only be used
// when the input is statically verifiable.
func RequireFromStringf(format string, args ...interface{}) ID {
id, err := FromStringf(format, args...)
panicOnErr(err)
return id
}
// RequireFromURI is similar to FromURI except that instead of returning an
// error on malformed input, it panics. It should only be used when the input is
// statically verifiable.
func RequireFromURI(uri *url.URL) ID {
id, err := FromURI(uri)
panicOnErr(err)
return id
}
// RequireTrustDomainFromString is similar to TrustDomainFromString except that
// instead of returning an error on malformed input, it panics. It should only
// be used when the input is statically verifiable.
func RequireTrustDomainFromString(s string) TrustDomain {
td, err := TrustDomainFromString(s)
panicOnErr(err)
return td
}
// RequireTrustDomainFromURI is similar to TrustDomainFromURI except that
// instead of returning an error on malformed input, it panics. It should only
// be used when the input is statically verifiable.
func RequireTrustDomainFromURI(uri *url.URL) TrustDomain {
td, err := TrustDomainFromURI(uri)
panicOnErr(err)
return td
}
// RequireFormatPath builds a path by formatting the given formatting string
// with the given args (i.e. fmt.Sprintf). The resulting path must be valid or
// the function panics. It should only be used when the input is statically
// verifiable.
func RequireFormatPath(format string, args ...interface{}) string {
path, err := FormatPath(format, args...)
panicOnErr(err)
return path
}
// RequireJoinPathSegments joins one or more path segments into a slash separated
// path. Segments cannot contain slashes. The resulting path must be valid or
// the function panics. It should only be used when the input is statically
// verifiable.
func RequireJoinPathSegments(segments ...string) string {
path, err := JoinPathSegments(segments...)
panicOnErr(err)
return path
}
func panicOnErr(err error) {
if err != nil {
panic(err)
}
}

View file

@ -0,0 +1,127 @@
package spiffeid
import (
"net/url"
"strings"
)
// TrustDomain represents the trust domain portion of a SPIFFE ID (e.g.
// example.org).
type TrustDomain struct {
name string
}
// TrustDomainFromString returns a new TrustDomain from a string. The string
// can either be a trust domain name (e.g. example.org), or a valid SPIFFE ID
// URI (e.g. spiffe://example.org), otherwise an error is returned.
// See https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md#21-trust-domain.
func TrustDomainFromString(idOrName string) (TrustDomain, error) {
switch {
case idOrName == "":
return TrustDomain{}, errMissingTrustDomain
case strings.Contains(idOrName, ":/"):
// The ID looks like it has something like a scheme separator, let's
// try to parse as an ID. We use :/ instead of :// since the
// diagnostics are better for a bad input like spiffe:/trustdomain.
id, err := FromString(idOrName)
if err != nil {
return TrustDomain{}, err
}
return id.TrustDomain(), nil
default:
for i := 0; i < len(idOrName); i++ {
if !isValidTrustDomainChar(idOrName[i]) {
return TrustDomain{}, errBadTrustDomainChar
}
}
return TrustDomain{name: idOrName}, nil
}
}
// TrustDomainFromURI returns a new TrustDomain from a URI. The URI must be a
// valid SPIFFE ID (see FromURI) or an error is returned. The trust domain is
// extracted from the host field.
func TrustDomainFromURI(uri *url.URL) (TrustDomain, error) {
id, err := FromURI(uri)
if err != nil {
return TrustDomain{}, err
}
return id.TrustDomain(), nil
}
// Name returns the trust domain name as a string, e.g. example.org.
func (td TrustDomain) Name() string {
return td.name
}
// String returns the trust domain name as a string, e.g. example.org.
func (td TrustDomain) String() string {
return td.name
}
// ID returns the SPIFFE ID of the trust domain.
func (td TrustDomain) ID() ID {
if id, err := makeID(td, ""); err == nil {
return id
}
return ID{}
}
// IDString returns a string representation of the the SPIFFE ID of the trust
// domain, e.g. "spiffe://example.org".
func (td TrustDomain) IDString() string {
return td.ID().String()
}
// IsZero returns true if the trust domain is the zero value.
func (td TrustDomain) IsZero() bool {
return td.name == ""
}
// Compare returns an integer comparing the trust domain to another
// lexicographically. The result will be 0 if td==other, -1 if td < other, and
// +1 if td > other.
func (td TrustDomain) Compare(other TrustDomain) int {
return strings.Compare(td.name, other.name)
}
// MarshalText returns a text representation of the trust domain. If the trust
// domain is the zero value, nil is returned.
func (td TrustDomain) MarshalText() ([]byte, error) {
if td.IsZero() {
return nil, nil
}
return []byte(td.String()), nil
}
// UnmarshalText decodes a text representation of the trust domain. If the text
// is empty, the trust domain is set to the zero value.
func (td *TrustDomain) UnmarshalText(text []byte) error {
if len(text) == 0 {
*td = TrustDomain{}
return nil
}
unmarshaled, err := TrustDomainFromString(string(text))
if err != nil {
return err
}
*td = unmarshaled
return nil
}
func isValidTrustDomainChar(c uint8) bool {
switch {
case c >= 'a' && c <= 'z':
return true
case c >= '0' && c <= '9':
return true
case c == '-', c == '.', c == '_':
return true
case isBackcompatTrustDomainChar(c):
return true
default:
return false
}
}