Skip to content

Commit

Permalink
feat: Add package:corks (#66)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dnys1 authored Mar 9, 2024
1 parent 6c29941 commit a892fed
Show file tree
Hide file tree
Showing 83 changed files with 15,114 additions and 1 deletion.
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

0 comments on commit a892fed

Please sign in to comment.