Skip to content
Open
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
15 changes: 10 additions & 5 deletions app/controlplane/internal/oidcauthenticator/auth.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2023 The Chainloop Authors.
// Copyright 2023-2026 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -17,8 +17,8 @@ package authenticator

import (
"context"
"errors"

"github.com/chainloop-dev/chainloop/pkg/errorsx"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
Expand All @@ -34,7 +34,7 @@ type OIDC struct {
func NewOIDC(discoveryDomain, clientID, clientSecret, redirectURL string) (*OIDC, error) {
provider, err := oidc.NewProvider(context.Background(), discoveryDomain)
if err != nil {
return nil, err
return nil, errorsx.WithStack(err)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
}

conf := oauth2.Config{
Expand All @@ -56,11 +56,16 @@ func NewOIDC(discoveryDomain, clientID, clientSecret, redirectURL string) (*OIDC
func (a *OIDC) VerifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, errors.New("no id_token field in oauth2 token")
return nil, errorsx.New("no id_token field in oauth2 token")
}

// default settings
oidcConfig := &oidc.Config{ClientID: a.ClientID}

return a.Verifier(oidcConfig).Verify(ctx, rawIDToken)
idToken, err := a.Verifier(oidcConfig).Verify(ctx, rawIDToken)
if err != nil {
return nil, errorsx.WithStack(err)
}

return idToken, nil
}
78 changes: 78 additions & 0 deletions app/controlplane/internal/oidcauthenticator/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// Copyright 2026 The Chainloop Authors.
//
// 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.

package authenticator

import (
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/chainloop-dev/chainloop/pkg/errorsx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
)

func TestNewOIDCDiscoveryFailureRecordsStack(t *testing.T) {
srv := httptest.NewServer(http.NotFoundHandler())
t.Cleanup(srv.Close)

_, err := NewOIDC(srv.URL, "client", "secret", "http://localhost/callback")
require.Error(t, err)
assert.True(t, errorsx.HasStack(err))
}

func TestVerifyIDTokenErrorsRecordStack(t *testing.T) {
testCases := []struct {
name string
token *oauth2.Token
}{
{name: "missing id_token", token: &oauth2.Token{}},
{name: "invalid id_token", token: (&oauth2.Token{}).WithExtra(map[string]any{"id_token": "not-a-jwt"})},
}

srv := newDiscoveryServer(t)
a, err := NewOIDC(srv.URL, "client", "secret", "http://localhost/callback")
require.NoError(t, err)

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err := a.VerifyIDToken(context.Background(), tc.token)
require.Error(t, err)
assert.True(t, errorsx.HasStack(err))
})
}
}

// newDiscoveryServer serves a minimal OIDC discovery document for its own URL.
func newDiscoveryServer(t *testing.T) *httptest.Server {
t.Helper()

var srv *httptest.Server
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/.well-known/openid-configuration" {
http.NotFound(w, r)
return
}

w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"issuer":"` + srv.URL + `","authorization_endpoint":"` + srv.URL + `/auth","token_endpoint":"` + srv.URL + `/token","jwks_uri":"` + srv.URL + `/keys"}`))
}))
t.Cleanup(srv.Close)

return srv
}
11 changes: 8 additions & 3 deletions app/controlplane/pkg/auditor/nats.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"strings"
"time"

"github.com/chainloop-dev/chainloop/pkg/errorsx"
"github.com/chainloop-dev/chainloop/pkg/natsconn"
"github.com/go-kratos/kratos/v2/log"
"github.com/nats-io/nats.go/jetstream"
Expand Down Expand Up @@ -85,7 +86,7 @@ func newPublisher(rc *natsconn.ReloadableConnection, logger log.Logger) *AuditLo
func (p *AuditLogPublisher) initJetStream() error {
js, err := jetstream.New(p.rc.Conn)
if err != nil {
return fmt.Errorf("creating jetstream context: %w", err)
return errorsx.Errorf("creating jetstream context: %w", err)
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
Expand All @@ -96,7 +97,7 @@ func (p *AuditLogPublisher) initJetStream() error {
Subjects: []string{subjectName},
Replicas: p.rc.Replicas,
}); err != nil {
return fmt.Errorf("creating stream: %w", err)
return errorsx.Errorf("creating stream: %w", err)
}

p.logger.Infow("msg", "stream created or updated", "name", streamName, "subject", subjectName, "replicas", p.rc.Replicas)
Expand Down Expand Up @@ -124,5 +125,9 @@ func (p *AuditLogPublisher) Publish(data *EventPayload) error {
}

specificSubject := fmt.Sprintf("%s.%s.%s", baseSubjectName, strings.ToLower(string(data.Data.TargetType)), strings.ToLower(data.Data.ActionType))
return p.rc.Publish(specificSubject, jsonPayload)
if err := p.rc.Publish(specificSubject, jsonPayload); err != nil {
return errorsx.Errorf("publishing audit event to %s: %w", specificSubject, err)
}

return nil
}
18 changes: 9 additions & 9 deletions app/controlplane/pkg/biz/signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"errors"
"fmt"
"net/url"
"os"

conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/ca"
"github.com/chainloop-dev/chainloop/pkg/errorsx"
"github.com/chainloop-dev/chainloop/pkg/otelx"
"github.com/chainloop-dev/chainloop/pkg/servicelogger"
"github.com/go-kratos/kratos/v2/log"
Expand Down Expand Up @@ -188,24 +188,24 @@ func (s *SigningUseCase) CreateSigningCert(ctx context.Context, orgID string, cs
var publicKey crypto.PublicKey

if len(csrRaw) == 0 {
return nil, errors.New("csr cannot be empty")
return nil, errorsx.New("csr cannot be empty")
}

// Parse CSR
csr, err := cryptoutils.ParseCSR(csrRaw)
if err != nil {
return nil, fmt.Errorf("parsing csr: %w", err)
return nil, errorsx.Errorf("parsing csr: %w", err)
}

// Parse public key and check for weak key parameters
publicKey = csr.PublicKey
if err := cryptoutils.ValidatePubKey(publicKey); err != nil {
return nil, fmt.Errorf("invalid public key: %w", err)
return nil, errorsx.Errorf("invalid public key: %w", err)
}

// Check the CSR signature is valid
if err := csr.CheckSignature(); err != nil {
return nil, fmt.Errorf("invalid signature: %w", err)
return nil, errorsx.Errorf("invalid signature: %w", err)
}

// Create certificate from CA provider (no Signed Certificate Timestamps for now)
Expand All @@ -221,13 +221,13 @@ func (s *SigningUseCase) CreateSigningCert(ctx context.Context, orgID string, cs
// Generated certificate
finalPEM, err := csc.CertPEM()
if err != nil {
return nil, fmt.Errorf("marshaling certificate to PEM: %w", err)
return nil, errorsx.Errorf("marshaling certificate to PEM: %w", err)
}

// Certificate chain
finalChainPEM, err := csc.ChainPEM()
if err != nil {
return nil, fmt.Errorf("marshaling chain to PEM: %w", err)
return nil, errorsx.Errorf("marshaling chain to PEM: %w", err)
}

return append([]string{finalPEM}, finalChainPEM...), nil
Expand All @@ -254,7 +254,7 @@ func (s *SigningUseCase) GetTrustedRoot(ctx context.Context) (*TrustedRoot, erro
for _, cert := range chain {
pemCert, err := cryptoutils.MarshalCertificateToPEM(cert)
if err != nil {
return nil, fmt.Errorf("marshaling certificate to PEM: %w", err)
return nil, errorsx.Errorf("marshaling certificate to PEM: %w", err)
}
trustedRoot.Keys[keyID] = append(trustedRoot.Keys[keyID], string(pemCert))
}
Expand All @@ -270,7 +270,7 @@ func (s *SigningUseCase) GetTrustedRoot(ctx context.Context) (*TrustedRoot, erro
for _, cert := range authority.CertChain {
pemCert, err := cryptoutils.MarshalCertificateToPEM(cert)
if err != nil {
return nil, fmt.Errorf("marshaling certificate to PEM: %w", err)
return nil, errorsx.Errorf("marshaling certificate to PEM: %w", err)
}
trustedRoot.TimestampAuthorities[authorityKeyID] = append(trustedRoot.TimestampAuthorities[authorityKeyID], string(pemCert))
}
Expand Down
57 changes: 41 additions & 16 deletions app/controlplane/pkg/ca/ejbca/ejbca.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2024 The Chainloop Authors.
// Copyright 2024-2026 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -28,13 +28,28 @@ import (
"net/http"
"os"

"github.com/chainloop-dev/chainloop/pkg/errorsx"
fulcioca "github.com/sigstore/fulcio/pkg/ca"
"github.com/sigstore/fulcio/pkg/identity"
"github.com/sigstore/sigstore/pkg/cryptoutils"
)

const CAName = "ejbCA"

// EnrollStatusError is returned when EJBCA answers a certificate enrollment
// with an unexpected HTTP status. It is a separate type so that error trackers
// do not group it with transport or decoding failures.
type EnrollStatusError struct {
// Status is the HTTP status line, for example "403 Forbidden".
Status string
// StatusCode is the HTTP status code.
StatusCode int
}

func (e *EnrollStatusError) Error() string {
return fmt.Sprintf("wrong status creating certificate: %v", e.Status)
}

type EJBCA struct {
// Connection settings
// EJBCA installation. ie https://localhost/ejbca/
Expand All @@ -55,7 +70,7 @@ type EJBCA struct {

func New(serverURL, keyPath, certPath, rootCAPath, certProfileName, endEntityProfileName, caName string) (*EJBCA, error) {
if serverURL == "" || keyPath == "" || certPath == "" || certProfileName == "" || endEntityProfileName == "" || caName == "" {
return nil, fmt.Errorf("ejbca: invalid arguments")
return nil, errorsx.New("ejbca: invalid arguments")
}

return &EJBCA{
Expand Down Expand Up @@ -102,12 +117,12 @@ func (e EJBCA) CreateCertificateFromCSR(ctx context.Context, principal identity.
}
ejbcaReqBytes, err := json.Marshal(ejbcaReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal EJBCA Request: %w", err)
return nil, errorsx.Errorf("failed to marshal EJBCA Request: %w", err)
}

cert, err := tls.LoadX509KeyPair(e.certPath, e.keyPath)
if err != nil {
return nil, fmt.Errorf("error loading X509 key pair: %w", err)
return nil, errorsx.Errorf("error loading X509 key pair: %w", err)
}

tlsConfig := &tls.Config{
Expand All @@ -118,7 +133,7 @@ func (e EJBCA) CreateCertificateFromCSR(ctx context.Context, principal identity.
if e.rootCAPath != "" {
caCert, err := os.ReadFile(e.rootCAPath)
if err != nil {
return nil, fmt.Errorf("error reading root CA: %w", err)
return nil, errorsx.Errorf("error reading root CA: %w", err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
Expand All @@ -131,51 +146,57 @@ func (e EJBCA) CreateCertificateFromCSR(ctx context.Context, principal identity.

req, err := http.NewRequest("POST", fmt.Sprintf("%s/ejbca-rest-api/v1/certificate/pkcs10enroll", e.serverURL), bytes.NewReader(ejbcaReqBytes))
if err != nil {
return nil, fmt.Errorf("unable to create request: %w", err)
return nil, errorsx.Errorf("unable to create request: %w", err)
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("unable to send request: %w", err)
return nil, errorsx.Errorf("unable to send request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("wrong status creating certificate: %v", resp.Status)
return nil, errorsx.WithStack(&EnrollStatusError{Status: resp.Status, StatusCode: resp.StatusCode})
}

var rsp response
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("unable to read response body: %w", err)
return nil, errorsx.Errorf("unable to read response body: %w", err)
}
if err := json.Unmarshal(body, &rsp); err != nil {
return nil, fmt.Errorf("unable to parse response body: %w", err)
return nil, errorsx.Errorf("unable to parse response body: %w", err)
}

// Decode certificate
derCert, err := base64.StdEncoding.DecodeString(rsp.Certificate)
if err != nil {
return nil, fmt.Errorf("unable to decode response body: %w", err)
return nil, errorsx.Errorf("unable to decode response body: %w", err)
}

// Decode chain
chain := make([]*x509.Certificate, 0)
for _, c := range rsp.CertificateChain {
decodedCert, err := base64.StdEncoding.DecodeString(c)
if err != nil {
return nil, fmt.Errorf("unable to decode response body: %w", err)
return nil, errorsx.Errorf("unable to decode response body: %w", err)
}
cert, err := x509.ParseCertificate(decodedCert)
if err != nil {
return nil, fmt.Errorf("unable to parse cert chain: %w", err)
return nil, errorsx.Errorf("unable to parse cert chain: %w", err)
}
chain = append(chain, cert)
}

return fulcioca.CreateCSCFromDER(derCert, chain)
csc, err := fulcioca.CreateCSCFromDER(derCert, chain)
if err != nil {
return nil, errorsx.Errorf("unable to create code signing certificate: %w", err)
}

return csc, nil
}

func (e EJBCA) GetRootChain(_ context.Context) ([]*x509.Certificate, error) {
Expand All @@ -184,9 +205,13 @@ func (e EJBCA) GetRootChain(_ context.Context) ([]*x509.Certificate, error) {
if e.rootCAPath != "" {
caCert, err := os.ReadFile(e.rootCAPath)
if err != nil {
return nil, fmt.Errorf("error reading root CA: %w", err)
return nil, errorsx.Errorf("error reading root CA: %w", err)
}
certs, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(caCert))
if err != nil {
return nil, errorsx.Errorf("error loading root CA: %w", err)
}
return cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(caCert))
return certs, nil
}
return nil, nil
}
Expand Down
Loading
Loading