Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add package:corks #66

Merged
merged 1 commit into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cedar_ffi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: cedar_ffi
on:
pull_request:
paths:
- ".github/workflows/packages_cedar_ffi.yaml"
- ".github/workflows/cedar_ffi.yaml"
- "packages/cedar_ffi/**"
- "packages/cedar_ffi/third_party/cedar"
- "packages/cedar_ffi/third_party/cedar/**"
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/corks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: corks
on:
pull_request:
paths:
- ".github/workflows/corks.yaml"
- "packages/corks/**"

# Prevent duplicate runs due to Graphite
# https://graphite.dev/docs/troubleshooting#why-are-my-actions-running-twice
concurrency:
group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }}-${{ github.ref == 'refs/heads/main' && github.sha || ''}}
cancel-in-progress: true

jobs:
test:
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-14
- windows-latest
runs-on: ${{ matrix.os }}
# TODO(dnys1): Speed up Rust builds
timeout-minutes: 15
steps:
- name: Git Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # 4.1.1
with:
submodules: true
- name: Setup Dart
uses: dart-lang/setup-dart@fedb1266e91cf51be2fdb382869461a434b920a3 # main
# Will often be out-of-date on runners
- name: Setup Rust
run: rustup update stable && rustup default stable
- name: Get Packages
working-directory: packages/corks
run: dart pub get
- name: Test
working-directory: packages/corks
run: dart --enable-experiment=native-assets test --fail-fast
7 changes: 7 additions & 0 deletions packages/corks/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/

# Avoid committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock
3 changes: 3 additions & 0 deletions packages/corks/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 0.1.0

- Initial version.
13 changes: 13 additions & 0 deletions packages/corks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Corks

Corks are authorization tokens which are based off Google's [Macaroons](https://research.google/pubs/macaroons-cookies-with-contextual-caveats-for-decentralized-authorization-in-the-cloud/) paper. They are bearer tokens which identify the entity possessing them, while providing a mechanism for embedding further restrictions via [Cedar](https://www.cedarpolicy.com/en) policy caveats.

## Development

Corks use Protobuf for serialization and deserialization of bearers and caveats. The proto definitions are located in the [proto](./proto) directory and the [Buf](https://buf.build) toolchain is used to generate Dart code from the Protobuf files.

To generate the Dart code, install Buf then run the following command from the `proto/` directory:

```sh
$ buf generate
```
12 changes: 12 additions & 0 deletions packages/corks/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
include: package:lints/recommended.yaml

analyzer:
exclude:
- "**/*.g.dart"
- "**/*.pb.dart"
- "**/*.pbenum.dart"
- "**/*.pbjson.dart"
- "**/*.pbserver.dart"
errors:
implementation_imports: ignore
public_member_api_docs: ignore
9 changes: 9 additions & 0 deletions packages/corks/lib/corks.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// Corks are authorization tokens which are based off Google's
/// [Macaroons](https://research.google/pubs/macaroons-cookies-with-contextual-caveats-for-decentralized-authorization-in-the-cloud/)
/// paper. They are bearer tokens which identify the entity possessing them,
/// while providing a mechanism for embedding further restrictions via
/// [Cedar](https://www.cedarpolicy.com/en) policy caveats.
library;

export 'src/cork.dart';
export 'src/signer.dart';
28 changes: 28 additions & 0 deletions packages/corks/lib/corks_proto.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export 'src/proto/cedar/v3/context.pb.dart';
export 'src/proto/cedar/v3/context.pbenum.dart';
export 'src/proto/cedar/v3/context.pbjson.dart';
export 'src/proto/cedar/v3/context.pbserver.dart';
export 'src/proto/cedar/v3/entity.pb.dart';
export 'src/proto/cedar/v3/entity.pbenum.dart';
export 'src/proto/cedar/v3/entity.pbjson.dart';
export 'src/proto/cedar/v3/entity.pbserver.dart';
export 'src/proto/cedar/v3/entity_id.pb.dart';
export 'src/proto/cedar/v3/entity_id.pbenum.dart';
export 'src/proto/cedar/v3/entity_id.pbjson.dart';
export 'src/proto/cedar/v3/entity_id.pbserver.dart';
export 'src/proto/cedar/v3/expr.pb.dart';
export 'src/proto/cedar/v3/expr.pbenum.dart';
export 'src/proto/cedar/v3/expr.pbjson.dart';
export 'src/proto/cedar/v3/expr.pbserver.dart';
export 'src/proto/cedar/v3/policy.pb.dart';
export 'src/proto/cedar/v3/policy.pbenum.dart';
export 'src/proto/cedar/v3/policy.pbjson.dart';
export 'src/proto/cedar/v3/policy.pbserver.dart';
export 'src/proto/cedar/v3/value.pb.dart';
export 'src/proto/cedar/v3/value.pbenum.dart';
export 'src/proto/cedar/v3/value.pbjson.dart';
export 'src/proto/cedar/v3/value.pbserver.dart';
export 'src/proto/corks/v1/cork.pb.dart';
export 'src/proto/corks/v1/cork.pbenum.dart';
export 'src/proto/corks/v1/cork.pbjson.dart';
export 'src/proto/corks/v1/cork.pbserver.dart';
228 changes: 228 additions & 0 deletions packages/corks/lib/src/cork.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';

import 'package:cedar/cedar.dart' as cedar;
import 'package:corks/corks_proto.dart' as proto;
import 'package:corks/src/interop/proto_interop.dart';
import 'package:corks/src/proto/google/protobuf/any.pb.dart' as proto;
import 'package:corks/src/signer.dart';
import 'package:crypto/crypto.dart';
import 'package:meta/meta.dart';

@immutable
final class Cork {
const Cork._({
required this.id,
required this.keyId,
this.bearer,
required List<SignedCaveat> caveats,
required this.signature,
}) : _caveats = caveats;

factory Cork.parse(String base64) => Cork.decode(base64Url.decode(base64));

factory Cork.decode(Uint8List bytes) {
final message = proto.Cork.fromBuffer(bytes);
return Cork.fromProto(message);
}

factory Cork.fromProto(proto.Cork proto) => Cork._(
id: Uint8List.fromList(proto.id),
keyId: Uint8List.fromList(proto.keyId),
bearer: proto.hasBearer() ? SignedBearer.fromProto(proto.bearer) : null,
caveats: [
for (final caveat in proto.caveats) SignedCaveat.fromProto(caveat),
],
signature: Uint8List.fromList(proto.signature),
);

static CorkBuilder builder({
Uint8List? id,
Bearer? bearer,
}) =>
CorkBuilder(
id: id,
bearer: bearer,
);

final Uint8List id;
final Uint8List keyId;
final SignedBearer? bearer;
final List<SignedCaveat> _caveats;
List<SignedCaveat> get caveats => UnmodifiableListView(_caveats);
final Uint8List signature;

proto.Cork toProto() => proto.Cork(
id: id,
keyId: keyId,
bearer: bearer?.toProto(),
caveats: _caveats.map((caveat) => caveat.toProto()),
signature: signature,
);

Uint8List encode() => toProto().writeToBuffer();

Future<bool> verify(Signer signer) async {
if (Digest(signer.keyId) != Digest(keyId)) {
return false;
}
await signer.sign(id);
if (bearer != null) {
// TODO(dnys1): https://github.com/dart-lang/sdk/issues/54664
if (!await bearer!.verify(signer)) {
return false;
}
}
for (final caveat in _caveats) {
if (!await caveat.verify(signer)) {
return false;
}
}
return Digest(await signer.close()) == Digest(signature);
}

@override
String toString() => base64Url.encode(encode());
}

final class CorkBuilder {
factory CorkBuilder({
Uint8List? id,
Bearer? bearer,
}) {
if (id == null) {
const nonceSize = 32;
id = Uint8List(nonceSize);
for (var i = 0; i < nonceSize; i++) {
id[i] = _secureRandom.nextInt(256);
}
}
return CorkBuilder._(id, bearer);
}

CorkBuilder._(
this._id,
this._bearer,
);

static final _secureRandom = Random.secure();

final Uint8List _id;
final Bearer? _bearer;
final List<Caveat> _caveats = [];

void addPolicyCaveat(cedar.CedarPolicy policy) {
if (policy.effect != cedar.CedarPolicyEffect.forbid) {
throw ArgumentError('Policy must have effect "forbid"');
}
_caveats.add(Caveat.policy(policy: policy));
}

Future<Cork> build(Signer signer) async {
await signer.sign(_id);

SignedBearer? signedBearer;
if (_bearer case final bearer?) {
signedBearer = SignedBearer(await bearer.sign(signer));
}
final signedCaveats = <SignedCaveat>[];
for (final caveat in _caveats) {
signedCaveats.add(SignedCaveat(await caveat.sign(signer)));
}
return Cork._(
id: _id,
keyId: signer.keyId,
bearer: signedBearer,
caveats: signedCaveats,
signature: await signer.close(),
);
}
}

@immutable
sealed class Bearer with Signable {
const Bearer();

factory Bearer.entity({
required cedar.CedarEntity entity,
}) =>
EntityBearer(entity: entity);

factory Bearer.entityId({
required cedar.CedarEntityId entityId,
}) =>
EntityBearer(entity: cedar.CedarEntity(id: entityId));
}

final class EntityBearer extends Bearer {
const EntityBearer({
required this.entity,
});

final cedar.CedarEntity entity;

@override
proto.Entity toProto() => entity.toProto();
}

extension type SignedBearer(SignedBlock _block) implements SignedBlock {
SignedBearer.fromProto(proto.SignedBlock proto)
: _block = SignedBlock.fromProto(proto);

Bearer get bearer {
final any = proto.Any(
typeUrl: typeUrl,
value: block,
);
final entity = proto.Entity();
if (!any.canUnpackInto(entity)) {
throw ArgumentError('Invalid bearer type: $typeUrl');
}
any.unpackInto(entity);
return Bearer.entity(
entity: entity.fromProto(),
);
}
}

@immutable
sealed class Caveat with Signable {
const Caveat();

factory Caveat.policy({
required cedar.CedarPolicy policy,
}) =>
PolicyCaveat(policy: policy);
}

final class PolicyCaveat extends Caveat {
const PolicyCaveat({
required this.policy,
});

final cedar.CedarPolicy policy;

@override
proto.Policy toProto() => policy.toProto();
}

extension type SignedCaveat(SignedBlock _block) implements SignedBlock {
SignedCaveat.fromProto(proto.SignedBlock proto)
: _block = SignedBlock.fromProto(proto);

Caveat get caveat {
final any = proto.Any(
typeUrl: typeUrl,
value: block,
);
final policy = proto.Policy();
if (!any.canUnpackInto(policy)) {
throw ArgumentError('Invalid caveat type: $typeUrl');
}
any.unpackInto(policy);
return PolicyCaveat(policy: policy.fromProto());
}
}
Loading
Loading