Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
926f5ab
Extend authentication exclusions and update namespace policy definiti…
pflynn-virtru Sep 30, 2025
48c4e62
Add X.509 certificate management to namespace policy definitions.
pflynn-virtru Oct 2, 2025
f61ecc0
Introduce namespace-to-certificate RPCs for X.509 certificate managem…
pflynn-virtru Oct 2, 2025
0187c8d
Remove namespace policy routes from public access and associated tests.
pflynn-virtru Oct 2, 2025
a57e637
Introduce X.509 certificate validation and enhance namespace policy f…
pflynn-virtru Oct 2, 2025
f0624f9
Remove unused error messages in namespace certificate assignment test.
pflynn-virtru Oct 2, 2025
9669a30
Add schema and migration for namespace certificate management
pflynn-virtru Oct 2, 2025
7b6f379
Introduce `is_root` flag to certificate schema for root/intermediate …
pflynn-virtru Oct 7, 2025
964a6c3
Add `is_root` flag to certificate schema for root/intermediate distin…
pflynn-virtru Oct 7, 2025
319f68b
Update all namespace-certificate management methods to use pointer-ba…
pflynn-virtru Oct 7, 2025
d1dca55
Change certificate field format from `x5c` to `pem` across schema, RP…
pflynn-virtru Oct 9, 2025
c5b2680
Merge remote-tracking branch 'origin/main' into feature/trust-by-name…
pflynn-virtru Oct 9, 2025
f7ed290
Remove deprecated `AttributeValueSelector` schema and update namespac…
pflynn-virtru Oct 9, 2025
8311e79
Update SQL queries to return specific fields instead of all columns i…
pflynn-virtru Oct 9, 2025
b2f88fa
Add trigger for `certificates.updated_at` and handle trigger removal …
pflynn-virtru Oct 9, 2025
b4868b7
Add `metadata` field to certificate schema across protobuf, OpenAPI s…
pflynn-virtru Oct 10, 2025
f5d8101
Apply suggestion from Jake
pflynn-virtru Oct 10, 2025
6454b1b
Update error logging in `namespaces.go` to use `slog.Any` for improve…
pflynn-virtru Oct 10, 2025
c9fdcea
Refactor error handling in `namespaces.go` to use `errors.Join` for e…
pflynn-virtru Oct 10, 2025
2695891
Remove `is_root` flag from certificate schema, update related SQL que…
pflynn-virtru Oct 10, 2025
80e7d39
Add validation for root certificates, improve namespace ID resolution…
pflynn-virtru Oct 10, 2025
82ba7b1
Refactor `CreateCertificate` to return full certificate object, updat…
pflynn-virtru Oct 14, 2025
0f478fb
Merge remote-tracking branch 'origin/main' into feature/trust-by-name…
pflynn-virtru Oct 15, 2025
b3e7bc3
Refactor error handling in `namespaces.go` and `errors.go` to add `Er…
pflynn-virtru Oct 15, 2025
a3a1b5a
Update test to expect `ErrFqnMismatch` instead of `ErrNotFound` for i…
pflynn-virtru Oct 15, 2025
5fad1f5
Refactor `UnsafeDeleteNamespace` to replace generic errors with struc…
pflynn-virtru Oct 15, 2025
3f0b299
Add certificate validation and signature checks; enhance root certifi…
pflynn-virtru Oct 17, 2025
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
2 changes: 2 additions & 0 deletions service/logger/audit/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
ObjectTypeKasAttributeDefinitionKeyAssignment
ObjectTypeKasAttributeValueKeyAssignment
ObjectTypeKasAttributeNamespaceKeyAssignment
ObjectTypeNamespaceCertificate
)

func (ot ObjectType) String() string {
Expand Down Expand Up @@ -61,6 +62,7 @@ func (ot ObjectType) String() string {
"kas_attribute_definition_key_assignment",
"kas_attribute_value_key_assignment",
"kas_attribute_namespace_key_assignment",
"namespace_certificate",
}[ot]
}

Expand Down
12 changes: 12 additions & 0 deletions service/pkg/db/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ var (
ErrKasURIMismatch = errors.New("ErrKasURIMismatch: KAS URI mismatch")
ErrInvalidOblTriParam = errors.New("ErrInvalidOblTriParam: either the obligation value, attribute value, or action provided was not found")
ErrCheckViolation = errors.New("ErrCheckViolation: check constraint violation")
ErrFqnMismatch = errors.New("ErrFqnMismatch: FQN mismatch")
ErrInvalidCertificate = errors.New("ErrInvalidCertificate: invalid certificate")
)

// Get helpful error message for PostgreSQL violation
Expand Down Expand Up @@ -132,6 +134,8 @@ const (
ErrorTextKasURIMismatch = "kas uri mismatch"
ErrorTextKIDMismatch = "key id mismatch"
ErrorTextInvalidOblTrigParam = "either the obligation value, attribute value, or action provided is invalid"
ErrorTextFqnMismatch = "fqn mismatch"
ErrorTextInvalidCertificate = "invalid certificate"
)

func StatusifyError(ctx context.Context, l *logger.Logger, err error, fallbackErr string, logs ...any) error {
Expand Down Expand Up @@ -200,6 +204,14 @@ func StatusifyError(ctx context.Context, l *logger.Logger, err error, fallbackEr
l.ErrorContext(ctx, ErrorTextInvalidOblTrigParam, logs...)
return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextInvalidOblTrigParam))
}
if errors.Is(err, ErrFqnMismatch) {
l.ErrorContext(ctx, ErrorTextFqnMismatch, logs...)
return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextFqnMismatch))
}
if errors.Is(err, ErrInvalidCertificate) {
l.ErrorContext(ctx, ErrorTextInvalidCertificate, logs...)
return connect.NewError(connect.CodeInvalidArgument, errors.New(ErrorTextInvalidCertificate))
}

l.ErrorContext(ctx, "request error", append(logs, slog.Any("error", err))...)
return connect.NewError(connect.CodeInternal, errors.New(fallbackErr))
Expand Down
31 changes: 31 additions & 0 deletions service/pkg/db/marshalHelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,34 @@ func UnmarshalSimpleKasKey(keysJSON []byte) (*policy.SimpleKasKey, error) {
}
return key, nil
}

func CertificatesProtoJSON(certsJSON []byte) ([]*policy.Certificate, error) {
var (
certs []*policy.Certificate
raw []json.RawMessage
)
if err := json.Unmarshal(certsJSON, &raw); err != nil {
return nil, err
}
for _, r := range raw {
c, err := UnmarshalCertificate([]byte(r))
if err != nil {
return nil, fmt.Errorf("failed to unmarshal certificate: %w", err)
}
if c != nil {
certs = append(certs, c)
}
}
return certs, nil
}

func UnmarshalCertificate(certJSON []byte) (*policy.Certificate, error) {
var cert *policy.Certificate
if certJSON != nil {
cert = &policy.Certificate{}
if err := protojson.Unmarshal(certJSON, cert); err != nil {
return nil, err
}
}
return cert, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Add Namespace Certificates

## Migration: 20251002000000_add_namespace_certificates.sql

### Purpose

This migration adds support for associating root certificates with attribute namespaces to establish a chain of trust for the OpenTDF platform.

### Changes

#### New Tables

1. **`certificates`** - Stores root certificate data
- `id` (UUID, PRIMARY KEY) - Unique identifier for the certificate
- `x5c` (TEXT, NOT NULL) - Base64-encoded DER certificate in x5c format
- `metadata` (JSONB) - Optional metadata for the certificate (labels, etc.)
- `created_at` (TIMESTAMP) - Creation timestamp
- `updated_at` (TIMESTAMP) - Last update timestamp

2. **`attribute_namespace_certificates`** - Junction table for many-to-many relationship
- `namespace_id` (UUID, FK to attribute_namespaces) - Reference to the namespace
- `certificate_id` (UUID, FK to certificates) - Reference to the certificate
- Composite PRIMARY KEY on (namespace_id, certificate_id)
- CASCADE deletion on both foreign keys

#### Indexes

- Primary key index on `certificates(id)`
- Composite primary key index on `attribute_namespace_certificates(namespace_id, certificate_id)`
- Foreign key indexes created automatically by PostgreSQL

### Motivation

Root certificates need to be associated with namespaces to:
1. Establish chain of trust for attribute-based access control
2. Support certificate validation in the policy enforcement flow
3. Enable namespace-scoped trust boundaries

The junction table design allows:
- Multiple certificates per namespace (certificate rotation/migration scenarios)
- Certificate reuse across namespaces (if needed)
- Clean cascade deletion when namespaces or certificates are removed

### Certificate Format

Certificates are stored in **x5c format**: base64-encoded DER (Distinguished Encoding Rules) representation without PEM headers/footers, following the JWT/JWS standard (RFC 7515 Section 4.1.6).

### Schema Relations

```
attribute_namespaces (1) ----< (N) attribute_namespace_certificates (N) >---- (1) certificates
```

- One namespace can have many certificates (one-to-many)
- One certificate can be assigned to many namespaces (many-to-one, though typically one-to-one)
- Junction table enables many-to-many flexibility

### Backward Compatibility

This migration is **non-breaking**:
- Only adds new tables, does not modify existing tables
- No data migration required
- Existing functionality continues to work unchanged
- New certificate fields (`root_certs`) in Namespace proto are optional

### Testing

Before merging, verify:
- [x] Migration up succeeds
- [x] Migration down succeeds and removes tables cleanly
- [x] CRUD operations on certificates work correctly
- [x] Foreign key constraints enforce referential integrity
- [x] CASCADE deletion works as expected
- [x] Schema ERD updated with new relations
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
-- +goose Up
-- +goose StatementBegin

CREATE TABLE IF NOT EXISTS certificates
(
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
pem TEXT NOT NULL,
metadata JSONB,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);

COMMENT ON TABLE certificates IS 'Table to store X.509 certificates for chain of trust (root only)';
COMMENT ON COLUMN certificates.id IS 'Unique identifier for the certificate';
COMMENT ON COLUMN certificates.pem IS 'PEM format - Base64-encoded DER certificate (not PEM; no headers/footers)';
COMMENT ON COLUMN certificates.metadata IS 'Optional metadata for the certificate';
COMMENT ON COLUMN certificates.created_at IS 'Timestamp when the certificate was created';
COMMENT ON COLUMN certificates.updated_at IS 'Timestamp when the certificate was last updated';

CREATE TABLE IF NOT EXISTS attribute_namespace_certificates
(
namespace_id UUID NOT NULL REFERENCES attribute_namespaces(id) ON DELETE CASCADE,
certificate_id UUID NOT NULL REFERENCES certificates(id) ON DELETE CASCADE,
PRIMARY KEY (namespace_id, certificate_id)
);

COMMENT ON TABLE attribute_namespace_certificates IS 'Junction table to map root certificates to attribute namespaces';
COMMENT ON COLUMN attribute_namespace_certificates.namespace_id IS 'Foreign key to the namespace';
COMMENT ON COLUMN attribute_namespace_certificates.certificate_id IS 'Foreign key to the certificate';

CREATE TRIGGER certificates_updated_at
BEFORE UPDATE ON certificates
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();

-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin

DROP TRIGGER IF EXISTS certificates_updated_at ON certificates;
DROP TABLE IF EXISTS attribute_namespace_certificates;
DROP TABLE IF EXISTS certificates;

-- +goose StatementEnd
22 changes: 22 additions & 0 deletions service/policy/db/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading